Merge branch 'develop' into test_coverage

This commit is contained in:
kryofly 2018-01-20 21:24:28 +01:00
commit e94e6292e9
20 changed files with 448 additions and 159 deletions

View File

@ -4,6 +4,7 @@
"stake_amount": 0.05, "stake_amount": 0.05,
"fiat_display_currency": "USD", "fiat_display_currency": "USD",
"dry_run": false, "dry_run": false,
"ticker_interval": "5",
"minimal_roi": { "minimal_roi": {
"40": 0.0, "40": 0.0,
"30": 0.01, "30": 0.01,

View File

@ -17,6 +17,7 @@ The table below will list all configuration parameters.
| `max_open_trades` | 3 | Yes | Number of trades open your bot will have. | `max_open_trades` | 3 | Yes | Number of trades open your bot will have.
| `stake_currency` | BTC | Yes | Crypto-currency used for trading. | `stake_currency` | BTC | Yes | Crypto-currency used for trading.
| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. | `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged.
| `ticker_interval` | ["1", "5", "30, "60", "1440"] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Defaut is 5 minutes
| `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below. | `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below.
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode. | `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
| `minimal_roi` | See below | Yes | Set the threshold in percent the bot will use to sell a trade. More information below. | `minimal_roi` | See below | Yes | Set the threshold in percent the bot will use to sell a trade. More information below.

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

@ -281,38 +281,31 @@ def analyze_ticker(ticker_history: List[Dict]) -> DataFrame:
return dataframe return dataframe
# FIX: 20180109, there could be some confusion because we will make a # FIX: Maybe return False, if an error has occured,
# boolean result (execute the action or not depending on the signal). # Otherwise we might mask an error as an non-signal-scenario
# But the above checks can also return False, and we hide that. def get_signal(pair: str, interval: int) -> (bool, bool):
# 20180119 Update to above fix, after an code update we now return
# a tuple (buy, sell). We could take advantage of this
# To distinguish an error from an non-signal situation (False, False)
# by just returning False.
# In short, if we return False it is error, If a tuple we
# get the signal situation.
def get_signal(pair: str) -> (bool, bool):
""" """
Calculates current signal based several technical analysis indicators Calculates current signal based several technical analysis indicators
:param pair: pair in format BTC_ANT or BTC-ANT :param pair: pair in format BTC_ANT or BTC-ANT
:return: (True, False) if pair is good for buying and not for selling :return: (Buy, Sell) A bool-tuple indicating buy/sell signal
""" """
ticker_hist = get_ticker_history(pair) ticker_hist = get_ticker_history(pair, interval)
if not ticker_hist: if not ticker_hist:
logger.warning('Empty ticker history for pair %s', pair) logger.warning('Empty ticker history for pair %s', pair)
return (False, False) return (False, False) # return False ?
try: try:
dataframe = analyze_ticker(ticker_hist) dataframe = analyze_ticker(ticker_hist)
except ValueError as ex: except ValueError as ex:
logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex)) logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex))
return (False, False) return (False, False) # return False ?
except Exception as ex: except Exception as ex:
logger.exception('Unexpected error when analyzing ticker for pair %s: %s', pair, str(ex)) logger.exception('Unexpected error when analyzing ticker for pair %s: %s', pair, str(ex))
return (False, False) return (False, False) # return False ?
if dataframe.empty: if dataframe.empty:
logger.warning('Empty dataframe for pair %s', pair) logger.warning('Empty dataframe for pair %s', pair)
return (False, False) return (False, False) # return False ?
latest = dataframe.iloc[-1] latest = dataframe.iloc[-1]
@ -320,7 +313,7 @@ def get_signal(pair: str) -> (bool, bool):
signal_date = arrow.get(latest['date']) signal_date = arrow.get(latest['date'])
if signal_date < arrow.now() - timedelta(minutes=10): if signal_date < arrow.now() - timedelta(minutes=10):
logger.warning('Too old dataframe for pair %s', pair) logger.warning('Too old dataframe for pair %s', pair)
return (False, False) return (False, False) # return False ?
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell)) logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell))

View File

@ -139,7 +139,7 @@ def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
@cached(TTLCache(maxsize=100, ttl=30)) @cached(TTLCache(maxsize=100, ttl=30))
def get_ticker_history(pair: str, tick_interval: Optional[int] = 5) -> List[Dict]: def get_ticker_history(pair: str, tick_interval) -> List[Dict]:
return _API.get_ticker_history(pair, tick_interval) return _API.get_ticker_history(pair, tick_interval)

View File

@ -122,9 +122,10 @@ class Bittrex(Exchange):
raise OperationalException('{message} params=({pair})'.format( raise OperationalException('{message} params=({pair})'.format(
message=data['message'], message=data['message'],
pair=pair)) pair=pair))
keys = ['Bid', 'Ask', 'Last']
if not data.get('result') or\ if not data.get('result') or\
not all(key in data.get('result', {}) for key in ['Bid', 'Ask', 'Last']): not all(key in data.get('result', {}) for key in keys) or\
not all(data.get('result', {})[key] is not None for key in keys):
raise ContentDecodingError('{message} params=({pair})'.format( raise ContentDecodingError('{message} params=({pair})'.format(
message='Got invalid response from bittrex', message='Got invalid response from bittrex',
pair=pair)) pair=pair))
@ -141,6 +142,12 @@ class Bittrex(Exchange):
interval = 'oneMin' interval = 'oneMin'
elif tick_interval == 5: elif tick_interval == 5:
interval = 'fiveMin' interval = 'fiveMin'
elif tick_interval == 30:
interval = 'thirtyMin'
elif tick_interval == 60:
interval = 'hour'
elif tick_interval == 1440:
interval = 'Day'
else: else:
raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval)) raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))

