Merge pull request #894 from freqtrade/feature/force_close_backtest
Display open trades after backtest period
This commit is contained in:
commit
a5511e2e30
@ -1,17 +1,19 @@
|
|||||||
# Backtesting
|
# Backtesting
|
||||||
|
|
||||||
This page explains how to validate your strategy performance by using
|
This page explains how to validate your strategy performance by using
|
||||||
Backtesting.
|
Backtesting.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Test your strategy with Backtesting](#test-your-strategy-with-backtesting)
|
- [Test your strategy with Backtesting](#test-your-strategy-with-backtesting)
|
||||||
- [Understand the backtesting result](#understand-the-backtesting-result)
|
- [Understand the backtesting result](#understand-the-backtesting-result)
|
||||||
|
|
||||||
## Test your strategy with Backtesting
|
## Test your strategy with Backtesting
|
||||||
|
|
||||||
Now you have good Buy and Sell strategies, you want to test it against
|
Now you have good Buy and Sell strategies, you want to test it against
|
||||||
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 (pair) from your config file
|
Backtesting will use the crypto-currencies (pair) from your config file
|
||||||
and load static tickers located in
|
and load static tickers located in
|
||||||
[/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata).
|
[/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata).
|
||||||
@ -19,70 +21,80 @@ If the 5 min and 1 min ticker for the crypto-currencies to test is not
|
|||||||
already in the `testdata` folder, backtesting will download them
|
already in the `testdata` folder, backtesting will download them
|
||||||
automatically. Testdata files will not be updated until you specify it.
|
automatically. Testdata files will not be updated until you specify it.
|
||||||
|
|
||||||
The result of backtesting will confirm you if your bot as more chance to
|
The result of backtesting will confirm you if your bot has better odds of making a profit than a loss.
|
||||||
make a profit than a loss.
|
|
||||||
|
|
||||||
|
|
||||||
The backtesting is very easy with freqtrade.
|
The backtesting is very easy with freqtrade.
|
||||||
|
|
||||||
### 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 tickers (Per default)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --realistic-simulation
|
python3 ./freqtrade/main.py backtesting --realistic-simulation
|
||||||
```
|
```
|
||||||
|
|
||||||
**With 1 min tickers**
|
#### With 1 min tickers
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1m
|
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1m
|
||||||
```
|
```
|
||||||
|
|
||||||
**Update cached pairs with the latest data**
|
#### Update cached pairs with the latest data
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --realistic-simulation --refresh-pairs-cached
|
python3 ./freqtrade/main.py backtesting --realistic-simulation --refresh-pairs-cached
|
||||||
```
|
```
|
||||||
|
|
||||||
**With live data (do not alter your testdata files)**
|
#### With live data (do not alter your testdata files)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --realistic-simulation --live
|
python3 ./freqtrade/main.py backtesting --realistic-simulation --live
|
||||||
```
|
```
|
||||||
|
|
||||||
**Using a different on-disk ticker-data source**
|
#### Using a different on-disk ticker-data source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101
|
python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101
|
||||||
```
|
```
|
||||||
|
|
||||||
**With a (custom) strategy file**
|
#### With a (custom) strategy file
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py -s TestStrategy backtesting
|
python3 ./freqtrade/main.py -s TestStrategy backtesting
|
||||||
```
|
```
|
||||||
|
|
||||||
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory
|
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory
|
||||||
|
|
||||||
**Exporting trades to file**
|
#### Exporting trades to file
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --export trades
|
python3 ./freqtrade/main.py backtesting --export trades
|
||||||
```
|
```
|
||||||
|
|
||||||
**Exporting trades to file specifying a custom filename**
|
#### Exporting trades to file specifying a custom filename
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --export trades --export-filename=backtest_teststrategy.json
|
python3 ./freqtrade/main.py backtesting --export trades --export-filename=backtest_teststrategy.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Running backtest with smaller testset
|
||||||
|
|
||||||
**Running backtest with smaller testset**
|
|
||||||
Use the `--timerange` argument to change how much of the testset
|
Use the `--timerange` argument to change how much of the testset
|
||||||
you want to use. The last N ticks/timeframes will be used.
|
you want to use. The last N ticks/timeframes will be used.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --timerange=-200
|
python3 ./freqtrade/main.py backtesting --timerange=-200
|
||||||
```
|
```
|
||||||
|
|
||||||
***Advanced use of timerange***
|
#### Advanced use of timerange
|
||||||
|
|
||||||
Doing `--timerange=-200` will get the last 200 timeframes
|
Doing `--timerange=-200` will get the last 200 timeframes
|
||||||
from your inputdata. You can also specify specific dates,
|
from your inputdata. You can also specify specific dates,
|
||||||
or a range span indexed by start and stop.
|
or a range span indexed by start and stop.
|
||||||
|
|
||||||
The full timerange specification:
|
The full timerange specification:
|
||||||
|
|
||||||
- Use last 123 tickframes of data: `--timerange=-123`
|
- Use last 123 tickframes of data: `--timerange=-123`
|
||||||
- Use first 123 tickframes of data: `--timerange=123-`
|
- Use first 123 tickframes of data: `--timerange=123-`
|
||||||
- Use tickframes from line 123 through 456: `--timerange=123-456`
|
- Use tickframes from line 123 through 456: `--timerange=123-456`
|
||||||
@ -92,11 +104,12 @@ The full timerange specification:
|
|||||||
- Use tickframes between POSIX timestamps 1527595200 1527618600:
|
- Use tickframes between POSIX timestamps 1527595200 1527618600:
|
||||||
`--timerange=1527595200-1527618600`
|
`--timerange=1527595200-1527618600`
|
||||||
|
|
||||||
|
#### Downloading new set of ticker data
|
||||||
|
|
||||||
**Downloading new set of ticker data**
|
|
||||||
To download new set of backtesting ticker data, you can use a download script.
|
To download new set of backtesting ticker data, you can use a download script.
|
||||||
|
|
||||||
If you are using Binance for example:
|
If you are using Binance for example:
|
||||||
|
|
||||||
- create a folder `user_data/data/binance` and copy `pairs.json` in that folder.
|
- create a folder `user_data/data/binance` and copy `pairs.json` in that folder.
|
||||||
- update the `pairs.json` to contain the currency pairs you are interested in.
|
- update the `pairs.json` to contain the currency pairs you are interested in.
|
||||||
|
|
||||||
@ -119,33 +132,55 @@ This will download ticker data for all the currency pairs you defined in `pairs.
|
|||||||
- To download ticker data for only 10 days, use `--days 10`.
|
- To download ticker data for only 10 days, use `--days 10`.
|
||||||
- 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 which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
|
||||||
|
|
||||||
|
For help about backtesting usage, please refer to [Backtesting commands](#backtesting-commands).
|
||||||
For help about backtesting usage, please refer to
|
|
||||||
[Backtesting commands](#backtesting-commands).
|
|
||||||
|
|
||||||
## Understand the backtesting result
|
## Understand the backtesting result
|
||||||
|
|
||||||
The most important in the backtesting is to understand the result.
|
The most important in the backtesting is to understand the result.
|
||||||
|
|
||||||
A backtesting result will look like that:
|
A backtesting result will look like that:
|
||||||
|
|
||||||
```
|
```
|
||||||
====================== BACKTESTING REPORT ================================
|
======================================== BACKTESTING REPORT =========================================
|
||||||
pair buy count avg profit % total profit BTC avg duration
|
| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss |
|
||||||
-------- ----------- -------------- ------------------ --------------
|
|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:|
|
||||||
ETH/BTC 56 -0.67 -0.00075455 62.3
|
| ETH/BTC | 44 | 0.18 | 0.00159118 | 50.9 | 44 | 0 |
|
||||||
LTC/BTC 38 -0.48 -0.00036315 57.9
|
| LTC/BTC | 27 | 0.10 | 0.00051931 | 103.1 | 26 | 1 |
|
||||||
ETC/BTC 42 -1.15 -0.00096469 67.0
|
| ETC/BTC | 24 | 0.05 | 0.00022434 | 166.0 | 22 | 2 |
|
||||||
DASH/BTC 72 -0.62 -0.00089368 39.9
|
| DASH/BTC | 29 | 0.18 | 0.00103223 | 192.2 | 29 | 0 |
|
||||||
ZEC/BTC 45 -0.46 -0.00041387 63.2
|
| ZEC/BTC | 65 | -0.02 | -0.00020621 | 202.7 | 62 | 3 |
|
||||||
XLM/BTC 24 -0.88 -0.00041846 47.7
|
| XLM/BTC | 35 | 0.02 | 0.00012877 | 242.4 | 32 | 3 |
|
||||||
NXT/BTC 24 0.68 0.00031833 40.2
|
| BCH/BTC | 12 | 0.62 | 0.00149284 | 50.0 | 12 | 0 |
|
||||||
POWR/BTC 35 0.98 0.00064887 45.3
|
| POWR/BTC | 21 | 0.26 | 0.00108215 | 134.8 | 21 | 0 |
|
||||||
ADA/BTC 43 -0.39 -0.00032292 55.0
|
| ADA/BTC | 54 | -0.19 | -0.00205202 | 191.3 | 47 | 7 |
|
||||||
XMR/BTC 40 -0.40 -0.00032181 47.4
|
| XMR/BTC | 24 | -0.43 | -0.00206013 | 120.6 | 20 | 4 |
|
||||||
TOTAL 419 -0.41 -0.00348593 52.9
|
| TOTAL | 335 | 0.03 | 0.00175246 | 157.9 | 315 | 20 |
|
||||||
|
2018-06-13 06:57:27,347 - freqtrade.optimize.backtesting - INFO -
|
||||||
|
====================================== LEFT OPEN TRADES REPORT ======================================
|
||||||
|
| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss |
|
||||||
|
|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:|
|
||||||
|
| ETH/BTC | 3 | 0.16 | 0.00009619 | 25.0 | 3 | 0 |
|
||||||
|
| LTC/BTC | 1 | -1.00 | -0.00020118 | 1085.0 | 0 | 1 |
|
||||||
|
| ETC/BTC | 2 | -1.80 | -0.00071933 | 1092.5 | 0 | 2 |
|
||||||
|
| DASH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
|
||||||
|
| ZEC/BTC | 3 | -4.27 | -0.00256826 | 1301.7 | 0 | 3 |
|
||||||
|
| XLM/BTC | 3 | -1.11 | -0.00066744 | 965.0 | 0 | 3 |
|
||||||
|
| BCH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
|
||||||
|
| POWR/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
|
||||||
|
| ADA/BTC | 7 | -3.58 | -0.00503604 | 850.0 | 0 | 7 |
|
||||||
|
| XMR/BTC | 4 | -3.79 | -0.00303456 | 291.2 | 0 | 4 |
|
||||||
|
| TOTAL | 23 | -2.63 | -0.01213062 | 750.4 | 3 | 20 |
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The 1st table will contain all trades the bot made.
|
||||||
|
|
||||||
|
The 2nd table will contain all trades the bot had to `forcesell` at the end of the backtest period to prsent a full picture.
|
||||||
|
These trades are also included in the first table, but are extracted separately for clarity.
|
||||||
|
|
||||||
The last line will give you the overall performance of your strategy,
|
The last line will give you the overall performance of your strategy,
|
||||||
here:
|
here:
|
||||||
|
|
||||||
```
|
```
|
||||||
TOTAL 419 -0.41 -0.00348593 52.9
|
TOTAL 419 -0.41 -0.00348593 52.9
|
||||||
```
|
```
|
||||||
@ -161,6 +196,7 @@ strategy, your sell strategy, and also by the `minimal_roi` and
|
|||||||
As for an example if your minimal_roi is only `"0": 0.01`. You cannot
|
As for an example if your minimal_roi is only `"0": 0.01`. You cannot
|
||||||
expect the bot to make more profit than 1% (because it will sell every
|
expect the bot to make more profit than 1% (because it will sell every
|
||||||
time a trade will reach 1%).
|
time a trade will reach 1%).
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"0": 0.01
|
"0": 0.01
|
||||||
@ -173,6 +209,7 @@ profit. Hence, keep in mind that your performance is a mix of your
|
|||||||
strategies, your configuration, and the crypto-currency you have set up.
|
strategies, your configuration, and the crypto-currency you have set up.
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
|
|
||||||
Great, your strategy is profitable. What if the bot can give your the
|
Great, your strategy is profitable. What if the bot can give your the
|
||||||
optimal parameters to use for your strategy?
|
optimal parameters to use for your strategy?
|
||||||
Your next step is to learn [how to find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
Your next step is to learn [how to find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||||
|
@ -6,7 +6,8 @@ This module contains the backtesting logic
|
|||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from typing import Dict, Tuple, Any, List, Optional
|
from datetime import datetime
|
||||||
|
from typing import Dict, Tuple, Any, List, Optional, NamedTuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@ -23,6 +24,21 @@ from freqtrade.persistence import Trade
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BacktestResult(NamedTuple):
|
||||||
|
"""
|
||||||
|
NamedTuple Defining BacktestResults inputs.
|
||||||
|
"""
|
||||||
|
pair: str
|
||||||
|
profit_percent: float
|
||||||
|
profit_abs: float
|
||||||
|
open_time: datetime
|
||||||
|
close_time: datetime
|
||||||
|
open_index: int
|
||||||
|
close_index: int
|
||||||
|
trade_duration: float
|
||||||
|
open_at_end: bool
|
||||||
|
|
||||||
|
|
||||||
class Backtesting(object):
|
class Backtesting(object):
|
||||||
"""
|
"""
|
||||||
Backtesting class, this class contains all the logic to run a backtest
|
Backtesting class, this class contains all the logic to run a backtest
|
||||||
@ -73,15 +89,15 @@ class Backtesting(object):
|
|||||||
headers = ['pair', 'buy count', 'avg profit %',
|
headers = ['pair', 'buy count', 'avg profit %',
|
||||||
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
||||||
for pair in data:
|
for pair in data:
|
||||||
result = results[results.currency == pair]
|
result = results[results.pair == pair]
|
||||||
tabular_data.append([
|
tabular_data.append([
|
||||||
pair,
|
pair,
|
||||||
len(result.index),
|
len(result.index),
|
||||||
result.profit_percent.mean() * 100.0,
|
result.profit_percent.mean() * 100.0,
|
||||||
result.profit_BTC.sum(),
|
result.profit_abs.sum(),
|
||||||
result.duration.mean(),
|
result.trade_duration.mean(),
|
||||||
len(result[result.profit_BTC > 0]),
|
len(result[result.profit_abs > 0]),
|
||||||
len(result[result.profit_BTC < 0])
|
len(result[result.profit_abs < 0])
|
||||||
])
|
])
|
||||||
|
|
||||||
# Append Total
|
# Append Total
|
||||||
@ -89,16 +105,28 @@ class Backtesting(object):
|
|||||||
'TOTAL',
|
'TOTAL',
|
||||||
len(results.index),
|
len(results.index),
|
||||||
results.profit_percent.mean() * 100.0,
|
results.profit_percent.mean() * 100.0,
|
||||||
results.profit_BTC.sum(),
|
results.profit_abs.sum(),
|
||||||
results.duration.mean(),
|
results.trade_duration.mean(),
|
||||||
len(results[results.profit_BTC > 0]),
|
len(results[results.profit_abs > 0]),
|
||||||
len(results[results.profit_BTC < 0])
|
len(results[results.profit_abs < 0])
|
||||||
])
|
])
|
||||||
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
|
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
|
||||||
|
|
||||||
|
def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None:
|
||||||
|
|
||||||
|
records = [(trade_entry.pair, trade_entry.profit_percent,
|
||||||
|
trade_entry.open_time.timestamp(),
|
||||||
|
trade_entry.close_time.timestamp(),
|
||||||
|
trade_entry.open_index - 1, trade_entry.trade_duration)
|
||||||
|
for index, trade_entry in results.iterrows()]
|
||||||
|
|
||||||
|
if records:
|
||||||
|
logger.info('Dumping backtest results to %s', recordfilename)
|
||||||
|
file_dump_json(recordfilename, records)
|
||||||
|
|
||||||
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, args: Dict) -> Optional[Tuple]:
|
partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[BacktestResult]:
|
||||||
|
|
||||||
stake_amount = args['stake_amount']
|
stake_amount = args['stake_amount']
|
||||||
max_open_trades = args.get('max_open_trades', 0)
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
@ -121,15 +149,33 @@ class Backtesting(object):
|
|||||||
buy_signal = sell_row.buy
|
buy_signal = sell_row.buy
|
||||||
if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal,
|
if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal,
|
||||||
sell_row.sell):
|
sell_row.sell):
|
||||||
return \
|
|
||||||
sell_row, \
|
return BacktestResult(pair=pair,
|
||||||
(
|
profit_percent=trade.calc_profit_percent(rate=sell_row.close),
|
||||||
pair,
|
profit_abs=trade.calc_profit(rate=sell_row.close),
|
||||||
trade.calc_profit_percent(rate=sell_row.close),
|
open_time=buy_row.date,
|
||||||
trade.calc_profit(rate=sell_row.close),
|
close_time=sell_row.date,
|
||||||
(sell_row.date - buy_row.date).seconds // 60
|
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
|
||||||
), \
|
open_index=buy_row.Index,
|
||||||
sell_row.date
|
close_index=sell_row.Index,
|
||||||
|
open_at_end=False
|
||||||
|
)
|
||||||
|
if partial_ticker:
|
||||||
|
# no sell condition found - trade stil open at end of backtest period
|
||||||
|
sell_row = partial_ticker[-1]
|
||||||
|
btr = BacktestResult(pair=pair,
|
||||||
|
profit_percent=trade.calc_profit_percent(rate=sell_row.close),
|
||||||
|
profit_abs=trade.calc_profit(rate=sell_row.close),
|
||||||
|
open_time=buy_row.date,
|
||||||
|
close_time=sell_row.date,
|
||||||
|
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
|
||||||
|
open_index=buy_row.Index,
|
||||||
|
close_index=sell_row.Index,
|
||||||
|
open_at_end=True
|
||||||
|
)
|
||||||
|
logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair,
|
||||||
|
btr.profit_percent, btr.profit_abs)
|
||||||
|
return btr
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def backtest(self, args: Dict) -> DataFrame:
|
def backtest(self, args: Dict) -> DataFrame:
|
||||||
@ -145,17 +191,12 @@ class Backtesting(object):
|
|||||||
processed: a processed dictionary with format {pair, data}
|
processed: a processed dictionary with format {pair, data}
|
||||||
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
||||||
realistic: do we try to simulate realistic trades? (default: True)
|
realistic: do we try to simulate realistic trades? (default: True)
|
||||||
sell_profit_only: sell if profit only
|
|
||||||
use_sell_signal: act on sell-signal
|
|
||||||
:return: DataFrame
|
:return: DataFrame
|
||||||
"""
|
"""
|
||||||
headers = ['date', 'buy', 'open', 'close', 'sell']
|
headers = ['date', 'buy', 'open', 'close', 'sell']
|
||||||
processed = args['processed']
|
processed = args['processed']
|
||||||
max_open_trades = args.get('max_open_trades', 0)
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
realistic = args.get('realistic', False)
|
realistic = args.get('realistic', False)
|
||||||
record = args.get('record', None)
|
|
||||||
recordfilename = args.get('recordfn', 'backtest-result.json')
|
|
||||||
records = []
|
|
||||||
trades = []
|
trades = []
|
||||||
trade_count_lock: Dict = {}
|
trade_count_lock: Dict = {}
|
||||||
for pair, pair_data in processed.items():
|
for pair, pair_data in processed.items():
|
||||||
@ -170,6 +211,8 @@ class Backtesting(object):
|
|||||||
|
|
||||||
ticker_data.drop(ticker_data.head(1).index, inplace=True)
|
ticker_data.drop(ticker_data.head(1).index, inplace=True)
|
||||||
|
|
||||||
|
# Convert from Pandas to list for performance reasons
|
||||||
|
# (Looping Pandas is slow.)
|
||||||
ticker = [x for x in ticker_data.itertuples()]
|
ticker = [x for x in ticker_data.itertuples()]
|
||||||
|
|
||||||
lock_pair_until = None
|
lock_pair_until = None
|
||||||
@ -187,28 +230,18 @@ class Backtesting(object):
|
|||||||
|
|
||||||
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
||||||
|
|
||||||
ret = self._get_sell_trade_entry(pair, row, ticker[index + 1:],
|
trade_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:],
|
||||||
trade_count_lock, args)
|
trade_count_lock, args)
|
||||||
|
|
||||||
if ret:
|
if trade_entry:
|
||||||
row2, trade_entry, next_date = ret
|
lock_pair_until = trade_entry.close_time
|
||||||
lock_pair_until = next_date
|
|
||||||
trades.append(trade_entry)
|
trades.append(trade_entry)
|
||||||
if record:
|
else:
|
||||||
# Note, need to be json.dump friendly
|
# Set lock_pair_until to end of testing period if trade could not be closed
|
||||||
# record a tuple of pair, current_profit_percent,
|
# This happens only if the buy-signal was with the last candle
|
||||||
# entry-date, duration
|
lock_pair_until = ticker_data.iloc[-1].date
|
||||||
records.append((pair, trade_entry[1],
|
|
||||||
row.date.strftime('%s'),
|
return DataFrame.from_records(trades, columns=BacktestResult._fields)
|
||||||
row2.date.strftime('%s'),
|
|
||||||
index, trade_entry[3]))
|
|
||||||
# For now export inside backtest(), maybe change so that backtest()
|
|
||||||
# returns a tuple like: (dataframe, records, logs, etc)
|
|
||||||
if record and record.find('trades') >= 0:
|
|
||||||
logger.info('Dumping backtest results to %s', recordfilename)
|
|
||||||
file_dump_json(recordfilename, records)
|
|
||||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
|
||||||
return DataFrame.from_records(trades, columns=labels)
|
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -259,24 +292,22 @@ class Backtesting(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Execute backtest and print results
|
# Execute backtest and print results
|
||||||
sell_profit_only = self.config.get('experimental', {}).get('sell_profit_only', False)
|
|
||||||
use_sell_signal = self.config.get('experimental', {}).get('use_sell_signal', False)
|
|
||||||
results = self.backtest(
|
results = self.backtest(
|
||||||
{
|
{
|
||||||
'stake_amount': self.config.get('stake_amount'),
|
'stake_amount': self.config.get('stake_amount'),
|
||||||
'processed': preprocessed,
|
'processed': preprocessed,
|
||||||
'max_open_trades': max_open_trades,
|
'max_open_trades': max_open_trades,
|
||||||
'realistic': self.config.get('realistic_simulation', False),
|
'realistic': self.config.get('realistic_simulation', False),
|
||||||
'sell_profit_only': sell_profit_only,
|
|
||||||
'use_sell_signal': use_sell_signal,
|
|
||||||
'record': self.config.get('export'),
|
|
||||||
'recordfn': self.config.get('exportfilename'),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.config.get('export', False):
|
||||||
|
self._store_backtest_result(self.config.get('exportfilename'), results)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'\n==================================== '
|
'\n======================================== '
|
||||||
'BACKTESTING REPORT'
|
'BACKTESTING REPORT'
|
||||||
' ====================================\n'
|
' =========================================\n'
|
||||||
'%s',
|
'%s',
|
||||||
self._generate_text_table(
|
self._generate_text_table(
|
||||||
data,
|
data,
|
||||||
@ -284,6 +315,17 @@ class Backtesting(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'\n====================================== '
|
||||||
|
'LEFT OPEN TRADES REPORT'
|
||||||
|
' ======================================\n'
|
||||||
|
'%s',
|
||||||
|
self._generate_text_table(
|
||||||
|
data,
|
||||||
|
results.loc[results.open_at_end]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
@ -449,7 +449,7 @@ class Hyperopt(Backtesting):
|
|||||||
|
|
||||||
total_profit = results.profit_percent.sum()
|
total_profit = results.profit_percent.sum()
|
||||||
trade_count = len(results.index)
|
trade_count = len(results.index)
|
||||||
trade_duration = results.duration.mean()
|
trade_duration = results.trade_duration.mean()
|
||||||
|
|
||||||
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
|
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
|
||||||
print('.', end='')
|
print('.', end='')
|
||||||
@ -486,10 +486,10 @@ class Hyperopt(Backtesting):
|
|||||||
'Total profit {: 11.8f} {} ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
|
'Total profit {: 11.8f} {} ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
|
||||||
len(results.index),
|
len(results.index),
|
||||||
results.profit_percent.mean() * 100.0,
|
results.profit_percent.mean() * 100.0,
|
||||||
results.profit_BTC.sum(),
|
results.profit_abs.sum(),
|
||||||
self.config['stake_currency'],
|
self.config['stake_currency'],
|
||||||
results.profit_percent.sum(),
|
results.profit_percent.sum(),
|
||||||
results.duration.mean(),
|
results.trade_duration.mean(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
|
@ -353,10 +353,10 @@ def test_generate_text_table(default_conf, mocker):
|
|||||||
|
|
||||||
results = pd.DataFrame(
|
results = pd.DataFrame(
|
||||||
{
|
{
|
||||||
'currency': ['ETH/BTC', 'ETH/BTC'],
|
'pair': ['ETH/BTC', 'ETH/BTC'],
|
||||||
'profit_percent': [0.1, 0.2],
|
'profit_percent': [0.1, 0.2],
|
||||||
'profit_BTC': [0.2, 0.4],
|
'profit_abs': [0.2, 0.4],
|
||||||
'duration': [10, 30],
|
'trade_duration': [10, 30],
|
||||||
'profit': [2, 0],
|
'profit': [2, 0],
|
||||||
'loss': [0, 0]
|
'loss': [0, 0]
|
||||||
}
|
}
|
||||||
@ -469,6 +469,7 @@ def test_backtest(default_conf, fee, mocker) -> None:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
|
assert len(results) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
||||||
@ -491,6 +492,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
|
assert len(results) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_processed(default_conf, mocker) -> None:
|
def test_processed(default_conf, mocker) -> None:
|
||||||
@ -512,7 +514,7 @@ def test_processed(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
||||||
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
|
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
|
||||||
tests = [['raise', 17], ['lower', 0], ['sine', 16]]
|
tests = [['raise', 18], ['lower', 0], ['sine', 16]]
|
||||||
for [contour, numres] in tests:
|
for [contour, numres] in tests:
|
||||||
simple_backtest(default_conf, contour, numres, mocker)
|
simple_backtest(default_conf, contour, numres, mocker)
|
||||||
|
|
||||||
@ -572,7 +574,10 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
|
|||||||
backtesting.populate_buy_trend = _trend_alternate # Override
|
backtesting.populate_buy_trend = _trend_alternate # Override
|
||||||
backtesting.populate_sell_trend = _trend_alternate # Override
|
backtesting.populate_sell_trend = _trend_alternate # Override
|
||||||
results = backtesting.backtest(backtest_conf)
|
results = backtesting.backtest(backtest_conf)
|
||||||
assert len(results) == 3
|
backtesting._store_backtest_result("test_.json", results)
|
||||||
|
assert len(results) == 4
|
||||||
|
# One trade was force-closed at the end
|
||||||
|
assert len(results.loc[results.open_at_end]) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_record(default_conf, fee, mocker):
|
def test_backtest_record(default_conf, fee, mocker):
|
||||||
@ -584,22 +589,30 @@ def test_backtest_record(default_conf, fee, mocker):
|
|||||||
'freqtrade.optimize.backtesting.file_dump_json',
|
'freqtrade.optimize.backtesting.file_dump_json',
|
||||||
new=lambda n, r: (names.append(n), records.append(r))
|
new=lambda n, r: (names.append(n), records.append(r))
|
||||||
)
|
)
|
||||||
backtest_conf = _make_backtest_conf(
|
|
||||||
mocker,
|
|
||||||
conf=default_conf,
|
|
||||||
pair='UNITTEST/BTC',
|
|
||||||
record="trades"
|
|
||||||
)
|
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting.populate_buy_trend = _trend_alternate # Override
|
results = pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC",
|
||||||
backtesting.populate_sell_trend = _trend_alternate # Override
|
"UNITTEST/BTC", "UNITTEST/BTC"],
|
||||||
results = backtesting.backtest(backtest_conf)
|
"profit_percent": [0.003312, 0.010801, 0.013803, 0.002780],
|
||||||
assert len(results) == 3
|
"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_index": [1, 119, 153, 185],
|
||||||
|
"close_index": [118, 151, 184, 199],
|
||||||
|
"trade_duration": [123, 34, 31, 14]})
|
||||||
|
backtesting._store_backtest_result("backtest-result.json", results)
|
||||||
|
assert len(results) == 4
|
||||||
# Assert file_dump_json was only called once
|
# Assert file_dump_json was only called once
|
||||||
assert names == ['backtest-result.json']
|
assert names == ['backtest-result.json']
|
||||||
records = records[0]
|
records = records[0]
|
||||||
# Ensure records are of correct type
|
# Ensure records are of correct type
|
||||||
assert len(records) == 3
|
assert len(records) == 4
|
||||||
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
|
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
|
||||||
# Below follows just a typecheck of the schema/type of trade-records
|
# Below follows just a typecheck of the schema/type of trade-records
|
||||||
oix = None
|
oix = None
|
||||||
|
@ -362,7 +362,7 @@ def test_format_results(init_hyperopt):
|
|||||||
('LTC/BTC', 1, 1, 123),
|
('LTC/BTC', 1, 1, 123),
|
||||||
('XPR/BTC', -1, -2, -246)
|
('XPR/BTC', -1, -2, -246)
|
||||||
]
|
]
|
||||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
|
||||||
df = pd.DataFrame.from_records(trades, columns=labels)
|
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||||
|
|
||||||
result = _HYPEROPT.format_results(df)
|
result = _HYPEROPT.format_results(df)
|
||||||
@ -492,7 +492,7 @@ def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
|
|||||||
trades = [
|
trades = [
|
||||||
('POWR/BTC', 0.023117, 0.000233, 100)
|
('POWR/BTC', 0.023117, 0.000233, 100)
|
||||||
]
|
]
|
||||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
|
||||||
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
|
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
|
||||||
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
|
Loading…
Reference in New Issue
Block a user