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
|
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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user