View File

@ -54,14 +54,14 @@ def refresh_whitelist(whitelist: List[str]) -> List[str]:
return final_list return final_list
def process_maybe_execute_buy(conf): def process_maybe_execute_buy(conf, interval):
""" """
Tries to execute a buy trade in a safe way Tries to execute a buy trade in a safe way
:return: True if executed :return: True if executed
""" """
try: try:
# Create entity and execute trade # Create entity and execute trade
if create_trade(float(conf['stake_amount'])): if create_trade(float(_CONF['stake_amount']), interval):
return True return True
else: else:
logger.info( logger.info(
@ -74,7 +74,7 @@ def process_maybe_execute_buy(conf):
return False return False
def process_maybe_execute_sell(trade): def process_maybe_execute_sell(trade, interval):
""" """
Tries to execute a sell trade Tries to execute a sell trade
:return: True if executed :return: True if executed
@ -87,11 +87,11 @@ def process_maybe_execute_sell(trade):
if trade.is_open and trade.open_order_id is None: if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair # Check if we can sell our current pair
return handle_trade(trade) return handle_trade(trade, interval)
return False return False
def _process(nb_assets: Optional[int] = 0) -> bool: def _process(interval: int, nb_assets: Optional[int] = 0) -> bool:
""" """
Queries the persistence layer for open trades and handles them, Queries the persistence layer for open trades and handles them,
otherwise a new trade is created. otherwise a new trade is created.
@ -114,10 +114,10 @@ def _process(nb_assets: Optional[int] = 0) -> bool:
# Query trades from persistence layer # Query trades from persistence layer
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if len(trades) < _CONF['max_open_trades']: if len(trades) < _CONF['max_open_trades']:
state_changed = process_maybe_execute_buy(_CONF) state_changed = process_maybe_execute_buy(_CONF, interval)
for trade in trades: for trade in trades:
state_changed = process_maybe_execute_sell(trade) or state_changed state_changed |= process_maybe_execute_sell(trade, interval)
if 'unfilledtimeout' in _CONF: if 'unfilledtimeout' in _CONF:
# Check and handle any timed out open orders # Check and handle any timed out open orders
@ -291,7 +291,7 @@ def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -
return False return False
def handle_trade(trade: Trade) -> bool: def handle_trade(trade: Trade, interval: int) -> bool:
""" """
Sells the current pair if the threshold is reached and updates the trade record. Sells the current pair if the threshold is reached and updates the trade record.
:return: True if trade has been sold, False otherwise :return: True if trade has been sold, False otherwise
@ -319,7 +319,6 @@ def handle_trade(trade: Trade) -> bool:
if not buy and trade.calc_profit(rate=current_rate) <= 0: if not buy and trade.calc_profit(rate=current_rate) <= 0:
return False return False
# Experimental: Check if sell signal has been enabled and triggered
if sell and not buy: if sell and not buy:
logger.debug('Executing sell due to sell signal ...') logger.debug('Executing sell due to sell signal ...')
execute_sell(trade, current_rate) execute_sell(trade, current_rate)
@ -336,7 +335,7 @@ def get_target_bid(ticker: Dict[str, float]) -> float:
return ticker['ask'] + balance * (ticker['last'] - ticker['ask']) return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
def create_trade(stake_amount: float) -> bool: def create_trade(stake_amount: float, interval: int) -> bool:
""" """
Checks the implemented trading indicator(s) for a randomly picked pair, Checks the implemented trading indicator(s) for a randomly picked pair,
if one pair triggers the buy_signal a new trade record gets created if one pair triggers the buy_signal a new trade record gets created
@ -518,6 +517,7 @@ def main(sysargv=sys.argv[1:]) -> int:
_process, _process,
min_secs=_CONF['internals'].get('process_throttle_secs', 10), min_secs=_CONF['internals'].get('process_throttle_secs', 10),
nb_assets=args.dynamic_whitelist, nb_assets=args.dynamic_whitelist,
interval=int(_CONF.get('ticker_interval', "5"))
) )
old_state = new_state old_state = new_state
except KeyboardInterrupt: except KeyboardInterrupt:

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,51 +155,43 @@ 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 (1, 5, 30, 60, 1440)',
dest='ticker_interval', dest='ticker_interval',
default=5, default=5,
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,11 +262,29 @@ 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',
'properties': { 'properties': {
'max_open_trades': {'type': 'integer', 'minimum': 1}, 'max_open_trades': {'type': 'integer', 'minimum': 1},
'ticker_interval': {'type': 'string', 'enum': ['1', '5', '30', '60', '1440']},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']}, 'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
'stake_amount': {'type': 'number', 'minimum': 0.0005}, 'stake_amount': {'type': 'number', 'minimum': 0.0005},
'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF', 'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF',
@ -329,7 +338,8 @@ CONF_SCHEMA = {
'internals': { 'internals': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'process_throttle_secs': {'type': 'number'} 'process_throttle_secs': {'type': 'number'},
'interval': {'type': 'integer'}
} }
} }
}, },
@ -365,6 +375,7 @@ CONF_SCHEMA = {
], ],
'required': [ 'required': [
'max_open_trades', 'max_open_trades',
'ticker_interval',
'stake_currency', 'stake_currency',
'stake_amount', 'stake_amount',
'fiat_display_currency', 'fiat_display_currency',

View File

@ -49,10 +49,8 @@ def load_tickerdata_file(datadir, pair, ticker_interval,
return pairdata return pairdata
def load_data(datadir: str, ticker_interval: int = 5, def load_data(datadir: str, ticker_interval: int, pairs: Optional[List[str]] = None,
pairs: Optional[List[str]] = None, refresh_pairs: Optional[bool] = False, timerange=None) -> Dict[str, List]:
refresh_pairs: Optional[bool] = False,
timerange=None) -> Dict[str, List]:
""" """
Loads ticker history data for the given parameters Loads ticker history data for the given parameters
:param ticker_interval: ticker interval in minutes :param ticker_interval: ticker interval in minutes
@ -66,7 +64,7 @@ def load_data(datadir: str, ticker_interval: int = 5,
# If the user force the refresh of pairs # If the user force the refresh of pairs
if refresh_pairs: if refresh_pairs:
logger.info('Download data for all pairs and store them in %s', datadir) logger.info('Download data for all pairs and store them in %s', datadir)
download_pairs(datadir, _pairs) download_pairs(datadir, _pairs, ticker_interval)
for pair in _pairs: for pair in _pairs:
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
@ -96,16 +94,15 @@ def make_testdata_path(datadir: str) -> str:
'..', 'tests', 'testdata')) '..', 'tests', 'testdata'))
def download_pairs(datadir, pairs: List[str]) -> bool: def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
"""For each pairs passed in parameters, download 1 and 5 ticker intervals""" """For each pairs passed in parameters, download the ticker intervals"""
for pair in pairs: for pair in pairs:
try: try:
for interval in [1, 5]: download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval)
download_backtesting_testdata(datadir, pair=pair, interval=interval)
except BaseException: except BaseException:
logger.info('Failed to download the pair: "{pair}", Interval: {interval} min'.format( logger.info('Failed to download the pair: "{pair}", Interval: {interval} min'.format(
pair=pair, pair=pair,
interval=interval, interval=ticker_interval,
)) ))
return False return False
return True return True

