Merge pull request #954 from freqtrade/feat/allow_backtest_plot
allow backtest ploting
This commit is contained in:
commit
6dd5f85fb6
@ -70,6 +70,34 @@ Where `-s TestStrategy` refers to the class name within the strategy file `test_
|
||||
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
|
||||
|
||||
```bash
|
||||
|
@ -38,6 +38,8 @@ class BacktestResult(NamedTuple):
|
||||
close_index: int
|
||||
trade_duration: float
|
||||
open_at_end: bool
|
||||
open_rate: float
|
||||
close_rate: float
|
||||
|
||||
|
||||
class Backtesting(object):
|
||||
@ -116,11 +118,10 @@ class Backtesting(object):
|
||||
|
||||
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()]
|
||||
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)
|
||||
for index, t in results.iterrows()]
|
||||
|
||||
if records:
|
||||
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,
|
||||
open_index=buy_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:
|
||||
# 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,
|
||||
open_index=buy_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,
|
||||
btr.profit_percent, btr.profit_abs)
|
||||
|
@ -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, 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]})
|
||||
"trade_duration": [123, 34, 31, 14],
|
||||
"open_at_end": [False, False, False, True]
|
||||
})
|
||||
backtesting._store_backtest_result("backtest-result.json", results)
|
||||
assert len(results) == 4
|
||||
# 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)
|
||||
# Below follows just a typecheck of the schema/type of trade-records
|
||||
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'
|
||||
isinstance(profit, float)
|
||||
assert isinstance(profit, float)
|
||||
# FIX: buy/sell should be converted to ints
|
||||
isinstance(date_buy, str)
|
||||
isinstance(date_sell, str)
|
||||
assert isinstance(date_buy, float)
|
||||
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)
|
||||
if oix:
|
||||
assert buy_index > oix
|
||||
|
@ -25,11 +25,13 @@ Example of usage:
|
||||
--indicators2 fastk,fastd
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
from argparse import Namespace
|
||||
from typing import Dict, List, Any
|
||||
|
||||
import pandas as pd
|
||||
import plotly.graph_objs as go
|
||||
from plotly import tools
|
||||
from plotly.offline import plot
|
||||
@ -37,7 +39,7 @@ from plotly.offline import plot
|
||||
import freqtrade.optimize as optimize
|
||||
from freqtrade import persistence
|
||||
from freqtrade.analyze import Analyze
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.arguments import Arguments, TimeRange
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.optimize.backtesting import setup_configuration
|
||||
from freqtrade.persistence import Trade
|
||||
@ -46,6 +48,45 @@ logger = logging.getLogger(__name__)
|
||||
_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:
|
||||
"""
|
||||
Calls analyze() and plots the returned dataframe
|
||||
@ -102,31 +143,32 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
||||
if tickers == {}:
|
||||
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
|
||||
trades: List[Trade] = []
|
||||
if args.db_url:
|
||||
persistence.init(_CONF)
|
||||
trades = Trade.query.filter(Trade.pair.is_(pair)).all()
|
||||
trades = load_trades(args, pair, timerange)
|
||||
|
||||
dataframes = analyze.tickerdata_to_dataframe(tickers)
|
||||
dataframe = dataframes[pair]
|
||||
dataframe = analyze.populate_buy_trend(dataframe)
|
||||
dataframe = analyze.populate_sell_trend(dataframe)
|
||||
|
||||
if len(dataframe.index) > 750:
|
||||
logger.warning('Ticker contained more than 750 candles, clipping.')
|
||||
|
||||
if len(dataframe.index) > args.plot_limit:
|
||||
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(
|
||||
pair=pair,
|
||||
trades=trades,
|
||||
data=dataframe.tail(750),
|
||||
data=dataframe,
|
||||
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
|
||||
: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(
|
||||
x=[t.open_date.isoformat() for t in trades],
|
||||
y=[t.open_rate for t in trades],
|
||||
x=trades["opents"],
|
||||
y=trades["open_rate"],
|
||||
mode='markers',
|
||||
name='trade_buy',
|
||||
marker=dict(
|
||||
@ -199,8 +241,8 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
|
||||
)
|
||||
)
|
||||
trade_sells = go.Scattergl(
|
||||
x=[t.close_date.isoformat() for t in trades],
|
||||
y=[t.close_rate for t in trades],
|
||||
x=trades["closets"],
|
||||
y=trades["close_rate"],
|
||||
mode='markers',
|
||||
name='trade_sell',
|
||||
marker=dict(
|
||||
@ -299,11 +341,17 @@ def plot_parse_args(args: List[str]) -> Namespace:
|
||||
default='macd',
|
||||
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.optimizer_shared_options(arguments.parser)
|
||||
arguments.backtesting_options(arguments.parser)
|
||||
|
||||
return arguments.parse_args()
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user