diff --git a/docs/plotting.md b/docs/plotting.md new file mode 100644 index 000000000..598443e12 --- /dev/null +++ b/docs/plotting.md @@ -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 +``` diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 40e6b4e83..5fe253fe5 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -116,6 +116,14 @@ def common_args_parser(description: str): type=str, 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 @@ -132,14 +140,6 @@ def parse_args(args: List[str], description: str): action='store_true', 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( '--dynamic-whitelist', help='dynamically generate and update whitelist \ @@ -155,22 +155,14 @@ def parse_args(args: List[str], description: str): return parser.parse_args(args) -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_cmd.add_argument( +def backtesting_options(parser: argparse.ArgumentParser) -> None: + parser.add_argument( '-l', '--live', action='store_true', dest='live', help='using live data', ) - backtesting_cmd.add_argument( + parser.add_argument( '-i', '--ticker-interval', help='specify ticker interval in minutes (default: 5)', dest='ticker_interval', @@ -178,28 +170,28 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: type=int, metavar='INT', ) - backtesting_cmd.add_argument( + parser.add_argument( '--realistic-simulation', help='uses max_open_trades from config to simulate real world limitations', action='store_true', dest='realistic_simulation', ) - backtesting_cmd.add_argument( + parser.add_argument( '-r', '--refresh-pairs-cached', 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.', action='store_true', dest='refresh_pairs', ) - backtesting_cmd.add_argument( + parser.add_argument( '--export', help='Export backtest results, argument are: trades\ - Example --export trades', + Example --export=trades', type=str, default=None, dest='export', ) - backtesting_cmd.add_argument( + parser.add_argument( '--timerange', help='Specify what timerange of data to use.', default=None, @@ -207,10 +199,9 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: dest='timerange', ) - # Add hyperopt subcommand - hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module') - hyperopt_cmd.set_defaults(func=hyperopt.start) - hyperopt_cmd.add_argument( + +def hyperopt_options(parser: argparse.ArgumentParser) -> None: + parser.add_argument( '-e', '--epochs', help='specify number of epochs (default: 100)', dest='epochs', @@ -218,13 +209,13 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: type=int, metavar='INT', ) - hyperopt_cmd.add_argument( + parser.add_argument( '--use-mongodb', help='parallelize evaluations with mongodb (requires mongod in PATH)', dest='mongodb', action='store_true', ) - hyperopt_cmd.add_argument( + parser.add_argument( '-i', '--ticker-interval', help='specify ticker interval in minutes (default: 5)', dest='ticker_interval', @@ -232,7 +223,7 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: type=int, metavar='INT', ) - hyperopt_cmd.add_argument( + parser.add_argument( '--timerange', help='Specify what timerange of data to use.', default=None, @@ -271,6 +262,23 @@ def parse_timerange(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 CONF_SCHEMA = { 'type': 'object', diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index f07033637..e9bf65f47 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -21,7 +21,7 @@ def plot_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 :param pair: pair as str diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py new file mode 100755 index 000000000..e8bdbee5c --- /dev/null +++ b/scripts/plot_profit.py @@ -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)