View File

@ -176,17 +176,20 @@ def start(args):
logger.info('Using config: %s ...', args.config) logger.info('Using config: %s ...', args.config)
config = misc.load_config(args.config) config = misc.load_config(args.config)
ticker_interval = config.get('ticker_interval', args.ticker_interval)
logger.info('Using ticker_interval: %s ...', args.ticker_interval) logger.info('Using ticker_interval: %s ...', ticker_interval)
data = {} data = {}
pairs = config['exchange']['pair_whitelist'] pairs = config['exchange']['pair_whitelist']
if args.live: if args.live:
logger.info('Downloading data for all pairs in whitelist ...') logger.info('Downloading data for all pairs in whitelist ...')
for pair in pairs: for pair in pairs:
data[pair] = exchange.get_ticker_history(pair, args.ticker_interval) data[pair] = exchange.get_ticker_history(pair, ticker_interval)
else: else:
logger.info('Using local backtesting data (using whitelist in given config) ...') logger.info('Using local backtesting data (using whitelist in given config) ...')
data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=ticker_interval,
refresh_pairs=args.refresh_pairs)
logger.info('Using stake_currency: %s ...', config['stake_currency']) logger.info('Using stake_currency: %s ...', config['stake_currency'])
logger.info('Using stake_amount: %s ...', config['stake_amount']) logger.info('Using stake_amount: %s ...', config['stake_amount'])

View File

