Merge pull request #369 from kryofly/plot_profit

Plot profit from exported backtesting results
This commit is contained in:
Janne Sinivirta 2018-01-20 11:54:46 +02:00 committed by GitHub
commit a7e561b55f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 243 additions and 32 deletions

48
docs/plotting.md Normal file
View File

@ -0,0 +1,48 @@
# Plotting
This page explains how to plot prices, indicator, profits.
## Table of Contents
- [Plot price and indicators](#plot-price-and-indicators)
- [Plot profit](#plot-profit)
## Plot price and indicators
Usage for the price plotter:
script/plot_dataframe.py [-h] [-p pair]
Example
```
python script/plot_dataframe.py -p BTC_ETH,BTC_LTC
```
The -p pair argument, can be used to specify what
pair you would like to plot.
## Plot profit
The profit plotter show a picture with three plots:
1) Average closing price for all pairs
2) The summarized profit made by backtesting.
Note that this is not the real-world profit, but
more of an estimate.
3) Each pair individually profit
The first graph is good to get a grip of how the overall market
progresses.
The second graph will show how you algorithm works or doesnt.
Perhaps you want an algorithm that steadily makes small profits,
or one that acts less seldom, but makes big swings.
The third graph can be useful to spot outliers, events in pairs
that makes profit spikes.
Usage for the profit plotter:
script/plot_profit.py [-h] [-p pair] [--datadir directory] [--ticker_interval num]
The -p pair argument, can be used to plot a single pair
Example
```
python python scripts/plot_profit.py --datadir ../freqtrade/freqtrade/tests/testdata-20171221/ -p BTC_LTC
```

View File

