2017-11-08 21:43:47 +00:00
|
|
|
import argparse
|
2017-09-08 22:31:40 +00:00
|
|
|
import enum
|
2017-11-08 21:43:47 +00:00
|
|
|
import logging
|
2017-11-14 21:15:24 +00:00
|
|
|
import os
|
2017-11-16 15:39:06 +00:00
|
|
|
import time
|
2017-11-17 16:18:31 +00:00
|
|
|
from typing import Any, Callable, List
|
2017-09-08 22:31:40 +00:00
|
|
|
|
|
|
|
from wrapt import synchronized
|
|
|
|
|
2017-11-08 21:43:47 +00:00
|
|
|
from freqtrade import __version__
|
|
|
|
|
2017-11-11 15:47:19 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2017-09-08 22:31:40 +00:00
|
|
|
|
2017-11-16 15:14:43 +00:00
|
|
|
class FreqtradeException(BaseException):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2017-09-08 22:31:40 +00:00
|
|
|
class State(enum.Enum):
|
|
|
|
RUNNING = 0
|
|
|
|
STOPPED = 1
|
|
|
|
|
|
|
|
|
|
|
|
# Current application state
|
|
|
|
_STATE = State.STOPPED
|
|
|
|
|
|
|
|
|
|
|
|
@synchronized
|
|
|
|
def update_state(state: State) -> None:
|
|
|
|
"""
|
|
|
|
Updates the application state
|
|
|
|
:param state: new state
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
global _STATE
|
|
|
|
_STATE = state
|
|
|
|
|
|
|
|
|
|
|
|
@synchronized
|
|
|
|
def get_state() -> State:
|
|
|
|
"""
|
|
|
|
Gets the current application state
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
return _STATE
|
|
|
|
|
2017-05-12 17:11:56 +00:00
|
|
|
|
2017-11-11 15:47:19 +00:00
|
|
|
def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
|
|
|
"""
|
|
|
|
Throttles the given callable that it
|
|
|
|
takes at least `min_secs` to finish execution.
|
|
|
|
:param func: Any callable
|
|
|
|
:param min_secs: minimum execution time in seconds
|
|
|
|
:return: Any
|
|
|
|
"""
|
|
|
|
start = time.time()
|
|
|
|
result = func(*args, **kwargs)
|
|
|
|
end = time.time()
|
|
|
|
duration = max(min_secs - (end - start), 0.0)
|
|
|
|
logger.debug('Throttling %s for %.2f seconds', func.__name__, duration)
|
|
|
|
time.sleep(duration)
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
2017-11-17 16:18:31 +00:00
|
|
|
def parse_args(args: List[str]):
|
|
|
|
"""
|
|
|
|
Parses given arguments and returns an argparse Namespace instance.
|
|
|
|
Returns None if a sub command has been selected and executed.
|
|
|
|
"""
|
2017-11-08 21:43:47 +00:00
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
description='Simple High Frequency Trading Bot for crypto currencies'
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'-c', '--config',
|
|
|
|
help='specify configuration file (default: config.json)',
|
|
|
|
dest='config',
|
|
|
|
default='config.json',
|
|
|
|
type=str,
|
|
|
|
metavar='PATH',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'-v', '--verbose',
|
|
|
|
help='be verbose',
|
|
|
|
action='store_const',
|
|
|
|
dest='loglevel',
|
|
|
|
const=logging.DEBUG,
|
|
|
|
default=logging.INFO,
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--version',
|
|
|
|
action='version',
|
|
|
|
version='%(prog)s {}'.format(__version__),
|
|
|
|
)
|
2017-11-11 18:20:53 +00:00
|
|
|
parser.add_argument(
|
|
|
|
'--dynamic-whitelist',
|
|
|
|
help='dynamically generate and update whitelist based on 24h BaseVolume',
|
|
|
|
action='store_true',
|
|
|
|
)
|
2017-11-14 21:15:24 +00:00
|
|
|
build_subcommands(parser)
|
2017-11-17 16:18:31 +00:00
|
|
|
parsed_args = parser.parse_args(args)
|
|
|
|
|
|
|
|
# No subcommand as been selected
|
|
|
|
if not hasattr(parsed_args, 'func'):
|
|
|
|
return parsed_args
|
|
|
|
|
|
|
|
parsed_args.func(parsed_args)
|
|
|
|
return None
|
2017-11-08 21:43:47 +00:00
|
|
|
|
|
|
|
|
2017-11-14 21:15:24 +00:00
|
|
|
def build_subcommands(parser: argparse.ArgumentParser) -> None:
|
|
|
|
""" Builds and attaches all subcommands """
|
|
|
|
subparsers = parser.add_subparsers(dest='subparser')
|
2017-11-14 21:18:31 +00:00
|
|
|
backtest = subparsers.add_parser('backtesting', help='backtesting module')
|
2017-11-14 21:15:24 +00:00
|
|
|
backtest.set_defaults(func=start_backtesting)
|
|
|
|
backtest.add_argument(
|
|
|
|
'-l', '--live',
|
|
|
|
action='store_true',
|
|
|
|
dest='live',
|
|
|
|
help='using live data',
|
|
|
|
)
|
2017-11-14 21:37:30 +00:00
|
|
|
backtest.add_argument(
|
|
|
|
'-i', '--ticker-interval',
|
|
|
|
help='specify ticker interval in minutes (default: 5)',
|
|
|
|
dest='ticker_interval',
|
|
|
|
default=5,
|
|
|
|
type=int,
|
|
|
|
metavar='INT',
|
|
|
|
)
|
2017-11-14 21:15:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
def start_backtesting(args) -> None:
|
|
|
|
"""
|
|
|
|
Exports all args as environment variables and starts backtesting via pytest.
|
|
|
|
:param args: arguments namespace
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
os.environ.update({
|
|
|
|
'BACKTEST': 'true',
|
|
|
|
'BACKTEST_LIVE': 'true' if args.live else '',
|
|
|
|
'BACKTEST_CONFIG': args.config,
|
2017-11-14 21:37:30 +00:00
|
|
|
'BACKTEST_TICKER_INTERVAL': str(args.ticker_interval),
|
2017-11-14 21:15:24 +00:00
|
|
|
})
|
|
|
|
path = os.path.join(os.path.dirname(__file__), 'tests', 'test_backtesting.py')
|
|
|
|
pytest.main(['-s', path])
|
|
|
|
|
|
|
|
|
2017-09-01 23:10:21 +00:00
|
|
|
# Required json-schema for user specified config
|
2017-09-08 21:10:22 +00:00
|
|
|
CONF_SCHEMA = {
|
2017-09-01 23:10:21 +00:00
|
|
|
'type': 'object',
|
|
|
|
'properties': {
|
2017-09-08 05:49:50 +00:00
|
|
|
'max_open_trades': {'type': 'integer', 'minimum': 1},
|
2017-09-08 05:51:10 +00:00
|
|
|
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
|
2017-09-08 05:45:27 +00:00
|
|
|
'stake_amount': {'type': 'number', 'minimum': 0.0005},
|
2017-09-01 23:10:21 +00:00
|
|
|
'dry_run': {'type': 'boolean'},
|
|
|
|
'minimal_roi': {
|
|
|
|
'type': 'object',
|
|
|
|
'patternProperties': {
|
|
|
|
'^[0-9.]+$': {'type': 'number'}
|
|
|
|
},
|
|
|
|
'minProperties': 1
|
|
|
|
},
|
2017-09-07 14:31:55 +00:00
|
|
|
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
2017-09-17 19:37:46 +00:00
|
|
|
'bid_strategy': {
|
|
|
|
'type': 'object',
|
|
|
|
'properties': {
|
|
|
|
'ask_last_balance': {
|
|
|
|
'type': 'number',
|
|
|
|
'minimum': 0,
|
|
|
|
'maximum': 1,
|
|
|
|
'exclusiveMaximum': False
|
|
|
|
},
|
|
|
|
},
|
|
|
|
'required': ['ask_last_balance']
|
|
|
|
},
|
2017-10-06 10:22:04 +00:00
|
|
|
'exchange': {'$ref': '#/definitions/exchange'},
|
2017-09-01 23:10:21 +00:00
|
|
|
'telegram': {
|
|
|
|
'type': 'object',
|
|
|
|
'properties': {
|
|
|
|
'enabled': {'type': 'boolean'},
|
|
|
|
'token': {'type': 'string'},
|
|
|
|
'chat_id': {'type': 'string'},
|
|
|
|
},
|
|
|
|
'required': ['enabled', 'token', 'chat_id']
|
2017-09-08 22:31:40 +00:00
|
|
|
},
|
|
|
|
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
2017-11-11 15:47:19 +00:00
|
|
|
'internals': {
|
|
|
|
'type': 'object',
|
|
|
|
'properties': {
|
|
|
|
'process_throttle_secs': {'type': 'number'}
|
|
|
|
}
|
|
|
|
}
|
2017-09-01 23:10:21 +00:00
|
|
|
},
|
|
|
|
'definitions': {
|
|
|
|
'exchange': {
|
|
|
|
'type': 'object',
|
|
|
|
'properties': {
|
2017-10-06 10:22:04 +00:00
|
|
|
'name': {'type': 'string'},
|
2017-09-01 23:10:21 +00:00
|
|
|
'key': {'type': 'string'},
|
|
|
|
'secret': {'type': 'string'},
|
|
|
|
'pair_whitelist': {
|
|
|
|
'type': 'array',
|
|
|
|
'items': {'type': 'string'},
|
|
|
|
'uniqueItems': True
|
|
|
|
}
|
|
|
|
},
|
2017-10-07 15:36:48 +00:00
|
|
|
'required': ['name', 'key', 'secret', 'pair_whitelist']
|
2017-09-01 23:10:21 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
'anyOf': [
|
2017-10-06 10:22:04 +00:00
|
|
|
{'required': ['exchange']}
|
2017-09-01 23:10:21 +00:00
|
|
|
],
|
|
|
|
'required': [
|
|
|
|
'max_open_trades',
|
|
|
|
'stake_currency',
|
|
|
|
'stake_amount',
|
|
|
|
'dry_run',
|
|
|
|
'minimal_roi',
|
2017-09-17 19:37:46 +00:00
|
|
|
'bid_strategy',
|
2017-09-01 23:10:21 +00:00
|
|
|
'telegram'
|
|
|
|
]
|
|
|
|
}
|