@ -27,6 +27,7 @@ def default_conf():
"stake_currency": "BTC", "stake_currency": "BTC",
"stake_amount": 0.001, "stake_amount": 0.001,
"fiat_display_currency": "USD", "fiat_display_currency": "USD",
"ticker_interval": "5",
"dry_run": True, "dry_run": True,
"minimal_roi": { "minimal_roi": {
"40": 0.0, "40": 0.0,

View File

@ -200,14 +200,14 @@ def test_get_ticker(default_conf, mocker, ticker):
assert ticker['ask'] == 1 assert ticker['ask'] == 1
def test_get_ticker_history(mocker, ticker): def test_get_ticker_history(default_conf, mocker, ticker):
api_mock = MagicMock() api_mock = MagicMock()
tick = 123 tick = 123
api_mock.get_ticker_history = MagicMock(return_value=tick) api_mock.get_ticker_history = MagicMock(return_value=tick)
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
# retrieve original ticker # retrieve original ticker
ticks = get_ticker_history(pair='BTC_ETH') ticks = get_ticker_history('BTC_ETH', int(default_conf['ticker_interval']))
assert ticks == 123 assert ticks == 123
# change the ticker # change the ticker
@ -216,7 +216,7 @@ def test_get_ticker_history(mocker, ticker):
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
# ensure caching will still return the original ticker # ensure caching will still return the original ticker
get_ticker_history(pair='BTC_ETH') ticks = get_ticker_history('BTC_ETH', int(default_conf['ticker_interval']))
assert ticks == 123 assert ticks == 123

View File

@ -232,6 +232,11 @@ def test_exchange_bittrex_get_ticker_bad():
with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'): with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'):
wb.get_ticker('BTC_ETH') wb.get_ticker('BTC_ETH')
fb.result = {'success': True,
'result': {'Bid': 1, 'Ask': 0, 'Last': None}} # incomplete result
with pytest.raises(ContentDecodingError, match=r'.*Got invalid response from bittrex params.*'):
wb.get_ticker('BTC_ETH')
def test_exchange_bittrex_get_ticker_history_one(): def test_exchange_bittrex_get_ticker_history_one():
wb = make_wrap_bittrex() wb = make_wrap_bittrex()

View File

@ -44,6 +44,23 @@ def _clean_test_file(file: str) -> None:
os.rename(file_swp, file) os.rename(file_swp, file)
def test_load_data_30min_ticker(default_conf, ticker_history, mocker, caplog):
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = 'freqtrade/tests/testdata/BTC_UNITTEST-30.json'
_backup_file(file, copy_file=True)
optimize.load_data(None, pairs=['BTC_UNITTEST'], ticker_interval=30)
assert os.path.isfile(file) is True
assert ('freqtrade.optimize',
logging.INFO,
'Download the pair: "BTC_ETH", Interval: 30 min'
) not in caplog.record_tuples
_clean_test_file(file)
def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog): def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog):
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
@ -52,7 +69,7 @@ def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog):
file = 'freqtrade/tests/testdata/BTC_ETH-5.json' file = 'freqtrade/tests/testdata/BTC_ETH-5.json'
_backup_file(file, copy_file=True) _backup_file(file, copy_file=True)
optimize.load_data(None, pairs=['BTC_ETH']) optimize.load_data(None, pairs=['BTC_ETH'], ticker_interval=5)
assert os.path.isfile(file) is True assert os.path.isfile(file) is True
assert ('freqtrade.optimize', assert ('freqtrade.optimize',
logging.INFO, logging.INFO,
@ -114,17 +131,28 @@ def test_download_pairs(default_conf, ticker_history, mocker):
_backup_file(file2_1) _backup_file(file2_1)
_backup_file(file2_5) _backup_file(file2_5)
assert download_pairs(None, pairs=['BTC-MEME', 'BTC-CFI']) is True assert os.path.isfile(file1_1) is False
assert os.path.isfile(file2_1) is False
assert download_pairs(None, pairs=['BTC-MEME', 'BTC-CFI'], ticker_interval=1) is True
assert os.path.isfile(file1_1) is True assert os.path.isfile(file1_1) is True
assert os.path.isfile(file1_5) is True
assert os.path.isfile(file2_1) is True assert os.path.isfile(file2_1) is True
assert os.path.isfile(file2_5) is True
# clean files freshly downloaded # clean files freshly downloaded
_clean_test_file(file1_1) _clean_test_file(file1_1)
_clean_test_file(file1_5)
_clean_test_file(file2_1) _clean_test_file(file2_1)
assert os.path.isfile(file1_5) is False
assert os.path.isfile(file2_5) is False
assert download_pairs(None, pairs=['BTC-MEME', 'BTC-CFI'], ticker_interval=5) is True
assert os.path.isfile(file1_5) is True
assert os.path.isfile(file2_5) is True
# clean files freshly downloaded
_clean_test_file(file1_5)
_clean_test_file(file2_5) _clean_test_file(file2_5)
@ -140,7 +168,7 @@ def test_download_pairs_exception(default_conf, ticker_history, mocker, caplog):
_backup_file(file1_1) _backup_file(file1_1)
_backup_file(file1_5) _backup_file(file1_5)
download_pairs(None, pairs=['BTC-MEME']) download_pairs(None, pairs=['BTC-MEME'], ticker_interval=1)
# clean files freshly downloaded # clean files freshly downloaded
_clean_test_file(file1_1) _clean_test_file(file1_1)
_clean_test_file(file1_5) _clean_test_file(file1_5)
@ -185,10 +213,11 @@ def test_load_tickerdata_file():
assert _btc_unittest_length == len(tickerdata) assert _btc_unittest_length == len(tickerdata)
def test_init(mocker): def test_init(default_conf, mocker):
conf = {'exchange': {'pair_whitelist': []}} conf = {'exchange': {'pair_whitelist': []}}
mocker.patch('freqtrade.optimize.hyperopt_optimize_conf', return_value=conf) mocker.patch('freqtrade.optimize.hyperopt_optimize_conf', return_value=conf)
assert {} == optimize.load_data('', pairs=[], refresh_pairs=True) assert {} == optimize.load_data('', pairs=[], refresh_pairs=True,
ticker_interval=int(default_conf['ticker_interval']))
def test_tickerdata_to_dataframe(): def test_tickerdata_to_dataframe():

View File

@ -102,7 +102,7 @@ def test_status_handle(default_conf, update, ticker, mocker):
msg_mock.reset_mock() msg_mock.reset_mock()
# Create some test data # Create some test data
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
# Trigger status while we have a fulfilled order for the open trade # Trigger status while we have a fulfilled order for the open trade
_status(bot=MagicMock(), update=update) _status(bot=MagicMock(), update=update)
@ -138,7 +138,7 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
msg_mock.reset_mock() msg_mock.reset_mock()
# Create some test data # Create some test data
create_trade(15.0) create_trade(15.0, int(default_conf['ticker_interval']))
_status_table(bot=MagicMock(), update=update) _status_table(bot=MagicMock(), update=update)
@ -176,7 +176,7 @@ def test_profit_handle(
msg_mock.reset_mock() msg_mock.reset_mock()
# Create some test data # Create some test data
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
@ -225,7 +225,7 @@ def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker):
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -262,7 +262,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, m
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
# Decrease the price and sell it # Decrease the price and sell it
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
@ -324,7 +324,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
# Create some test data # Create some test data
for _ in range(4): for _ in range(4):
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
rpc_mock.reset_mock() rpc_mock.reset_mock()
update.message.text = '/forcesell all' update.message.text = '/forcesell all'
@ -389,7 +389,7 @@ def test_performance_handle(
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -427,7 +427,7 @@ def test_daily_handle(
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -454,8 +454,8 @@ def test_daily_handle(
# Reset msg_mock # Reset msg_mock
msg_mock.reset_mock() msg_mock.reset_mock()
# Add two other trades # Add two other trades
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trades = Trade.query.all() trades = Trade.query.all()
for trade in trades: for trade in trades:
@ -502,7 +502,7 @@ def test_count_handle(default_conf, update, ticker, mocker):
update_state(State.RUNNING) update_state(State.RUNNING)
# Create some test data # Create some test data
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
msg_mock.reset_mock() msg_mock.reset_mock()
_count(bot=MagicMock(), update=update) _count(bot=MagicMock(), update=update)

View File

@ -44,13 +44,13 @@ def test_returns_latest_buy_signal(mocker):
'freqtrade.analyze.analyze_ticker', 'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}]) return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
) )
assert get_signal('BTC-ETH') == (True, False) assert get_signal('BTC-ETH', 5) == (True, False)
mocker.patch( mocker.patch(
'freqtrade.analyze.analyze_ticker', 'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}]) return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
) )
assert get_signal('BTC-ETH') == (False, True) assert get_signal('BTC-ETH', 5) == (False, True)
def test_returns_latest_sell_signal(mocker): def test_returns_latest_sell_signal(mocker):
@ -59,46 +59,46 @@ def test_returns_latest_sell_signal(mocker):
'freqtrade.analyze.analyze_ticker', 'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}]) return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
) )
assert get_signal('BTC-ETH') == (False, True) assert get_signal('BTC-ETH', 5) == (False, True)
mocker.patch( mocker.patch(
'freqtrade.analyze.analyze_ticker', 'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}]) return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
) )
assert get_signal('BTC-ETH') == (True, False) assert get_signal('BTC-ETH', 5) == (True, False)
def test_get_signal_empty(mocker, caplog): def test_get_signal_empty(default_conf, mocker, caplog):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=None) mocker.patch('freqtrade.analyze.get_ticker_history', return_value=None)
assert (False, False) == get_signal('foo') assert (False, False) == get_signal('foo', int(default_conf['ticker_interval']))
assert tt.log_has('Empty ticker history for pair foo', assert tt.log_has('Empty ticker history for pair foo',
caplog.record_tuples) caplog.record_tuples)
def test_get_signal_execption_valueerror(mocker, caplog): def test_get_signal_exception_valueerror(default_conf, mocker, caplog):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1) mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
mocker.patch('freqtrade.analyze.analyze_ticker', mocker.patch('freqtrade.analyze.analyze_ticker',
side_effect=ValueError('xyz')) side_effect=ValueError('xyz'))
assert (False, False) == get_signal('foo') assert (False, False) == get_signal('foo', int(default_conf['ticker_interval']))
assert tt.log_has('Unable to analyze ticker for pair foo: xyz', assert tt.log_has('Unable to analyze ticker for pair foo: xyz',
caplog.record_tuples) caplog.record_tuples)
def test_get_signal_empty_dataframe(mocker, caplog): def test_get_signal_empty_dataframe(default_conf, mocker, caplog):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1) mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=DataFrame([])) mocker.patch('freqtrade.analyze.analyze_ticker', return_value=DataFrame([]))
assert (False, False) == get_signal('xyz') assert (False, False) == get_signal('xyz', int(default_conf['ticker_interval']))
assert tt.log_has('Empty dataframe for pair xyz', assert tt.log_has('Empty dataframe for pair xyz',
caplog.record_tuples) caplog.record_tuples)
def test_get_signal_old_dataframe(mocker, caplog): def test_get_signal_old_dataframe(default_conf, mocker, caplog):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1) mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
# FIX: The get_signal function has hardcoded 10, which we must inturn hardcode # FIX: The get_signal function has hardcoded 10, which we must inturn hardcode
oldtime = arrow.utcnow() - datetime.timedelta(minutes=11) oldtime = arrow.utcnow() - datetime.timedelta(minutes=11)
ticks = DataFrame([{'buy': 1, 'date': oldtime}]) ticks = DataFrame([{'buy': 1, 'date': oldtime}])
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=DataFrame(ticks)) mocker.patch('freqtrade.analyze.analyze_ticker', return_value=DataFrame(ticks))
assert (False, False) == get_signal('xyz') assert (False, False) == get_signal('xyz', int(default_conf['ticker_interval']))
assert tt.log_has('Too old dataframe for pair xyz', assert tt.log_has('Too old dataframe for pair xyz',
caplog.record_tuples) caplog.record_tuples)
@ -108,4 +108,4 @@ def test_get_signal_handles_exceptions(mocker):
mocker.patch('freqtrade.analyze.analyze_ticker', mocker.patch('freqtrade.analyze.analyze_ticker',
side_effect=Exception('invalid ticker history ')) side_effect=Exception('invalid ticker history '))
assert get_signal('BTC-ETH') == (False, False) assert get_signal('BTC-ETH', 5) == (False, False)

