Merge branch 'develop' into progress-bar

This commit is contained in:
Fredrik81 2020-04-08 01:56:43 +02:00
commit cdc774549e
63 changed files with 1025 additions and 740 deletions

1
.gitignore vendored
View File

@ -6,7 +6,6 @@ user_data/*
!user_data/strategy/sample_strategy.py !user_data/strategy/sample_strategy.py
!user_data/notebooks !user_data/notebooks
user_data/notebooks/* user_data/notebooks/*
!user_data/notebooks/*example.ipynb
freqtrade-plot.html freqtrade-plot.html
freqtrade-profit-plot.html freqtrade-profit-plot.html

View File

@ -2,3 +2,4 @@ include LICENSE
include README.md include README.md
include config.json.example include config.json.example
recursive-include freqtrade *.py recursive-include freqtrade *.py
recursive-include freqtrade/templates/ *.j2 *.ipynb

View File

@ -11,8 +11,8 @@ Now you have good Buy and Sell strategies and some historic data, you want to te
real data. This is what we call real data. This is what we call
[backtesting](https://en.wikipedia.org/wiki/Backtesting). [backtesting](https://en.wikipedia.org/wiki/Backtesting).
Backtesting will use the crypto-currencies (pairs) from your config file and load ticker data from `user_data/data/<exchange>` by default. Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHCLV) data from `user_data/data/<exchange>` by default.
If no data is available for the exchange / pair / ticker interval combination, backtesting will ask you to download them first using `freqtrade download-data`. If no data is available for the exchange / pair / timeframe (ticker interval) combination, backtesting will ask you to download them first using `freqtrade download-data`.
For details on downloading, please refer to the [Data Downloading](data-download.md) section in the documentation. For details on downloading, please refer to the [Data Downloading](data-download.md) section in the documentation.
The result of backtesting will confirm if your bot has better odds of making a profit than a loss. The result of backtesting will confirm if your bot has better odds of making a profit than a loss.
@ -22,19 +22,19 @@ The result of backtesting will confirm if your bot has better odds of making a p
### Run a backtesting against the currencies listed in your config file ### Run a backtesting against the currencies listed in your config file
#### With 5 min tickers (Per default) #### With 5 min candle (OHLCV) data (per default)
```bash ```bash
freqtrade backtesting freqtrade backtesting
``` ```
#### With 1 min tickers #### With 1 min candle (OHLCV) data
```bash ```bash
freqtrade backtesting --ticker-interval 1m freqtrade backtesting --ticker-interval 1m
``` ```
#### Using a different on-disk ticker-data source #### Using a different on-disk historical candle (OHLCV) data source
Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory. Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory.
You can then use this data for backtesting as follows: You can then use this data for backtesting as follows:
@ -223,7 +223,7 @@ You can then load the trades to perform further analysis as shown in our [data a
To compare multiple strategies, a list of Strategies can be provided to backtesting. To compare multiple strategies, a list of Strategies can be provided to backtesting.
This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple This is limited to 1 timeframe (ticker interval) value per run. However, data is only loaded once from disk so if you have multiple
strategies you'd like to compare, this will give a nice runtime boost. strategies you'd like to compare, this will give a nice runtime boost.
All listed Strategies need to be in the same directory. All listed Strategies need to be in the same directory.

View File

@ -144,10 +144,10 @@ It is recommended to use version control to keep track of changes to your strate
### How to use **--strategy**? ### How to use **--strategy**?
This parameter will allow you to load your custom strategy class. This parameter will allow you to load your custom strategy class.
Per default without `--strategy` or `-s` the bot will load the To test the bot installation, you can use the `SampleStrategy` installed by the `create-userdir` subcommand (usually `user_data/strategy/sample_strategy.py`).
`DefaultStrategy` included with the bot (`freqtrade/strategy/default_strategy.py`).
The bot will search your strategy file within `user_data/strategies` and `freqtrade/strategy`. The bot will search your strategy file within `user_data/strategies`.
To use other directories, please read the next section about `--strategy-path`.
To load a strategy, simply pass the class name (e.g.: `CustomStrategy`) in this parameter. To load a strategy, simply pass the class name (e.g.: `CustomStrategy`) in this parameter.

View File

@ -40,14 +40,14 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| Parameter | Description | | Parameter | Description |
|------------|-------------| |------------|-------------|
| `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades). [More information below](#configuring-amount-per-trade).<br> **Datatype:** Positive integer or -1. | `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation which can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade).<br> **Datatype:** Positive integer or -1.
| `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String | `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Positive float or `"unlimited"`. | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Positive float or `"unlimited"`.
| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`. | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`.
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio) | `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio. | `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio.
| `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String | `ticker_interval` | The timeframe (ticker interval) to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float | `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
@ -113,8 +113,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> **Datatype:** Boolean | `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> **Datatype:** Boolean
| `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> **Datatype:** String | `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> **Datatype:** String
| `user_data_dir` | Directory containing user data. <br> *Defaults to `./user_data/`*. <br> **Datatype:** String | `user_data_dir` | Directory containing user data. <br> *Defaults to `./user_data/`*. <br> **Datatype:** String
| `dataformat_ohlcv` | Data format to use to store OHLCV historic data. <br> *Defaults to `json`*. <br> **Datatype:** String | `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `json`*. <br> **Datatype:** String
| `dataformat_trades` | Data format to use to store trades historic data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String | `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String
### Parameters in the strategy ### Parameters in the strategy
@ -413,7 +413,7 @@ Advanced options can be configured using the `_ft_has_params` setting, which wil
Available options are listed in the exchange-class as `_ft_has_default`. Available options are listed in the exchange-class as `_ft_has_default`.
For example, to test the order type `FOK` with Kraken, and modify candle_limit to 200 (so you only get 200 candles per call): For example, to test the order type `FOK` with Kraken, and modify candle limit to 200 (so you only get 200 candles per API call):
```json ```json
"exchange": { "exchange": {

View File

@ -33,7 +33,7 @@ optional arguments:
Specify which tickers to download. Space-separated list. Default: `1m 5m`. Specify which tickers to download. Space-separated list. Default: `1m 5m`.
--erase Clean all existing data for the selected exchange/pairs/timeframes. --erase Clean all existing data for the selected exchange/pairs/timeframes.
--data-format-ohlcv {json,jsongz} --data-format-ohlcv {json,jsongz}
Storage format for downloaded ohlcv data. (default: `json`). Storage format for downloaded candle (OHLCV) data. (default: `json`).
--data-format-trades {json,jsongz} --data-format-trades {json,jsongz}
Storage format for downloaded trades data. (default: `jsongz`). Storage format for downloaded trades data. (default: `jsongz`).
@ -105,7 +105,7 @@ Common arguments:
##### Example converting data ##### Example converting data
The following command will convert all ohlcv (candle) data available in `~/.freqtrade/data/binance` from json to jsongz, saving diskspace in the process. The following command will convert all candle (OHLCV) data available in `~/.freqtrade/data/binance` from json to jsongz, saving diskspace in the process.
It'll also remove original json data files (`--erase` parameter). It'll also remove original json data files (`--erase` parameter).
``` bash ``` bash
@ -192,15 +192,15 @@ Then run:
freqtrade download-data --exchange binance freqtrade download-data --exchange binance
``` ```
This will download ticker data for all the currency pairs you defined in `pairs.json`. This will download historical candle (OHLCV) data for all the currency pairs you defined in `pairs.json`.
### Other Notes ### Other Notes
- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`. - To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
- To change the exchange used to download the tickers, please use a different configuration file (you'll probably need to adjust ratelimits etc.) - To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust ratelimits etc.)
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`. - To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
- To download ticker data for only 10 days, use `--days 10` (defaults to 30 days). - To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days).
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers. - Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options. - To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
### Trades (tick) data ### Trades (tick) data

View File

@ -165,7 +165,7 @@ Since CCXT does not provide unification for Stoploss On Exchange yet, we'll need
### Incomplete candles ### Incomplete candles
While fetching OHLCV data, we're may end up getting incomplete candles (Depending on the exchange). While fetching candle (OHLCV) data, we may end up getting incomplete candles (depending on the exchange).
To demonstrate this, we'll use daily candles (`"1d"`) to keep things simple. To demonstrate this, we'll use daily candles (`"1d"`) to keep things simple.
We query the api (`ct.fetch_ohlcv()`) for the timeframe and look at the date of the last entry. If this entry changes or shows the date of a "incomplete" candle, then we should drop this since having incomplete candles is problematic because indicators assume that only complete candles are passed to them, and will generate a lot of false buy signals. By default, we're therefore removing the last candle assuming it's incomplete. We query the api (`ct.fetch_ohlcv()`) for the timeframe and look at the date of the last entry. If this entry changes or shows the date of a "incomplete" candle, then we should drop this since having incomplete candles is problematic because indicators assume that only complete candles are passed to them, and will generate a lot of false buy signals. By default, we're therefore removing the last candle assuming it's incomplete.
@ -174,14 +174,14 @@ To check how the new exchange behaves, you can use the following snippet:
``` python ``` python
import ccxt import ccxt
from datetime import datetime from datetime import datetime
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import ohlcv_to_dataframe
ct = ccxt.binance() ct = ccxt.binance()
timeframe = "1d" timeframe = "1d"
pair = "XLM/BTC" # Make sure to use a pair that exists on that exchange! pair = "XLM/BTC" # Make sure to use a pair that exists on that exchange!
raw = ct.fetch_ohlcv(pair, timeframe=timeframe) raw = ct.fetch_ohlcv(pair, timeframe=timeframe)
# convert to dataframe # convert to dataframe
df1 = parse_ticker_dataframe(raw, timeframe, pair=pair, drop_incomplete=False) df1 = ohlcv_to_dataframe(raw, timeframe, pair=pair, drop_incomplete=False)
print(df1.tail(1)) print(df1.tail(1))
print(datetime.utcnow()) print(datetime.utcnow())

View File

@ -156,7 +156,7 @@ Edge module has following configuration options:
| `minimum_winrate` | It filters out pairs which don't have at least minimum_winrate. <br>This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio. <br>*Defaults to `0.60`.* <br> **Datatype:** Float | `minimum_winrate` | It filters out pairs which don't have at least minimum_winrate. <br>This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio. <br>*Defaults to `0.60`.* <br> **Datatype:** Float
| `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number. <br>Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return. <br>*Defaults to `0.20`.* <br> **Datatype:** Float | `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number. <br>Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return. <br>*Defaults to `0.20`.* <br> **Datatype:** Float
| `min_trade_number` | When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable. <br>Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something. <br>*Defaults to `10` (it is highly recommended not to decrease this number).* <br> **Datatype:** Integer | `min_trade_number` | When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable. <br>Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something. <br>*Defaults to `10` (it is highly recommended not to decrease this number).* <br> **Datatype:** Integer
| `max_trade_duration_minute` | Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.<br>**NOTICE:** While configuring this value, you should take into consideration your ticker interval. As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.).<br>*Defaults to `1440` (one day).* <br> **Datatype:** Integer | `max_trade_duration_minute` | Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.<br>**NOTICE:** While configuring this value, you should take into consideration your timeframe (ticker interval). As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.).<br>*Defaults to `1440` (one day).* <br> **Datatype:** Integer
| `remove_pumps` | Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.<br>*Defaults to `false`.* <br> **Datatype:** Boolean | `remove_pumps` | Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.<br>*Defaults to `false`.* <br> **Datatype:** Boolean
## Running Edge independently ## Running Edge independently

View File

@ -74,23 +74,13 @@ Should you experience constant errors with Nonce (like `InvalidNonce`), it is be
$ pip3 install web3 $ pip3 install web3
``` ```
### Send incomplete candles to the strategy ### Getting latest price / Incomplete candles
Most exchanges return incomplete candles via their ohlcv / klines interface. Most exchanges return current incomplete candle via their OHLCV/klines API interface.
By default, Freqtrade assumes that incomplete candles are returned and removes the last candle assuming it's an incomplete candle. By default, Freqtrade assumes that incomplete candle is fetched from the exchange and removes the last candle assuming it's the incomplete candle.
Whether your exchange returns incomplete candles or not can be checked using [the helper script](developer.md#Incomplete-candles) from the Contributor documentation. Whether your exchange returns incomplete candles or not can be checked using [the helper script](developer.md#Incomplete-candles) from the Contributor documentation.
If the exchange does return incomplete candles and you would like to have incomplete candles in your strategy, you can set the following parameter in the configuration file. Due to the danger of repainting, Freqtrade does not allow you to use this incomplete candle.
``` json However, if it is based on the need for the latest price for your strategy - then this requirement can be acquired using the [data provider](strategy-customization.md#possible-options-for-dataprovider) from within the strategy.
{
"exchange": {
"_ft_has_params": {"ohlcv_partial_candle": false}
}
}
```
!!! Warning "Danger of repainting"
Changing this parameter makes the strategy responsible to avoid repainting and handle this accordingly. Doing this is therefore not recommended, and should only be performed by experienced users who are fully aware of the impact this setting has.

View File

@ -103,9 +103,10 @@ Place the corresponding settings into the following methods
The configuration and rules are the same than for buy signals. The configuration and rules are the same than for buy signals.
To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`.
#### Using ticker-interval as part of the Strategy #### Using timeframe as a part of the Strategy
The Strategy exposes the ticker-interval as `self.ticker_interval`. The same value is available as class-attribute `HyperoptName.ticker_interval`. The Strategy class exposes the timeframe (ticker interval) value as the `self.ticker_interval` attribute.
The same value is available as class-attribute `HyperoptName.ticker_interval`.
In the case of the linked sample-value this would be `SampleHyperOpt.ticker_interval`. In the case of the linked sample-value this would be `SampleHyperOpt.ticker_interval`.
## Solving a Mystery ## Solving a Mystery
@ -159,6 +160,9 @@ So let's write the buy strategy using these values:
dataframe['macd'], dataframe['macdsignal'] dataframe['macd'], dataframe['macdsignal']
)) ))
# Check that volume is not 0
conditions.append(dataframe['volume'] > 0)
if conditions: if conditions:
dataframe.loc[ dataframe.loc[
reduce(lambda x, y: x & y, conditions), reduce(lambda x, y: x & y, conditions),
@ -222,11 +226,11 @@ The `--spaces all` option determines that all possible parameters should be opti
!!! Warning !!! Warning
When switching parameters or changing configuration options, make sure to not use the argument `--continue` so temporary results can be removed. When switching parameters or changing configuration options, make sure to not use the argument `--continue` so temporary results can be removed.
### Execute Hyperopt with Different Ticker-Data Source ### Execute Hyperopt with different historical 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 historical data set that
you have on-disk, use the `--datadir PATH` option. Default hyperopt will you have on-disk, use the `--datadir PATH` option. By default, hyperopt
use data from directory `user_data/data`. uses data from directory `user_data/data`.
### Running Hyperopt with Smaller Testset ### Running Hyperopt with Smaller Testset
@ -380,7 +384,7 @@ As stated in the comment, you can also use it as the value of the `minimal_roi`
#### Default ROI Search Space #### Default ROI Search Space
If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the ticker_interval used. By default the values vary in the following ranges (for some of the most used ticker intervals, values are rounded to 5 digits after the decimal point): If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the ticker_interval used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point):
| # step | 1m | | 5m | | 1h | | 1d | | | # step | 1m | | 5m | | 1h | | 1d | |
| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- | | ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- |
@ -389,7 +393,7 @@ If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace f
| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 | | 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 |
| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | | 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 |
These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the ticker interval used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the ticker interval used. These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe (ticker interval) used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used.
If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default.

View File

@ -23,44 +23,64 @@ The `freqtrade plot-dataframe` subcommand shows an interactive graph with three
Possible arguments: Possible arguments:
``` ```
usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[--strategy-path PATH] [-p PAIRS [PAIRS ...]] [--indicators1 INDICATORS1 [INDICATORS1 ...]] [-d PATH] [--userdir PATH] [-s NAME]
[--indicators2 INDICATORS2 [INDICATORS2 ...]] [--plot-limit INT] [--db-url PATH] [--strategy-path PATH] [-p PAIRS [PAIRS ...]]
[--trade-source {DB,file}] [--export EXPORT] [--export-filename PATH] [--timerange TIMERANGE] [--indicators1 INDICATORS1 [INDICATORS1 ...]]
[-i TICKER_INTERVAL] [--indicators2 INDICATORS2 [INDICATORS2 ...]]
[--plot-limit INT] [--db-url PATH]
[--trade-source {DB,file}] [--export EXPORT]
[--export-filename PATH]
[--timerange TIMERANGE] [-i TICKER_INTERVAL]
[--no-trades]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
Show profits for only these pairs. Pairs are space-separated. Show profits for only these pairs. Pairs are space-
separated.
--indicators1 INDICATORS1 [INDICATORS1 ...] --indicators1 INDICATORS1 [INDICATORS1 ...]
Set indicators from your strategy you want in the first row of the graph. Space-separated list. Example: Set indicators from your strategy you want in the
first row of the graph. Space-separated list. Example:
`ema3 ema5`. Default: `['sma', 'ema3', 'ema5']`. `ema3 ema5`. Default: `['sma', 'ema3', 'ema5']`.
--indicators2 INDICATORS2 [INDICATORS2 ...] --indicators2 INDICATORS2 [INDICATORS2 ...]
Set indicators from your strategy you want in the third row of the graph. Space-separated list. Example: Set indicators from your strategy you want in the
third row of the graph. Space-separated list. Example:
`fastd fastk`. Default: `['macd', 'macdsignal']`. `fastd fastk`. Default: `['macd', 'macdsignal']`.
--plot-limit INT Specify tick limit for plotting. Notice: too high values cause huge files. Default: 750. --plot-limit INT Specify tick limit for plotting. Notice: too high
--db-url PATH Override trades database URL, this is useful in custom deployments (default: `sqlite:///tradesv3.sqlite` values cause huge files. Default: 750.
for Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for Dry Run). --db-url PATH Override trades database URL, this is useful in custom
deployments (default: `sqlite:///tradesv3.sqlite` for
Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for
Dry Run).
--trade-source {DB,file} --trade-source {DB,file}
Specify the source for trades (Can be DB or file (backtest file)) Default: file Specify the source for trades (Can be DB or file
--export EXPORT Export backtest results, argument are: trades. Example: `--export=trades` (backtest file)) Default: file
--export EXPORT Export backtest results, argument are: trades.
Example: `--export=trades`
--export-filename PATH --export-filename PATH
Save backtest results to the file with this filename. Requires `--export` to be set as well. Example: Save backtest results to the file with this filename.
`--export-filename=user_data/backtest_results/backtest_today.json` Requires `--export` to be set as well. Example:
`--export-filename=user_data/backtest_results/backtest
_today.json`
--timerange TIMERANGE --timerange TIMERANGE
Specify what timerange of data to use. Specify what timerange of data to use.
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`). Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
`1d`).
--no-trades Skip using trades from backtesting file and DB.
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. Special values are: 'syslog', 'journald'. See the documentation for more --logfile FILE Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more
details. details.
-V, --version show program's version number and exit -V, --version show program's version number and exit
-c PATH, --config PATH -c PATH, --config PATH
Specify configuration file (default: `config.json`). Multiple --config options may be used. Can be set to Specify configuration file (default:
`-` to read config from stdin. `userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin.
-d PATH, --datadir PATH -d PATH, --datadir PATH
Path to directory with historical backtesting data. Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH --userdir PATH, --user-data-dir PATH
@ -68,9 +88,9 @@ Common arguments:
Strategy arguments: Strategy arguments:
-s NAME, --strategy NAME -s NAME, --strategy NAME
Specify strategy class name which will be used by the bot. Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path. --strategy-path PATH Specify additional strategy lookup path.
``` ```
Example: Example:

View File

@ -84,7 +84,7 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame
Performance Note: For the best performance be frugal on the number of indicators Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage. or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() :param dataframe: Dataframe with data from the exchange
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies :return: a Dataframe with all mandatory indicators for the strategies
""" """
@ -284,13 +284,14 @@ If your exchange supports it, it's recommended to also set `"stoploss_on_exchang
For more information on order_types please look [here](configuration.md#understand-order_types). For more information on order_types please look [here](configuration.md#understand-order_types).
### Ticker interval ### Timeframe (ticker interval)
This is the set of candles the bot should download and use for the analysis. This is the set of candles the bot should download and use for the analysis.
Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work. Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work.
Please note that the same buy/sell signals may work with one interval, but not the other. Please note that the same buy/sell signals may work well with one timeframe, but not with the others.
This setting is accessible within the strategy by using `self.ticker_interval`.
This setting is accessible within the strategy methods as the `self.ticker_interval` attribute.
### Metadata dict ### Metadata dict
@ -335,14 +336,14 @@ Please always check the mode of operation to select the correct method to get da
#### Possible options for DataProvider #### Possible options for DataProvider
- `available_pairs` - Property with tuples listing cached pairs with their intervals (pair, interval). - `available_pairs` - Property with tuples listing cached pairs with their intervals (pair, interval).
- `ohlcv(pair, timeframe)` - Currently cached ticker data for the pair, returns DataFrame or empty DataFrame. - `ohlcv(pair, timeframe)` - Currently cached candle (OHLCV) data for the pair, returns DataFrame or empty DataFrame.
- `historic_ohlcv(pair, timeframe)` - Returns historical data stored on disk. - `historic_ohlcv(pair, timeframe)` - Returns historical data stored on disk.
- `get_pair_dataframe(pair, timeframe)` - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes). - `get_pair_dataframe(pair, timeframe)` - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes).
- `orderbook(pair, maximum)` - Returns latest orderbook data for the pair, a dict with bids/asks with a total of `maximum` entries. - `orderbook(pair, maximum)` - Returns latest orderbook data for the pair, a dict with bids/asks with a total of `maximum` entries.
- `market(pair)` - Returns market data for the pair: fees, limits, precisions, activity flag, etc. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#markets) for more details on Market data structure. - `market(pair)` - Returns market data for the pair: fees, limits, precisions, activity flag, etc. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#markets) for more details on Market data structure.
- `runmode` - Property containing the current runmode. - `runmode` - Property containing the current runmode.
#### Example: fetch live ohlcv / historic data for the first informative pair #### Example: fetch live / historical candle (OHLCV) data for the first informative pair
``` python ``` python
if self.dp: if self.dp:
@ -377,8 +378,8 @@ if self.dp:
``` python ``` python
if self.dp: if self.dp:
for pair, ticker in self.dp.available_pairs: for pair, timeframe in self.dp.available_pairs:
print(f"available {pair}, {ticker}") print(f"available {pair}, {timeframe}")
``` ```
#### Get data for non-tradeable pairs #### Get data for non-tradeable pairs

View File

@ -61,8 +61,8 @@ $ freqtrade new-config --config config_binance.json
? Do you want to enable Dry-run (simulated trades)? Yes ? Do you want to enable Dry-run (simulated trades)? Yes
? Please insert your stake currency: BTC ? Please insert your stake currency: BTC
? Please insert your stake amount: 0.05 ? Please insert your stake amount: 0.05
? Please insert max_open_trades (Integer or 'unlimited'): 5 ? Please insert max_open_trades (Integer or 'unlimited'): 3
? Please insert your ticker interval: 15m ? Please insert your timeframe (ticker interval): 5m
? Please insert your display Currency (for reporting): USD ? Please insert your display Currency (for reporting): USD
? Select exchange binance ? Select exchange binance
? Do you want to enable Telegram? No ? Do you want to enable Telegram? No
@ -258,7 +258,7 @@ All exchanges supported by the ccxt library: _1btcxe, acx, adara, allcoin, anxpr
## List Timeframes ## List Timeframes
Use the `list-timeframes` subcommand to see the list of ticker intervals (timeframes) available for the exchange. Use the `list-timeframes` subcommand to see the list of timeframes (ticker intervals) available for the exchange.
``` ```
usage: freqtrade list-timeframes [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--exchange EXCHANGE] [-1] usage: freqtrade list-timeframes [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--exchange EXCHANGE] [-1]

View File

@ -59,7 +59,7 @@ ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchang
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
"db_url", "trade_source", "export", "exportfilename", "db_url", "trade_source", "export", "exportfilename",
"timerange", "ticker_interval"] "timerange", "ticker_interval", "no_trades"]
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
"trade_source", "ticker_interval"] "trade_source", "ticker_interval"]
@ -297,7 +297,7 @@ class Arguments:
# Add convert-data subcommand # Add convert-data subcommand
convert_data_cmd = subparsers.add_parser( convert_data_cmd = subparsers.add_parser(
'convert-data', 'convert-data',
help='Convert OHLCV data from one format to another.', help='Convert candle (OHLCV) data from one format to another.',
parents=[_common_parser], parents=[_common_parser],
) )
convert_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=True)) convert_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=True))
@ -306,7 +306,7 @@ class Arguments:
# Add convert-trade-data subcommand # Add convert-trade-data subcommand
convert_trade_data_cmd = subparsers.add_parser( convert_trade_data_cmd = subparsers.add_parser(
'convert-trade-data', 'convert-trade-data',
help='Convert trade-data from one format to another.', help='Convert trade data from one format to another.',
parents=[_common_parser], parents=[_common_parser],
) )
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False)) convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))

View File

@ -76,7 +76,7 @@ def ask_user_config() -> Dict[str, Any]:
{ {
"type": "text", "type": "text",
"name": "ticker_interval", "name": "ticker_interval",
"message": "Please insert your ticker interval:", "message": "Please insert your timeframe (ticker interval):",
"default": "5m", "default": "5m",
}, },
{ {

View File

@ -355,7 +355,7 @@ AVAILABLE_CLI_OPTIONS = {
), ),
"dataformat_ohlcv": Arg( "dataformat_ohlcv": Arg(
'--data-format-ohlcv', '--data-format-ohlcv',
help='Storage format for downloaded ohlcv data. (default: `%(default)s`).', help='Storage format for downloaded candle (OHLCV) data. (default: `%(default)s`).',
choices=constants.AVAILABLE_DATAHANDLERS, choices=constants.AVAILABLE_DATAHANDLERS,
default='json' default='json'
), ),
@ -413,6 +413,11 @@ AVAILABLE_CLI_OPTIONS = {
metavar='INT', metavar='INT',
default=750, default=750,
), ),
"no_trades": Arg(
'--no-trades',
help='Skip using trades from backtesting file and DB.',
action='store_true',
),
"trade_source": Arg( "trade_source": Arg(
'--trade-source', '--trade-source',
help='Specify the source for trades (Can be DB or file (backtest file)) ' help='Specify the source for trades (Can be DB or file (backtest file)) '

View File

@ -196,6 +196,7 @@ class Configuration:
if self.args.get('exportfilename'): if self.args.get('exportfilename'):
self._args_to_config(config, argname='exportfilename', self._args_to_config(config, argname='exportfilename',
logstring='Storing backtest results to {} ...') logstring='Storing backtest results to {} ...')
config['exportfilename'] = Path(config['exportfilename'])
else: else:
config['exportfilename'] = (config['user_data_dir'] config['exportfilename'] = (config['user_data_dir']
/ 'backtest_results/backtest-result.json') / 'backtest_results/backtest-result.json')
@ -358,6 +359,9 @@ class Configuration:
self._args_to_config(config, argname='erase', self._args_to_config(config, argname='erase',
logstring='Erase detected. Deleting existing data.') logstring='Erase detected. Deleting existing data.')
self._args_to_config(config, argname='no_trades',
logstring='Parameter --no-trades detected.')
self._args_to_config(config, argname='timeframes', self._args_to_config(config, argname='timeframes',
logstring='timeframes --timeframes: {}') logstring='timeframes --timeframes: {}')

View File

@ -1,13 +1,15 @@
""" """
This module contain functions to load the configuration file This module contain functions to load the configuration file
""" """
import rapidjson
import logging import logging
import re
import sys import sys
from pathlib import Path
from typing import Any, Dict from typing import Any, Dict
from freqtrade.exceptions import OperationalException import rapidjson
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -15,6 +17,26 @@ logger = logging.getLogger(__name__)
CONFIG_PARSE_MODE = rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS CONFIG_PARSE_MODE = rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS
def log_config_error_range(path: str, errmsg: str) -> str:
"""
Parses configuration file and prints range around error
"""
if path != '-':
offsetlist = re.findall(r'(?<=Parse\serror\sat\soffset\s)\d+', errmsg)
if offsetlist:
offset = int(offsetlist[0])
text = Path(path).read_text()
# Fetch an offset of 80 characters around the error line
subtext = text[offset-min(80, offset):offset+80]
segments = subtext.split('\n')
if len(segments) > 3:
# Remove first and last lines, to avoid odd truncations
return '\n'.join(segments[1:-1])
else:
return subtext
return ''
def load_config_file(path: str) -> Dict[str, Any]: def load_config_file(path: str) -> Dict[str, Any]:
""" """
Loads a config file from the given path Loads a config file from the given path
@ -29,5 +51,12 @@ def load_config_file(path: str) -> Dict[str, Any]:
raise OperationalException( raise OperationalException(
f'Config file "{path}" not found!' f'Config file "{path}" not found!'
' Please create a config file or check whether it exists.') ' Please create a config file or check whether it exists.')
except rapidjson.JSONDecodeError as e:
err_range = log_config_error_range(path, str(e))
raise OperationalException(
f'{e}\n'
f'Please verify the following segment of your configuration:\n{err_range}'
if err_range else 'Please verify your configuration file for syntax errors.'
)
return config return config

View File

@ -45,7 +45,7 @@ class TimeRange:
""" """
Adjust startts by <startup_candles> candles. Adjust startts by <startup_candles> candles.
Applies only if no startup-candles have been available. Applies only if no startup-candles have been available.
:param timeframe_secs: Ticker timeframe in seconds e.g. `timeframe_to_seconds('5m')` :param timeframe_secs: Timeframe in seconds e.g. `timeframe_to_seconds('5m')`
:param startup_candles: Number of candles to move start-date forward :param startup_candles: Number of candles to move start-date forward
:param min_date: Minimum data date loaded. Key kriterium to decide if start-time :param min_date: Minimum data date loaded. Key kriterium to decide if start-time
has to be moved has to be moved

View File

@ -111,7 +111,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
t.calc_profit(), t.calc_profit_ratio(), t.calc_profit(), t.calc_profit_ratio(),
t.open_rate, t.close_rate, t.amount, t.open_rate, t.close_rate, t.amount,
(round((t.close_date.timestamp() - t.open_date.timestamp()) / 60, 2) (round((t.close_date.timestamp() - t.open_date.timestamp()) / 60, 2)
if t.close_date else None), if t.close_date else None),
t.sell_reason, t.sell_reason,
t.fee_open, t.fee_close, t.fee_open, t.fee_close,
t.open_rate_requested, t.open_rate_requested,
@ -129,39 +129,56 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
return trades return trades
def load_trades(source: str, db_url: str, exportfilename: str) -> pd.DataFrame: def load_trades(source: str, db_url: str, exportfilename: Path,
no_trades: bool = False) -> pd.DataFrame:
""" """
Based on configuration option "trade_source": Based on configuration option "trade_source":
* loads data from DB (using `db_url`) * loads data from DB (using `db_url`)
* loads data from backtestfile (using `exportfilename`) * loads data from backtestfile (using `exportfilename`)
:param source: "DB" or "file" - specify source to load from
:param db_url: sqlalchemy formatted url to a database
:param exportfilename: Json file generated by backtesting
:param no_trades: Skip using trades, only return backtesting data columns
:return: DataFrame containing trades
""" """
if no_trades:
df = pd.DataFrame(columns=BT_DATA_COLUMNS)
return df
if source == "DB": if source == "DB":
return load_trades_from_db(db_url) return load_trades_from_db(db_url)
elif source == "file": elif source == "file":
return load_backtest_data(Path(exportfilename)) return load_backtest_data(exportfilename)
def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> pd.DataFrame: def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame,
date_index=False) -> pd.DataFrame:
""" """
Compare trades and backtested pair DataFrames to get trades performed on backtested period Compare trades and backtested pair DataFrames to get trades performed on backtested period
:return: the DataFrame of a trades of period :return: the DataFrame of a trades of period
""" """
trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) & if date_index:
(trades['close_time'] <= dataframe.iloc[-1]['date'])] trades_start = dataframe.index[0]
trades_stop = dataframe.index[-1]
else:
trades_start = dataframe.iloc[0]['date']
trades_stop = dataframe.iloc[-1]['date']
trades = trades.loc[(trades['open_time'] >= trades_start) &
(trades['close_time'] <= trades_stop)]
return trades return trades
def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame], def combine_dataframes_with_mean(data: Dict[str, pd.DataFrame],
column: str = "close") -> pd.DataFrame: column: str = "close") -> pd.DataFrame:
""" """
Combine multiple dataframes "column" Combine multiple dataframes "column"
:param tickers: Dict of Dataframes, dict key should be pair. :param data: Dict of Dataframes, dict key should be pair.
:param column: Column in the original dataframes to use :param column: Column in the original dataframes to use
:return: DataFrame with the column renamed to the dict key, and a column :return: DataFrame with the column renamed to the dict key, and a column
named mean, containing the mean of all pairs. named mean, containing the mean of all pairs.
""" """
df_comb = pd.concat([tickers[pair].set_index('date').rename( df_comb = pd.concat([data[pair].set_index('date').rename(
{column: pair}, axis=1)[pair] for pair in tickers], axis=1) {column: pair}, axis=1)[pair] for pair in data], axis=1)
df_comb['mean'] = df_comb.mean(axis=1) df_comb['mean'] = df_comb.mean(axis=1)

View File

@ -13,12 +13,12 @@ from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def parse_ticker_dataframe(ticker: list, timeframe: str, pair: str, *, def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *,
fill_missing: bool = True, fill_missing: bool = True, drop_incomplete: bool = True) -> DataFrame:
drop_incomplete: bool = True) -> DataFrame:
""" """
Converts a ticker-list (format ccxt.fetch_ohlcv) to a Dataframe Converts a list with candle (OHLCV) data (in format returned by ccxt.fetch_ohlcv)
:param ticker: ticker list, as returned by exchange.async_get_candle_history to a Dataframe
:param ohlcv: list with candle (OHLCV) data, as returned by exchange.async_get_candle_history
:param timeframe: timeframe (e.g. 5m). Used to fill up eventual missing data :param timeframe: timeframe (e.g. 5m). Used to fill up eventual missing data
:param pair: Pair this data is for (used to warn if fillup was necessary) :param pair: Pair this data is for (used to warn if fillup was necessary)
:param fill_missing: fill up missing candles with 0 candles :param fill_missing: fill up missing candles with 0 candles
@ -26,21 +26,18 @@ def parse_ticker_dataframe(ticker: list, timeframe: str, pair: str, *,
:param drop_incomplete: Drop the last candle of the dataframe, assuming it's incomplete :param drop_incomplete: Drop the last candle of the dataframe, assuming it's incomplete
:return: DataFrame :return: DataFrame
""" """
logger.debug("Parsing tickerlist to dataframe") logger.debug(f"Converting candle (OHLCV) data to dataframe for pair {pair}.")
cols = DEFAULT_DATAFRAME_COLUMNS cols = DEFAULT_DATAFRAME_COLUMNS
frame = DataFrame(ticker, columns=cols) df = DataFrame(ohlcv, columns=cols)
frame['date'] = to_datetime(frame['date'], df['date'] = to_datetime(df['date'], unit='ms', utc=True, infer_datetime_format=True)
unit='ms',
utc=True,
infer_datetime_format=True)
# Some exchanges return int values for volume and even for ohlc. # Some exchanges return int values for Volume and even for OHLC.
# Convert them since TA-LIB indicators used in the strategy assume floats # Convert them since TA-LIB indicators used in the strategy assume floats
# and fail with exception... # and fail with exception...
frame = frame.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float', df = df.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float',
'volume': 'float'}) 'volume': 'float'})
return clean_ohlcv_dataframe(frame, timeframe, pair, return clean_ohlcv_dataframe(df, timeframe, pair,
fill_missing=fill_missing, fill_missing=fill_missing,
drop_incomplete=drop_incomplete) drop_incomplete=drop_incomplete)
@ -49,11 +46,11 @@ def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *,
fill_missing: bool = True, fill_missing: bool = True,
drop_incomplete: bool = True) -> DataFrame: drop_incomplete: bool = True) -> DataFrame:
""" """
Clense a ohlcv dataframe by Clense a OHLCV dataframe by
* Grouping it by date (removes duplicate tics) * Grouping it by date (removes duplicate tics)
* dropping last candles if requested * dropping last candles if requested
* Filling up missing data (if requested) * Filling up missing data (if requested)
:param data: DataFrame containing ohlcv data. :param data: DataFrame containing candle (OHLCV) data.
:param timeframe: timeframe (e.g. 5m). Used to fill up eventual missing data :param timeframe: timeframe (e.g. 5m). Used to fill up eventual missing data
:param pair: Pair this data is for (used to warn if fillup was necessary) :param pair: Pair this data is for (used to warn if fillup was necessary)
:param fill_missing: fill up missing candles with 0 candles :param fill_missing: fill up missing candles with 0 candles
@ -88,16 +85,16 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
""" """
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
ohlc_dict = { ohlcv_dict = {
'open': 'first', 'open': 'first',
'high': 'max', 'high': 'max',
'low': 'min', 'low': 'min',
'close': 'last', 'close': 'last',
'volume': 'sum' 'volume': 'sum'
} }
ticker_minutes = timeframe_to_minutes(timeframe) timeframe_minutes = timeframe_to_minutes(timeframe)
# Resample to create "NAN" values # Resample to create "NAN" values
df = dataframe.resample(f'{ticker_minutes}min', on='date').agg(ohlc_dict) df = dataframe.resample(f'{timeframe_minutes}min', on='date').agg(ohlcv_dict)
# Forwardfill close for missing columns # Forwardfill close for missing columns
df['close'] = df['close'].fillna(method='ffill') df['close'] = df['close'].fillna(method='ffill')
@ -159,20 +156,20 @@ def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
def trades_to_ohlcv(trades: list, timeframe: str) -> DataFrame: def trades_to_ohlcv(trades: list, timeframe: str) -> DataFrame:
""" """
Converts trades list to ohlcv list Converts trades list to OHLCV list
TODO: This should get a dedicated test TODO: This should get a dedicated test
:param trades: List of trades, as returned by ccxt.fetch_trades. :param trades: List of trades, as returned by ccxt.fetch_trades.
:param timeframe: Ticker timeframe to resample data to :param timeframe: Timeframe to resample data to
:return: ohlcv Dataframe. :return: OHLCV Dataframe.
""" """
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
ticker_minutes = timeframe_to_minutes(timeframe) timeframe_minutes = timeframe_to_minutes(timeframe)
df = pd.DataFrame(trades) df = pd.DataFrame(trades)
df['datetime'] = pd.to_datetime(df['datetime']) df['datetime'] = pd.to_datetime(df['datetime'])
df = df.set_index('datetime') df = df.set_index('datetime')
df_new = df['price'].resample(f'{ticker_minutes}min').ohlc() df_new = df['price'].resample(f'{timeframe_minutes}min').ohlc()
df_new['volume'] = df['amount'].resample(f'{ticker_minutes}min').sum() df_new['volume'] = df['amount'].resample(f'{timeframe_minutes}min').sum()
df_new['date'] = df_new.index df_new['date'] = df_new.index
# Drop 0 volume rows # Drop 0 volume rows
df_new = df_new.dropna() df_new = df_new.dropna()
@ -206,7 +203,7 @@ def convert_trades_format(config: Dict[str, Any], convert_from: str, convert_to:
def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to: str, erase: bool): def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to: str, erase: bool):
""" """
Convert ohlcv from one format to another format. Convert OHLCV from one format to another
:param config: Config dictionary :param config: Config dictionary
:param convert_from: Source format :param convert_from: Source format
:param convert_to: Target format :param convert_to: Target format
@ -216,7 +213,7 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to:
src = get_datahandler(config['datadir'], convert_from) src = get_datahandler(config['datadir'], convert_from)
trg = get_datahandler(config['datadir'], convert_to) trg = get_datahandler(config['datadir'], convert_to)
timeframes = config.get('timeframes', [config.get('ticker_interval')]) timeframes = config.get('timeframes', [config.get('ticker_interval')])
logger.info(f"Converting OHLCV for timeframe {timeframes}") logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}")
if 'pairs' not in config: if 'pairs' not in config:
config['pairs'] = [] config['pairs'] = []
@ -224,7 +221,7 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to:
for timeframe in timeframes: for timeframe in timeframes:
config['pairs'].extend(src.ohlcv_get_pairs(config['datadir'], config['pairs'].extend(src.ohlcv_get_pairs(config['datadir'],
timeframe)) timeframe))
logger.info(f"Converting OHLCV for {config['pairs']}") logger.info(f"Converting candle (OHLCV) data for {config['pairs']}")
for timeframe in timeframes: for timeframe in timeframes:
for pair in config['pairs']: for pair in config['pairs']:

View File

@ -1,7 +1,7 @@
""" """
Dataprovider Dataprovider
Responsible to provide data to the bot Responsible to provide data to the bot
including Klines, tickers, historic data including ticker and orderbook data, live and historical candle (OHLCV) data
Common Interface for bot and strategy to access data. Common Interface for bot and strategy to access data.
""" """
import logging import logging
@ -43,10 +43,10 @@ class DataProvider:
def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame: def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame:
""" """
Get ohlcv data for the given pair as DataFrame Get candle (OHLCV) data for the given pair as DataFrame
Please use the `available_pairs` method to verify which pairs are currently cached. Please use the `available_pairs` method to verify which pairs are currently cached.
:param pair: pair to get the data for :param pair: pair to get the data for
:param timeframe: Ticker timeframe to get data for :param timeframe: Timeframe to get data for
:param copy: copy dataframe before returning if True. :param copy: copy dataframe before returning if True.
Use False only for read-only operations (where the dataframe is not modified) Use False only for read-only operations (where the dataframe is not modified)
""" """
@ -58,7 +58,7 @@ class DataProvider:
def historic_ohlcv(self, pair: str, timeframe: str = None) -> DataFrame: def historic_ohlcv(self, pair: str, timeframe: str = None) -> DataFrame:
""" """
Get stored historic ohlcv data Get stored historical candle (OHLCV) data
:param pair: pair to get the data for :param pair: pair to get the data for
:param timeframe: timeframe to get data for :param timeframe: timeframe to get data for
""" """
@ -69,17 +69,17 @@ class DataProvider:
def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame: def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame:
""" """
Return pair ohlcv data, either live or cached historical -- depending Return pair candle (OHLCV) data, either live or cached historical -- depending
on the runmode. on the runmode.
:param pair: pair to get the data for :param pair: pair to get the data for
:param timeframe: timeframe to get data for :param timeframe: timeframe to get data for
:return: Dataframe for this pair :return: Dataframe for this pair
""" """
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
# Get live ohlcv data. # Get live OHLCV data.
data = self.ohlcv(pair=pair, timeframe=timeframe) data = self.ohlcv(pair=pair, timeframe=timeframe)
else: else:
# Get historic ohlcv data (cached on disk). # Get historical OHLCV data (cached on disk).
data = self.historic_ohlcv(pair=pair, timeframe=timeframe) data = self.historic_ohlcv(pair=pair, timeframe=timeframe)
if len(data) == 0: if len(data) == 0:
logger.warning(f"No data found for ({pair}, {timeframe}).") logger.warning(f"No data found for ({pair}, {timeframe}).")

View File

@ -9,7 +9,7 @@ from pandas import DataFrame
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
from freqtrade.data.converter import parse_ticker_dataframe, trades_to_ohlcv from freqtrade.data.converter import ohlcv_to_dataframe, trades_to_ohlcv
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -28,10 +28,10 @@ def load_pair_history(pair: str,
data_handler: IDataHandler = None, data_handler: IDataHandler = None,
) -> DataFrame: ) -> DataFrame:
""" """
Load cached ticker history for the given pair. Load cached ohlcv history for the given pair.
:param pair: Pair to load data for :param pair: Pair to load data for
:param timeframe: Ticker timeframe (e.g. "5m") :param timeframe: Timeframe (e.g. "5m")
:param datadir: Path to the data storage location. :param datadir: Path to the data storage location.
:param data_format: Format of the data. Ignored if data_handler is set. :param data_format: Format of the data. Ignored if data_handler is set.
:param timerange: Limit data to be loaded to this timerange :param timerange: Limit data to be loaded to this timerange
@ -63,10 +63,10 @@ def load_data(datadir: Path,
data_format: str = 'json', data_format: str = 'json',
) -> Dict[str, DataFrame]: ) -> Dict[str, DataFrame]:
""" """
Load ticker history data for a list of pairs. Load ohlcv history data for a list of pairs.
:param datadir: Path to the data storage location. :param datadir: Path to the data storage location.
:param timeframe: Ticker Timeframe (e.g. "5m") :param timeframe: Timeframe (e.g. "5m")
:param pairs: List of pairs to load :param pairs: List of pairs to load
:param timerange: Limit data to be loaded to this timerange :param timerange: Limit data to be loaded to this timerange
:param fill_up_missing: Fill missing values with "No action"-candles :param fill_up_missing: Fill missing values with "No action"-candles
@ -104,10 +104,10 @@ def refresh_data(datadir: Path,
timerange: Optional[TimeRange] = None, timerange: Optional[TimeRange] = None,
) -> None: ) -> None:
""" """
Refresh ticker history data for a list of pairs. Refresh ohlcv history data for a list of pairs.
:param datadir: Path to the data storage location. :param datadir: Path to the data storage location.
:param timeframe: Ticker Timeframe (e.g. "5m") :param timeframe: Timeframe (e.g. "5m")
:param pairs: List of pairs to load :param pairs: List of pairs to load
:param exchange: Exchange object :param exchange: Exchange object
:param timerange: Limit data to be loaded to this timerange :param timerange: Limit data to be loaded to this timerange
@ -165,7 +165,7 @@ def _download_pair_history(datadir: Path,
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
:param pair: pair to download :param pair: pair to download
:param timeframe: Ticker Timeframe (e.g 5m) :param timeframe: Timeframe (e.g "5m")
:param timerange: range of time to download :param timerange: range of time to download
:return: bool with success state :return: bool with success state
""" """
@ -194,8 +194,8 @@ def _download_pair_history(datadir: Path,
days=-30).float_timestamp) * 1000 days=-30).float_timestamp) * 1000
) )
# TODO: Maybe move parsing to exchange class (?) # TODO: Maybe move parsing to exchange class (?)
new_dataframe = parse_ticker_dataframe(new_data, timeframe, pair, new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair,
fill_missing=False, drop_incomplete=True) fill_missing=False, drop_incomplete=True)
if data.empty: if data.empty:
data = new_dataframe data = new_dataframe
else: else:
@ -362,7 +362,7 @@ def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime,
:param pair: pair used for log output. :param pair: pair used for log output.
:param min_date: start-date of the data :param min_date: start-date of the data
:param max_date: end-date of the data :param max_date: end-date of the data
:param timeframe_min: ticker Timeframe in minutes :param timeframe_min: Timeframe in minutes
""" """
# total difference in minutes / timeframe-minutes # total difference in minutes / timeframe-minutes
expected_frames = int((max_date - min_date).total_seconds() // 60 // timeframe_min) expected_frames = int((max_date - min_date).total_seconds() // 60 // timeframe_min)

View File

@ -55,7 +55,7 @@ class IDataHandler(ABC):
Implements the loading and conversion to a Pandas dataframe. Implements the loading and conversion to a Pandas dataframe.
Timerange trimming and dataframe validation happens outside of this method. Timerange trimming and dataframe validation happens outside of this method.
:param pair: Pair to load data :param pair: Pair to load data
:param timeframe: Ticker timeframe (e.g. "5m") :param timeframe: Timeframe (e.g. "5m")
:param timerange: Limit data to be loaded to this timerange. :param timerange: Limit data to be loaded to this timerange.
Optionally implemented by subclasses to avoid loading Optionally implemented by subclasses to avoid loading
all data where possible. all data where possible.
@ -67,7 +67,7 @@ class IDataHandler(ABC):
""" """
Remove data for this pair Remove data for this pair
:param pair: Delete data for this pair. :param pair: Delete data for this pair.
:param timeframe: Ticker timeframe (e.g. "5m") :param timeframe: Timeframe (e.g. "5m")
:return: True when deleted, false if file did not exist. :return: True when deleted, false if file did not exist.
""" """
@ -129,10 +129,10 @@ class IDataHandler(ABC):
warn_no_data: bool = True warn_no_data: bool = True
) -> DataFrame: ) -> DataFrame:
""" """
Load cached ticker history for the given pair. Load cached candle (OHLCV) data for the given pair.
:param pair: Pair to load data for :param pair: Pair to load data for
:param timeframe: Ticker timeframe (e.g. "5m") :param timeframe: Timeframe (e.g. "5m")
:param timerange: Limit data to be loaded to this timerange :param timerange: Limit data to be loaded to this timerange
:param fill_missing: Fill missing values with "No action"-candles :param fill_missing: Fill missing values with "No action"-candles
:param drop_incomplete: Drop last candle assuming it may be incomplete. :param drop_incomplete: Drop last candle assuming it may be incomplete.
@ -147,12 +147,7 @@ class IDataHandler(ABC):
pairdf = self._ohlcv_load(pair, timeframe, pairdf = self._ohlcv_load(pair, timeframe,
timerange=timerange_startup) timerange=timerange_startup)
if pairdf.empty: if self._check_empty_df(pairdf, pair, timeframe, warn_no_data):
if warn_no_data:
logger.warning(
f'No history data for pair: "{pair}", timeframe: {timeframe}. '
'Use `freqtrade download-data` to download the data'
)
return pairdf return pairdf
else: else:
enddate = pairdf.iloc[-1]['date'] enddate = pairdf.iloc[-1]['date']
@ -160,13 +155,30 @@ class IDataHandler(ABC):
if timerange_startup: if timerange_startup:
self._validate_pairdata(pair, pairdf, timerange_startup) self._validate_pairdata(pair, pairdf, timerange_startup)
pairdf = trim_dataframe(pairdf, timerange_startup) pairdf = trim_dataframe(pairdf, timerange_startup)
if self._check_empty_df(pairdf, pair, timeframe, warn_no_data):
return pairdf
# incomplete candles should only be dropped if we didn't trim the end beforehand. # incomplete candles should only be dropped if we didn't trim the end beforehand.
return clean_ohlcv_dataframe(pairdf, timeframe, pairdf = clean_ohlcv_dataframe(pairdf, timeframe,
pair=pair, pair=pair,
fill_missing=fill_missing, fill_missing=fill_missing,
drop_incomplete=(drop_incomplete and drop_incomplete=(drop_incomplete and
enddate == pairdf.iloc[-1]['date'])) enddate == pairdf.iloc[-1]['date']))
self._check_empty_df(pairdf, pair, timeframe, warn_no_data)
return pairdf
def _check_empty_df(self, pairdf: DataFrame, pair: str, timeframe: str, warn_no_data: bool):
"""
Warn on empty dataframe
"""
if pairdf.empty:
if warn_no_data:
logger.warning(
f'No history data for pair: "{pair}", timeframe: {timeframe}. '
'Use `freqtrade download-data` to download the data'
)
return True
return False
def _validate_pairdata(self, pair, pairdata: DataFrame, timerange: TimeRange): def _validate_pairdata(self, pair, pairdata: DataFrame, timerange: TimeRange):
""" """

View File

@ -60,7 +60,7 @@ class JsonDataHandler(IDataHandler):
Implements the loading and conversion to a Pandas dataframe. Implements the loading and conversion to a Pandas dataframe.
Timerange trimming and dataframe validation happens outside of this method. Timerange trimming and dataframe validation happens outside of this method.
:param pair: Pair to load data :param pair: Pair to load data
:param timeframe: Ticker timeframe (e.g. "5m") :param timeframe: Timeframe (e.g. "5m")
:param timerange: Limit data to be loaded to this timerange. :param timerange: Limit data to be loaded to this timerange.
Optionally implemented by subclasses to avoid loading Optionally implemented by subclasses to avoid loading
all data where possible. all data where possible.
@ -83,7 +83,7 @@ class JsonDataHandler(IDataHandler):
""" """
Remove data for this pair Remove data for this pair
:param pair: Delete data for this pair. :param pair: Delete data for this pair.
:param timeframe: Ticker timeframe (e.g. "5m") :param timeframe: Timeframe (e.g. "5m")
:return: True when deleted, false if file did not exist. :return: True when deleted, false if file did not exist.
""" """
filename = self._pair_data_filename(self._datadir, pair, timeframe) filename = self._pair_data_filename(self._datadir, pair, timeframe)

View File

@ -8,10 +8,10 @@ import numpy as np
import utils_find_1st as utf1st import utils_find_1st as utf1st
from pandas import DataFrame from pandas import DataFrame
from freqtrade import constants
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data import history from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.data.history import get_timerange, load_data, refresh_data
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -54,7 +54,7 @@ class Edge:
if self.config['max_open_trades'] != float('inf'): if self.config['max_open_trades'] != float('inf'):
logger.critical('max_open_trades should be -1 in config !') logger.critical('max_open_trades should be -1 in config !')
if self.config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT: if self.config['stake_amount'] != UNLIMITED_STAKE_AMOUNT:
raise OperationalException('Edge works only with unlimited stake amount') raise OperationalException('Edge works only with unlimited stake amount')
# Deprecated capital_available_percentage. Will use tradable_balance_ratio in the future. # Deprecated capital_available_percentage. Will use tradable_balance_ratio in the future.
@ -96,7 +96,7 @@ class Edge:
logger.info('Using local backtesting data (using whitelist in given config) ...') logger.info('Using local backtesting data (using whitelist in given config) ...')
if self._refresh_pairs: if self._refresh_pairs:
history.refresh_data( refresh_data(
datadir=self.config['datadir'], datadir=self.config['datadir'],
pairs=pairs, pairs=pairs,
exchange=self.exchange, exchange=self.exchange,
@ -104,7 +104,7 @@ class Edge:
timerange=self._timerange, timerange=self._timerange,
) )
data = history.load_data( data = load_data(
datadir=self.config['datadir'], datadir=self.config['datadir'],
pairs=pairs, pairs=pairs,
timeframe=self.strategy.ticker_interval, timeframe=self.strategy.ticker_interval,
@ -119,10 +119,10 @@ class Edge:
logger.critical("No data found. Edge is stopped ...") logger.critical("No data found. Edge is stopped ...")
return False return False
preprocessed = self.strategy.tickerdata_to_dataframe(data) preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
# Print timeframe # Print timeframe
min_date, max_date = history.get_timerange(preprocessed) min_date, max_date = get_timerange(preprocessed)
logger.info( logger.info(
'Measuring data from %s up to %s (%s days) ...', 'Measuring data from %s up to %s (%s days) ...',
min_date.isoformat(), min_date.isoformat(),
@ -137,10 +137,10 @@ class Edge:
pair_data = pair_data.sort_values(by=['date']) pair_data = pair_data.sort_values(by=['date'])
pair_data = pair_data.reset_index(drop=True) pair_data = pair_data.reset_index(drop=True)
ticker_data = self.strategy.advise_sell( df_analyzed = self.strategy.advise_sell(
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
trades += self._find_trades_for_stoploss_range(ticker_data, pair, self._stoploss_range) trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range)
# If no trade found then exit # If no trade found then exit
if len(trades) == 0: if len(trades) == 0:
@ -317,7 +317,7 @@ class Edge:
} }
# Group by (pair and stoploss) by applying above aggregator # Group by (pair and stoploss) by applying above aggregator
df = results.groupby(['pair', 'stoploss'])['profit_abs', 'trade_duration'].agg( df = results.groupby(['pair', 'stoploss'])[['profit_abs', 'trade_duration']].agg(
groupby_aggregator).reset_index(col_level=1) groupby_aggregator).reset_index(col_level=1)
# Dropping level 0 as we don't need it # Dropping level 0 as we don't need it
@ -359,11 +359,11 @@ class Edge:
# Returning a list of pairs in order of "expectancy" # Returning a list of pairs in order of "expectancy"
return final return final
def _find_trades_for_stoploss_range(self, ticker_data, pair, stoploss_range): def _find_trades_for_stoploss_range(self, df, pair, stoploss_range):
buy_column = ticker_data['buy'].values buy_column = df['buy'].values
sell_column = ticker_data['sell'].values sell_column = df['sell'].values
date_column = ticker_data['date'].values date_column = df['date'].values
ohlc_columns = ticker_data[['open', 'high', 'low', 'close']].values ohlc_columns = df[['open', 'high', 'low', 'close']].values
result: list = [] result: list = []
for stoploss in stoploss_range: for stoploss in stoploss_range:

View File

@ -18,7 +18,7 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE,
TRUNCATE, decimal_to_precision) TRUNCATE, decimal_to_precision)
from pandas import DataFrame from pandas import DataFrame
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.exceptions import (DependencyException, InvalidOrderException, from freqtrade.exceptions import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
@ -351,7 +351,7 @@ class Exchange:
def validate_timeframes(self, timeframe: Optional[str]) -> None: def validate_timeframes(self, timeframe: Optional[str]) -> None:
""" """
Checks if ticker interval from config is a supported timeframe on the exchange Check if timeframe from config is a supported timeframe on the exchange
""" """
if not hasattr(self._api, "timeframes") or self._api.timeframes is None: if not hasattr(self._api, "timeframes") or self._api.timeframes is None:
# If timeframes attribute is missing (or is None), the exchange probably # If timeframes attribute is missing (or is None), the exchange probably
@ -364,7 +364,7 @@ class Exchange:
if timeframe and (timeframe not in self.timeframes): if timeframe and (timeframe not in self.timeframes):
raise OperationalException( raise OperationalException(
f"Invalid ticker interval '{timeframe}'. This exchange supports: {self.timeframes}") f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}")
if timeframe and timeframe_to_minutes(timeframe) < 1: if timeframe and timeframe_to_minutes(timeframe) < 1:
raise OperationalException( raise OperationalException(
@ -599,7 +599,7 @@ class Exchange:
return self._api.fetch_tickers() return self._api.fetch_tickers()
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
f'Exchange {self._api.name} does not support fetching tickers in batch.' f'Exchange {self._api.name} does not support fetching tickers in batch. '
f'Message: {e}') from e f'Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
@ -623,13 +623,13 @@ class Exchange:
def get_historic_ohlcv(self, pair: str, timeframe: str, def get_historic_ohlcv(self, pair: str, timeframe: str,
since_ms: int) -> List: since_ms: int) -> List:
""" """
Gets candle history using asyncio and returns the list of candles. Get candle history using asyncio and returns the list of candles.
Handles all async doing. Handles all async work for this.
Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call. Async over one pair, assuming we get `self._ohlcv_candle_limit` candles per call.
:param pair: Pair to download :param pair: Pair to download
:param timeframe: Ticker Timeframe to get :param timeframe: Timeframe to get data for
:param since_ms: Timestamp in milliseconds to get history from :param since_ms: Timestamp in milliseconds to get history from
:returns List of tickers :returns List with candle (OHLCV) data
""" """
return asyncio.get_event_loop().run_until_complete( return asyncio.get_event_loop().run_until_complete(
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe, self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
@ -649,26 +649,27 @@ class Exchange:
pair, timeframe, since) for since in pair, timeframe, since) for since in
range(since_ms, arrow.utcnow().timestamp * 1000, one_call)] range(since_ms, arrow.utcnow().timestamp * 1000, one_call)]
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True) results = await asyncio.gather(*input_coroutines, return_exceptions=True)
# Combine tickers # Combine gathered results
data: List = [] data: List = []
for p, timeframe, ticker in tickers: for p, timeframe, res in results:
if p == pair: if p == pair:
data.extend(ticker) data.extend(res)
# Sort data again after extending the result - above calls return in "async order" # Sort data again after extending the result - above calls return in "async order"
data = sorted(data, key=lambda x: x[0]) data = sorted(data, key=lambda x: x[0])
logger.info("downloaded %s with length %s.", pair, len(data)) logger.info("Downloaded data for %s with length %s.", pair, len(data))
return data return data
def refresh_latest_ohlcv(self, pair_list: List[Tuple[str, str]]) -> List[Tuple[str, List]]: def refresh_latest_ohlcv(self, pair_list: List[Tuple[str, str]]) -> List[Tuple[str, List]]:
""" """
Refresh in-memory ohlcv asynchronously and set `_klines` with the result Refresh in-memory OHLCV asynchronously and set `_klines` with the result
Loops asynchronously over pair_list and downloads all pairs async (semi-parallel). Loops asynchronously over pair_list and downloads all pairs async (semi-parallel).
Only used in the dataprovider.refresh() method.
:param pair_list: List of 2 element tuples containing pair, interval to refresh :param pair_list: List of 2 element tuples containing pair, interval to refresh
:return: Returns a List of ticker-dataframes. :return: TODO: return value is only used in the tests, get rid of it
""" """
logger.debug("Refreshing ohlcv data for %d pairs", len(pair_list)) logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
input_coroutines = [] input_coroutines = []
@ -679,15 +680,15 @@ class Exchange:
input_coroutines.append(self._async_get_candle_history(pair, timeframe)) input_coroutines.append(self._async_get_candle_history(pair, timeframe))
else: else:
logger.debug( logger.debug(
"Using cached ohlcv data for pair %s, timeframe %s ...", "Using cached candle (OHLCV) data for pair %s, timeframe %s ...",
pair, timeframe pair, timeframe
) )
tickers = asyncio.get_event_loop().run_until_complete( results = asyncio.get_event_loop().run_until_complete(
asyncio.gather(*input_coroutines, return_exceptions=True)) asyncio.gather(*input_coroutines, return_exceptions=True))
# handle caching # handle caching
for res in tickers: for res in results:
if isinstance(res, Exception): if isinstance(res, Exception):
logger.warning("Async code raised an exception: %s", res.__class__.__name__) logger.warning("Async code raised an exception: %s", res.__class__.__name__)
continue continue
@ -698,13 +699,14 @@ class Exchange:
if ticks: if ticks:
self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000 self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
# keeping parsed dataframe in cache # keeping parsed dataframe in cache
self._klines[(pair, timeframe)] = parse_ticker_dataframe( self._klines[(pair, timeframe)] = ohlcv_to_dataframe(
ticks, timeframe, pair=pair, fill_missing=True, ticks, timeframe, pair=pair, fill_missing=True,
drop_incomplete=self._ohlcv_partial_candle) drop_incomplete=self._ohlcv_partial_candle)
return tickers
return results
def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool: def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool:
# Calculating ticker interval in seconds # Timeframe in seconds
interval_in_sec = timeframe_to_seconds(timeframe) interval_in_sec = timeframe_to_seconds(timeframe)
return not ((self._pairs_last_refresh_time.get((pair, timeframe), 0) return not ((self._pairs_last_refresh_time.get((pair, timeframe), 0)
@ -714,11 +716,11 @@ class Exchange:
async def _async_get_candle_history(self, pair: str, timeframe: str, async def _async_get_candle_history(self, pair: str, timeframe: str,
since_ms: Optional[int] = None) -> Tuple[str, str, List]: since_ms: Optional[int] = None) -> Tuple[str, str, List]:
""" """
Asynchronously gets candle histories using fetch_ohlcv Asynchronously get candle history data using fetch_ohlcv
returns tuple: (pair, timeframe, ohlcv_list) returns tuple: (pair, timeframe, ohlcv_list)
""" """
try: try:
# fetch ohlcv asynchronously # Fetch OHLCV asynchronously
s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else '' s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else ''
logger.debug( logger.debug(
"Fetching pair %s, interval %s, since %s %s...", "Fetching pair %s, interval %s, since %s %s...",
@ -728,9 +730,9 @@ class Exchange:
data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe,
since=since_ms) since=since_ms)
# Because some exchange sort Tickers ASC and other DESC. # Some exchanges sort OHLCV in ASC order and others in DESC.
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last) # Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
# when GDAX returns a list of tickers DESC (newest first, oldest last) # while GDAX returns the list of OHLCV in DESC order (newest first, oldest last)
# Only sort if necessary to save computing time # Only sort if necessary to save computing time
try: try:
if data and data[0][0] > data[-1][0]: if data and data[0][0] > data[-1][0]:
@ -743,14 +745,15 @@ class Exchange:
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
f'Exchange {self._api.name} does not support fetching historical candlestick data.' f'Exchange {self._api.name} does not support fetching historical '
f'Message: {e}') from e f'candle (OHLCV) data. Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(f'Could not load ticker history for pair {pair} due to ' raise TemporaryError(f'Could not fetch historical candle (OHLCV) data '
f'{e.__class__.__name__}. Message: {e}') from e f'for pair {pair} due to {e.__class__.__name__}. '
f'Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(f'Could not fetch ticker data for pair {pair}. ' raise OperationalException(f'Could not fetch historical candle (OHLCV) data '
f'Msg: {e}') from e f'for pair {pair}. Message: {e}') from e
@retrier_async @retrier_async
async def _async_fetch_trades(self, pair: str, async def _async_fetch_trades(self, pair: str,
@ -883,14 +886,14 @@ class Exchange:
until: Optional[int] = None, until: Optional[int] = None,
from_id: Optional[str] = None) -> Tuple[str, List]: from_id: Optional[str] = None) -> Tuple[str, List]:
""" """
Gets candle history using asyncio and returns the list of candles. Get trade history data using asyncio.
Handles all async doing. Handles all async work and returns the list of candles.
Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call. Async over one pair, assuming we get `self._ohlcv_candle_limit` candles per call.
:param pair: Pair to download :param pair: Pair to download
:param since: Timestamp in milliseconds to get history from :param since: Timestamp in milliseconds to get history from
:param until: Timestamp in milliseconds. Defaults to current timestamp if not defined. :param until: Timestamp in milliseconds. Defaults to current timestamp if not defined.
:param from_id: Download data starting with ID (if id is known) :param from_id: Download data starting with ID (if id is known)
:returns List of tickers :returns List of trade data
""" """
if not self.exchange_has("fetchTrades"): if not self.exchange_has("fetchTrades"):
raise OperationalException("This exchange does not suport downloading Trades.") raise OperationalException("This exchange does not suport downloading Trades.")

View File

@ -172,8 +172,8 @@ class FreqtradeBot:
_whitelist = self.edge.adjust(_whitelist) _whitelist = self.edge.adjust(_whitelist)
if trades: if trades:
# Extend active-pair whitelist with pairs from open trades # Extend active-pair whitelist with pairs of open trades
# It ensures that tickers are downloaded for open trades # It ensures that candle (OHLCV) data are downloaded for open trades as well
_whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist]) _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist])
return _whitelist return _whitelist
@ -394,16 +394,18 @@ class FreqtradeBot:
logger.info(f"Pair {pair} is currently locked.") logger.info(f"Pair {pair} is currently locked.")
return False return False
# get_free_open_trades is checked before create_trade is called
# but it is still used here to prevent opening too many trades within one iteration
if not self.get_free_open_trades():
logger.debug(f"Can't open a new trade for {pair}: max number of trades is reached.")
return False
# running get_signal on historical data fetched # running get_signal on historical data fetched
(buy, sell) = self.strategy.get_signal( (buy, sell) = self.strategy.get_signal(
pair, self.strategy.ticker_interval, pair, self.strategy.ticker_interval,
self.dataprovider.ohlcv(pair, self.strategy.ticker_interval)) self.dataprovider.ohlcv(pair, self.strategy.ticker_interval))
if buy and not sell: if buy and not sell:
if not self.get_free_open_trades():
logger.debug("Can't open a new trade: max number of trades is reached.")
return False
stake_amount = self.get_trade_stake_amount(pair) stake_amount = self.get_trade_stake_amount(pair)
if not stake_amount: if not stake_amount:
logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.")
@ -628,7 +630,7 @@ class FreqtradeBot:
def get_sell_rate(self, pair: str, refresh: bool) -> float: def get_sell_rate(self, pair: str, refresh: bool) -> float:
""" """
Get sell rate - either using get-ticker bid or first bid based on orderbook Get sell rate - either using ticker bid or first bid based on orderbook
The orderbook portion is only used for rpc messaging, which would otherwise fail The orderbook portion is only used for rpc messaging, which would otherwise fail
for BitMex (has no bid/ask in fetch_ticker) for BitMex (has no bid/ask in fetch_ticker)
or remain static in any other case since it's not updating. or remain static in any other case since it's not updating.
@ -891,6 +893,9 @@ class FreqtradeBot:
if order['status'] != 'canceled': if order['status'] != 'canceled':
reason = "cancelled due to timeout" reason = "cancelled due to timeout"
corder = self.exchange.cancel_order(trade.open_order_id, trade.pair) corder = self.exchange.cancel_order(trade.open_order_id, trade.pair)
# Some exchanges don't return a dict here.
if not isinstance(corder, dict):
corder = {}
logger.info('Buy order %s for %s.', reason, trade) logger.info('Buy order %s for %s.', reason, trade)
else: else:
# Order was cancelled already, so we can reuse the existing dict # Order was cancelled already, so we can reuse the existing dict
@ -949,6 +954,7 @@ class FreqtradeBot:
trade.close_rate = None trade.close_rate = None
trade.close_profit = None trade.close_profit = None
trade.close_profit_abs = None
trade.close_date = None trade.close_date = None
trade.is_open = True trade.is_open = True
trade.open_order_id = None trade.open_order_id = None
@ -1043,7 +1049,7 @@ class FreqtradeBot:
""" """
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_trade = trade.calc_profit(rate=profit_rate) profit_trade = trade.calc_profit(rate=profit_rate)
# Use cached ticker here - it was updated seconds ago. # Use cached rates here - it was updated seconds ago.
current_rate = self.get_sell_rate(trade.pair, False) current_rate = self.get_sell_rate(trade.pair, False)
profit_ratio = trade.calc_profit_ratio(profit_rate) profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss" gain = "profit" if profit_ratio > 0 else "loss"

View File

@ -81,13 +81,13 @@ def file_load_json(file):
gzipfile = file gzipfile = file
# Try gzip file first, otherwise regular json file. # Try gzip file first, otherwise regular json file.
if gzipfile.is_file(): if gzipfile.is_file():
logger.debug('Loading ticker data from file %s', gzipfile) logger.debug(f"Loading historical data from file {gzipfile}")
with gzip.open(gzipfile) as tickerdata: with gzip.open(gzipfile) as datafile:
pairdata = json_load(tickerdata) pairdata = json_load(datafile)
elif file.is_file(): elif file.is_file():
logger.debug('Loading ticker data from file %s', file) logger.debug(f"Loading historical data from file {file}")
with open(file) as tickerdata: with open(file) as datafile:
pairdata = json_load(tickerdata) pairdata = json_load(datafile)
else: else:
return None return None
return pairdata return pairdata

View File

@ -6,8 +6,7 @@ This module contains the backtesting logic
import logging import logging
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from typing import Any, Dict, List, NamedTuple, Optional, Tuple
from typing import Any, Dict, List, NamedTuple, Optional
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
@ -19,10 +18,8 @@ from freqtrade.data.converter import trim_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.misc import file_dump_json from freqtrade.optimize.optimize_reports import (show_backtest_results,
from freqtrade.optimize.optimize_reports import ( store_backtest_result)
generate_text_table, generate_text_table_sell_reason,
generate_text_table_strategy)
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.state import RunMode from freqtrade.state import RunMode
@ -88,8 +85,8 @@ class Backtesting:
validate_config_consistency(self.config) validate_config_consistency(self.config)
if "ticker_interval" not in self.config: if "ticker_interval" not in self.config:
raise OperationalException("Ticker-interval needs to be set in either configuration " raise OperationalException("Timeframe (ticker interval) needs to be set in either "
"or as cli argument `--ticker-interval 5m`") "configuration or as cli argument `--ticker-interval 5m`")
self.timeframe = str(self.config.get('ticker_interval')) self.timeframe = str(self.config.get('ticker_interval'))
self.timeframe_min = timeframe_to_minutes(self.timeframe) self.timeframe_min = timeframe_to_minutes(self.timeframe)
@ -108,7 +105,7 @@ class Backtesting:
# And the regular "stoploss" function would not apply to that case # And the regular "stoploss" function would not apply to that case
self.strategy.order_types['stoploss_on_exchange'] = False self.strategy.order_types['stoploss_on_exchange'] = False
def load_bt_data(self): def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]:
timerange = TimeRange.parse_timerange(None if self.config.get( timerange = TimeRange.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange'))) 'timerange') is None else str(self.config.get('timerange')))
@ -134,49 +131,33 @@ class Backtesting:
return data, timerange return data, timerange
def _store_backtest_result(self, recordfilename: Path, results: DataFrame, def _get_ohlcv_as_lists(self, processed: Dict) -> Dict[str, DataFrame]:
strategyname: Optional[str] = None) -> None:
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value)
for index, t in results.iterrows()]
if records:
if strategyname:
# Inject strategyname to filename
recordfilename = Path.joinpath(
recordfilename.parent,
f'{recordfilename.stem}-{strategyname}').with_suffix(recordfilename.suffix)
logger.info(f'Dumping backtest results to {recordfilename}')
file_dump_json(recordfilename, records)
def _get_ticker_list(self, processed: Dict) -> Dict[str, DataFrame]:
""" """
Helper function to convert a processed tickerlist into a list for performance reasons. Helper function to convert a processed dataframes into lists for performance reasons.
Used by backtest() - so keep this optimized for performance. Used by backtest() - so keep this optimized for performance.
""" """
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
ticker: Dict = {} data: Dict = {}
# Create ticker dict # Create dict with data
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
pair_data.loc[:, 'buy'] = 0 # cleanup from previous run pair_data.loc[:, 'buy'] = 0 # cleanup from previous run
pair_data.loc[:, 'sell'] = 0 # cleanup from previous run pair_data.loc[:, 'sell'] = 0 # cleanup from previous run
ticker_data = self.strategy.advise_sell( df_analyzed = self.strategy.advise_sell(
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
# to avoid using data from future, we buy/sell with signal from previous candle # To avoid using data from future, we use buy/sell signals shifted
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) # from the previous candle
ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1) df_analyzed.loc[:, 'buy'] = df_analyzed['buy'].shift(1)
df_analyzed.loc[:, 'sell'] = df_analyzed['sell'].shift(1)
ticker_data.drop(ticker_data.head(1).index, inplace=True) df_analyzed.drop(df_analyzed.head(1).index, inplace=True)
# Convert from Pandas to list for performance reasons # Convert from Pandas to list for performance reasons
# (Looping Pandas is slow.) # (Looping Pandas is slow.)
ticker[pair] = [x for x in ticker_data.itertuples()] data[pair] = [x for x in df_analyzed.itertuples()]
return ticker return data
def _get_close_rate(self, sell_row, trade: Trade, sell: SellCheckTuple, def _get_close_rate(self, sell_row, trade: Trade, sell: SellCheckTuple,
trade_dur: int) -> float: trade_dur: int) -> float:
@ -220,7 +201,7 @@ class Backtesting:
def _get_sell_trade_entry( def _get_sell_trade_entry(
self, pair: str, buy_row: DataFrame, self, pair: str, buy_row: DataFrame,
partial_ticker: List, trade_count_lock: Dict, partial_ohlcv: List, trade_count_lock: Dict,
stake_amount: float, max_open_trades: int) -> Optional[BacktestResult]: stake_amount: float, max_open_trades: int) -> Optional[BacktestResult]:
trade = Trade( trade = Trade(
@ -235,7 +216,7 @@ class Backtesting:
) )
logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.")
# calculate win/lose forwards from buy point # calculate win/lose forwards from buy point
for sell_row in partial_ticker: for sell_row in partial_ohlcv:
if max_open_trades > 0: if max_open_trades > 0:
# Increase trade_count_lock for every iteration # Increase trade_count_lock for every iteration
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1 trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
@ -259,9 +240,9 @@ class Backtesting:
close_rate=closerate, close_rate=closerate,
sell_reason=sell.sell_type sell_reason=sell.sell_type
) )
if partial_ticker: if partial_ohlcv:
# no sell condition found - trade stil open at end of backtest period # no sell condition found - trade stil open at end of backtest period
sell_row = partial_ticker[-1] sell_row = partial_ohlcv[-1]
bt_res = BacktestResult(pair=pair, bt_res = BacktestResult(pair=pair,
profit_percent=trade.calc_profit_ratio(rate=sell_row.open), profit_percent=trade.calc_profit_ratio(rate=sell_row.open),
profit_abs=trade.calc_profit(rate=sell_row.open), profit_abs=trade.calc_profit(rate=sell_row.open),
@ -308,8 +289,9 @@ class Backtesting:
trades = [] trades = []
trade_count_lock: Dict = {} trade_count_lock: Dict = {}
# Dict of ticker-lists for performance (looping lists is a lot faster than dataframes) # Use dict of lists with data for performance
ticker: Dict = self._get_ticker_list(processed) # (looping lists is a lot faster than pandas DataFrames)
data: Dict = self._get_ohlcv_as_lists(processed)
lock_pair_until: Dict = {} lock_pair_until: Dict = {}
# Indexes per pair, so some pairs are allowed to have a missing start. # Indexes per pair, so some pairs are allowed to have a missing start.
@ -319,12 +301,12 @@ class Backtesting:
# Loop timerange and get candle for each pair at that point in time # Loop timerange and get candle for each pair at that point in time
while tmp < end_date: while tmp < end_date:
for i, pair in enumerate(ticker): for i, pair in enumerate(data):
if pair not in indexes: if pair not in indexes:
indexes[pair] = 0 indexes[pair] = 0
try: try:
row = ticker[pair][indexes[pair]] row = data[pair][indexes[pair]]
except IndexError: except IndexError:
# missing Data for one pair at the end. # missing Data for one pair at the end.
# Warnings for this are shown during data loading # Warnings for this are shown during data loading
@ -352,7 +334,7 @@ class Backtesting:
# since indexes has been incremented before, we need to go one step back to # since indexes has been incremented before, we need to go one step back to
# also check the buying candle for sell conditions. # also check the buying candle for sell conditions.
trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]-1:], trade_entry = self._get_sell_trade_entry(pair, row, data[pair][indexes[pair]-1:],
trade_count_lock, stake_amount, trade_count_lock, stake_amount,
max_open_trades) max_open_trades)
@ -395,7 +377,7 @@ class Backtesting:
self._set_strategy(strat) self._set_strategy(strat)
# need to reprocess data every time to populate signals # need to reprocess data every time to populate signals
preprocessed = self.strategy.tickerdata_to_dataframe(data) preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
# Trim startup period from analyzed dataframe # Trim startup period from analyzed dataframe
for pair, df in preprocessed.items(): for pair, df in preprocessed.items():
@ -416,44 +398,7 @@ class Backtesting:
position_stacking=position_stacking, position_stacking=position_stacking,
) )
for strategy, results in all_results.items(): if self.config.get('export', False):
store_backtest_result(self.config['exportfilename'], all_results)
if self.config.get('export', False): # Show backtest results
self._store_backtest_result(Path(self.config['exportfilename']), results, show_backtest_results(self.config, data, all_results)
strategy if len(self.strategylist) > 1 else None)
print(f"Result for strategy {strategy}")
table = generate_text_table(data, stake_currency=self.config['stake_currency'],
max_open_trades=self.config['max_open_trades'],
results=results)
if isinstance(table, str):
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
table = generate_text_table_sell_reason(data,
stake_currency=self.config['stake_currency'],
max_open_trades=self.config['max_open_trades'],
results=results)
if isinstance(table, str):
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
print(table)
table = generate_text_table(data,
stake_currency=self.config['stake_currency'],
max_open_trades=self.config['max_open_trades'],
results=results.loc[results.open_at_end], skip_nan=True)
if isinstance(table, str):
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
if isinstance(table, str):
print('=' * len(table.splitlines()[0]))
print()
if len(all_results) > 1:
# Print Strategy summary table
table = generate_text_table_strategy(self.config['stake_currency'],
self.config['max_open_trades'],
all_results=all_results)
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
print(table)
print('=' * len(table.splitlines()[0]))
print('\nFor more details, please look at the detail tables above')

View File

@ -79,8 +79,8 @@ class Hyperopt:
self.trials_file = (self.config['user_data_dir'] / self.trials_file = (self.config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_results.pickle') 'hyperopt_results' / 'hyperopt_results.pickle')
self.tickerdata_pickle = (self.config['user_data_dir'] / self.data_pickle_file = (self.config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_tickerdata.pkl') 'hyperopt_results' / 'hyperopt_tickerdata.pkl')
self.total_epochs = config.get('epochs', 0) self.total_epochs = config.get('epochs', 0)
self.current_best_loss = 100 self.current_best_loss = 100
@ -134,7 +134,7 @@ class Hyperopt:
""" """
Remove hyperopt pickle files to restart hyperopt. Remove hyperopt pickle files to restart hyperopt.
""" """
for f in [self.tickerdata_pickle, self.trials_file]: for f in [self.data_pickle_file, self.trials_file]:
p = Path(f) p = Path(f)
if p.is_file(): if p.is_file():
logger.info(f"Removing `{p}`.") logger.info(f"Removing `{p}`.")
@ -533,7 +533,7 @@ class Hyperopt:
self.backtesting.strategy.trailing_only_offset_is_reached = \ self.backtesting.strategy.trailing_only_offset_is_reached = \
d['trailing_only_offset_is_reached'] d['trailing_only_offset_is_reached']
processed = load(self.tickerdata_pickle) processed = load(self.data_pickle_file)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
@ -660,7 +660,7 @@ class Hyperopt:
'Hyperopting with data from %s up to %s (%s days)..', 'Hyperopting with data from %s up to %s (%s days)..',
min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days
) )
dump(preprocessed, self.tickerdata_pickle) dump(preprocessed, self.data_pickle_file)
# We don't need exchange instance anymore while running hyperopt # We don't need exchange instance anymore while running hyperopt
self.backtesting.exchange = None # type: ignore self.backtesting.exchange = None # type: ignore

View File

@ -1,9 +1,37 @@
import logging
from datetime import timedelta from datetime import timedelta
from pathlib import Path
from typing import Dict from typing import Dict
from pandas import DataFrame from pandas import DataFrame
from tabulate import tabulate from tabulate import tabulate
from freqtrade.misc import file_dump_json
logger = logging.getLogger(__name__)
def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame]) -> None:
"""
Stores backtest results to file (one file per strategy)
:param recordfilename: Destination filename
:param all_results: Dict of Dataframes, one results dataframe per strategy
"""
for strategy, results in all_results.items():
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value)
for index, t in results.iterrows()]
if records:
if len(all_results) > 1:
# Inject strategy to filename
recordfilename = Path.joinpath(
recordfilename.parent,
f'{recordfilename.stem}-{strategy}').with_suffix(recordfilename.suffix)
logger.info(f'Dumping backtest results to {recordfilename}')
file_dump_json(recordfilename, records)
def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_trades: int,
results: DataFrame, skip_nan: bool = False) -> str: results: DataFrame, skip_nan: bool = False) -> str:
@ -69,12 +97,12 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
def generate_text_table_sell_reason( def generate_text_table_sell_reason(stake_currency: str, max_open_trades: int,
data: Dict[str, Dict], stake_currency: str, max_open_trades: int, results: DataFrame results: DataFrame) -> str:
) -> str:
""" """
Generate small table outlining Backtest results Generate small table outlining Backtest results
:param data: Dict of <pair: dataframe> containing data that was used during backtesting. :param stake_currency: Stakecurrency used
:param max_open_trades: Max_open_trades parameter
:param results: Dataframe containing the backtest results :param results: Dataframe containing the backtest results
:return: pretty printed table with tabulate as string :return: pretty printed table with tabulate as string
""" """
@ -173,3 +201,43 @@ def generate_edge_table(results: dict) -> str:
# Ignore type as floatfmt does allow tuples but mypy does not know that # Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(tabular_data, headers=headers, return tabulate(tabular_data, headers=headers,
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame],
all_results: Dict[str, DataFrame]):
for strategy, results in all_results.items():
print(f"Result for strategy {strategy}")
table = generate_text_table(btdata, stake_currency=config['stake_currency'],
max_open_trades=config['max_open_trades'],
results=results)
if isinstance(table, str):
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
table = generate_text_table_sell_reason(stake_currency=config['stake_currency'],
max_open_trades=config['max_open_trades'],
results=results)
if isinstance(table, str):
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
print(table)
table = generate_text_table(btdata,
stake_currency=config['stake_currency'],
max_open_trades=config['max_open_trades'],
results=results.loc[results.open_at_end], skip_nan=True)
if isinstance(table, str):
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
if isinstance(table, str):
print('=' * len(table.splitlines()[0]))
print()
if len(all_results) > 1:
# Print Strategy summary table
table = generate_text_table_strategy(config['stake_currency'],
config['max_open_trades'],
all_results=all_results)
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
print(table)
print('=' * len(table.splitlines()[0]))
print('\nFor more details, please look at the detail tables above')

View File

@ -86,7 +86,7 @@ def check_migrate(engine) -> None:
logger.debug(f'trying {table_back_name}') logger.debug(f'trying {table_back_name}')
# Check for latest column # Check for latest column
if not has_column(cols, 'open_trade_price'): if not has_column(cols, 'close_profit_abs'):
logger.info(f'Running database migration - backup available as {table_back_name}') logger.info(f'Running database migration - backup available as {table_back_name}')
fee_open = get_column_def(cols, 'fee_open', 'fee') fee_open = get_column_def(cols, 'fee_open', 'fee')
@ -106,6 +106,9 @@ def check_migrate(engine) -> None:
ticker_interval = get_column_def(cols, 'ticker_interval', 'null') ticker_interval = get_column_def(cols, 'ticker_interval', 'null')
open_trade_price = get_column_def(cols, 'open_trade_price', open_trade_price = get_column_def(cols, 'open_trade_price',
f'amount * open_rate * (1 + {fee_open})') f'amount * open_rate * (1 + {fee_open})')
close_profit_abs = get_column_def(
cols, 'close_profit_abs',
f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}")
# Schema migration necessary # Schema migration necessary
engine.execute(f"alter table trades rename to {table_back_name}") engine.execute(f"alter table trades rename to {table_back_name}")
@ -123,7 +126,7 @@ def check_migrate(engine) -> None:
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
stoploss_order_id, stoploss_last_update, stoploss_order_id, stoploss_last_update,
max_rate, min_rate, sell_reason, strategy, max_rate, min_rate, sell_reason, strategy,
ticker_interval, open_trade_price ticker_interval, open_trade_price, close_profit_abs
) )
select id, lower(exchange), select id, lower(exchange),
case case
@ -143,7 +146,7 @@ def check_migrate(engine) -> None:
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
{strategy} strategy, {ticker_interval} ticker_interval, {strategy} strategy, {ticker_interval} ticker_interval,
{open_trade_price} open_trade_price {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs
from {table_back_name} from {table_back_name}
""") """)
@ -190,6 +193,7 @@ class Trade(_DECL_BASE):
close_rate = Column(Float) close_rate = Column(Float)
close_rate_requested = Column(Float) close_rate_requested = Column(Float)
close_profit = Column(Float) close_profit = Column(Float)
close_profit_abs = Column(Float)
stake_amount = Column(Float, nullable=False) stake_amount = Column(Float, nullable=False)
amount = Column(Float) amount = Column(Float)
open_date = Column(DateTime, nullable=False, default=datetime.utcnow) open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
@ -334,6 +338,7 @@ class Trade(_DECL_BASE):
""" """
self.close_rate = Decimal(rate) self.close_rate = Decimal(rate)
self.close_profit = self.calc_profit_ratio() self.close_profit = self.calc_profit_ratio()
self.close_profit_abs = self.calc_profit()
self.close_date = datetime.utcnow() self.close_date = datetime.utcnow()
self.is_open = False self.is_open = False
self.open_order_id = None self.open_order_id = None

View File

@ -6,10 +6,11 @@ import pandas as pd
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.btanalysis import (calculate_max_drawdown, from freqtrade.data.btanalysis import (calculate_max_drawdown,
combine_tickers_with_mean, combine_dataframes_with_mean,
create_cum_profit, create_cum_profit,
extract_trades_of_period, load_trades) extract_trades_of_period, load_trades)
from freqtrade.data.converter import trim_dataframe from freqtrade.data.converter import trim_dataframe
from freqtrade.exchange import timeframe_to_prev_date
from freqtrade.data.history import load_data from freqtrade.data.history import load_data
from freqtrade.misc import pair_to_filename from freqtrade.misc import pair_to_filename
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
@ -29,7 +30,7 @@ except ImportError:
def init_plotscript(config): def init_plotscript(config):
""" """
Initialize objects needed for plotting Initialize objects needed for plotting
:return: Dict with tickers, trades and pairs :return: Dict with candle (OHLCV) data, trades and pairs
""" """
if "pairs" in config: if "pairs" in config:
@ -40,7 +41,7 @@ def init_plotscript(config):
# Set timerange to use # Set timerange to use
timerange = TimeRange.parse_timerange(config.get("timerange")) timerange = TimeRange.parse_timerange(config.get("timerange"))
tickers = load_data( data = load_data(
datadir=config.get("datadir"), datadir=config.get("datadir"),
pairs=pairs, pairs=pairs,
timeframe=config.get('ticker_interval', '5m'), timeframe=config.get('ticker_interval', '5m'),
@ -48,12 +49,22 @@ def init_plotscript(config):
data_format=config.get('dataformat_ohlcv', 'json'), data_format=config.get('dataformat_ohlcv', 'json'),
) )
trades = load_trades(config['trade_source'], no_trades = False
db_url=config.get('db_url'), if config.get('no_trades', False):
exportfilename=config.get('exportfilename'), no_trades = True
) elif not config['exportfilename'].is_file() and config['trade_source'] == 'file':
logger.warning("Backtest file is missing skipping trades.")
no_trades = True
trades = load_trades(
config['trade_source'],
db_url=config.get('db_url'),
exportfilename=config.get('exportfilename'),
no_trades=no_trades
)
trades = trim_dataframe(trades, timerange, 'open_time') trades = trim_dataframe(trades, timerange, 'open_time')
return {"tickers": tickers,
return {"ohlcv": data,
"trades": trades, "trades": trades,
"pairs": pairs, "pairs": pairs,
} }
@ -112,7 +123,8 @@ def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_sub
return fig return fig
def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame) -> make_subplots: def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame,
timeframe: str) -> make_subplots:
""" """
Add scatter points indicating max drawdown Add scatter points indicating max drawdown
""" """
@ -122,12 +134,12 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame) -> m
drawdown = go.Scatter( drawdown = go.Scatter(
x=[highdate, lowdate], x=[highdate, lowdate],
y=[ y=[
df_comb.loc[highdate, 'cum_profit'], df_comb.loc[timeframe_to_prev_date(timeframe, highdate), 'cum_profit'],
df_comb.loc[lowdate, 'cum_profit'], df_comb.loc[timeframe_to_prev_date(timeframe, lowdate), 'cum_profit'],
], ],
mode='markers', mode='markers',
name=f"Max drawdown {max_drawdown:.2f}%", name=f"Max drawdown {max_drawdown * 100:.2f}%",
text=f"Max drawdown {max_drawdown:.2f}%", text=f"Max drawdown {max_drawdown * 100:.2f}%",
marker=dict( marker=dict(
symbol='square-open', symbol='square-open',
size=9, size=9,
@ -368,10 +380,13 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
return fig return fig
def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame],
trades: pd.DataFrame, timeframe: str) -> go.Figure: trades: pd.DataFrame, timeframe: str) -> go.Figure:
# Combine close-values for all pairs, rename columns to "pair" # Combine close-values for all pairs, rename columns to "pair"
df_comb = combine_tickers_with_mean(tickers, "close") df_comb = combine_dataframes_with_mean(data, "close")
# Trim trades to available OHLCV data
trades = extract_trades_of_period(df_comb, trades, date_index=True)
# Add combined cumulative profit # Add combined cumulative profit
df_comb = create_cum_profit(df_comb, trades, 'cum_profit', timeframe) df_comb = create_cum_profit(df_comb, trades, 'cum_profit', timeframe)
@ -395,7 +410,7 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame],
fig.add_trace(avgclose, 1, 1) fig.add_trace(avgclose, 1, 1)
fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit')
fig = add_max_drawdown(fig, 2, trades, df_comb) fig = add_max_drawdown(fig, 2, trades, df_comb, timeframe)
for pair in pairs: for pair in pairs:
profit_col = f'cum_profit_{pair}' profit_col = f'cum_profit_{pair}'
@ -439,7 +454,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
""" """
From configuration provided From configuration provided
- Initializes plot-script - Initializes plot-script
- Get tickers data - Get candle (OHLCV) data
- Generate Dafaframes populated with indicators and signals based on configured strategy - Generate Dafaframes populated with indicators and signals based on configured strategy
- Load trades excecuted during the selected period - Load trades excecuted during the selected period
- Generate Plotly plot objects - Generate Plotly plot objects
@ -451,19 +466,17 @@ def load_and_plot_trades(config: Dict[str, Any]):
plot_elements = init_plotscript(config) plot_elements = init_plotscript(config)
trades = plot_elements['trades'] trades = plot_elements['trades']
pair_counter = 0 pair_counter = 0
for pair, data in plot_elements["tickers"].items(): for pair, data in plot_elements["ohlcv"].items():
pair_counter += 1 pair_counter += 1
logger.info("analyse pair %s", pair) logger.info("analyse pair %s", pair)
tickers = {}
tickers[pair] = data
dataframe = strategy.analyze_ticker(tickers[pair], {'pair': pair}) df_analyzed = strategy.analyze_ticker(data, {'pair': pair})
trades_pair = trades.loc[trades['pair'] == pair] trades_pair = trades.loc[trades['pair'] == pair]
trades_pair = extract_trades_of_period(dataframe, trades_pair) trades_pair = extract_trades_of_period(df_analyzed, trades_pair)
fig = generate_candlestick_graph( fig = generate_candlestick_graph(
pair=pair, pair=pair,
data=dataframe, data=df_analyzed,
trades=trades_pair, trades=trades_pair,
indicators1=config.get("indicators1", []), indicators1=config.get("indicators1", []),
indicators2=config.get("indicators2", []), indicators2=config.get("indicators2", []),
@ -494,7 +507,7 @@ def plot_profit(config: Dict[str, Any]) -> None:
# Create an average close price of all the pairs that were involved. # Create an average close price of all the pairs that were involved.
# this could be useful to gauge the overall market trend # this could be useful to gauge the overall market trend
fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"], fig = generate_profit_graph(plot_elements["pairs"], plot_elements["ohlcv"],
trades, config.get('ticker_interval', '5m')) trades, config.get('ticker_interval', '5m'))
store_plot_file(fig, filename='freqtrade-profit-plot.html', store_plot_file(fig, filename='freqtrade-profit-plot.html',
directory=config['user_data_dir'] / "plot", auto_open=True) directory=config['user_data_dir'] / "plot", auto_open=True)

View File

@ -197,7 +197,7 @@ class RPC:
Trade.close_date >= profitday, Trade.close_date >= profitday,
Trade.close_date < (profitday + timedelta(days=1)) Trade.close_date < (profitday + timedelta(days=1))
]).order_by(Trade.close_date).all() ]).order_by(Trade.close_date).all()
curdayprofit = sum(trade.calc_profit() for trade in trades) curdayprofit = sum(trade.close_profit_abs for trade in trades)
profit_days[profitday] = { profit_days[profitday] = {
'amount': f'{curdayprofit:.8f}', 'amount': f'{curdayprofit:.8f}',
'trades': len(trades) 'trades': len(trades)
@ -246,8 +246,8 @@ class RPC:
durations.append((trade.close_date - trade.open_date).total_seconds()) durations.append((trade.close_date - trade.open_date).total_seconds())
if not trade.is_open: if not trade.is_open:
profit_ratio = trade.calc_profit_ratio() profit_ratio = trade.close_profit
profit_closed_coin.append(trade.calc_profit()) profit_closed_coin.append(trade.close_profit_abs)
profit_closed_ratio.append(profit_ratio) profit_closed_ratio.append(profit_ratio)
else: else:
# Get current rate # Get current rate

View File

@ -16,6 +16,7 @@ from freqtrade.data.dataprovider import DataProvider
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
from freqtrade.exceptions import DependencyException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -59,7 +60,7 @@ class IStrategy(ABC):
Attributes you can use: Attributes you can use:
minimal_roi -> Dict: Minimal ROI designed for the strategy minimal_roi -> Dict: Minimal ROI designed for the strategy
stoploss -> float: optimal stoploss designed for the strategy stoploss -> float: optimal stoploss designed for the strategy
ticker_interval -> str: value of the ticker interval to use for the strategy ticker_interval -> str: value of the timeframe (ticker interval) to use with the strategy
""" """
# Strategy interface version # Strategy interface version
# Default to version 2 # Default to version 2
@ -125,7 +126,7 @@ class IStrategy(ABC):
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Populate indicators that will be used in the Buy and Sell strategy Populate indicators that will be used in the Buy and Sell strategy
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() :param dataframe: DataFrame with data from the exchange
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies :return: a Dataframe with all mandatory indicators for the strategies
""" """
@ -200,11 +201,11 @@ class IStrategy(ABC):
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Parses the given ticker history and returns a populated DataFrame Parses the given candle (OHLCV) data and returns a populated DataFrame
add several TA indicators and buy signal to it add several TA indicators and buy signal to it
:param dataframe: Dataframe containing ticker data :param dataframe: Dataframe containing data from exchange
:param metadata: Metadata dictionary with additional data (e.g. 'pair') :param metadata: Metadata dictionary with additional data (e.g. 'pair')
:return: DataFrame with ticker data and indicator data :return: DataFrame of candle (OHLCV) data with indicator data and signals added
""" """
logger.debug("TA Analysis Launched") logger.debug("TA Analysis Launched")
dataframe = self.advise_indicators(dataframe, metadata) dataframe = self.advise_indicators(dataframe, metadata)
@ -214,12 +215,12 @@ class IStrategy(ABC):
def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Parses the given ticker history and returns a populated DataFrame Parses the given candle (OHLCV) data and returns a populated DataFrame
add several TA indicators and buy signal to it add several TA indicators and buy signal to it
WARNING: Used internally only, may skip analysis if `process_only_new_candles` is set. WARNING: Used internally only, may skip analysis if `process_only_new_candles` is set.
:param dataframe: Dataframe containing ticker data :param dataframe: Dataframe containing data from exchange
:param metadata: Metadata dictionary with additional data (e.g. 'pair') :param metadata: Metadata dictionary with additional data (e.g. 'pair')
:return: DataFrame with ticker data and indicator data :return: DataFrame of candle (OHLCV) data with indicator data and signals added
""" """
pair = str(metadata.get('pair')) pair = str(metadata.get('pair'))
@ -241,8 +242,26 @@ class IStrategy(ABC):
return dataframe return dataframe
def get_signal(self, pair: str, interval: str, @staticmethod
dataframe: DataFrame) -> Tuple[bool, bool]: def preserve_df(dataframe: DataFrame) -> Tuple[int, float, datetime]:
""" keep some data for dataframes """
return len(dataframe), dataframe["close"].iloc[-1], dataframe["date"].iloc[-1]
@staticmethod
def assert_df(dataframe: DataFrame, df_len: int, df_close: float, df_date: datetime):
""" make sure data is unmodified """
message = ""
if df_len != len(dataframe):
message = "length"
elif df_close != dataframe["close"].iloc[-1]:
message = "last close price"
elif df_date != dataframe["date"].iloc[-1]:
message = "last date"
if message:
raise DependencyException("Dataframe returned from strategy has mismatching "
f"{message}.")
def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]:
""" """
Calculates current signal based several technical analysis indicators Calculates current signal based several technical analysis indicators
:param pair: pair in format ANT/BTC :param pair: pair in format ANT/BTC
@ -251,21 +270,25 @@ class IStrategy(ABC):
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal :return: (Buy, Sell) A bool-tuple indicating buy/sell signal
""" """
if not isinstance(dataframe, DataFrame) or dataframe.empty: if not isinstance(dataframe, DataFrame) or dataframe.empty:
logger.warning('Empty ticker history for pair %s', pair) logger.warning('Empty candle (OHLCV) data for pair %s', pair)
return False, False return False, False
latest_date = dataframe['date'].max()
try: try:
df_len, df_close, df_date = self.preserve_df(dataframe)
dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair}) dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair})
self.assert_df(dataframe, df_len, df_close, df_date)
except ValueError as error: except ValueError as error:
logger.warning( logger.warning('Unable to analyze candle (OHLCV) data for pair %s: %s',
'Unable to analyze ticker for pair %s: %s', pair, str(error))
pair, return False, False
str(error) except DependencyException as error:
) logger.warning("Unable to analyze candle (OHLCV) data for pair %s: %s",
pair, str(error))
return False, False return False, False
except Exception as error: except Exception as error:
logger.exception( logger.exception(
'Unexpected error when analyzing ticker for pair %s: %s', 'Unexpected error when analyzing candle (OHLCV) data for pair %s: %s',
pair, pair,
str(error) str(error)
) )
@ -275,7 +298,7 @@ class IStrategy(ABC):
logger.warning('Empty dataframe for pair %s', pair) logger.warning('Empty dataframe for pair %s', pair)
return False, False return False, False
latest = dataframe.iloc[-1] latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1]
# Check if dataframe is out of date # Check if dataframe is out of date
signal_date = arrow.get(latest['date']) signal_date = arrow.get(latest['date'])
@ -440,19 +463,19 @@ class IStrategy(ABC):
else: else:
return current_profit > roi return current_profit > roi
def tickerdata_to_dataframe(self, tickerdata: Dict[str, DataFrame]) -> Dict[str, DataFrame]: def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
""" """
Creates a dataframe and populates indicators for given ticker data Creates a dataframe and populates indicators for given candle (OHLCV) data
Used by optimize operations only, not during dry / live runs. Used by optimize operations only, not during dry / live runs.
""" """
return {pair: self.advise_indicators(pair_data, {'pair': pair}) return {pair: self.advise_indicators(pair_data, {'pair': pair})
for pair, pair_data in tickerdata.items()} for pair, pair_data in data.items()}
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Populate indicators that will be used in the Buy and Sell strategy Populate indicators that will be used in the Buy and Sell strategy
This method should not be overridden. This method should not be overridden.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() :param dataframe: Dataframe with data from the exchange
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies :return: a Dataframe with all mandatory indicators for the strategies
""" """

View File

@ -66,6 +66,9 @@ class {{ hyperopt }}(IHyperOpt):
dataframe['close'], dataframe['sar'] dataframe['close'], dataframe['sar']
)) ))
# Check that the candle had volume
conditions.append(dataframe['volume'] > 0)
if conditions: if conditions:
dataframe.loc[ dataframe.loc[
reduce(lambda x, y: x & y, conditions), reduce(lambda x, y: x & y, conditions),
@ -111,6 +114,9 @@ class {{ hyperopt }}(IHyperOpt):
dataframe['sar'], dataframe['close'] dataframe['sar'], dataframe['close']
)) ))
# Check that the candle had volume
conditions.append(dataframe['volume'] > 0)
if conditions: if conditions:
dataframe.loc[ dataframe.loc[
reduce(lambda x, y: x & y, conditions), reduce(lambda x, y: x & y, conditions),

View File

@ -99,7 +99,7 @@ class {{ strategy }}(IStrategy):
Performance Note: For the best performance be frugal on the number of indicators Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage. or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() :param dataframe: Dataframe with data from the exchange
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies :return: a Dataframe with all mandatory indicators for the strategies
""" """

View File

@ -78,6 +78,9 @@ class SampleHyperOpt(IHyperOpt):
dataframe['close'], dataframe['sar'] dataframe['close'], dataframe['sar']
)) ))
# Check that volume is not 0
conditions.append(dataframe['volume'] > 0)
if conditions: if conditions:
dataframe.loc[ dataframe.loc[
reduce(lambda x, y: x & y, conditions), reduce(lambda x, y: x & y, conditions),
@ -138,6 +141,9 @@ class SampleHyperOpt(IHyperOpt):
dataframe['sar'], dataframe['close'] dataframe['sar'], dataframe['close']
)) ))
# Check that volume is not 0
conditions.append(dataframe['volume'] > 0)
if conditions: if conditions:
dataframe.loc[ dataframe.loc[
reduce(lambda x, y: x & y, conditions), reduce(lambda x, y: x & y, conditions),

View File

@ -93,6 +93,9 @@ class AdvancedSampleHyperOpt(IHyperOpt):
dataframe['close'], dataframe['sar'] dataframe['close'], dataframe['sar']
)) ))
# Check that volume is not 0
conditions.append(dataframe['volume'] > 0)
if conditions: if conditions:
dataframe.loc[ dataframe.loc[
reduce(lambda x, y: x & y, conditions), reduce(lambda x, y: x & y, conditions),
@ -153,6 +156,9 @@ class AdvancedSampleHyperOpt(IHyperOpt):
dataframe['sar'], dataframe['close'] dataframe['sar'], dataframe['close']
)) ))
# Check that volume is not 0
conditions.append(dataframe['volume'] > 0)
if conditions: if conditions:
dataframe.loc[ dataframe.loc[
reduce(lambda x, y: x & y, conditions), reduce(lambda x, y: x & y, conditions),

View File

@ -116,7 +116,7 @@ class SampleStrategy(IStrategy):
Performance Note: For the best performance be frugal on the number of indicators Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage. or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() :param dataframe: Dataframe with data from the exchange
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies :return: a Dataframe with all mandatory indicators for the strategies
""" """

View File

@ -1,8 +1,8 @@
# requirements without requirements installable via conda # requirements without requirements installable via conda
# mainly used for Raspberry pi installs # mainly used for Raspberry pi installs
ccxt==1.23.81 ccxt==1.25.81
SQLAlchemy==1.3.13 SQLAlchemy==1.3.15
python-telegram-bot==12.4.2 python-telegram-bot==12.5.1
arrow==0.15.5 arrow==0.15.5
cachetools==4.0.0 cachetools==4.0.0
requests==2.23.0 requests==2.23.0
@ -10,7 +10,7 @@ urllib3==1.25.8
wrapt==1.12.1 wrapt==1.12.1
jsonschema==3.2.0 jsonschema==3.2.0
TA-Lib==0.4.17 TA-Lib==0.4.17
tabulate==0.8.6 tabulate==0.8.7
pycoingecko==1.2.0 pycoingecko==1.2.0
jinja2==2.11.1 jinja2==2.11.1
@ -24,10 +24,10 @@ python-rapidjson==0.9.1
sdnotify==0.3.2 sdnotify==0.3.2
# Api server # Api server
flask==1.1.1 flask==1.1.2
# Support for colorized terminal output # Support for colorized terminal output
colorama==0.4.3 colorama==0.4.3
# Building config files interactively # Building config files interactively
questionary==1.5.1 questionary==1.5.1
prompt-toolkit==3.0.4 prompt-toolkit==3.0.5

View File

@ -6,12 +6,12 @@
coveralls==1.11.1 coveralls==1.11.1
flake8==3.7.9 flake8==3.7.9
flake8-type-annotations==0.1.0 flake8-type-annotations==0.1.0
flake8-tidy-imports==4.0.0 flake8-tidy-imports==4.1.0
mypy==0.761 mypy==0.770
pytest==5.3.5 pytest==5.4.1
pytest-asyncio==0.10.0 pytest-asyncio==0.10.0
pytest-cov==2.8.1 pytest-cov==2.8.1
pytest-mock==2.0.0 pytest-mock==3.0.0
pytest-random-order==1.0.4 pytest-random-order==1.0.4
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents

View File

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

View File

@ -1,5 +1,5 @@
# Load common requirements # Load common requirements
-r requirements-common.txt -r requirements-common.txt
numpy==1.18.1 numpy==1.18.2
pandas==1.0.1 pandas==1.0.3

View File

@ -15,7 +15,7 @@ from telegram import Chat, Message, Update
from freqtrade import constants, persistence from freqtrade import constants, persistence
from freqtrade.commands import Arguments from freqtrade.commands import Arguments
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.edge import Edge, PairInfo from freqtrade.edge import Edge, PairInfo
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
@ -849,15 +849,15 @@ def order_book_l2():
@pytest.fixture @pytest.fixture
def ticker_history_list(): def ohlcv_history_list():
return [ return [
[ [
1511686200000, # unix timestamp ms 1511686200000, # unix timestamp ms
8.794e-05, # open 8.794e-05, # open
8.948e-05, # high 8.948e-05, # high
8.794e-05, # low 8.794e-05, # low
8.88e-05, # close 8.88e-05, # close
0.0877869, # volume (in quote currency) 0.0877869, # volume (in quote currency)
], ],
[ [
1511686500000, 1511686500000,
@ -879,8 +879,9 @@ def ticker_history_list():
@pytest.fixture @pytest.fixture
def ticker_history(ticker_history_list): def ohlcv_history(ohlcv_history_list):
return parse_ticker_dataframe(ticker_history_list, "5m", pair="UNITTEST/BTC", fill_missing=True) return ohlcv_to_dataframe(ohlcv_history_list, "5m", pair="UNITTEST/BTC",
fill_missing=True)
@pytest.fixture @pytest.fixture
@ -1195,8 +1196,8 @@ def tickers():
@pytest.fixture @pytest.fixture
def result(testdatadir): def result(testdatadir):
with (testdatadir / 'UNITTEST_BTC-1m.json').open('r') as data_file: with (testdatadir / 'UNITTEST_BTC-1m.json').open('r') as data_file:
return parse_ticker_dataframe(json.load(data_file), '1m', pair="UNITTEST/BTC", return ohlcv_to_dataframe(json.load(data_file), '1m', pair="UNITTEST/BTC",
fill_missing=True) fill_missing=True)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")

View File

@ -1,14 +1,15 @@
from pathlib import Path
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from arrow import Arrow from arrow import Arrow
from pandas import DataFrame, DateOffset, to_datetime, Timestamp from pandas import DataFrame, DateOffset, Timestamp, to_datetime
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, from freqtrade.data.btanalysis import (BT_DATA_COLUMNS,
analyze_trade_parallelism, analyze_trade_parallelism,
calculate_max_drawdown, calculate_max_drawdown,
combine_tickers_with_mean, combine_dataframes_with_mean,
create_cum_profit, create_cum_profit,
extract_trades_of_period, extract_trades_of_period,
load_backtest_data, load_trades, load_backtest_data, load_trades,
@ -104,6 +105,7 @@ def test_load_trades(default_conf, mocker):
load_trades("DB", load_trades("DB",
db_url=default_conf.get('db_url'), db_url=default_conf.get('db_url'),
exportfilename=default_conf.get('exportfilename'), exportfilename=default_conf.get('exportfilename'),
no_trades=False
) )
assert db_mock.call_count == 1 assert db_mock.call_count == 1
@ -111,22 +113,32 @@ def test_load_trades(default_conf, mocker):
db_mock.reset_mock() db_mock.reset_mock()
bt_mock.reset_mock() bt_mock.reset_mock()
default_conf['exportfilename'] = "testfile.json" default_conf['exportfilename'] = Path("testfile.json")
load_trades("file", load_trades("file",
db_url=default_conf.get('db_url'), db_url=default_conf.get('db_url'),
exportfilename=default_conf.get('exportfilename'),) exportfilename=default_conf.get('exportfilename'),
)
assert db_mock.call_count == 0 assert db_mock.call_count == 0
assert bt_mock.call_count == 1 assert bt_mock.call_count == 1
db_mock.reset_mock()
bt_mock.reset_mock()
default_conf['exportfilename'] = "testfile.json"
load_trades("file",
db_url=default_conf.get('db_url'),
exportfilename=default_conf.get('exportfilename'),
no_trades=True
)
def test_combine_tickers_with_mean(testdatadir): assert db_mock.call_count == 0
assert bt_mock.call_count == 0
def test_combine_dataframes_with_mean(testdatadir):
pairs = ["ETH/BTC", "ADA/BTC"] pairs = ["ETH/BTC", "ADA/BTC"]
tickers = load_data(datadir=testdatadir, data = load_data(datadir=testdatadir, pairs=pairs, timeframe='5m')
pairs=pairs, df = combine_dataframes_with_mean(data)
timeframe='5m'
)
df = combine_tickers_with_mean(tickers)
assert isinstance(df, DataFrame) assert isinstance(df, DataFrame)
assert "ETH/BTC" in df.columns assert "ETH/BTC" in df.columns
assert "ADA/BTC" in df.columns assert "ADA/BTC" in df.columns

View File

@ -5,9 +5,12 @@ from freqtrade.configuration.timerange import TimeRange
from freqtrade.data.converter import (convert_ohlcv_format, from freqtrade.data.converter import (convert_ohlcv_format,
convert_trades_format, convert_trades_format,
ohlcv_fill_up_missing_data, ohlcv_fill_up_missing_data,
parse_ticker_dataframe, trim_dataframe) ohlcv_to_dataframe,
from freqtrade.data.history import (get_timerange, load_data, trim_dataframe)
load_pair_history, validate_backtest_data) from freqtrade.data.history import (get_timerange,
load_data,
load_pair_history,
validate_backtest_data)
from tests.conftest import log_has from tests.conftest import log_has
from tests.data.test_history import _backup_file, _clean_test_file from tests.data.test_history import _backup_file, _clean_test_file
@ -16,15 +19,15 @@ def test_dataframe_correct_columns(result):
assert result.columns.tolist() == ['date', 'open', 'high', 'low', 'close', 'volume'] assert result.columns.tolist() == ['date', 'open', 'high', 'low', 'close', 'volume']
def test_parse_ticker_dataframe(ticker_history_list, caplog): def test_ohlcv_to_dataframe(ohlcv_history_list, caplog):
columns = ['date', 'open', 'high', 'low', 'close', 'volume'] columns = ['date', 'open', 'high', 'low', 'close', 'volume']
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
# Test file with BV data # Test file with BV data
dataframe = parse_ticker_dataframe(ticker_history_list, '5m', dataframe = ohlcv_to_dataframe(ohlcv_history_list, '5m', pair="UNITTEST/BTC",
pair="UNITTEST/BTC", fill_missing=True) fill_missing=True)
assert dataframe.columns.tolist() == columns assert dataframe.columns.tolist() == columns
assert log_has('Parsing tickerlist to dataframe', caplog) assert log_has('Converting candle (OHLCV) data to dataframe for pair UNITTEST/BTC.', caplog)
def test_ohlcv_fill_up_missing_data(testdatadir, caplog): def test_ohlcv_fill_up_missing_data(testdatadir, caplog):
@ -84,7 +87,8 @@ def test_ohlcv_fill_up_missing_data2(caplog):
] ]
# Generate test-data without filling missing # Generate test-data without filling missing
data = parse_ticker_dataframe(ticks, timeframe, pair="UNITTEST/BTC", fill_missing=False) data = ohlcv_to_dataframe(ticks, timeframe, pair="UNITTEST/BTC",
fill_missing=False)
assert len(data) == 3 assert len(data) == 3
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
data2 = ohlcv_fill_up_missing_data(data, timeframe, "UNITTEST/BTC") data2 = ohlcv_fill_up_missing_data(data, timeframe, "UNITTEST/BTC")
@ -140,14 +144,14 @@ def test_ohlcv_drop_incomplete(caplog):
] ]
] ]
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
data = parse_ticker_dataframe(ticks, timeframe, pair="UNITTEST/BTC", data = ohlcv_to_dataframe(ticks, timeframe, pair="UNITTEST/BTC",
fill_missing=False, drop_incomplete=False) fill_missing=False, drop_incomplete=False)
assert len(data) == 4 assert len(data) == 4
assert not log_has("Dropping last candle", caplog) assert not log_has("Dropping last candle", caplog)
# Drop last candle # Drop last candle
data = parse_ticker_dataframe(ticks, timeframe, pair="UNITTEST/BTC", data = ohlcv_to_dataframe(ticks, timeframe, pair="UNITTEST/BTC",
fill_missing=False, drop_incomplete=True) fill_missing=False, drop_incomplete=True)
assert len(data) == 3 assert len(data) == 3
assert log_has("Dropping last candle", caplog) assert log_has("Dropping last candle", caplog)

View File

@ -7,19 +7,19 @@ from freqtrade.state import RunMode
from tests.conftest import get_patched_exchange from tests.conftest import get_patched_exchange
def test_ohlcv(mocker, default_conf, ticker_history): def test_ohlcv(mocker, default_conf, ohlcv_history):
default_conf["runmode"] = RunMode.DRY_RUN default_conf["runmode"] = RunMode.DRY_RUN
timeframe = default_conf["ticker_interval"] timeframe = default_conf["ticker_interval"]
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
exchange._klines[("XRP/BTC", timeframe)] = ticker_history exchange._klines[("XRP/BTC", timeframe)] = ohlcv_history
exchange._klines[("UNITTEST/BTC", timeframe)] = ticker_history exchange._klines[("UNITTEST/BTC", timeframe)] = ohlcv_history
dp = DataProvider(default_conf, exchange) dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.DRY_RUN assert dp.runmode == RunMode.DRY_RUN
assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", timeframe)) assert ohlcv_history.equals(dp.ohlcv("UNITTEST/BTC", timeframe))
assert isinstance(dp.ohlcv("UNITTEST/BTC", timeframe), DataFrame) assert isinstance(dp.ohlcv("UNITTEST/BTC", timeframe), DataFrame)
assert dp.ohlcv("UNITTEST/BTC", timeframe) is not ticker_history assert dp.ohlcv("UNITTEST/BTC", timeframe) is not ohlcv_history
assert dp.ohlcv("UNITTEST/BTC", timeframe, copy=False) is ticker_history assert dp.ohlcv("UNITTEST/BTC", timeframe, copy=False) is ohlcv_history
assert not dp.ohlcv("UNITTEST/BTC", timeframe).empty assert not dp.ohlcv("UNITTEST/BTC", timeframe).empty
assert dp.ohlcv("NONESENSE/AAA", timeframe).empty assert dp.ohlcv("NONESENSE/AAA", timeframe).empty
@ -37,8 +37,8 @@ def test_ohlcv(mocker, default_conf, ticker_history):
assert dp.ohlcv("UNITTEST/BTC", timeframe).empty assert dp.ohlcv("UNITTEST/BTC", timeframe).empty
def test_historic_ohlcv(mocker, default_conf, ticker_history): def test_historic_ohlcv(mocker, default_conf, ohlcv_history):
historymock = MagicMock(return_value=ticker_history) historymock = MagicMock(return_value=ohlcv_history)
mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock) mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock)
dp = DataProvider(default_conf, None) dp = DataProvider(default_conf, None)
@ -48,18 +48,18 @@ def test_historic_ohlcv(mocker, default_conf, ticker_history):
assert historymock.call_args_list[0][1]["timeframe"] == "5m" assert historymock.call_args_list[0][1]["timeframe"] == "5m"
def test_get_pair_dataframe(mocker, default_conf, ticker_history): def test_get_pair_dataframe(mocker, default_conf, ohlcv_history):
default_conf["runmode"] = RunMode.DRY_RUN default_conf["runmode"] = RunMode.DRY_RUN
ticker_interval = default_conf["ticker_interval"] ticker_interval = default_conf["ticker_interval"]
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history exchange._klines[("XRP/BTC", ticker_interval)] = ohlcv_history
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history exchange._klines[("UNITTEST/BTC", ticker_interval)] = ohlcv_history
dp = DataProvider(default_conf, exchange) dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.DRY_RUN assert dp.runmode == RunMode.DRY_RUN
assert ticker_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval)) assert ohlcv_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval))
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame) assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval) is not ticker_history assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval) is not ohlcv_history
assert not dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval).empty assert not dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval).empty
assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
@ -73,7 +73,7 @@ def test_get_pair_dataframe(mocker, default_conf, ticker_history):
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame) assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
historymock = MagicMock(return_value=ticker_history) historymock = MagicMock(return_value=ohlcv_history)
mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock) mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock)
default_conf["runmode"] = RunMode.BACKTEST default_conf["runmode"] = RunMode.BACKTEST
dp = DataProvider(default_conf, exchange) dp = DataProvider(default_conf, exchange)
@ -82,11 +82,11 @@ def test_get_pair_dataframe(mocker, default_conf, ticker_history):
# assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty # assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
def test_available_pairs(mocker, default_conf, ticker_history): def test_available_pairs(mocker, default_conf, ohlcv_history):
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
ticker_interval = default_conf["ticker_interval"] ticker_interval = default_conf["ticker_interval"]
exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history exchange._klines[("XRP/BTC", ticker_interval)] = ohlcv_history
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history exchange._klines[("UNITTEST/BTC", ticker_interval)] = ohlcv_history
dp = DataProvider(default_conf, exchange) dp = DataProvider(default_conf, exchange)
assert len(dp.available_pairs) == 2 assert len(dp.available_pairs) == 2
@ -96,7 +96,7 @@ def test_available_pairs(mocker, default_conf, ticker_history):
] ]
def test_refresh(mocker, default_conf, ticker_history): def test_refresh(mocker, default_conf, ohlcv_history):
refresh_mock = MagicMock() refresh_mock = MagicMock()
mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock) mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock)

View File

@ -12,7 +12,7 @@ from pandas import DataFrame
from pandas.testing import assert_frame_equal from pandas.testing import assert_frame_equal
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.data.history.history_utils import ( from freqtrade.data.history.history_utils import (
_download_pair_history, _download_trades_history, _download_pair_history, _download_trades_history,
_load_cached_data_for_updating, convert_trades_to_ohlcv, get_timerange, _load_cached_data_for_updating, convert_trades_to_ohlcv, get_timerange,
@ -63,7 +63,7 @@ def _clean_test_file(file: Path) -> None:
file_swp.rename(file) file_swp.rename(file)
def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> None: def test_load_data_30min_timeframe(mocker, caplog, default_conf, testdatadir) -> None:
ld = load_pair_history(pair='UNITTEST/BTC', timeframe='30m', datadir=testdatadir) ld = load_pair_history(pair='UNITTEST/BTC', timeframe='30m', datadir=testdatadir)
assert isinstance(ld, DataFrame) assert isinstance(ld, DataFrame)
assert not log_has( assert not log_has(
@ -72,7 +72,7 @@ def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> No
) )
def test_load_data_7min_ticker(mocker, caplog, default_conf, testdatadir) -> None: def test_load_data_7min_timeframe(mocker, caplog, default_conf, testdatadir) -> None:
ld = load_pair_history(pair='UNITTEST/BTC', timeframe='7m', datadir=testdatadir) ld = load_pair_history(pair='UNITTEST/BTC', timeframe='7m', datadir=testdatadir)
assert isinstance(ld, DataFrame) assert isinstance(ld, DataFrame)
assert ld.empty assert ld.empty
@ -82,8 +82,8 @@ def test_load_data_7min_ticker(mocker, caplog, default_conf, testdatadir) -> Non
) )
def test_load_data_1min_ticker(ticker_history, mocker, caplog, testdatadir) -> None: def test_load_data_1min_timeframe(ohlcv_history, mocker, caplog, testdatadir) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history) mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history)
file = testdatadir / 'UNITTEST_BTC-1m.json' file = testdatadir / 'UNITTEST_BTC-1m.json'
_backup_file(file, copy_file=True) _backup_file(file, copy_file=True)
load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC']) load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'])
@ -110,12 +110,12 @@ def test_load_data_startup_candles(mocker, caplog, default_conf, testdatadir) ->
assert ltfmock.call_args_list[0][1]['timerange'].startts == timerange.startts - 20 * 60 assert ltfmock.call_args_list[0][1]['timerange'].startts == timerange.startts - 20 * 60
def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog,
default_conf, testdatadir) -> None: default_conf, testdatadir) -> None:
""" """
Test load_pair_history() with 1 min ticker Test load_pair_history() with 1 min timeframe
""" """
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history_list) mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history_list)
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
file = testdatadir / 'MEME_BTC-1m.json' file = testdatadir / 'MEME_BTC-1m.json'
@ -188,8 +188,8 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None:
with open(test_filename, "rt") as file: with open(test_filename, "rt") as file:
test_data = json.load(file) test_data = json.load(file)
test_data_df = parse_ticker_dataframe(test_data, '1m', 'UNITTEST/BTC', test_data_df = ohlcv_to_dataframe(test_data, '1m', 'UNITTEST/BTC',
fill_missing=False, drop_incomplete=False) fill_missing=False, drop_incomplete=False)
# now = last cached item + 1 hour # now = last cached item + 1 hour
now_ts = test_data[-1][0] / 1000 + 60 * 60 now_ts = test_data[-1][0] / 1000 + 60 * 60
mocker.patch('arrow.utcnow', return_value=arrow.get(now_ts)) mocker.patch('arrow.utcnow', return_value=arrow.get(now_ts))
@ -230,8 +230,8 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None:
assert start_ts is None assert start_ts is None
def test_download_pair_history(ticker_history_list, mocker, default_conf, testdatadir) -> None: def test_download_pair_history(ohlcv_history_list, mocker, default_conf, testdatadir) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history_list) mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history_list)
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
file1_1 = testdatadir / 'MEME_BTC-1m.json' file1_1 = testdatadir / 'MEME_BTC-1m.json'
file1_5 = testdatadir / 'MEME_BTC-5m.json' file1_5 = testdatadir / 'MEME_BTC-5m.json'
@ -293,7 +293,7 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None:
assert json_dump_mock.call_count == 2 assert json_dump_mock.call_count == 2
def test_download_backtesting_data_exception(ticker_history, mocker, caplog, def test_download_backtesting_data_exception(ohlcv_history, mocker, caplog,
default_conf, testdatadir) -> None: default_conf, testdatadir) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv',
side_effect=Exception('File Error')) side_effect=Exception('File Error'))
@ -321,15 +321,15 @@ def test_load_partial_missing(testdatadir, caplog) -> None:
# Make sure we start fresh - test missing data at start # Make sure we start fresh - test missing data at start
start = arrow.get('2018-01-01T00:00:00') start = arrow.get('2018-01-01T00:00:00')
end = arrow.get('2018-01-11T00:00:00') end = arrow.get('2018-01-11T00:00:00')
tickerdata = load_data(testdatadir, '5m', ['UNITTEST/BTC'], startup_candles=20, data = load_data(testdatadir, '5m', ['UNITTEST/BTC'], startup_candles=20,
timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) timerange=TimeRange('date', 'date', start.timestamp, end.timestamp))
assert log_has( assert log_has(
'Using indicator startup period: 20 ...', caplog 'Using indicator startup period: 20 ...', caplog
) )
# timedifference in 5 minutes # timedifference in 5 minutes
td = ((end - start).total_seconds() // 60 // 5) + 1 td = ((end - start).total_seconds() // 60 // 5) + 1
assert td != len(tickerdata['UNITTEST/BTC']) assert td != len(data['UNITTEST/BTC'])
start_real = tickerdata['UNITTEST/BTC'].iloc[0, 0] start_real = data['UNITTEST/BTC'].iloc[0, 0]
assert log_has(f'Missing data at start for pair ' assert log_has(f'Missing data at start for pair '
f'UNITTEST/BTC, data starts at {start_real.strftime("%Y-%m-%d %H:%M:%S")}', f'UNITTEST/BTC, data starts at {start_real.strftime("%Y-%m-%d %H:%M:%S")}',
caplog) caplog)
@ -337,14 +337,14 @@ def test_load_partial_missing(testdatadir, caplog) -> None:
caplog.clear() caplog.clear()
start = arrow.get('2018-01-10T00:00:00') start = arrow.get('2018-01-10T00:00:00')
end = arrow.get('2018-02-20T00:00:00') end = arrow.get('2018-02-20T00:00:00')
tickerdata = load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], data = load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) timerange=TimeRange('date', 'date', start.timestamp, end.timestamp))
# timedifference in 5 minutes # timedifference in 5 minutes
td = ((end - start).total_seconds() // 60 // 5) + 1 td = ((end - start).total_seconds() // 60 // 5) + 1
assert td != len(tickerdata['UNITTEST/BTC']) assert td != len(data['UNITTEST/BTC'])
# Shift endtime with +5 - as last candle is dropped (partial candle) # Shift endtime with +5 - as last candle is dropped (partial candle)
end_real = arrow.get(tickerdata['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5) end_real = arrow.get(data['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5)
assert log_has(f'Missing data at end for pair ' assert log_has(f'Missing data at end for pair '
f'UNITTEST/BTC, data ends at {end_real.strftime("%Y-%m-%d %H:%M:%S")}', f'UNITTEST/BTC, data ends at {end_real.strftime("%Y-%m-%d %H:%M:%S")}',
caplog) caplog)
@ -403,7 +403,7 @@ def test_get_timerange(default_conf, mocker, testdatadir) -> None:
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
data = strategy.tickerdata_to_dataframe( data = strategy.ohlcvdata_to_dataframe(
load_data( load_data(
datadir=testdatadir, datadir=testdatadir,
timeframe='1m', timeframe='1m',
@ -421,7 +421,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir)
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
data = strategy.tickerdata_to_dataframe( data = strategy.ohlcvdata_to_dataframe(
load_data( load_data(
datadir=testdatadir, datadir=testdatadir,
timeframe='1m', timeframe='1m',
@ -446,7 +446,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
timerange = TimeRange('index', 'index', 200, 250) timerange = TimeRange('index', 'index', 200, 250)
data = strategy.tickerdata_to_dataframe( data = strategy.ohlcvdata_to_dataframe(
load_data( load_data(
datadir=testdatadir, datadir=testdatadir,
timeframe='5m', timeframe='5m',

View File

@ -11,7 +11,7 @@ import pytest
from pandas import DataFrame, to_datetime from pandas import DataFrame, to_datetime
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.edge import Edge, PairInfo from freqtrade.edge import Edge, PairInfo
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
from tests.conftest import get_patched_freqtradebot, log_has from tests.conftest import get_patched_freqtradebot, log_has
@ -26,7 +26,7 @@ from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe,
# 5) Stoploss and sell are hit. should sell on stoploss # 5) Stoploss and sell are hit. should sell on stoploss
#################################################################### ####################################################################
ticker_start_time = arrow.get(2018, 10, 3) tests_start_time = arrow.get(2018, 10, 3)
ticker_interval_in_minute = 60 ticker_interval_in_minute = 60
_ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7} _ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7}
@ -43,10 +43,10 @@ def _validate_ohlc(buy_ohlc_sell_matrice):
def _build_dataframe(buy_ohlc_sell_matrice): def _build_dataframe(buy_ohlc_sell_matrice):
_validate_ohlc(buy_ohlc_sell_matrice) _validate_ohlc(buy_ohlc_sell_matrice)
tickers = [] data = []
for ohlc in buy_ohlc_sell_matrice: for ohlc in buy_ohlc_sell_matrice:
ticker = { d = {
'date': ticker_start_time.shift( 'date': tests_start_time.shift(
minutes=( minutes=(
ohlc[0] * ohlc[0] *
ticker_interval_in_minute)).timestamp * ticker_interval_in_minute)).timestamp *
@ -57,9 +57,9 @@ def _build_dataframe(buy_ohlc_sell_matrice):
'low': ohlc[4], 'low': ohlc[4],
'close': ohlc[5], 'close': ohlc[5],
'sell': ohlc[6]} 'sell': ohlc[6]}
tickers.append(ticker) data.append(d)
frame = DataFrame(tickers) frame = DataFrame(data)
frame['date'] = to_datetime(frame['date'], frame['date'] = to_datetime(frame['date'],
unit='ms', unit='ms',
utc=True, utc=True,
@ -69,7 +69,7 @@ def _build_dataframe(buy_ohlc_sell_matrice):
def _time_on_candle(number): def _time_on_candle(number):
return np.datetime64(ticker_start_time.shift( return np.datetime64(tests_start_time.shift(
minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms') minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms')
@ -163,8 +163,8 @@ def test_edge_results(edge_conf, mocker, caplog, data) -> None:
for c, trade in enumerate(data.trades): for c, trade in enumerate(data.trades):
res = results.iloc[c] res = results.iloc[c]
assert res.exit_type == trade.sell_reason assert res.exit_type == trade.sell_reason
assert res.open_time == np.datetime64(_get_frame_time_from_offset(trade.open_tick)) assert res.open_time == _get_frame_time_from_offset(trade.open_tick).replace(tzinfo=None)
assert res.close_time == np.datetime64(_get_frame_time_from_offset(trade.close_tick)) assert res.close_time == _get_frame_time_from_offset(trade.close_tick).replace(tzinfo=None)
def test_adjust(mocker, edge_conf): def test_adjust(mocker, edge_conf):
@ -262,7 +262,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m',
NEOBTC = [ NEOBTC = [
[ [
ticker_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000, tests_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000,
math.sin(x * hz) / 1000 + base, math.sin(x * hz) / 1000 + base,
math.sin(x * hz) / 1000 + base + 0.0001, math.sin(x * hz) / 1000 + base + 0.0001,
math.sin(x * hz) / 1000 + base - 0.0001, math.sin(x * hz) / 1000 + base - 0.0001,
@ -274,7 +274,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m',
base = 0.002 base = 0.002
LTCBTC = [ LTCBTC = [
[ [
ticker_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000, tests_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000,
math.sin(x * hz) / 1000 + base, math.sin(x * hz) / 1000 + base,
math.sin(x * hz) / 1000 + base + 0.0001, math.sin(x * hz) / 1000 + base + 0.0001,
math.sin(x * hz) / 1000 + base - 0.0001, math.sin(x * hz) / 1000 + base - 0.0001,
@ -282,16 +282,18 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m',
123.45 123.45
] for x in range(0, 500)] ] for x in range(0, 500)]
pairdata = {'NEO/BTC': parse_ticker_dataframe(NEOBTC, '1h', pair="NEO/BTC", fill_missing=True), pairdata = {'NEO/BTC': ohlcv_to_dataframe(NEOBTC, '1h', pair="NEO/BTC",
'LTC/BTC': parse_ticker_dataframe(LTCBTC, '1h', pair="LTC/BTC", fill_missing=True)} fill_missing=True),
'LTC/BTC': ohlcv_to_dataframe(LTCBTC, '1h', pair="LTC/BTC",
fill_missing=True)}
return pairdata return pairdata
def test_edge_process_downloaded_data(mocker, edge_conf): def test_edge_process_downloaded_data(mocker, edge_conf):
freqtrade = get_patched_freqtradebot(mocker, edge_conf) freqtrade = get_patched_freqtradebot(mocker, edge_conf)
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
mocker.patch('freqtrade.data.history.refresh_data', MagicMock()) mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock())
mocker.patch('freqtrade.data.history.load_data', mocked_load_data) mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data)
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
assert edge.calculate() assert edge.calculate()
@ -302,8 +304,8 @@ def test_edge_process_downloaded_data(mocker, edge_conf):
def test_edge_process_no_data(mocker, edge_conf, caplog): def test_edge_process_no_data(mocker, edge_conf, caplog):
freqtrade = get_patched_freqtradebot(mocker, edge_conf) freqtrade = get_patched_freqtradebot(mocker, edge_conf)
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
mocker.patch('freqtrade.data.history.refresh_data', MagicMock()) mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock())
mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={})) mocker.patch('freqtrade.edge.edge_positioning.load_data', MagicMock(return_value={}))
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
assert not edge.calculate() assert not edge.calculate()
@ -315,8 +317,8 @@ def test_edge_process_no_data(mocker, edge_conf, caplog):
def test_edge_process_no_trades(mocker, edge_conf, caplog): def test_edge_process_no_trades(mocker, edge_conf, caplog):
freqtrade = get_patched_freqtradebot(mocker, edge_conf) freqtrade = get_patched_freqtradebot(mocker, edge_conf)
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
mocker.patch('freqtrade.data.history.refresh_data', MagicMock()) mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock())
mocker.patch('freqtrade.data.history.load_data', mocked_load_data) mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data)
# Return empty # Return empty
mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[])) mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[]))
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)

View File

@ -581,7 +581,7 @@ def test_validate_timeframes_failed(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match=r"Invalid ticker interval '3m'. This exchange supports.*"): match=r"Invalid timeframe '3m'. This exchange supports.*"):
Exchange(default_conf) Exchange(default_conf)
default_conf["ticker_interval"] = "15s" default_conf["ticker_interval"] = "15s"
@ -1211,7 +1211,7 @@ def test_fetch_ticker(default_conf, mocker, exchange_name):
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
tick = [ ohlcv = [
[ [
arrow.utcnow().timestamp * 1000, # unix timestamp ms arrow.utcnow().timestamp * 1000, # unix timestamp ms
1, # open 1, # open
@ -1224,7 +1224,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
pair = 'ETH/BTC' pair = 'ETH/BTC'
async def mock_candle_hist(pair, timeframe, since_ms): async def mock_candle_hist(pair, timeframe, since_ms):
return pair, timeframe, tick return pair, timeframe, ohlcv
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls # one_call calculation * 1.8 should do 2 calls
@ -1232,12 +1232,12 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
assert exchange._async_get_candle_history.call_count == 2 assert exchange._async_get_candle_history.call_count == 2
# Returns twice the above tick # Returns twice the above OHLCV data
assert len(ret) == 2 assert len(ret) == 2
def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
tick = [ ohlcv = [
[ [
(arrow.utcnow().timestamp - 1) * 1000, # unix timestamp ms (arrow.utcnow().timestamp - 1) * 1000, # unix timestamp ms
1, # open 1, # open
@ -1258,14 +1258,14 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
exchange._api_async.fetch_ohlcv = get_mock_coro(tick) exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
pairs = [('IOTA/ETH', '5m'), ('XRP/ETH', '5m')] pairs = [('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]
# empty dicts # empty dicts
assert not exchange._klines assert not exchange._klines
exchange.refresh_latest_ohlcv(pairs) exchange.refresh_latest_ohlcv(pairs)
assert log_has(f'Refreshing ohlcv data for {len(pairs)} pairs', caplog) assert log_has(f'Refreshing candle (OHLCV) data for {len(pairs)} pairs', caplog)
assert exchange._klines assert exchange._klines
assert exchange._api_async.fetch_ohlcv.call_count == 2 assert exchange._api_async.fetch_ohlcv.call_count == 2
for pair in pairs: for pair in pairs:
@ -1283,14 +1283,15 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]) exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')])
assert exchange._api_async.fetch_ohlcv.call_count == 2 assert exchange._api_async.fetch_ohlcv.call_count == 2
assert log_has(f"Using cached ohlcv data for pair {pairs[0][0]}, timeframe {pairs[0][1]} ...", assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, "
f"timeframe {pairs[0][1]} ...",
caplog) caplog)
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name): async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name):
tick = [ ohlcv = [
[ [
arrow.utcnow().timestamp * 1000, # unix timestamp ms arrow.utcnow().timestamp * 1000, # unix timestamp ms
1, # open 1, # open
@ -1304,7 +1305,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
# Monkey-patch async function # Monkey-patch async function
exchange._api_async.fetch_ohlcv = get_mock_coro(tick) exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
pair = 'ETH/BTC' pair = 'ETH/BTC'
res = await exchange._async_get_candle_history(pair, "5m") res = await exchange._async_get_candle_history(pair, "5m")
@ -1312,9 +1313,9 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
assert len(res) == 3 assert len(res) == 3
assert res[0] == pair assert res[0] == pair
assert res[1] == "5m" assert res[1] == "5m"
assert res[2] == tick assert res[2] == ohlcv
assert exchange._api_async.fetch_ohlcv.call_count == 1 assert exchange._api_async.fetch_ohlcv.call_count == 1
assert not log_has(f"Using cached ohlcv data for {pair} ...", caplog) assert not log_has(f"Using cached candle (OHLCV) data for {pair} ...", caplog)
# exchange = Exchange(default_conf) # exchange = Exchange(default_conf)
await async_ccxt_exception(mocker, default_conf, MagicMock(), await async_ccxt_exception(mocker, default_conf, MagicMock(),
@ -1322,14 +1323,15 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
pair='ABCD/BTC', timeframe=default_conf['ticker_interval']) pair='ABCD/BTC', timeframe=default_conf['ticker_interval'])
api_mock = MagicMock() api_mock = MagicMock()
with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'): with pytest.raises(OperationalException,
match=r'Could not fetch historical candle \(OHLCV\) data.*'):
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error")) api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
await exchange._async_get_candle_history(pair, "5m", await exchange._async_get_candle_history(pair, "5m",
(arrow.utcnow().timestamp - 2000) * 1000) (arrow.utcnow().timestamp - 2000) * 1000)
with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching ' with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching '
r'historical candlestick data\..*'): r'historical candle \(OHLCV\) data\..*'):
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported")) api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
await exchange._async_get_candle_history(pair, "5m", await exchange._async_get_candle_history(pair, "5m",
@ -1339,7 +1341,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
@pytest.mark.asyncio @pytest.mark.asyncio
async def test__async_get_candle_history_empty(default_conf, mocker, caplog): async def test__async_get_candle_history_empty(default_conf, mocker, caplog):
""" Test empty exchange result """ """ Test empty exchange result """
tick = [] ohlcv = []
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
@ -1353,7 +1355,7 @@ async def test__async_get_candle_history_empty(default_conf, mocker, caplog):
assert len(res) == 3 assert len(res) == 3
assert res[0] == pair assert res[0] == pair
assert res[1] == "5m" assert res[1] == "5m"
assert res[2] == tick assert res[2] == ohlcv
assert exchange._api_async.fetch_ohlcv.call_count == 1 assert exchange._api_async.fetch_ohlcv.call_count == 1
@ -1431,8 +1433,8 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
return sorted(data, key=key) return sorted(data, key=key)
# GDAX use-case (real data from GDAX) # GDAX use-case (real data from GDAX)
# This ticker history is ordered DESC (newest first, oldest last) # This OHLCV data is ordered DESC (newest first, oldest last)
tick = [ ohlcv = [
[1527833100000, 0.07666, 0.07671, 0.07666, 0.07668, 16.65244264], [1527833100000, 0.07666, 0.07671, 0.07666, 0.07668, 16.65244264],
[1527832800000, 0.07662, 0.07666, 0.07662, 0.07666, 1.30051526], [1527832800000, 0.07662, 0.07666, 0.07662, 0.07666, 1.30051526],
[1527832500000, 0.07656, 0.07661, 0.07656, 0.07661, 12.034778840000001], [1527832500000, 0.07656, 0.07661, 0.07656, 0.07661, 12.034778840000001],
@ -1445,31 +1447,31 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
[1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867] [1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867]
] ]
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
exchange._api_async.fetch_ohlcv = get_mock_coro(tick) exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
sort_mock = mocker.patch('freqtrade.exchange.exchange.sorted', MagicMock(side_effect=sort_data)) sort_mock = mocker.patch('freqtrade.exchange.exchange.sorted', MagicMock(side_effect=sort_data))
# Test the ticker history sort # Test the OHLCV data sort
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval']) res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert res[0] == 'ETH/BTC' assert res[0] == 'ETH/BTC'
ticks = res[2] res_ohlcv = res[2]
assert sort_mock.call_count == 1 assert sort_mock.call_count == 1
assert ticks[0][0] == 1527830400000 assert res_ohlcv[0][0] == 1527830400000
assert ticks[0][1] == 0.07649 assert res_ohlcv[0][1] == 0.07649
assert ticks[0][2] == 0.07651 assert res_ohlcv[0][2] == 0.07651
assert ticks[0][3] == 0.07649 assert res_ohlcv[0][3] == 0.07649
assert ticks[0][4] == 0.07651 assert res_ohlcv[0][4] == 0.07651
assert ticks[0][5] == 2.5734867 assert res_ohlcv[0][5] == 2.5734867
assert ticks[9][0] == 1527833100000 assert res_ohlcv[9][0] == 1527833100000
assert ticks[9][1] == 0.07666 assert res_ohlcv[9][1] == 0.07666
assert ticks[9][2] == 0.07671 assert res_ohlcv[9][2] == 0.07671
assert ticks[9][3] == 0.07666 assert res_ohlcv[9][3] == 0.07666
assert ticks[9][4] == 0.07668 assert res_ohlcv[9][4] == 0.07668
assert ticks[9][5] == 16.65244264 assert res_ohlcv[9][5] == 16.65244264
# Bittrex use-case (real data from Bittrex) # Bittrex use-case (real data from Bittrex)
# This ticker history is ordered ASC (oldest first, newest last) # This OHLCV data is ordered ASC (oldest first, newest last)
tick = [ ohlcv = [
[1527827700000, 0.07659999, 0.0766, 0.07627, 0.07657998, 1.85216924], [1527827700000, 0.07659999, 0.0766, 0.07627, 0.07657998, 1.85216924],
[1527828000000, 0.07657995, 0.07657995, 0.0763, 0.0763, 26.04051037], [1527828000000, 0.07657995, 0.07657995, 0.0763, 0.0763, 26.04051037],
[1527828300000, 0.0763, 0.07659998, 0.0763, 0.0764, 10.36434124], [1527828300000, 0.0763, 0.07659998, 0.0763, 0.0764, 10.36434124],
@ -1481,29 +1483,29 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
[1527830100000, 0.076695, 0.07671, 0.07624171, 0.07671, 1.80689244], [1527830100000, 0.076695, 0.07671, 0.07624171, 0.07671, 1.80689244],
[1527830400000, 0.07671, 0.07674399, 0.07629216, 0.07655213, 2.31452783] [1527830400000, 0.07671, 0.07674399, 0.07629216, 0.07655213, 2.31452783]
] ]
exchange._api_async.fetch_ohlcv = get_mock_coro(tick) exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
# Reset sort mock # Reset sort mock
sort_mock = mocker.patch('freqtrade.exchange.sorted', MagicMock(side_effect=sort_data)) sort_mock = mocker.patch('freqtrade.exchange.sorted', MagicMock(side_effect=sort_data))
# Test the ticker history sort # Test the OHLCV data sort
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval']) res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert res[0] == 'ETH/BTC' assert res[0] == 'ETH/BTC'
assert res[1] == default_conf['ticker_interval'] assert res[1] == default_conf['ticker_interval']
ticks = res[2] res_ohlcv = res[2]
# Sorted not called again - data is already in order # Sorted not called again - data is already in order
assert sort_mock.call_count == 0 assert sort_mock.call_count == 0
assert ticks[0][0] == 1527827700000 assert res_ohlcv[0][0] == 1527827700000
assert ticks[0][1] == 0.07659999 assert res_ohlcv[0][1] == 0.07659999
assert ticks[0][2] == 0.0766 assert res_ohlcv[0][2] == 0.0766
assert ticks[0][3] == 0.07627 assert res_ohlcv[0][3] == 0.07627
assert ticks[0][4] == 0.07657998 assert res_ohlcv[0][4] == 0.07657998
assert ticks[0][5] == 1.85216924 assert res_ohlcv[0][5] == 1.85216924
assert ticks[9][0] == 1527830400000 assert res_ohlcv[9][0] == 1527830400000
assert ticks[9][1] == 0.07671 assert res_ohlcv[9][1] == 0.07671
assert ticks[9][2] == 0.07674399 assert res_ohlcv[9][2] == 0.07674399
assert ticks[9][3] == 0.07629216 assert res_ohlcv[9][3] == 0.07629216
assert ticks[9][4] == 0.07655213 assert res_ohlcv[9][4] == 0.07655213
assert ticks[9][5] == 2.31452783 assert res_ohlcv[9][5] == 2.31452783
@pytest.mark.asyncio @pytest.mark.asyncio

View File

@ -6,7 +6,7 @@ from pandas import DataFrame
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
ticker_start_time = arrow.get(2018, 10, 3) tests_start_time = arrow.get(2018, 10, 3)
tests_timeframe = '1h' tests_timeframe = '1h'
@ -36,14 +36,14 @@ class BTContainer(NamedTuple):
def _get_frame_time_from_offset(offset): def _get_frame_time_from_offset(offset):
return ticker_start_time.shift(minutes=(offset * timeframe_to_minutes(tests_timeframe)) minutes = offset * timeframe_to_minutes(tests_timeframe)
).datetime return tests_start_time.shift(minutes=minutes).datetime
def _build_backtest_dataframe(ticker_with_signals): def _build_backtest_dataframe(data):
columns = ['date', 'open', 'high', 'low', 'close', 'volume', 'buy', 'sell'] columns = ['date', 'open', 'high', 'low', 'close', 'volume', 'buy', 'sell']
frame = DataFrame.from_records(ticker_with_signals, columns=columns) frame = DataFrame.from_records(data, columns=columns)
frame['date'] = frame['date'].apply(_get_frame_time_from_offset) frame['date'] = frame['date'].apply(_get_frame_time_from_offset)
# Ensure floats are in place # Ensure floats are in place
for column in ['open', 'high', 'low', 'close', 'volume']: for column in ['open', 'high', 'low', 'close', 'volume']:

View File

@ -84,7 +84,7 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None:
backtesting = Backtesting(config) backtesting = Backtesting(config)
data = load_data_test(contour, testdatadir) data = load_data_test(contour, testdatadir)
processed = backtesting.strategy.tickerdata_to_dataframe(data) processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
assert isinstance(processed, dict) assert isinstance(processed, dict)
results = backtesting.backtest( results = backtesting.backtest(
@ -105,7 +105,7 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
data = trim_dictlist(data, -201) data = trim_dictlist(data, -201)
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(conf) backtesting = Backtesting(conf)
processed = backtesting.strategy.tickerdata_to_dataframe(data) processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
return { return {
'processed': processed, 'processed': processed,
@ -224,6 +224,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
assert 'export' in config assert 'export' in config
assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog) assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog)
assert 'exportfilename' in config assert 'exportfilename' in config
assert isinstance(config['exportfilename'], Path)
assert log_has('Storing backtest results to {} ...'.format(config['exportfilename']), caplog) assert log_has('Storing backtest results to {} ...'.format(config['exportfilename']), caplog)
assert 'fee' in config assert 'fee' in config
@ -275,7 +276,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
assert backtesting.config == default_conf assert backtesting.config == default_conf
assert backtesting.timeframe == '5m' assert backtesting.timeframe == '5m'
assert callable(backtesting.strategy.tickerdata_to_dataframe) assert callable(backtesting.strategy.ohlcvdata_to_dataframe)
assert callable(backtesting.strategy.advise_buy) assert callable(backtesting.strategy.advise_buy)
assert callable(backtesting.strategy.advise_sell) assert callable(backtesting.strategy.advise_sell)
assert isinstance(backtesting.strategy.dp, DataProvider) assert isinstance(backtesting.strategy.dp, DataProvider)
@ -297,7 +298,7 @@ def test_backtesting_init_no_ticker_interval(mocker, default_conf, caplog) -> No
"or as cli argument `--ticker-interval 5m`", caplog) "or as cli argument `--ticker-interval 5m`", caplog)
def test_tickerdata_with_fee(default_conf, mocker, testdatadir) -> None: def test_data_with_fee(default_conf, mocker, testdatadir) -> None:
patch_exchange(mocker) patch_exchange(mocker)
default_conf['fee'] = 0.1234 default_conf['fee'] = 0.1234
@ -307,21 +308,21 @@ def test_tickerdata_with_fee(default_conf, mocker, testdatadir) -> None:
assert fee_mock.call_count == 0 assert fee_mock.call_count == 0
def test_tickerdata_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
patch_exchange(mocker) patch_exchange(mocker)
timerange = TimeRange.parse_timerange('1510694220-1510700340') timerange = TimeRange.parse_timerange('1510694220-1510700340')
tickerlist = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, data = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
fill_up_missing=True) fill_up_missing=True)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
data = backtesting.strategy.tickerdata_to_dataframe(tickerlist) processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
assert len(data['UNITTEST/BTC']) == 102 assert len(processed['UNITTEST/BTC']) == 102
# Load strategy to compare the result between Backtesting function and strategy are the same # Load strategy to compare the result between Backtesting function and strategy are the same
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
data2 = strategy.tickerdata_to_dataframe(tickerlist) processed2 = strategy.ohlcvdata_to_dataframe(data)
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC']) assert processed['UNITTEST/BTC'].equals(processed2['UNITTEST/BTC'])
def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
@ -329,10 +330,9 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
mocker.patch('freqtrade.data.history.get_timerange', get_timerange) mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock(return_value=1)) mocker.patch('freqtrade.optimize.backtesting.show_backtest_results')
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
default_conf['ticker_interval'] = '1m' default_conf['ticker_interval'] = '1m'
@ -360,10 +360,9 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
mocker.patch('freqtrade.data.history.history_utils.load_pair_history', mocker.patch('freqtrade.data.history.history_utils.load_pair_history',
MagicMock(return_value=pd.DataFrame())) MagicMock(return_value=pd.DataFrame()))
mocker.patch('freqtrade.data.history.get_timerange', get_timerange) mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock(return_value=1)) mocker.patch('freqtrade.optimize.backtesting.show_backtest_results')
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
default_conf['ticker_interval'] = "1m" default_conf['ticker_interval'] = "1m"
@ -385,10 +384,10 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
timerange = TimeRange('date', None, 1517227800, 0) timerange = TimeRange('date', None, 1517227800, 0)
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
timerange=timerange) timerange=timerange)
data_processed = backtesting.strategy.tickerdata_to_dataframe(data) processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
min_date, max_date = get_timerange(data_processed) min_date, max_date = get_timerange(processed)
results = backtesting.backtest( results = backtesting.backtest(
processed=data_processed, processed=processed,
stake_amount=default_conf['stake_amount'], stake_amount=default_conf['stake_amount'],
start_date=min_date, start_date=min_date,
end_date=max_date, end_date=max_date,
@ -416,7 +415,7 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
'sell_reason': [SellType.ROI, SellType.ROI] 'sell_reason': [SellType.ROI, SellType.ROI]
}) })
pd.testing.assert_frame_equal(results, expected) pd.testing.assert_frame_equal(results, expected)
data_pair = data_processed[pair] data_pair = processed[pair]
for _, t in results.iterrows(): for _, t in results.iterrows():
ln = data_pair.loc[data_pair["date"] == t["open_time"]] ln = data_pair.loc[data_pair["date"] == t["open_time"]]
# Check open trade rate alignes to open rate # Check open trade rate alignes to open rate
@ -439,7 +438,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker, testdatadir) -
timerange = TimeRange.parse_timerange('1510688220-1510700340') timerange = TimeRange.parse_timerange('1510688220-1510700340')
data = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'], data = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'],
timerange=timerange) timerange=timerange)
processed = backtesting.strategy.tickerdata_to_dataframe(data) processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
results = backtesting.backtest( results = backtesting.backtest(
processed=processed, processed=processed,
@ -458,7 +457,7 @@ def test_processed(default_conf, mocker, testdatadir) -> None:
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
dict_of_tickerrows = load_data_test('raise', testdatadir) dict_of_tickerrows = load_data_test('raise', testdatadir)
dataframes = backtesting.strategy.tickerdata_to_dataframe(dict_of_tickerrows) dataframes = backtesting.strategy.ohlcvdata_to_dataframe(dict_of_tickerrows)
dataframe = dataframes['UNITTEST/BTC'] dataframe = dataframes['UNITTEST/BTC']
cols = dataframe.columns cols = dataframe.columns
# assert the dataframe got some of the indicator columns # assert the dataframe got some of the indicator columns
@ -508,7 +507,6 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir):
def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch('freqtrade.optimize.backtesting.file_dump_json', MagicMock())
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
pair='UNITTEST/BTC', datadir=testdatadir) pair='UNITTEST/BTC', datadir=testdatadir)
default_conf['ticker_interval'] = '1m' default_conf['ticker_interval'] = '1m'
@ -516,7 +514,6 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
backtesting.strategy.advise_buy = _trend_alternate # Override backtesting.strategy.advise_buy = _trend_alternate # Override
backtesting.strategy.advise_sell = _trend_alternate # Override backtesting.strategy.advise_sell = _trend_alternate # Override
results = backtesting.backtest(**backtest_conf) results = backtesting.backtest(**backtest_conf)
backtesting._store_backtest_result("test_.json", results)
# 200 candles in backtest data # 200 candles in backtest data
# won't buy on first (shifted by 1) # won't buy on first (shifted by 1)
# 100 buys signals # 100 buys signals
@ -557,10 +554,10 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
backtesting.strategy.advise_buy = _trend_alternate_hold # Override backtesting.strategy.advise_buy = _trend_alternate_hold # Override
backtesting.strategy.advise_sell = _trend_alternate_hold # Override backtesting.strategy.advise_sell = _trend_alternate_hold # Override
data_processed = backtesting.strategy.tickerdata_to_dataframe(data) processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
min_date, max_date = get_timerange(data_processed) min_date, max_date = get_timerange(processed)
backtest_conf = { backtest_conf = {
'processed': data_processed, 'processed': processed,
'stake_amount': default_conf['stake_amount'], 'stake_amount': default_conf['stake_amount'],
'start_date': min_date, 'start_date': min_date,
'end_date': max_date, 'end_date': max_date,
@ -576,7 +573,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
assert len(evaluate_result_multi(results, '5m', 3)) == 0 assert len(evaluate_result_multi(results, '5m', 3)) == 0
backtest_conf = { backtest_conf = {
'processed': data_processed, 'processed': processed,
'stake_amount': default_conf['stake_amount'], 'stake_amount': default_conf['stake_amount'],
'start_date': min_date, 'start_date': min_date,
'end_date': max_date, 'end_date': max_date,
@ -587,84 +584,12 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
assert len(evaluate_result_multi(results, '5m', 1)) == 0 assert len(evaluate_result_multi(results, '5m', 1)) == 0
def test_backtest_record(default_conf, fee, mocker):
names = []
records = []
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch(
'freqtrade.optimize.backtesting.file_dump_json',
new=lambda n, r: (names.append(n), records.append(r))
)
backtesting = Backtesting(default_conf)
results = pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC",
"UNITTEST/BTC", "UNITTEST/BTC"],
"profit_percent": [0.003312, 0.010801, 0.013803, 0.002780],
"profit_abs": [0.000003, 0.000011, 0.000014, 0.000003],
"open_time": [Arrow(2017, 11, 14, 19, 32, 00).datetime,
Arrow(2017, 11, 14, 21, 36, 00).datetime,
Arrow(2017, 11, 14, 22, 12, 00).datetime,
Arrow(2017, 11, 14, 22, 44, 00).datetime],
"close_time": [Arrow(2017, 11, 14, 21, 35, 00).datetime,
Arrow(2017, 11, 14, 22, 10, 00).datetime,
Arrow(2017, 11, 14, 22, 43, 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],
"close_index": [118, 151, 184, 199],
"trade_duration": [123, 34, 31, 14],
"open_at_end": [False, False, False, True],
"sell_reason": [SellType.ROI, SellType.STOP_LOSS,
SellType.ROI, SellType.FORCE_SELL]
})
backtesting._store_backtest_result("backtest-result.json", results)
assert len(results) == 4
# Assert file_dump_json was only called once
assert names == ['backtest-result.json']
records = records[0]
# Ensure records are of correct type
assert len(records) == 4
# reset test to test with strategy name
names = []
records = []
backtesting._store_backtest_result(Path("backtest-result.json"), results, "DefStrat")
assert len(results) == 4
# Assert file_dump_json was only called once
assert names == [Path('backtest-result-DefStrat.json')]
records = records[0]
# Ensure records are of correct type
assert len(records) == 4
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
# Below follows just a typecheck of the schema/type of trade-records
oix = None
for (pair, profit, date_buy, date_sell, buy_index, dur,
openr, closer, open_at_end, sell_reason) in records:
assert pair == 'UNITTEST/BTC'
assert isinstance(profit, float)
# FIX: buy/sell should be converted to ints
assert isinstance(date_buy, float)
assert isinstance(date_sell, float)
assert isinstance(openr, float)
assert isinstance(closer, float)
assert isinstance(open_at_end, bool)
assert isinstance(sell_reason, str)
isinstance(buy_index, pd._libs.tslib.Timestamp)
if oix:
assert buy_index > oix
oix = buy_index
assert dur > 0
def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.show_backtest_results', MagicMock())
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
@ -706,9 +631,10 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
backtestmock = MagicMock() backtestmock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
gen_table_mock = MagicMock() gen_table_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.generate_text_table', gen_table_mock) mocker.patch('freqtrade.optimize.optimize_reports.generate_text_table', gen_table_mock)
gen_strattable_mock = MagicMock() gen_strattable_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.generate_text_table_strategy', gen_strattable_mock) mocker.patch('freqtrade.optimize.optimize_reports.generate_text_table_strategy',
gen_strattable_mock)
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = [ args = [

View File

@ -540,7 +540,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
}]) }])
) )
patch_exchange(mocker) patch_exchange(mocker)
# Co-test loading ticker-interval from strategy # Co-test loading timeframe from strategy
del default_conf['ticker_interval'] del default_conf['ticker_interval']
default_conf.update({'config': 'config.json.example', default_conf.update({'config': 'config.json.example',
'hyperopt': 'DefaultHyperOpt', 'hyperopt': 'DefaultHyperOpt',
@ -550,7 +550,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
'hyperopt_jobs': 1, }) 'hyperopt_jobs': 1, })
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start() hyperopt.start()
@ -560,7 +560,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
assert dumper.called assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations # Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2 assert dumper.call_count == 2
assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
@ -646,8 +646,8 @@ def test_has_space(hyperopt, spaces, expected_results):
def test_populate_indicators(hyperopt, testdatadir) -> None: def test_populate_indicators(hyperopt, testdatadir) -> None:
tickerlist = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
dataframes = hyperopt.backtesting.strategy.tickerdata_to_dataframe(tickerlist) dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data)
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
{'pair': 'UNITTEST/BTC'}) {'pair': 'UNITTEST/BTC'})
@ -658,8 +658,8 @@ def test_populate_indicators(hyperopt, testdatadir) -> None:
def test_buy_strategy_generator(hyperopt, testdatadir) -> None: def test_buy_strategy_generator(hyperopt, testdatadir) -> None:
tickerlist = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
dataframes = hyperopt.backtesting.strategy.tickerdata_to_dataframe(tickerlist) dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data)
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
{'pair': 'UNITTEST/BTC'}) {'pair': 'UNITTEST/BTC'})
@ -799,7 +799,7 @@ def test_clean_hyperopt(mocker, default_conf, caplog):
h = Hyperopt(default_conf) h = Hyperopt(default_conf)
assert unlinkmock.call_count == 2 assert unlinkmock.call_count == 2
assert log_has(f"Removing `{h.tickerdata_pickle}`.", caplog) assert log_has(f"Removing `{h.data_pickle_file}`.", caplog)
def test_continue_hyperopt(mocker, default_conf, caplog): def test_continue_hyperopt(mocker, default_conf, caplog):
@ -861,7 +861,7 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
}) })
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start() hyperopt.start()
@ -875,7 +875,7 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
) )
assert result_str in out # noqa: E501 assert result_str in out # noqa: E501
assert dumper.called assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations # Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2 assert dumper.call_count == 2
@ -919,7 +919,7 @@ def test_print_json_spaces_default(mocker, default_conf, caplog, capsys) -> None
}) })
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start() hyperopt.start()
@ -929,7 +929,7 @@ def test_print_json_spaces_default(mocker, default_conf, caplog, capsys) -> None
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert '{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi":{},"stoploss":null}' in out # noqa: E501 assert '{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi":{},"stoploss":null}' in out # noqa: E501
assert dumper.called assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations # Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2 assert dumper.call_count == 2
@ -969,7 +969,7 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) ->
}) })
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start() hyperopt.start()
@ -979,7 +979,7 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) ->
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert '{"minimal_roi":{},"stoploss":null}' in out assert '{"minimal_roi":{},"stoploss":null}' in out
assert dumper.called assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations # Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2 assert dumper.call_count == 2
@ -1016,7 +1016,7 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys)
'hyperopt_jobs': 1, }) 'hyperopt_jobs': 1, })
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
del hyperopt.custom_hyperopt.__class__.buy_strategy_generator del hyperopt.custom_hyperopt.__class__.buy_strategy_generator
@ -1031,7 +1031,7 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys)
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
assert dumper.called assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations # Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2 assert dumper.call_count == 2
assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
@ -1059,7 +1059,7 @@ def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) -
'hyperopt_jobs': 1, }) 'hyperopt_jobs': 1, })
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
del hyperopt.custom_hyperopt.__class__.buy_strategy_generator del hyperopt.custom_hyperopt.__class__.buy_strategy_generator
@ -1104,7 +1104,7 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None:
'hyperopt_jobs': 1, }) 'hyperopt_jobs': 1, })
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
# TODO: sell_strategy_generator() is actually not called because # TODO: sell_strategy_generator() is actually not called because
@ -1119,7 +1119,7 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None:
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
assert dumper.called assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations # Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2 assert dumper.call_count == 2
assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
@ -1161,7 +1161,7 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None
'hyperopt_jobs': 1, }) 'hyperopt_jobs': 1, })
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
# TODO: buy_strategy_generator() is actually not called because # TODO: buy_strategy_generator() is actually not called because
@ -1176,7 +1176,7 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
assert dumper.called assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations # Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2 assert dumper.call_count == 2
assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
@ -1210,7 +1210,7 @@ def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, metho
'hyperopt_jobs': 1, }) 'hyperopt_jobs': 1, })
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
delattr(hyperopt.custom_hyperopt.__class__, method) delattr(hyperopt.custom_hyperopt.__class__, method)

View File

@ -1,10 +1,14 @@
from pathlib import Path
import pandas as pd import pandas as pd
from arrow import Arrow
from freqtrade.edge import PairInfo from freqtrade.edge import PairInfo
from freqtrade.optimize.optimize_reports import ( from freqtrade.optimize.optimize_reports import (
generate_edge_table, generate_text_table, generate_text_table_sell_reason, generate_edge_table, generate_text_table, generate_text_table_sell_reason,
generate_text_table_strategy) generate_text_table_strategy, store_backtest_result)
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
from tests.conftest import patch_exchange
def test_generate_text_table(default_conf, mocker): def test_generate_text_table(default_conf, mocker):
@ -61,10 +65,8 @@ def test_generate_text_table_sell_reason(default_conf, mocker):
'| stop_loss | 1 | 0 | 0 | 1 |' '| stop_loss | 1 | 0 | 0 | 1 |'
' -10 | -10 | -0.2 | -5 |' ' -10 | -10 | -0.2 | -5 |'
) )
assert generate_text_table_sell_reason( assert generate_text_table_sell_reason(stake_currency='BTC', max_open_trades=2,
data={'ETH/BTC': {}}, results=results) == result_str
stake_currency='BTC', max_open_trades=2,
results=results) == result_str
def test_generate_text_table_strategy(default_conf, mocker): def test_generate_text_table_strategy(default_conf, mocker):
@ -115,3 +117,73 @@ def test_generate_edge_table(edge_conf, mocker):
assert generate_edge_table(results).count('| ETH/BTC |') == 1 assert generate_edge_table(results).count('| ETH/BTC |') == 1
assert generate_edge_table(results).count( assert generate_edge_table(results).count(
'| Risk Reward Ratio | Required Risk Reward | Expectancy |') == 1 '| Risk Reward Ratio | Required Risk Reward | Expectancy |') == 1
def test_backtest_record(default_conf, fee, mocker):
names = []
records = []
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch(
'freqtrade.optimize.optimize_reports.file_dump_json',
new=lambda n, r: (names.append(n), records.append(r))
)
results = {'DefStrat': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC",
"UNITTEST/BTC", "UNITTEST/BTC"],
"profit_percent": [0.003312, 0.010801, 0.013803, 0.002780],
"profit_abs": [0.000003, 0.000011, 0.000014, 0.000003],
"open_time": [Arrow(2017, 11, 14, 19, 32, 00).datetime,
Arrow(2017, 11, 14, 21, 36, 00).datetime,
Arrow(2017, 11, 14, 22, 12, 00).datetime,
Arrow(2017, 11, 14, 22, 44, 00).datetime],
"close_time": [Arrow(2017, 11, 14, 21, 35, 00).datetime,
Arrow(2017, 11, 14, 22, 10, 00).datetime,
Arrow(2017, 11, 14, 22, 43, 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],
"close_index": [118, 151, 184, 199],
"trade_duration": [123, 34, 31, 14],
"open_at_end": [False, False, False, True],
"sell_reason": [SellType.ROI, SellType.STOP_LOSS,
SellType.ROI, SellType.FORCE_SELL]
})}
store_backtest_result(Path("backtest-result.json"), results)
# Assert file_dump_json was only called once
assert names == [Path('backtest-result.json')]
records = records[0]
# Ensure records are of correct type
assert len(records) == 4
# reset test to test with strategy name
names = []
records = []
results['Strat'] = pd.DataFrame()
store_backtest_result(Path("backtest-result.json"), results)
# Assert file_dump_json was only called once
assert names == [Path('backtest-result-DefStrat.json')]
records = records[0]
# Ensure records are of correct type
assert len(records) == 4
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
# Below follows just a typecheck of the schema/type of trade-records
oix = None
for (pair, profit, date_buy, date_sell, buy_index, dur,
openr, closer, open_at_end, sell_reason) in records:
assert pair == 'UNITTEST/BTC'
assert isinstance(profit, float)
# FIX: buy/sell should be converted to ints
assert isinstance(date_buy, float)
assert isinstance(date_sell, float)
assert isinstance(openr, float)
assert isinstance(closer, float)
assert isinstance(open_at_end, bool)
assert isinstance(sell_reason, str)
isinstance(buy_index, pd._libs.tslib.Timestamp)
if oix:
assert buy_index > oix
oix = buy_index
assert dur > 0

View File

@ -68,7 +68,7 @@ class DefaultStrategy(IStrategy):
Performance Note: For the best performance be frugal on the number of indicators Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage. or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() :param dataframe: Dataframe with data from the exchange
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies :return: a Dataframe with all mandatory indicators for the strategies
""" """

View File

@ -4,94 +4,148 @@ import logging
from unittest.mock import MagicMock from unittest.mock import MagicMock
import arrow import arrow
import pytest
from pandas import DataFrame from pandas import DataFrame
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.history import load_data from freqtrade.data.history import load_data
from freqtrade.exceptions import DependencyException
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
from .strats.default_strategy import DefaultStrategy
from tests.conftest import get_patched_exchange, log_has from tests.conftest import get_patched_exchange, log_has
from .strats.default_strategy import DefaultStrategy
# Avoid to reinit the same object again and again # Avoid to reinit the same object again and again
_STRATEGY = DefaultStrategy(config={}) _STRATEGY = DefaultStrategy(config={})
def test_returns_latest_buy_signal(mocker, default_conf, ticker_history): def test_returns_latest_signal(mocker, default_conf, ohlcv_history):
mocker.patch.object( ohlcv_history.loc[1, 'date'] = arrow.utcnow()
_STRATEGY, '_analyze_ticker_internal', # Take a copy to correctly modify the call
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}]) mocked_history = ohlcv_history.copy()
) mocked_history['sell'] = 0
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False) mocked_history['buy'] = 0
mocked_history.loc[1, 'sell'] = 1
mocker.patch.object( mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal', _STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}]) return_value=mocked_history
)
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True)
def test_returns_latest_sell_signal(mocker, default_conf, ticker_history):
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
) )
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True) assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, True)
mocked_history.loc[1, 'sell'] = 0
mocked_history.loc[1, 'buy'] = 1
mocker.patch.object( mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal', _STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}]) return_value=mocked_history
) )
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False) assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (True, False)
mocked_history.loc[1, 'sell'] = 0
mocked_history.loc[1, 'buy'] = 0
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
return_value=mocked_history
)
assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, False)
def test_get_signal_empty(default_conf, mocker, caplog): def test_get_signal_empty(default_conf, mocker, caplog):
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
DataFrame()) DataFrame())
assert log_has('Empty ticker history for pair foo', caplog) assert log_has('Empty candle (OHLCV) data for pair foo', caplog)
caplog.clear() caplog.clear()
assert (False, False) == _STRATEGY.get_signal('bar', default_conf['ticker_interval'], assert (False, False) == _STRATEGY.get_signal('bar', default_conf['ticker_interval'],
[]) [])
assert log_has('Empty ticker history for pair bar', caplog) assert log_has('Empty candle (OHLCV) data for pair bar', caplog)
def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_history): def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
mocker.patch.object( mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal', _STRATEGY, '_analyze_ticker_internal',
side_effect=ValueError('xyz') side_effect=ValueError('xyz')
) )
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
ticker_history) ohlcv_history)
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog) assert log_has('Unable to analyze candle (OHLCV) data for pair foo: xyz', caplog)
def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history): def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
mocker.patch.object( mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal', _STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame([]) return_value=DataFrame([])
) )
mocker.patch.object(_STRATEGY, 'assert_df')
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
ticker_history) ohlcv_history)
assert log_has('Empty dataframe for pair xyz', caplog) assert log_has('Empty dataframe for pair xyz', caplog)
def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history): def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO)
# default_conf defines a 5m interval. we check interval * 2 + 5m # default_conf defines a 5m interval. we check interval * 2 + 5m
# this is necessary as the last candle is removed (partial candles) by default # this is necessary as the last candle is removed (partial candles) by default
oldtime = arrow.utcnow().shift(minutes=-16) ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16)
ticks = DataFrame([{'buy': 1, 'date': oldtime}]) # Take a copy to correctly modify the call
mocked_history = ohlcv_history.copy()
mocked_history['sell'] = 0
mocked_history['buy'] = 0
mocked_history.loc[1, 'buy'] = 1
caplog.set_level(logging.INFO)
mocker.patch.object( mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal', _STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame(ticks) return_value=mocked_history
)
mocker.patch.object(_STRATEGY, 'assert_df')
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
ohlcv_history)
assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog)
def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history):
# default_conf defines a 5m interval. we check interval * 2 + 5m
# this is necessary as the last candle is removed (partial candles) by default
ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16)
# Take a copy to correctly modify the call
mocked_history = ohlcv_history.copy()
mocked_history['sell'] = 0
mocked_history['buy'] = 0
mocked_history.loc[1, 'buy'] = 1
caplog.set_level(logging.INFO)
mocker.patch.object(
_STRATEGY, 'assert_df',
side_effect=DependencyException('Dataframe returned...')
) )
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
ticker_history) ohlcv_history)
assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) assert log_has('Unable to analyze candle (OHLCV) data for pair xyz: Dataframe returned...',
caplog)
def test_assert_df(default_conf, mocker, ohlcv_history):
# Ensure it's running when passed correctly
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date'])
with pytest.raises(DependencyException, match=r"Dataframe returned from strategy.*length\."):
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history) + 1,
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date'])
with pytest.raises(DependencyException,
match=r"Dataframe returned from strategy.*last close price\."):
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
ohlcv_history.loc[1, 'close'] + 0.01, ohlcv_history.loc[1, 'date'])
with pytest.raises(DependencyException,
match=r"Dataframe returned from strategy.*last date\."):
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[0, 'date'])
def test_get_signal_handles_exceptions(mocker, default_conf): def test_get_signal_handles_exceptions(mocker, default_conf):
@ -103,15 +157,15 @@ def test_get_signal_handles_exceptions(mocker, default_conf):
assert _STRATEGY.get_signal(exchange, 'ETH/BTC', '5m') == (False, False) assert _STRATEGY.get_signal(exchange, 'ETH/BTC', '5m') == (False, False)
def test_tickerdata_to_dataframe(default_conf, testdatadir) -> None: def test_ohlcvdata_to_dataframe(default_conf, testdatadir) -> None:
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
timerange = TimeRange.parse_timerange('1510694220-1510700340') timerange = TimeRange.parse_timerange('1510694220-1510700340')
tickerlist = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
fill_up_missing=True) fill_up_missing=True)
data = strategy.tickerdata_to_dataframe(tickerlist) processed = strategy.ohlcvdata_to_dataframe(data)
assert len(data['UNITTEST/BTC']) == 102 # partial candle was removed assert len(processed['UNITTEST/BTC']) == 102 # partial candle was removed
def test_min_roi_reached(default_conf, fee) -> None: def test_min_roi_reached(default_conf, fee) -> None:
@ -222,7 +276,7 @@ def test_min_roi_reached3(default_conf, fee) -> None:
assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime) assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime)
def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x) ind_mock = MagicMock(side_effect=lambda x, meta: x)
buy_mock = MagicMock(side_effect=lambda x, meta: x) buy_mock = MagicMock(side_effect=lambda x, meta: x)
@ -235,7 +289,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
) )
strategy = DefaultStrategy({}) strategy = DefaultStrategy({})
strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'}) strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'})
assert ind_mock.call_count == 1 assert ind_mock.call_count == 1
assert buy_mock.call_count == 1 assert buy_mock.call_count == 1
assert buy_mock.call_count == 1 assert buy_mock.call_count == 1
@ -244,7 +298,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
caplog.clear() caplog.clear()
strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'}) strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'})
# No analysis happens as process_only_new_candles is true # No analysis happens as process_only_new_candles is true
assert ind_mock.call_count == 2 assert ind_mock.call_count == 2
assert buy_mock.call_count == 2 assert buy_mock.call_count == 2
@ -253,7 +307,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) -> None: def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x) ind_mock = MagicMock(side_effect=lambda x, meta: x)
buy_mock = MagicMock(side_effect=lambda x, meta: x) buy_mock = MagicMock(side_effect=lambda x, meta: x)
@ -268,7 +322,7 @@ def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) -
strategy = DefaultStrategy({}) strategy = DefaultStrategy({})
strategy.process_only_new_candles = True strategy.process_only_new_candles = True
ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'}) ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'})
assert 'high' in ret.columns assert 'high' in ret.columns
assert 'low' in ret.columns assert 'low' in ret.columns
assert 'close' in ret.columns assert 'close' in ret.columns
@ -280,7 +334,7 @@ def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) -
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
caplog.clear() caplog.clear()
ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'}) ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'})
# No analysis happens as process_only_new_candles is true # No analysis happens as process_only_new_candles is true
assert ind_mock.call_count == 1 assert ind_mock.call_count == 1
assert buy_mock.call_count == 1 assert buy_mock.call_count == 1

View File

@ -18,7 +18,7 @@ from freqtrade.configuration.config_validation import validate_config_schema
from freqtrade.configuration.deprecated_settings import ( from freqtrade.configuration.deprecated_settings import (
check_conflicting_settings, process_deprecated_setting, check_conflicting_settings, process_deprecated_setting,
process_temporary_deprecated_settings) process_temporary_deprecated_settings)
from freqtrade.configuration.load_config import load_config_file from freqtrade.configuration.load_config import load_config_file, log_config_error_range
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.loggers import _set_loggers, setup_logging from freqtrade.loggers import _set_loggers, setup_logging
@ -66,6 +66,30 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
assert validated_conf.items() >= default_conf.items() assert validated_conf.items() >= default_conf.items()
def test_load_config_file_error(default_conf, mocker, caplog) -> None:
del default_conf['user_data_dir']
filedata = json.dumps(default_conf).replace(
'"stake_amount": 0.001,', '"stake_amount": .001,')
mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(read_data=filedata))
mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata))
with pytest.raises(OperationalException, match=f".*Please verify the following segment.*"):
load_config_file('somefile')
def test_load_config_file_error_range(default_conf, mocker, caplog) -> None:
del default_conf['user_data_dir']
filedata = json.dumps(default_conf).replace(
'"stake_amount": 0.001,', '"stake_amount": .001,')
mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata))
x = log_config_error_range('somefile', 'Parse error at offset 64: Invalid value.')
assert isinstance(x, str)
assert (x == '{"max_open_trades": 1, "stake_currency": "BTC", '
'"stake_amount": .001, "fiat_display_currency": "USD", '
'"ticker_interval": "5m", "dry_run": true, ')
def test__args_to_config(caplog): def test__args_to_config(caplog):
arg_list = ['trade', '--strategy-path', 'TestTest'] arg_list = ['trade', '--strategy-path', 'TestTest']
@ -73,6 +97,7 @@ def test__args_to_config(caplog):
configuration = Configuration(args) configuration = Configuration(args)
config = {} config = {}
with warnings.catch_warnings(record=True) as w: with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
# No warnings ... # No warnings ...
configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef") configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef")
assert len(w) == 0 assert len(w) == 0
@ -82,6 +107,7 @@ def test__args_to_config(caplog):
configuration = Configuration(args) configuration = Configuration(args)
config = {} config = {}
with warnings.catch_warnings(record=True) as w: with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
# Deprecation warnings! # Deprecation warnings!
configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef", configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef",
deprecated_msg="Going away soon!") deprecated_msg="Going away soon!")

View File

@ -2228,10 +2228,16 @@ def test_handle_timedout_limit_buy(mocker, default_conf, limit_buy_order) -> Non
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
def test_handle_timedout_limit_buy_corder_empty(mocker, default_conf, limit_buy_order) -> None: @pytest.mark.parametrize('cancelorder', [
{},
'String Return value',
123
])
def test_handle_timedout_limit_buy_corder_empty(mocker, default_conf, limit_buy_order,
cancelorder) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
cancel_order_mock = MagicMock(return_value={}) cancel_order_mock = MagicMock(return_value=cancelorder)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
cancel_order=cancel_order_mock cancel_order=cancel_order_mock

View File

@ -6,7 +6,7 @@ from unittest.mock import MagicMock
import pytest import pytest
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json, from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json,
file_load_json, format_ms_time, pair_to_filename, file_load_json, format_ms_time, pair_to_filename,
plural, shorten_date) plural, shorten_date)
@ -18,9 +18,9 @@ def test_shorten_date() -> None:
assert shorten_date(str_data) == str_shorten_data assert shorten_date(str_data) == str_shorten_data
def test_datesarray_to_datetimearray(ticker_history_list): def test_datesarray_to_datetimearray(ohlcv_history_list):
dataframes = parse_ticker_dataframe(ticker_history_list, "5m", pair="UNITTEST/BTC", dataframes = ohlcv_to_dataframe(ohlcv_history_list, "5m", pair="UNITTEST/BTC",
fill_missing=True) fill_missing=True)
dates = datesarray_to_datetimearray(dataframes['date']) dates = datesarray_to_datetimearray(dataframes['date'])
assert isinstance(dates[0], datetime.datetime) assert isinstance(dates[0], datetime.datetime)

View File

@ -476,12 +476,22 @@ def test_migrate_old(mocker, default_conf, fee):
stake=default_conf.get("stake_amount"), stake=default_conf.get("stake_amount"),
amount=amount amount=amount
) )
insert_table_old2 = """INSERT INTO trades (exchange, pair, is_open, fee,
open_rate, close_rate, stake_amount, amount, open_date)
VALUES ('BITTREX', 'BTC_ETC', 0, {fee},
0.00258580, 0.00268580, {stake}, {amount},
'2017-11-28 12:44:24.000000')
""".format(fee=fee.return_value,
stake=default_conf.get("stake_amount"),
amount=amount
)
engine = create_engine('sqlite://') engine = create_engine('sqlite://')
mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine) mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
# 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)
engine.execute(insert_table_old2)
# Run init to test migration # Run init to test migration
init(default_conf['db_url'], default_conf['dry_run']) init(default_conf['db_url'], default_conf['dry_run'])
@ -500,6 +510,15 @@ def test_migrate_old(mocker, default_conf, fee):
assert trade.stop_loss == 0.0 assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0 assert trade.initial_stop_loss == 0.0
assert trade.open_trade_price == trade._calc_open_trade_price() assert trade.open_trade_price == trade._calc_open_trade_price()
assert trade.close_profit_abs is None
trade = Trade.query.filter(Trade.id == 2).first()
assert trade.close_rate is not None
assert trade.is_open == 0
assert trade.open_rate_requested is None
assert trade.close_rate_requested is None
assert trade.close_rate is not None
assert pytest.approx(trade.close_profit_abs) == trade.calc_profit()
def test_migrate_new(mocker, default_conf, fee, caplog): def test_migrate_new(mocker, default_conf, fee, caplog):
@ -583,6 +602,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert log_has("trying trades_bak2", caplog) assert log_has("trying trades_bak2", caplog)
assert log_has("Running database migration - backup available as trades_bak2", caplog) assert log_has("Running database migration - backup available as trades_bak2", caplog)
assert trade.open_trade_price == trade._calc_open_trade_price() assert trade.open_trade_price == trade._calc_open_trade_price()
assert trade.close_profit_abs is None
def test_migrate_mid_state(mocker, default_conf, fee, caplog): def test_migrate_mid_state(mocker, default_conf, fee, caplog):

View File

@ -49,17 +49,17 @@ def test_init_plotscript(default_conf, mocker, testdatadir):
default_conf['trade_source'] = "file" default_conf['trade_source'] = "file"
default_conf['ticker_interval'] = "5m" default_conf['ticker_interval'] = "5m"
default_conf["datadir"] = testdatadir default_conf["datadir"] = testdatadir
default_conf['exportfilename'] = str(testdatadir / "backtest-result_test.json") default_conf['exportfilename'] = testdatadir / "backtest-result_test.json"
ret = init_plotscript(default_conf) ret = init_plotscript(default_conf)
assert "tickers" in ret assert "ohlcv" in ret
assert "trades" in ret assert "trades" in ret
assert "pairs" in ret assert "pairs" in ret
default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"] default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"]
ret = init_plotscript(default_conf) ret = init_plotscript(default_conf)
assert "tickers" in ret assert "ohlcv" in ret
assert "TRX/BTC" in ret["tickers"] assert "TRX/BTC" in ret["ohlcv"]
assert "ADA/BTC" in ret["tickers"] assert "ADA/BTC" in ret["ohlcv"]
def test_add_indicators(default_conf, testdatadir, caplog): def test_add_indicators(default_conf, testdatadir, caplog):
@ -269,14 +269,14 @@ def test_generate_profit_graph(testdatadir):
pairs = ["TRX/BTC", "ADA/BTC"] pairs = ["TRX/BTC", "ADA/BTC"]
trades = trades[trades['close_time'] < pd.Timestamp('2018-01-12', tz='UTC')] trades = trades[trades['close_time'] < pd.Timestamp('2018-01-12', tz='UTC')]
tickers = history.load_data(datadir=testdatadir, data = history.load_data(datadir=testdatadir,
pairs=pairs, pairs=pairs,
timeframe='5m', timeframe='5m',
timerange=timerange timerange=timerange)
)
trades = trades[trades['pair'].isin(pairs)] trades = trades[trades['pair'].isin(pairs)]
fig = generate_profit_graph(pairs, tickers, trades, timeframe="5m") fig = generate_profit_graph(pairs, data, trades, timeframe="5m")
assert isinstance(fig, go.Figure) assert isinstance(fig, go.Figure)
assert fig.layout.title.text == "Freqtrade Profit plot" assert fig.layout.title.text == "Freqtrade Profit plot"
@ -318,7 +318,7 @@ def test_start_plot_dataframe(mocker):
def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir): def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir):
default_conf['trade_source'] = 'file' default_conf['trade_source'] = 'file'
default_conf["datadir"] = testdatadir default_conf["datadir"] = testdatadir
default_conf['exportfilename'] = str(testdatadir / "backtest-result_test.json") default_conf['exportfilename'] = testdatadir / "backtest-result_test.json"
default_conf['indicators1'] = ["sma5", "ema10"] default_conf['indicators1'] = ["sma5", "ema10"]
default_conf['indicators2'] = ["macd"] default_conf['indicators2'] = ["macd"]
default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"] default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"]
@ -374,7 +374,7 @@ def test_start_plot_profit_error(mocker):
def test_plot_profit(default_conf, mocker, testdatadir, caplog): def test_plot_profit(default_conf, mocker, testdatadir, caplog):
default_conf['trade_source'] = 'file' default_conf['trade_source'] = 'file'
default_conf["datadir"] = testdatadir default_conf["datadir"] = testdatadir
default_conf['exportfilename'] = str(testdatadir / "backtest-result_test.json") default_conf['exportfilename'] = testdatadir / "backtest-result_test.json"
default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"] default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"]
profit_mock = MagicMock() profit_mock = MagicMock()