Merge pull request #954 from freqtrade/feat/allow_backtest_plot

allow backtest ploting
This commit is contained in:
Michael Egger 2018-06-29 19:44:06 +02:00 committed by GitHub
commit 6dd5f85fb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 119 additions and 30 deletions

View File

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

View File

@ -38,6 +38,8 @@ class BacktestResult(NamedTuple):
close_index: int close_index: int
trade_duration: float trade_duration: float
open_at_end: bool open_at_end: bool
open_rate: float
close_rate: float
class Backtesting(object): class Backtesting(object):
@ -116,11 +118,10 @@ class Backtesting(object):
def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None: def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None:
records = [(trade_entry.pair, trade_entry.profit_percent, records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
trade_entry.open_time.timestamp(), t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
trade_entry.close_time.timestamp(), t.open_rate, t.close_rate, t.open_at_end)
trade_entry.open_index - 1, trade_entry.trade_duration) for index, t in results.iterrows()]
for index, trade_entry in results.iterrows()]
if records: if records:
logger.info('Dumping backtest results to %s', recordfilename) logger.info('Dumping backtest results to %s', recordfilename)
@ -159,7 +160,9 @@ class Backtesting(object):
trade_duration=(sell_row.date - buy_row.date).seconds // 60, trade_duration=(sell_row.date - buy_row.date).seconds // 60,
open_index=buy_row.Index, open_index=buy_row.Index,
close_index=sell_row.Index, close_index=sell_row.Index,
open_at_end=False open_at_end=False,
open_rate=buy_row.close,
close_rate=sell_row.close
) )
if partial_ticker: if partial_ticker:
# no sell condition found - trade stil open at end of backtest period # no sell condition found - trade stil open at end of backtest period
@ -172,7 +175,9 @@ class Backtesting(object):
trade_duration=(sell_row.date - buy_row.date).seconds // 60, trade_duration=(sell_row.date - buy_row.date).seconds // 60,
open_index=buy_row.Index, open_index=buy_row.Index,
close_index=sell_row.Index, close_index=sell_row.Index,
open_at_end=True open_at_end=True,
open_rate=buy_row.close,
close_rate=sell_row.close
) )
logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair, logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair,
btr.profit_percent, btr.profit_abs) btr.profit_percent, btr.profit_abs)

View File

@ -627,9 +627,13 @@ def test_backtest_record(default_conf, fee, mocker):
Arrow(2017, 11, 14, 22, 10, 00).datetime, Arrow(2017, 11, 14, 22, 10, 00).datetime,
Arrow(2017, 11, 14, 22, 43, 00).datetime, Arrow(2017, 11, 14, 22, 43, 00).datetime,
Arrow(2017, 11, 14, 22, 58, 00).datetime], Arrow(2017, 11, 14, 22, 58, 00).datetime],
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
"open_index": [1, 119, 153, 185], "open_index": [1, 119, 153, 185],
"close_index": [118, 151, 184, 199], "close_index": [118, 151, 184, 199],
"trade_duration": [123, 34, 31, 14]}) "trade_duration": [123, 34, 31, 14],
"open_at_end": [False, False, False, True]
})
backtesting._store_backtest_result("backtest-result.json", results) backtesting._store_backtest_result("backtest-result.json", results)
assert len(results) == 4 assert len(results) == 4
# Assert file_dump_json was only called once # Assert file_dump_json was only called once
@ -640,12 +644,16 @@ def test_backtest_record(default_conf, fee, mocker):
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117) # ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
# Below follows just a typecheck of the schema/type of trade-records # Below follows just a typecheck of the schema/type of trade-records
oix = None oix = None
for (pair, profit, date_buy, date_sell, buy_index, dur) in records: for (pair, profit, date_buy, date_sell, buy_index, dur,
openr, closer, open_at_end) in records:
assert pair == 'UNITTEST/BTC' assert pair == 'UNITTEST/BTC'
isinstance(profit, float) assert isinstance(profit, float)
# FIX: buy/sell should be converted to ints # FIX: buy/sell should be converted to ints
isinstance(date_buy, str) assert isinstance(date_buy, float)
isinstance(date_sell, str) assert isinstance(date_sell, float)
assert isinstance(openr, float)
assert isinstance(closer, float)
assert isinstance(open_at_end, bool)
isinstance(buy_index, pd._libs.tslib.Timestamp) isinstance(buy_index, pd._libs.tslib.Timestamp)
if oix: if oix:
assert buy_index > oix assert buy_index > oix

View File

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