View File

@ -50,9 +50,9 @@ def test_main_start_hyperopt(mocker):
def test_process_maybe_execute_buy(default_conf, mocker): def test_process_maybe_execute_buy(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.create_trade', return_value=True) mocker.patch('freqtrade.main.create_trade', return_value=True)
assert main.process_maybe_execute_buy(default_conf) assert main.process_maybe_execute_buy(default_conf, int(default_conf['ticker_interval']))
mocker.patch('freqtrade.main.create_trade', return_value=False) mocker.patch('freqtrade.main.create_trade', return_value=False)
assert not main.process_maybe_execute_buy(default_conf) assert not main.process_maybe_execute_buy(default_conf, int(default_conf['ticker_interval']))
def test_process_maybe_execute_sell(default_conf, mocker): def test_process_maybe_execute_sell(default_conf, mocker):
@ -61,17 +61,17 @@ def test_process_maybe_execute_sell(default_conf, mocker):
mocker.patch('freqtrade.exchange.get_order', return_value=1) mocker.patch('freqtrade.exchange.get_order', return_value=1)
trade = MagicMock() trade = MagicMock()
trade.open_order_id = '123' trade.open_order_id = '123'
assert not main.process_maybe_execute_sell(trade) assert not main.process_maybe_execute_sell(trade, int(default_conf['ticker_interval']))
trade.is_open = True trade.is_open = True
trade.open_order_id = None trade.open_order_id = None
# Assert we call handle_trade() if trade is feasible for execution # Assert we call handle_trade() if trade is feasible for execution
assert main.process_maybe_execute_sell(trade) assert main.process_maybe_execute_sell(trade, int(default_conf['ticker_interval']))
def test_process_maybe_execute_buy_exception(default_conf, mocker, caplog): def test_process_maybe_execute_buy_exception(default_conf, mocker, caplog):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.create_trade', MagicMock(side_effect=DependencyException)) mocker.patch('freqtrade.main.create_trade', MagicMock(side_effect=DependencyException))
main.process_maybe_execute_buy(default_conf) main.process_maybe_execute_buy(default_conf, int(default_conf['ticker_interval']))
tt.log_has('Unable to create trade:', caplog.record_tuples) tt.log_has('Unable to create trade:', caplog.record_tuples)
@ -90,7 +90,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, m
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert not trades assert not trades
result = _process() result = _process(interval=int(default_conf['ticker_interval']))
assert result is True assert result is True
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
@ -116,7 +116,7 @@ def test_process_exchange_failures(default_conf, ticker, health, mocker):
get_wallet_health=health, get_wallet_health=health,
buy=MagicMock(side_effect=requests.exceptions.RequestException)) buy=MagicMock(side_effect=requests.exceptions.RequestException))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
result = _process() result = _process(interval=int(default_conf['ticker_interval']))
assert result is False assert result is False
assert sleep_mock.has_calls() assert sleep_mock.has_calls()
@ -134,7 +134,7 @@ def test_process_operational_exception(default_conf, ticker, health, mocker):
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
assert get_state() == State.RUNNING assert get_state() == State.RUNNING
result = _process() result = _process(interval=int(default_conf['ticker_interval']))
assert result is False assert result is False
assert get_state() == State.STOPPED assert get_state() == State.STOPPED
assert 'OperationalException' in msg_mock.call_args_list[-1][0][0] assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]
@ -154,12 +154,12 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, m
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert not trades assert not trades
result = _process() result = _process(interval=int(default_conf['ticker_interval']))
assert result is True assert result is True
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 1 assert len(trades) == 1
result = _process() result = _process(interval=int(default_conf['ticker_interval']))
assert result is False assert result is False
@ -175,7 +175,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist']) whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first() trade = Trade.query.first()
assert trade is not None assert trade is not None
@ -205,7 +205,7 @@ def test_create_trade_minimal_amount(default_conf, ticker, mocker):
get_ticker=ticker) get_ticker=ticker)
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
min_stake_amount = 0.0005 min_stake_amount = 0.0005
create_trade(min_stake_amount) create_trade(min_stake_amount, int(default_conf['ticker_interval']))
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2] rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
assert rate * amount >= min_stake_amount assert rate * amount >= min_stake_amount
@ -220,7 +220,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
buy=MagicMock(return_value='mocked_limit_buy'), buy=MagicMock(return_value='mocked_limit_buy'),
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5)) get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
with pytest.raises(DependencyException, match=r'.*stake amount.*'): with pytest.raises(DependencyException, match=r'.*stake amount.*'):
create_trade(default_conf['stake_amount']) create_trade(default_conf['stake_amount'], int(default_conf['ticker_interval']))
def test_create_trade_no_pairs(default_conf, ticker, mocker): def test_create_trade_no_pairs(default_conf, ticker, mocker):
@ -236,7 +236,7 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker):
conf = copy.deepcopy(default_conf) conf = copy.deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = [] conf['exchange']['pair_whitelist'] = []
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
create_trade(default_conf['stake_amount']) create_trade(default_conf['stake_amount'], int(default_conf['ticker_interval']))
def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker): def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker):
@ -253,7 +253,7 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker):
conf['exchange']['pair_whitelist'] = ["BTC_ETH"] conf['exchange']['pair_whitelist'] = ["BTC_ETH"]
conf['exchange']['pair_blacklist'] = ["BTC_ETH"] conf['exchange']['pair_blacklist'] = ["BTC_ETH"]
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
create_trade(default_conf['stake_amount']) create_trade(default_conf['stake_amount'], int(default_conf['ticker_interval']))
def test_create_trade_no_signal(default_conf, ticker, mocker): def test_create_trade_no_signal(default_conf, ticker, mocker):
@ -267,7 +267,7 @@ def test_create_trade_no_signal(default_conf, ticker, mocker):
stake_amount = 10 stake_amount = 10
Trade.query = MagicMock() Trade.query = MagicMock()
Trade.query.filter = MagicMock() Trade.query.filter = MagicMock()
assert not create_trade(stake_amount) assert not create_trade(stake_amount, int(default_conf['ticker_interval']))
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker): def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
@ -287,7 +287,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
ticker=MagicMock(return_value={'price_usd': 15000.0}), ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1})) _cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -296,7 +296,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
assert trade.is_open is True assert trade.is_open is True
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True))
handle_trade(trade) assert handle_trade(trade, int(default_conf['ticker_interval'])) is True
assert trade.open_order_id == 'mocked_limit_sell' assert trade.open_order_id == 'mocked_limit_sell'
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
@ -321,7 +321,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, mocker, caplog):
mocker.patch('freqtrade.main.min_roi_reached', return_value=False) mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
# Buy and Sell triggering, so doing nothing ... # Buy and Sell triggering, so doing nothing ...
trades = Trade.query.all() trades = Trade.query.all()
@ -329,21 +329,21 @@ def test_handle_overlpapping_signals(default_conf, ticker, mocker, caplog):
# Buy is triggering, so buying ... # Buy is triggering, so buying ...
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False))
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trades = Trade.query.all() trades = Trade.query.all()
assert len(trades) == 1 assert len(trades) == 1
assert trades[0].is_open is True assert trades[0].is_open is True
# Buy and Sell are not triggering, so doing nothing ... # Buy and Sell are not triggering, so doing nothing ...
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, False)) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, False))
assert handle_trade(trades[0]) is False assert handle_trade(trades[0], int(default_conf['ticker_interval'])) is False
trades = Trade.query.all() trades = Trade.query.all()
assert len(trades) == 1 assert len(trades) == 1
assert trades[0].is_open is True assert trades[0].is_open is True
# Buy and Sell are triggering, so doing nothing ... # Buy and Sell are triggering, so doing nothing ...
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, True)) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, True))
assert handle_trade(trades[0]) is False assert handle_trade(trades[0], int(default_conf['ticker_interval'])) is False
trades = Trade.query.all() trades = Trade.query.all()
assert len(trades) == 1 assert len(trades) == 1
assert trades[0].is_open is True assert trades[0].is_open is True
@ -351,7 +351,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, mocker, caplog):
# Sell is triggering, guess what : we are Selling! # Sell is triggering, guess what : we are Selling!
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True))
trades = Trade.query.all() trades = Trade.query.all()
assert handle_trade(trades[0]) is True assert handle_trade(trades[0], int(default_conf['ticker_interval'])) is True
def test_handle_trade_roi(default_conf, ticker, mocker, caplog): def test_handle_trade_roi(default_conf, ticker, mocker, caplog):
@ -367,7 +367,7 @@ def test_handle_trade_roi(default_conf, ticker, mocker, caplog):
mocker.patch('freqtrade.main.min_roi_reached', return_value=True) mocker.patch('freqtrade.main.min_roi_reached', return_value=True)
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first() trade = Trade.query.first()
trade.is_open = True trade.is_open = True
@ -378,11 +378,11 @@ def test_handle_trade_roi(default_conf, ticker, mocker, caplog):
# executing # executing
# if ROI is reached we must sell # if ROI is reached we must sell
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True))
assert handle_trade(trade) assert handle_trade(trade, interval=int(default_conf['ticker_interval']))
assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples
# if ROI is reached we must sell even if sell-signal is not signalled # if ROI is reached we must sell even if sell-signal is not signalled
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True))
assert handle_trade(trade) assert handle_trade(trade, interval=int(default_conf['ticker_interval']))
assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples
@ -399,16 +399,16 @@ def test_handle_trade_experimental(default_conf, ticker, mocker, caplog):
mocker.patch('freqtrade.main.min_roi_reached', return_value=False) mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first() trade = Trade.query.first()
trade.is_open = True trade.is_open = True
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, False)) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, False))
value_returned = handle_trade(trade) value_returned = handle_trade(trade, int(default_conf['ticker_interval']))
assert value_returned is False assert value_returned is False
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True))
assert handle_trade(trade) assert handle_trade(trade, int(default_conf['ticker_interval']))
s = 'Executing sell due to sell signal ...' s = 'Executing sell due to sell signal ...'
assert ('freqtrade', logging.DEBUG, s) in caplog.record_tuples assert ('freqtrade', logging.DEBUG, s) in caplog.record_tuples
@ -424,7 +424,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
# Create trade and sell it # Create trade and sell it
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -434,7 +434,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
assert trade.is_open is False assert trade.is_open is False
with pytest.raises(ValueError, match=r'.*closed trade.*'): with pytest.raises(ValueError, match=r'.*closed trade.*'):
handle_trade(trade) handle_trade(trade, int(default_conf['ticker_interval']))
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mocker): def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mocker):
@ -600,7 +600,7 @@ def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker):
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -637,7 +637,7 @@ def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker):
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -656,7 +656,7 @@ def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker):
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0] assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
def test_execute_sell_without_conf(default_conf, ticker, ticker_sell_up, mocker): def test_execute_sell_without_conf_sell_down(default_conf, ticker, ticker_sell_down, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False))
mocker.patch('freqtrade.rpc.init', MagicMock()) mocker.patch('freqtrade.rpc.init', MagicMock())
@ -667,7 +667,39 @@ def test_execute_sell_without_conf(default_conf, ticker, ticker_sell_up, mocker)
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first()
assert trade
# Decrease the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_down)
mocker.patch('freqtrade.main._CONF', {})
execute_sell(trade=trade, limit=ticker_sell_down()['bid'])
print(rpc_mock.call_args_list[-1][0][0])
assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
def test_execute_sell_without_conf_sell_up(default_conf, ticker, ticker_sell_up, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False))
mocker.patch('freqtrade.rpc.init', MagicMock())
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
init(default_conf, create_engine('sqlite://'))
# Create some test data
create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -707,12 +739,12 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker):
buy=MagicMock(return_value='mocked_limit_buy')) buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order) trade.update(limit_buy_order)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True))
assert handle_trade(trade) is True assert handle_trade(trade, int(default_conf['ticker_interval'])) is True
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker): def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker):
@ -735,12 +767,12 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker):
buy=MagicMock(return_value='mocked_limit_buy')) buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order) trade.update(limit_buy_order)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True))
assert handle_trade(trade) is True assert handle_trade(trade, int(default_conf['ticker_interval'])) is True
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker): def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker):
@ -763,12 +795,12 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker):
buy=MagicMock(return_value='mocked_limit_buy')) buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order) trade.update(limit_buy_order)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True))
assert handle_trade(trade) is False assert handle_trade(trade, int(default_conf['ticker_interval'])) is False
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker): def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker):
@ -791,10 +823,9 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker):
buy=MagicMock(return_value='mocked_limit_buy')) buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
create_trade(0.001) create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order) trade.update(limit_buy_order)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True))
assert handle_trade(trade) is True assert handle_trade(trade, int(default_conf['ticker_interval'])) is True