@ -116,6 +116,14 @@ def common_args_parser(description: str):
type=str, type=str,
metavar='PATH', metavar='PATH',
) )
parser.add_argument(
'--datadir',
help='path to backtest data (default freqdata/tests/testdata)',
dest='datadir',
default=os.path.join('freqtrade', 'tests', 'testdata'),
type=str,
metavar='PATH',
)
return parser return parser
@ -132,14 +140,6 @@ def parse_args(args: List[str], description: str):
action='store_true', action='store_true',
dest='dry_run_db', dest='dry_run_db',
) )
parser.add_argument(
'--datadir',
help='path to backtest data (default freqdata/tests/testdata',
dest='datadir',
default=os.path.join('freqtrade', 'tests', 'testdata'),
type=str,
metavar='PATH',
)
parser.add_argument( parser.add_argument(
'--dynamic-whitelist', '--dynamic-whitelist',
help='dynamically generate and update whitelist \ help='dynamically generate and update whitelist \
@ -155,22 +155,14 @@ def parse_args(args: List[str], description: str):
return parser.parse_args(args) return parser.parse_args(args)
def build_subcommands(parser: argparse.ArgumentParser) -> None: def backtesting_options(parser: argparse.ArgumentParser) -> None:
""" Builds and attaches all subcommands """ parser.add_argument(
from freqtrade.optimize import backtesting, hyperopt
subparsers = parser.add_subparsers(dest='subparser')
# Add backtesting subcommand
backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module')
backtesting_cmd.set_defaults(func=backtesting.start)
backtesting_cmd.add_argument(
'-l', '--live', '-l', '--live',
action='store_true', action='store_true',
dest='live', dest='live',
help='using live data', help='using live data',
) )
backtesting_cmd.add_argument( parser.add_argument(
'-i', '--ticker-interval', '-i', '--ticker-interval',
help='specify ticker interval in minutes (default: 5)', help='specify ticker interval in minutes (default: 5)',
dest='ticker_interval', dest='ticker_interval',
@ -178,28 +170,28 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None:
type=int, type=int,
metavar='INT', metavar='INT',
) )
backtesting_cmd.add_argument( parser.add_argument(
'--realistic-simulation', '--realistic-simulation',
help='uses max_open_trades from config to simulate real world limitations', help='uses max_open_trades from config to simulate real world limitations',
action='store_true', action='store_true',
dest='realistic_simulation', dest='realistic_simulation',
) )
backtesting_cmd.add_argument( parser.add_argument(
'-r', '--refresh-pairs-cached', '-r', '--refresh-pairs-cached',
help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \ help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \
Use it if you want to run your backtesting with up-to-date data.', Use it if you want to run your backtesting with up-to-date data.',
action='store_true', action='store_true',
dest='refresh_pairs', dest='refresh_pairs',
) )
backtesting_cmd.add_argument( parser.add_argument(
'--export', '--export',
help='Export backtest results, argument are: trades\ help='Export backtest results, argument are: trades\
Example --export trades', Example --export=trades',
type=str, type=str,
default=None, default=None,
dest='export', dest='export',
) )
backtesting_cmd.add_argument( parser.add_argument(
'--timerange', '--timerange',
help='Specify what timerange of data to use.', help='Specify what timerange of data to use.',
default=None, default=None,
@ -207,10 +199,9 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None:
dest='timerange', dest='timerange',
) )
# Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module') def hyperopt_options(parser: argparse.ArgumentParser) -> None:
hyperopt_cmd.set_defaults(func=hyperopt.start) parser.add_argument(
hyperopt_cmd.add_argument(
'-e', '--epochs', '-e', '--epochs',
help='specify number of epochs (default: 100)', help='specify number of epochs (default: 100)',
dest='epochs', dest='epochs',
@ -218,13 +209,13 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None:
type=int, type=int,
metavar='INT', metavar='INT',
) )
hyperopt_cmd.add_argument( parser.add_argument(
'--use-mongodb', '--use-mongodb',
help='parallelize evaluations with mongodb (requires mongod in PATH)', help='parallelize evaluations with mongodb (requires mongod in PATH)',
dest='mongodb', dest='mongodb',
action='store_true', action='store_true',
) )
hyperopt_cmd.add_argument( parser.add_argument(
'-i', '--ticker-interval', '-i', '--ticker-interval',
help='specify ticker interval in minutes (default: 5)', help='specify ticker interval in minutes (default: 5)',
dest='ticker_interval', dest='ticker_interval',
@ -232,7 +223,7 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None:
type=int, type=int,
metavar='INT', metavar='INT',
) )
hyperopt_cmd.add_argument( parser.add_argument(
'--timerange', '--timerange',
help='Specify what timerange of data to use.', help='Specify what timerange of data to use.',
default=None, default=None,
@ -271,6 +262,23 @@ def parse_timerange(text):
raise Exception('Incorrect syntax for timerange "%s"' % text) raise Exception('Incorrect syntax for timerange "%s"' % text)
def build_subcommands(parser: argparse.ArgumentParser) -> None:
""" Builds and attaches all subcommands """
from freqtrade.optimize import backtesting, hyperopt
subparsers = parser.add_subparsers(dest='subparser')
# Add backtesting subcommand
backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module')
backtesting_cmd.set_defaults(func=backtesting.start)
backtesting_options(backtesting_cmd)
# Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
hyperopt_cmd.set_defaults(func=hyperopt.start)
hyperopt_options(hyperopt_cmd)
# Required json-schema for user specified config # Required json-schema for user specified config
CONF_SCHEMA = { CONF_SCHEMA = {
'type': 'object', 'type': 'object',

View File

@ -21,7 +21,7 @@ def plot_parse_args(args ):
return parser.parse_args(args) return parser.parse_args(args)
def plot_analyzed_dataframe(args) -> None: def plot_analyzed_dataframe(args):
""" """
Calls analyze() and plots the returned dataframe Calls analyze() and plots the returned dataframe
:param pair: pair as str :param pair: pair as str

155
scripts/plot_profit.py Executable file
View File

@ -0,0 +1,155 @@
#!/usr/bin/env python3
import sys
import argparse
import json
import matplotlib.pyplot as plt
import numpy as np
import freqtrade.optimize as optimize
import freqtrade.misc as misc
import freqtrade.exchange as exchange
import freqtrade.analyze as analyze
def plot_parse_args(args ):
parser = misc.common_args_parser('Graph utility')
# FIX: perhaps delete those backtesting options that are not feasible (shows up in -h)
misc.backtesting_options(parser)
parser.add_argument(
'-p', '--pair',
help = 'Show profits for only this pairs. Pairs are comma-separated.',
dest = 'pair',
default = None
)
return parser.parse_args(args)
# data:: [ pair, profit-%, enter, exit, time, duration]
# data:: ['BTC_XMR', 0.00537847, '1511176800', '1511178000', 5057, 1]
# FIX: make use of the enter/exit dates to insert the
# profit more precisely into the pg array
def make_profit_array(data, px, filter_pairs=[]):
pg = np.zeros(px)
# Go through the trades
# and make an total profit
# array
for trade in data:
pair = trade[0]
if filter_pairs and pair not in filter_pairs:
continue
profit = trade[1]
tim = trade[4]
dur = trade[5]
pg[tim+dur-1] += profit
# rewrite the pg array to go from
# total profits at each timeframe
# to accumulated profits
pa = 0
for x in range(0,len(pg)):
p = pg[x] # Get current total percent
pa += p # Add to the accumulated percent
pg[x] = pa # write back to save memory
return pg
def plot_profit(args) -> None:
"""
Plots the total profit for all pairs.
Note, the profit calculation isn't realistic.
But should be somewhat proportional, and therefor useful
in helping out to find a good algorithm.
"""
# We need to use the same pairs, same tick_interval
# and same timeperiod as used in backtesting
# to match the tickerdata against the profits-results
filter_pairs = args.pair
config = misc.load_config(args.config)
pairs = config['exchange']['pair_whitelist']
if filter_pairs:
filter_pairs = filter_pairs.split(',')
pairs = list(set(pairs) & set(filter_pairs))
print('Filter, keep pairs %s' % pairs)
tickers = optimize.load_data(args.datadir, pairs=pairs,
ticker_interval=args.ticker_interval,
refresh_pairs=False)
dataframes = optimize.preprocess(tickers)
# Make an average close price of all the pairs that was involved.
# this could be useful to gauge the overall market trend
# FIX: since the dataframes are of unequal length,
# andor has different dates, we need to merge them
# But we dont have the date information in the
# backtesting results, this is needed to match the dates
# For now, assume the dataframes are aligned.
max_x = 0
for pair, pair_data in dataframes.items():
n = len(pair_data['close'])
max_x = max(max_x, n)
# if max_x != n:
# raise Exception('Please rerun script. Input data has different lengths %s'
# %('Different pair length: %s <=> %s' %(max_x, n)))
print('max_x: %s' %(max_x))
# We are essentially saying:
# array <- sum dataframes[*]['close'] / num_items dataframes
# FIX: there should be some onliner numpy/panda for this
avgclose = np.zeros(max_x)
num = 0
for pair, pair_data in dataframes.items():
close = pair_data['close']
maxprice = max(close) # Normalize price to [0,1]
print('Pair %s has length %s' %(pair, len(close)))
for x in range(0, len(close)):
avgclose[x] += close[x] / maxprice
# avgclose += close
num += 1
avgclose /= num
# Load the profits results
# And make an profits-growth array
filename = 'backtest-result.json'
with open(filename) as file:
data = json.load(file)
pg = make_profit_array(data, max_x, filter_pairs)
#
# Plot the pairs average close prices, and total profit growth
#
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
fig.suptitle('total profit')
ax1.plot(avgclose, label='avgclose')
ax2.plot(pg, label='profit')
ax1.legend(loc='upper left')
ax2.legend(loc='upper left')
# FIX if we have one line pair in paris
# then skip the plotting of the third graph,
# or change what we plot
# In third graph, we plot each profit separately
for pair in pairs:
pg = make_profit_array(data, max_x, pair)
ax3.plot(pg, label=pair)
ax3.legend(loc='upper left')
# black background to easier see multiple colors
ax3.set_facecolor('black')
# Fine-tune figure; make subplots close to each other and hide x ticks for
# all but bottom plot.
fig.subplots_adjust(hspace=0)
plt.setp([a.get_xticklabels() for a in fig.axes[:-1]], visible=False)
plt.show()
if __name__ == '__main__':
args = plot_parse_args(sys.argv[1:])
plot_profit(args)