File diff suppressed because one or more lines are too long

View File

@ -18,20 +18,26 @@ def plot_parse_args(args ):
default = 'BTC_ETH', default = 'BTC_ETH',
type = str, type = str,
) )
parser.add_argument(
'-i', '--interval',
help = 'what interval to use',
dest = 'interval',
default = '5',
type = int,
)
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
:return: None :return: None
""" """
pair = args.pair
# Init Bittrex to use public API # Init Bittrex to use public API
exchange._API = exchange.Bittrex({'key': '', 'secret': ''}) exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
ticker = exchange.get_ticker_history(pair) ticker = exchange.get_ticker_history(args.pair,args.interval)
dataframe = analyze.analyze_ticker(ticker) dataframe = analyze.analyze_ticker(ticker)
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close'] dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
@ -39,7 +45,7 @@ def plot_analyzed_dataframe(args) -> None:
# Two subplots sharing x axis # Two subplots sharing x axis
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
fig.suptitle(pair, fontsize=14, fontweight='bold') fig.suptitle(args.pair + " " + str(args.interval), fontsize=14, fontweight='bold')
ax1.plot(dataframe.index.values, dataframe['close'], label='close') ax1.plot(dataframe.index.values, dataframe['close'], label='close')
# ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell') # ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell')
ax1.plot(dataframe.index.values, dataframe['sma'], '--', label='SMA') ax1.plot(dataframe.index.values, dataframe['sma'], '--', label='SMA')

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)