Merge pull request #537 from gcarq/feature/objectify

Switch from procedural code to object + Code coverage 99.09%
This commit is contained in:
Janne Sinivirta 2018-03-21 08:59:28 +02:00 committed by GitHub
commit 62a3366fbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 8077 additions and 5077 deletions

View File

@ -1,4 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from freqtrade.main import main import sys
main()
from freqtrade.main import main, set_loggers
set_loggers()
main(sys.argv[1:])

View File

@ -66,7 +66,7 @@ python3 ./freqtrade/main.py --strategy my_awesome_strategy
``` ```
If the bot does not find your strategy file, it will display in an error If the bot does not find your strategy file, it will display in an error
message the reason (File not found, or errors in your code). message the reason (File not found, or errors in your code).
Learn more about strategy file in [optimize your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md). Learn more about strategy file in [optimize your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md).

View File

@ -1,122 +1,205 @@
""" """
Functions to analyze ticker data with indicators and produce buy and sell signals Functions to analyze ticker data with indicators and produce buy and sell signals
""" """
import logging from datetime import datetime, timedelta
from datetime import timedelta
from enum import Enum from enum import Enum
from typing import Dict, List from typing import Dict, List, Tuple
import arrow import arrow
from pandas import DataFrame, to_datetime from pandas import DataFrame, to_datetime
from freqtrade.exchange import get_ticker_history from freqtrade.exchange import get_ticker_history
from freqtrade.logger import Logger
from freqtrade.persistence import Trade
from freqtrade.strategy.strategy import Strategy from freqtrade.strategy.strategy import Strategy
logger = logging.getLogger(__name__)
class SignalType(Enum): class SignalType(Enum):
""" Enum to distinguish between buy and sell signals """ """
Enum to distinguish between buy and sell signals
"""
BUY = "buy" BUY = "buy"
SELL = "sell" SELL = "sell"
def parse_ticker_dataframe(ticker: list) -> DataFrame: class Analyze(object):
""" """
Analyses the trend for the given ticker history Analyze class contains everything the bot need to determine if the situation is good for
:param ticker: See exchange.get_ticker_history buying or selling.
:return: DataFrame
""" """
columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'} def __init__(self, config: dict) -> None:
frame = DataFrame(ticker) \ """
.rename(columns=columns) Init Analyze
if 'BV' in frame: :param config: Bot configuration (use the one from Configuration())
frame.drop('BV', 1, inplace=True) """
frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True) self.logger = Logger(name=__name__, level=config.get('loglevel')).get_logger()
frame.sort_values('date', inplace=True)
return frame
self.config = config
self.strategy = Strategy(self.config)
def populate_indicators(dataframe: DataFrame) -> DataFrame: @staticmethod
""" def parse_ticker_dataframe(ticker: list) -> DataFrame:
Adds several different TA indicators to the given DataFrame """
Analyses the trend for the given ticker history
:param ticker: See exchange.get_ticker_history
:return: DataFrame
"""
columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'}
frame = DataFrame(ticker) \
.rename(columns=columns)
if 'BV' in frame:
frame.drop('BV', 1, inplace=True)
frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True)
frame.sort_values('date', inplace=True)
return frame
Performance Note: For the best performance be frugal on the number of indicators def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
you are using. Let uncomment only the indicator you are using in your strategies """
or your hyperopt configuration, otherwise you will waste your memory and CPU usage. Adds several different TA indicators to the given DataFrame
"""
strategy = Strategy()
return strategy.populate_indicators(dataframe=dataframe)
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
"""
return self.strategy.populate_indicators(dataframe=dataframe)
def populate_buy_trend(dataframe: DataFrame) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
strategy = Strategy() return self.strategy.populate_buy_trend(dataframe=dataframe)
return strategy.populate_buy_trend(dataframe=dataframe)
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
return self.strategy.populate_sell_trend(dataframe=dataframe)
def populate_sell_trend(dataframe: DataFrame) -> DataFrame: def get_ticker_interval(self) -> int:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Return ticker interval to use
:param dataframe: DataFrame :return: Ticker interval value to use
:return: DataFrame with buy column """
""" return self.strategy.ticker_interval
strategy = Strategy()
return strategy.populate_sell_trend(dataframe=dataframe)
def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame:
"""
Parses the given ticker history and returns a populated DataFrame
add several TA indicators and buy signal to it
:return DataFrame with ticker data and indicator data
"""
dataframe = self.parse_ticker_dataframe(ticker_history)
dataframe = self.populate_indicators(dataframe)
dataframe = self.populate_buy_trend(dataframe)
dataframe = self.populate_sell_trend(dataframe)
return dataframe
def analyze_ticker(ticker_history: List[Dict]) -> DataFrame: def get_signal(self, pair: str, interval: int) -> Tuple[bool, bool]:
""" """
Parses the given ticker history and returns a populated DataFrame Calculates current signal based several technical analysis indicators
add several TA indicators and buy signal to it :param pair: pair in format BTC_ANT or BTC-ANT
:return DataFrame with ticker data and indicator data :param interval: Interval to use (in min)
""" :return: (Buy, Sell) A bool-tuple indicating buy/sell signal
dataframe = parse_ticker_dataframe(ticker_history) """
dataframe = populate_indicators(dataframe) ticker_hist = get_ticker_history(pair, interval)
dataframe = populate_buy_trend(dataframe) if not ticker_hist:
dataframe = populate_sell_trend(dataframe) self.logger.warning('Empty ticker history for pair %s', pair)
return dataframe return False, False
try:
dataframe = self.analyze_ticker(ticker_hist)
except ValueError as error:
self.logger.warning(
'Unable to analyze ticker for pair %s: %s',
pair,
str(error)
)
return False, False
except Exception as error:
self.logger.exception(
'Unexpected error when analyzing ticker for pair %s: %s',
pair,
str(error)
)
return False, False
# FIX: Maybe return False, if an error has occured, if dataframe.empty:
# Otherwise we might mask an error as an non-signal-scenario self.logger.warning('Empty dataframe for pair %s', pair)
def get_signal(pair: str, interval: int) -> (bool, bool): return False, False
"""
Calculates current signal based several technical analysis indicators
:param pair: pair in format BTC_ANT or BTC-ANT
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
"""
ticker_hist = get_ticker_history(pair, interval)
if not ticker_hist:
logger.warning('Empty ticker history for pair %s', pair)
return (False, False) # return False ?
try: latest = dataframe.iloc[-1]
dataframe = analyze_ticker(ticker_hist)
except ValueError as ex:
logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex))
return (False, False) # return False ?
except Exception as ex:
logger.exception('Unexpected error when analyzing ticker for pair %s: %s', pair, str(ex))
return (False, False) # return False ?
if dataframe.empty: # Check if dataframe is out of date
logger.warning('Empty dataframe for pair %s', pair) signal_date = arrow.get(latest['date'])
return (False, False) # return False ? if signal_date < arrow.utcnow() - timedelta(minutes=(interval + 5)):
self.logger.warning(
'Outdated history for pair %s. Last tick is %s minutes old',
pair,
(arrow.utcnow() - signal_date).seconds // 60
)
return False, False
latest = dataframe.iloc[-1] (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
self.logger.debug(
'trigger: %s (pair=%s) buy=%s sell=%s',
latest['date'],
pair,
str(buy),
str(sell)
)
return buy, sell
# Check if dataframe is out of date def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, sell: bool) -> bool:
signal_date = arrow.get(latest['date']) """
if signal_date < arrow.utcnow() - timedelta(minutes=(interval + 5)): This function evaluate if on the condition required to trigger a sell has been reached
logger.warning('Outdated history for pair %s. Last tick is %s minutes old', if the threshold is reached and updates the trade record.
pair, (arrow.utcnow() - signal_date).seconds // 60) :return: True if trade should be sold, False otherwise
return (False, False) # return False ? """
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
if self.min_roi_reached(trade=trade, current_rate=rate, current_time=date):
self.logger.debug('Required profit reached. Selling..')
return True
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 # Experimental: Check if the trade is profitable before selling it (avoid selling at loss)
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell)) if self.config.get('experimental', {}).get('sell_profit_only', False):
return (buy, sell) self.logger.debug('Checking if trade is profitable..')
if trade.calc_profit(rate=rate) <= 0:
return False
if sell and not buy and self.config.get('experimental', {}).get('use_sell_signal', False):
self.logger.debug('Sell signal received. Selling..')
return True
return False
def min_roi_reached(self, trade: Trade, current_rate: float, current_time: datetime) -> bool:
"""
Based an earlier trade and current price and ROI configuration, decides whether bot should
sell
:return True if bot should sell at current rate
"""
current_profit = trade.calc_profit_percent(current_rate)
if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss:
self.logger.debug('Stop loss hit.')
return True
# Check if time matches and current rate is above threshold
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
for duration, threshold in self.strategy.minimal_roi.items():
if time_diff <= duration:
return False
if current_profit > threshold:
return True
return False
def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
"""
Creates a dataframe and populates indicators for given ticker data
"""
return {pair: self.populate_indicators(self.parse_ticker_dataframe(pair_data))
for pair, pair_data in tickerdata.items()}

251
freqtrade/arguments.py Normal file
View File

@ -0,0 +1,251 @@
"""
This module contains the argument manager class
"""
import argparse
import logging
import os
import re
from typing import List, Tuple, Optional
from freqtrade import __version__
from freqtrade.constants import Constants
class Arguments(object):
"""
Arguments Class. Manage the arguments received by the cli
"""
def __init__(self, args: List[str], description: str):
self.args = args
self.parsed_arg = None
self.parser = argparse.ArgumentParser(description=description)
def _load_args(self) -> None:
self.common_args_parser()
self._build_subcommands()
def get_parsed_arg(self) -> argparse.Namespace:
"""
Return the list of arguments
:return: List[str] List of arguments
"""
if self.parsed_arg is None:
self._load_args()
self.parsed_arg = self.parse_args()
return self.parsed_arg
def parse_args(self) -> argparse.Namespace:
"""
Parses given arguments and returns an argparse Namespace instance.
"""
parsed_arg = self.parser.parse_args(self.args)
return parsed_arg
def common_args_parser(self) -> None:
"""
Parses given common arguments and returns them as a parsed object.
"""
self.parser.add_argument(
'-v', '--verbose',
help='be verbose',
action='store_const',
dest='loglevel',
const=logging.DEBUG,
default=logging.INFO,
)
self.parser.add_argument(
'--version',
action='version',
version='%(prog)s {}'.format(__version__),
)
self.parser.add_argument(
'-c', '--config',
help='specify configuration file (default: %(default)s)',
dest='config',
default='config.json',
type=str,
metavar='PATH',
)
self.parser.add_argument(
'-d', '--datadir',
help='path to backtest data (default: %(default)s',
dest='datadir',
default=os.path.join('freqtrade', 'tests', 'testdata'),
type=str,
metavar='PATH',
)
self.parser.add_argument(
'-s', '--strategy',
help='specify strategy file (default: %(default)s)',
dest='strategy',
default='default_strategy',
type=str,
metavar='PATH',
)
self.parser.add_argument(
'--dynamic-whitelist',
help='dynamically generate and update whitelist \
based on 24h BaseVolume (Default 20 currencies)', # noqa
dest='dynamic_whitelist',
const=Constants.DYNAMIC_WHITELIST,
type=int,
metavar='INT',
nargs='?',
)
self.parser.add_argument(
'--dry-run-db',
help='Force dry run to use a local DB "tradesv3.dry_run.sqlite" \
instead of memory DB. Work only if dry_run is enabled.',
action='store_true',
dest='dry_run_db',
)
@staticmethod
def backtesting_options(parser: argparse.ArgumentParser) -> None:
"""
Parses given arguments for Backtesting scripts.
"""
parser.add_argument(
'-l', '--live',
help='using live data',
action='store_true',
dest='live',
)
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',
)
parser.add_argument(
'--export',
help='export backtest results, argument are: trades\
Example --export=trades',
type=str,
default=None,
dest='export',
)
@staticmethod
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
'-i', '--ticker-interval',
help='specify ticker interval in minutes (1, 5, 30, 60, 1440)',
dest='ticker_interval',
type=int,
metavar='INT',
)
parser.add_argument(
'--realistic-simulation',
help='uses max_open_trades from config to simulate real world limitations',
action='store_true',
dest='realistic_simulation',
)
parser.add_argument(
'--timerange',
help='specify what timerange of data to use.',
default=None,
type=str,
dest='timerange',
)
@staticmethod
def hyperopt_options(parser: argparse.ArgumentParser) -> None:
"""
Parses given arguments for Hyperopt scripts.
"""
parser.add_argument(
'-e', '--epochs',
help='specify number of epochs (default: %(default)d)',
dest='epochs',
default=Constants.HYPEROPT_EPOCH,
type=int,
metavar='INT',
)
parser.add_argument(
'--use-mongodb',
help='parallelize evaluations with mongodb (requires mongod in PATH)',
dest='mongodb',
action='store_true',
)
parser.add_argument(
'-s', '--spaces',
help='Specify which parameters to hyperopt. Space separate list. \
Default: %(default)s',
choices=['all', 'buy', 'roi', 'stoploss'],
default='all',
nargs='+',
dest='spaces',
)
def _build_subcommands(self) -> None:
"""
Builds and attaches all subcommands
:return: None
"""
from freqtrade.optimize import backtesting, hyperopt
subparsers = self.parser.add_subparsers(dest='subparser')
# Add backtesting subcommand
backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module')
backtesting_cmd.set_defaults(func=backtesting.start)
self.optimizer_shared_options(backtesting_cmd)
self.backtesting_options(backtesting_cmd)
# Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
hyperopt_cmd.set_defaults(func=hyperopt.start)
self.optimizer_shared_options(hyperopt_cmd)
self.hyperopt_options(hyperopt_cmd)
@staticmethod
def parse_timerange(text: str) -> Optional[Tuple[List, int, int]]:
"""
Parse the value of the argument --timerange to determine what is the range desired
:param text: value from --timerange
:return: Start and End range period
"""
if text is None:
return None
syntax = [(r'^-(\d{8})$', (None, 'date')),
(r'^(\d{8})-$', ('date', None)),
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
(r'^(-\d+)$', (None, 'line')),
(r'^(\d+)-$', ('line', None)),
(r'^(\d+)-(\d+)$', ('index', 'index'))]
for rex, stype in syntax:
# Apply the regular expression to text
match = re.match(rex, text)
if match: # Regex has matched
rvals = match.groups()
index = 0
start = None
stop = None
if stype[0]:
start = rvals[index]
if stype[0] != 'date':
start = int(start)
index += 1
if stype[1]:
stop = rvals[index]
if stype[1] != 'date':
stop = int(stop)
return stype, start, stop
raise Exception('Incorrect syntax for timerange "%s"' % text)
def scripts_options(self) -> None:
"""
Parses given arguments for plot scripts.
"""
self.parser.add_argument(
'-p', '--pair',
help='Show profits for only this pairs. Pairs are comma-separated.',
dest='pair',
default=None
)

200
freqtrade/configuration.py Normal file
View File

@ -0,0 +1,200 @@
"""
This module contains the configuration class
"""
import json
from argparse import Namespace
from typing import Dict, Any
from jsonschema import Draft4Validator, validate
from jsonschema.exceptions import ValidationError, best_match
from freqtrade.constants import Constants
from freqtrade.logger import Logger
class Configuration(object):
"""
Class to read and init the bot configuration
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
"""
def __init__(self, args: Namespace) -> None:
self.args = args
self.logging = Logger(name=__name__)
self.logger = self.logging.get_logger()
self.config = None
def load_config(self) -> Dict[str, Any]:
"""
Extract information for sys.argv and load the bot configuration
:return: Configuration dictionary
"""
self.logger.info('Using config: %s ...', self.args.config)
config = self._load_config_file(self.args.config)
# Add the strategy file to use
config.update({'strategy': self.args.strategy})
# Load Common configuration
config = self._load_common_config(config)
# Load Backtesting
config = self._load_backtesting_config(config)
# Load Hyperopt
config = self._load_hyperopt_config(config)
return config
def _load_config_file(self, path: str) -> Dict[str, Any]:
"""
Loads a config file from the given path
:param path: path as str
:return: configuration as dictionary
"""
try:
with open(path) as file:
conf = json.load(file)
except FileNotFoundError:
self.logger.critical(
'Config file "%s" not found. Please create your config file',
path
)
exit(0)
if 'internals' not in conf:
conf['internals'] = {}
self.logger.info('Validating configuration ...')
return self._validate_config(conf)
def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract information for sys.argv and load common configuration
:return: configuration as dictionary
"""
# Log level
if 'loglevel' in self.args and self.args.loglevel:
config.update({'loglevel': self.args.loglevel})
self.logging.set_level(self.args.loglevel)
self.logger.info('Log level set at %s', config['loglevel'])
# Add dynamic_whitelist if found
if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist:
config.update({'dynamic_whitelist': self.args.dynamic_whitelist})
self.logger.info(
'Parameter --dynamic-whitelist detected. '
'Using dynamically generated whitelist. '
'(not applicable with Backtesting and Hyperopt)'
)
# Add dry_run_db if found and the bot in dry run
if self.args.dry_run_db and config.get('dry_run', False):
config.update({'dry_run_db': True})
self.logger.info('Parameter --dry-run-db detected ...')
if config.get('dry_run_db', False):
if config.get('dry_run', False):
self.logger.info('Dry_run will use the DB file: "tradesv3.dry_run.sqlite"')
else:
self.logger.info('Dry run is disabled. (--dry_run_db ignored)')
return config
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract information for sys.argv and load Backtesting configuration
:return: configuration as dictionary
"""
# If -i/--ticker-interval is used we override the configuration parameter
# (that will override the strategy configuration)
if 'ticker_interval' in self.args and self.args.ticker_interval:
config.update({'ticker_interval': self.args.ticker_interval})
self.logger.info('Parameter -i/--ticker-interval detected ...')
self.logger.info('Using ticker_interval: %d ...', config.get('ticker_interval'))
# If -l/--live is used we add it to the configuration
if 'live' in self.args and self.args.live:
config.update({'live': True})
self.logger.info('Parameter -l/--live detected ...')
# If --realistic-simulation is used we add it to the configuration
if 'realistic_simulation' in self.args and self.args.realistic_simulation:
config.update({'realistic_simulation': True})
self.logger.info('Parameter --realistic-simulation detected ...')
self.logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
# If --timerange is used we add it to the configuration
if 'timerange' in self.args and self.args.timerange:
config.update({'timerange': self.args.timerange})
self.logger.info('Parameter --timerange detected: %s ...', self.args.timerange)
# If --datadir is used we add it to the configuration
if 'datadir' in self.args and self.args.datadir:
config.update({'datadir': self.args.datadir})
self.logger.info('Parameter --datadir detected: %s ...', self.args.datadir)
# If -r/--refresh-pairs-cached is used we add it to the configuration
if 'refresh_pairs' in self.args and self.args.refresh_pairs:
config.update({'refresh_pairs': True})
self.logger.info('Parameter -r/--refresh-pairs-cached detected ...')
# If --export is used we add it to the configuration
if 'export' in self.args and self.args.export:
config.update({'export': self.args.export})
self.logger.info('Parameter --export detected: %s ...', self.args.export)
return config
def _load_hyperopt_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract information for sys.argv and load Hyperopt configuration
:return: configuration as dictionary
"""
# If --realistic-simulation is used we add it to the configuration
if 'epochs' in self.args and self.args.epochs:
config.update({'epochs': self.args.epochs})
self.logger.info('Parameter --epochs detected ...')
self.logger.info('Will run Hyperopt with for %s epochs ...', config.get('epochs'))
# If --mongodb is used we add it to the configuration
if 'mongodb' in self.args and self.args.mongodb:
config.update({'mongodb': self.args.mongodb})
self.logger.info('Parameter --use-mongodb detected ...')
# If --spaces is used we add it to the configuration
if 'spaces' in self.args and self.args.spaces:
config.update({'spaces': self.args.spaces})
self.logger.info('Parameter -s/--spaces detected: %s', config.get('spaces'))
return config
def _validate_config(self, conf: Dict[str, Any]) -> Dict[str, Any]:
"""
Validate the configuration follow the Config Schema
:param conf: Config in JSON format
:return: Returns the config if valid, otherwise throw an exception
"""
try:
validate(conf, Constants.CONF_SCHEMA)
return conf
except ValidationError as exception:
self.logger.fatal(
'Invalid configuration. See config.json.example. Reason: %s',
exception
)
raise ValidationError(
best_match(Draft4Validator(Constants.CONF_SCHEMA).iter_errors(conf)).message
)
def get_config(self) -> Dict[str, Any]:
"""
Return the config. Use this method to get the bot config
:return: Dict: Bot config
"""
if self.config is None:
self.config = self.load_config()
return self.config

122
freqtrade/constants.py Normal file
View File

@ -0,0 +1,122 @@
# pragma pylint: disable=too-few-public-methods
"""
List bot constants
"""
class Constants(object):
"""
Static class that contain all bot constants
"""
DYNAMIC_WHITELIST = 20 # pairs
PROCESS_THROTTLE_SECS = 5 # sec
TICKER_INTERVAL = 5 # min
HYPEROPT_EPOCH = 100 # epochs
RETRY_TIMEOUT = 30 # sec
DEFAULT_STRATEGY = 'default_strategy'
# Required json-schema for user specified config
CONF_SCHEMA = {
'type': 'object',
'properties': {
'max_open_trades': {'type': 'integer', 'minimum': 1},
'ticker_interval': {'type': 'integer', 'enum': [1, 5, 30, 60, 1440]},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
'stake_amount': {'type': 'number', 'minimum': 0.0005},
'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF',
'CLP', 'CNY', 'CZK', 'DKK',
'EUR', 'GBP', 'HKD', 'HUF',
'IDR', 'ILS', 'INR', 'JPY',
'KRW', 'MXN', 'MYR', 'NOK',
'NZD', 'PHP', 'PKR', 'PLN',
'RUB', 'SEK', 'SGD', 'THB',
'TRY', 'TWD', 'ZAR', 'USD']},
'dry_run': {'type': 'boolean'},
'minimal_roi': {
'type': 'object',
'patternProperties': {
'^[0-9.]+$': {'type': 'number'}
},
'minProperties': 1
},
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
'unfilledtimeout': {'type': 'integer', 'minimum': 0},
'bid_strategy': {
'type': 'object',
'properties': {
'ask_last_balance': {
'type': 'number',
'minimum': 0,
'maximum': 1,
'exclusiveMaximum': False
},
},
'required': ['ask_last_balance']
},
'exchange': {'$ref': '#/definitions/exchange'},
'experimental': {
'type': 'object',
'properties': {
'use_sell_signal': {'type': 'boolean'},
'sell_profit_only': {'type': 'boolean'}
}
},
'telegram': {
'type': 'object',
'properties': {
'enabled': {'type': 'boolean'},
'token': {'type': 'string'},
'chat_id': {'type': 'string'},
},
'required': ['enabled', 'token', 'chat_id']
},
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
'internals': {
'type': 'object',
'properties': {
'process_throttle_secs': {'type': 'number'},
'interval': {'type': 'integer'}
}
}
},
'definitions': {
'exchange': {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'key': {'type': 'string'},
'secret': {'type': 'string'},
'pair_whitelist': {
'type': 'array',
'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
},
'uniqueItems': True
},
'pair_blacklist': {
'type': 'array',
'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
},
'uniqueItems': True
}
},
'required': ['name', 'key', 'secret', 'pair_whitelist']
}
},
'anyOf': [
{'required': ['exchange']}
],
'required': [
'max_open_trades',
'stake_currency',
'stake_amount',
'fiat_display_currency',
'dry_run',
'bid_strategy',
'telegram'
]
}

View File

@ -1,8 +1,8 @@
import logging import logging
from typing import Dict, List, Optional from typing import Dict, List, Optional
from bittrex.bittrex import Bittrex as _Bittrex
from bittrex.bittrex import API_V1_1, API_V2_0 from bittrex.bittrex import API_V1_1, API_V2_0
from bittrex.bittrex import Bittrex as _Bittrex
from requests.exceptions import ContentDecodingError from requests.exceptions import ContentDecodingError
from freqtrade import OperationalException from freqtrade import OperationalException

View File

@ -5,12 +5,13 @@ e.g BTC to USD
import logging import logging
import time import time
from coinmarketcap import Market from coinmarketcap import Market
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CryptoFiat(): class CryptoFiat(object):
""" """
Object to describe what is the price of Crypto-currency in a FIAT Object to describe what is the price of Crypto-currency in a FIAT
""" """

537
freqtrade/freqtradebot.py Normal file
View File

@ -0,0 +1,537 @@
"""
Freqtrade is the main module of this bot. It contains the class Freqtrade()
"""
import copy
import json
import time
import traceback
from datetime import datetime
from typing import Dict, List, Optional, Any, Callable
import arrow
import requests
from cachetools import cached, TTLCache
from freqtrade import (DependencyException, OperationalException, exchange, persistence)
from freqtrade.analyze import Analyze
from freqtrade.constants import Constants
from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.logger import Logger
from freqtrade.persistence import Trade
from freqtrade.rpc.rpc_manager import RPCManager
from freqtrade.state import State
class FreqtradeBot(object):
"""
Freqtrade is the main class of the bot.
This is from here the bot start its logic.
"""
def __init__(self, config: Dict[str, Any], db_url: Optional[str] = None):
"""
Init all variables and object the bot need to work
:param config: configuration dict, you can use the Configuration.get_config()
method to get the config dict.
:param db_url: database connector string for sqlalchemy (Optional)
"""
# Init the logger
self.logger = Logger(name=__name__, level=config.get('loglevel')).get_logger()
# Init bot states
self._state = State.STOPPED
# Init objects
self.config = config
self.analyze = None
self.fiat_converter = None
self.rpc = None
self.persistence = None
self.exchange = None
self._init_modules(db_url=db_url)
def _init_modules(self, db_url: Optional[str] = None) -> None:
"""
Initializes all modules and updates the config
:param db_url: database connector string for sqlalchemy (Optional)
:return: None
"""
# Initialize all modules
self.analyze = Analyze(self.config)
self.fiat_converter = CryptoToFiatConverter()
self.rpc = RPCManager(self)
persistence.init(self.config, db_url)
exchange.init(self.config)
# Set initial application state
initial_state = self.config.get('initial_state')
if initial_state:
self.update_state(State[initial_state.upper()])
else:
self.update_state(State.STOPPED)
def clean(self) -> bool:
"""
Cleanup the application state und finish all pending tasks
:return: None
"""
self.rpc.send_msg('*Status:* `Stopping trader...`')
self.logger.info('Stopping trader and cleaning up modules...')
self.update_state(State.STOPPED)
self.rpc.cleanup()
persistence.cleanup()
return True
def update_state(self, state: State) -> None:
"""
Updates the application state
:param state: new state
:return: None
"""
self._state = state
def get_state(self) -> State:
"""
Gets the current application state
:return:
"""
return self._state
def worker(self, old_state: None) -> State:
"""
Trading routine that must be run at each loop
:param old_state: the previous service state from the previous call
:return: current service state
"""
new_state = self.get_state()
# Log state transition
if new_state != old_state:
self.rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
self.logger.info('Changing state to: %s', new_state.name)
if new_state == State.STOPPED:
time.sleep(1)
elif new_state == State.RUNNING:
min_secs = self.config.get('internals', {}).get(
'process_throttle_secs',
Constants.PROCESS_THROTTLE_SECS
)
nb_assets = self.config.get(
'dynamic_whitelist',
Constants.DYNAMIC_WHITELIST
)
self._throttle(func=self._process,
min_secs=min_secs,
nb_assets=nb_assets)
return new_state
def _throttle(self, 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)
self.logger.debug('Throttling %s for %.2f seconds', func.__name__, duration)
time.sleep(duration)
return result
def _process(self, nb_assets: Optional[int] = 0) -> bool:
"""
Queries the persistence layer for open trades and handles them,
otherwise a new trade is created.
:param: nb_assets: the maximum number of pairs to be traded at the same time
:return: True if one or more trades has been created or closed, False otherwise
"""
state_changed = False
try:
# Refresh whitelist based on wallet maintenance
sanitized_list = self._refresh_whitelist(
self._gen_pair_whitelist(
self.config['stake_currency']
) if nb_assets else self.config['exchange']['pair_whitelist']
)
# Keep only the subsets of pairs wanted (up to nb_assets)
final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
self.config['exchange']['pair_whitelist'] = final_list
# Query trades from persistence layer
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
# First process current opened trades
for trade in trades:
state_changed |= self.process_maybe_execute_sell(trade)
# Then looking for buy opportunities
if len(trades) < self.config['max_open_trades']:
state_changed = self.process_maybe_execute_buy()
if 'unfilledtimeout' in self.config:
# Check and handle any timed out open orders
self.check_handle_timedout(self.config['unfilledtimeout'])
Trade.session.flush()
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
self.logger.warning('%s, retrying in 30 seconds...', error)
time.sleep(Constants.RETRY_TIMEOUT)
except OperationalException:
self.rpc.send_msg(
'*Status:* OperationalException:\n```\n{traceback}```{hint}'
.format(
traceback=traceback.format_exc(),
hint='Issue `/start` if you think it is safe to restart.'
)
)
self.logger.exception('OperationalException. Stopping trader ...')
self.update_state(State.STOPPED)
return state_changed
@cached(TTLCache(maxsize=1, ttl=1800))
def _gen_pair_whitelist(self, base_currency: str, key: str = 'BaseVolume') -> List[str]:
"""
Updates the whitelist with with a dynamically generated list
:param base_currency: base currency as str
:param key: sort key (defaults to 'BaseVolume')
:return: List of pairs
"""
summaries = sorted(
(s for s in exchange.get_market_summaries() if
s['MarketName'].startswith(base_currency)),
key=lambda s: s.get(key) or 0.0,
reverse=True
)
return [s['MarketName'].replace('-', '_') for s in summaries]
def _refresh_whitelist(self, whitelist: List[str]) -> List[str]:
"""
Check wallet health and remove pair from whitelist if necessary
:param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to
trade
:return: the list of pairs the user wants to trade without the one unavailable or
black_listed
"""
sanitized_whitelist = whitelist
health = exchange.get_wallet_health()
known_pairs = set()
for status in health:
pair = '{}_{}'.format(self.config['stake_currency'], status['Currency'])
# pair is not int the generated dynamic market, or in the blacklist ... ignore it
if pair not in whitelist or pair in self.config['exchange'].get('pair_blacklist', []):
continue
# else the pair is valid
known_pairs.add(pair)
# Market is not active
if not status['IsActive']:
sanitized_whitelist.remove(pair)
self.logger.info(
'Ignoring %s from whitelist (reason: %s).',
pair, status.get('Notice') or 'wallet is not active'
)
# We need to remove pairs that are unknown
final_list = [x for x in sanitized_whitelist if x in known_pairs]
return final_list
def get_target_bid(self, ticker: Dict[str, float]) -> float:
"""
Calculates bid target between current ask price and last price
:param ticker: Ticker to use for getting Ask and Last Price
:return: float: Price
"""
if ticker['ask'] < ticker['last']:
return ticker['ask']
balance = self.config['bid_strategy']['ask_last_balance']
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
def create_trade(self) -> bool:
"""
Checks the implemented trading indicator(s) for a randomly picked pair,
if one pair triggers the buy_signal a new trade record gets created
:param stake_amount: amount of btc to spend
:param interval: Ticker interval used for Analyze
:return: True if a trade object has been created and persisted, False otherwise
"""
stake_amount = self.config['stake_amount']
interval = self.analyze.get_ticker_interval()
self.logger.info(
'Checking buy signals to create a new trade with stake_amount: %f ...',
stake_amount
)
whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
# Check if stake_amount is fulfilled
if exchange.get_balance(self.config['stake_currency']) < stake_amount:
raise DependencyException(
'stake amount is not fulfilled (currency={})'.format(self.config['stake_currency'])
)
# Remove currently opened and latest pairs from whitelist
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
if trade.pair in whitelist:
whitelist.remove(trade.pair)
self.logger.debug('Ignoring %s in pair whitelist', trade.pair)
if not whitelist:
raise DependencyException('No currency pairs in whitelist')
# Pick pair based on StochRSI buy signals
for _pair in whitelist:
(buy, sell) = self.analyze.get_signal(_pair, interval)
if buy and not sell:
pair = _pair
break
else:
return False
# Calculate amount
buy_limit = self.get_target_bid(exchange.get_ticker(pair))
amount = stake_amount / buy_limit
order_id = exchange.buy(pair, buy_limit, amount)
stake_amount_fiat = self.fiat_converter.convert_amount(
stake_amount,
self.config['stake_currency'],
self.config['fiat_display_currency']
)
# Create trade entity and return
self.rpc.send_msg(
'*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '
.format(
exchange.get_name().upper(),
pair.replace('_', '/'),
exchange.get_pair_detail_url(pair),
buy_limit,
stake_amount,
self.config['stake_currency'],
stake_amount_fiat,
self.config['fiat_display_currency']
)
)
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
trade = Trade(
pair=pair,
stake_amount=stake_amount,
amount=amount,
fee=exchange.get_fee(),
open_rate=buy_limit,
open_date=datetime.utcnow(),
exchange=exchange.get_name().upper(),
open_order_id=order_id
)
Trade.session.add(trade)
Trade.session.flush()
return True
def process_maybe_execute_buy(self) -> bool:
"""
Tries to execute a buy trade in a safe way
:return: True if executed
"""
try:
# Create entity and execute trade
if self.create_trade():
return True
self.logger.info('Found no buy signals for whitelisted currencies. Trying again..')
return False
except DependencyException as exception:
self.logger.warning('Unable to create trade: %s', exception)
return False
def process_maybe_execute_sell(self, trade: Trade) -> bool:
"""
Tries to execute a sell trade
:return: True if executed
"""
# Get order details for actual price per unit
if trade.open_order_id:
# Update trade with order values
self.logger.info('Found open order for %s', trade)
trade.update(exchange.get_order(trade.open_order_id))
if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair
return self.handle_trade(trade)
return False
def handle_trade(self, trade: Trade) -> bool:
"""
Sells the current pair if the threshold is reached and updates the trade record.
:return: True if trade has been sold, False otherwise
"""
if not trade.is_open:
raise ValueError('attempt to handle closed trade: {}'.format(trade))
self.logger.debug('Handling %s ...', trade)
current_rate = exchange.get_ticker(trade.pair)['bid']
(buy, sell) = (False, False)
if self.config.get('experimental', {}).get('use_sell_signal'):
(buy, sell) = self.analyze.get_signal(trade.pair, self.analyze.get_ticker_interval())
if self.analyze.should_sell(trade, current_rate, datetime.utcnow(), buy, sell):
self.execute_sell(trade, current_rate)
return True
return False
def check_handle_timedout(self, timeoutvalue: int) -> None:
"""
Check if any orders are timed out and cancel if neccessary
:param timeoutvalue: Number of minutes until order is considered timed out
:return: None
"""
timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
try:
order = exchange.get_order(trade.open_order_id)
except requests.exceptions.RequestException:
self.logger.info(
'Cannot query order for %s due to %s',
trade,
traceback.format_exc())
continue
ordertime = arrow.get(order['opened'])
# Check if trade is still actually open
if int(order['remaining']) == 0:
continue
if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold:
self.handle_timedout_limit_buy(trade, order)
elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold:
self.handle_timedout_limit_sell(trade, order)
# FIX: 20180110, why is cancel.order unconditionally here, whereas
# it is conditionally called in the
# handle_timedout_limit_sell()?
def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool:
"""Buy timeout - cancel order
:return: True if order was fully cancelled
"""
exchange.cancel_order(trade.open_order_id)
if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade
Trade.session.delete(trade)
# FIX? do we really need to flush, caller of
# check_handle_timedout will flush afterwards
Trade.session.flush()
self.logger.info('Buy order timeout for %s.', trade)
self.rpc.send_msg('*Timeout:* Unfilled buy order for {} cancelled'.format(
trade.pair.replace('_', '/')))
return True
# if trade is partially complete, edit the stake details for the trade
# and close the order
trade.amount = order['amount'] - order['remaining']
trade.stake_amount = trade.amount * trade.open_rate
trade.open_order_id = None
self.logger.info('Partial buy order timeout for %s.', trade)
self.rpc.send_msg('*Timeout:* Remaining buy order for {} cancelled'.format(
trade.pair.replace('_', '/')))
return False
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> bool:
"""
Sell timeout - cancel order and update trade
:return: True if order was fully cancelled
"""
if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade
exchange.cancel_order(trade.open_order_id)
trade.close_rate = None
trade.close_profit = None
trade.close_date = None
trade.is_open = True
trade.open_order_id = None
self.rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format(
trade.pair.replace('_', '/')))
self.logger.info('Sell order timeout for %s.', trade)
return True
# TODO: figure out how to handle partially complete sell orders
return False
def execute_sell(self, trade: Trade, limit: float) -> None:
"""
Executes a limit sell for the given trade and limit
:param trade: Trade instance
:param limit: limit rate for the sell order
:return: None
"""
# Execute sell and update trade record
order_id = exchange.sell(str(trade.pair), limit, trade.amount)
trade.open_order_id = order_id
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
profit_trade = trade.calc_profit(rate=limit)
current_rate = exchange.get_ticker(trade.pair, False)['bid']
profit = trade.calc_profit_percent(current_rate)
message = "*{exchange}:* Selling\n" \
"*Current Pair:* [{pair}]({pair_url})\n" \
"*Limit:* `{limit}`\n" \
"*Amount:* `{amount}`\n" \
"*Open Rate:* `{open_rate:.8f}`\n" \
"*Current Rate:* `{current_rate:.8f}`\n" \
"*Profit:* `{profit:.2f}%`" \
"".format(
exchange=trade.exchange,
pair=trade.pair,
pair_url=exchange.get_pair_detail_url(trade.pair),
limit=limit,
open_rate=trade.open_rate,
current_rate=current_rate,
amount=round(trade.amount, 8),
profit=round(profit * 100, 2),
)
# For regular case, when the configuration exists
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
fiat_converter = CryptoToFiatConverter()
profit_fiat = fiat_converter.convert_amount(
profit_trade,
self.config['stake_currency'],
self.config['fiat_display_currency']
)
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \
'` / {profit_fiat:.3f} {fiat})`' \
''.format(
gain="profit" if fmt_exp_profit > 0 else "loss",
profit_percent=fmt_exp_profit,
profit_coin=profit_trade,
coin=self.config['stake_currency'],
profit_fiat=profit_fiat,
fiat=self.config['fiat_display_currency'],
)
# Because telegram._forcesell does not have the configuration
# Ignore the FIAT value and does not show the stake_currency as well
else:
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f})`'.format(
gain="profit" if fmt_exp_profit > 0 else "loss",
profit_percent=fmt_exp_profit,
profit_coin=profit_trade
)
# Send the message
self.rpc.send_msg(message)
Trade.session.flush()

View File

@ -1,19 +1,19 @@
from math import exp, pi, sqrt, cos from math import exp, pi, sqrt, cos
import numpy import numpy as np
import talib as ta import talib as ta
from pandas import Series from pandas import Series
def went_up(series: Series) -> Series: def went_up(series: Series) -> bool:
return series > series.shift(1) return series > series.shift(1)
def went_down(series: Series) -> Series: def went_down(series: Series) -> bool:
return series < series.shift(1) return series < series.shift(1)
def ehlers_super_smoother(series: Series, smoothing: float = 6): def ehlers_super_smoother(series: Series, smoothing: float = 6) -> type(Series):
magic = pi * sqrt(2) / smoothing magic = pi * sqrt(2) / smoothing
a1 = exp(-magic) a1 = exp(-magic)
coeff2 = 2 * a1 * cos(magic) coeff2 = 2 * a1 * cos(magic)
@ -29,7 +29,7 @@ def ehlers_super_smoother(series: Series, smoothing: float = 6):
return filtered return filtered
def fishers_inverse(series: Series, smoothing: float = 0): def fishers_inverse(series: Series, smoothing: float = 0) -> np.ndarray:
""" Does a smoothed fishers inverse transformation. """ Does a smoothed fishers inverse transformation.
Can be used with any oscillator that goes from 0 to 100 like RSI or MFI """ Can be used with any oscillator that goes from 0 to 100 like RSI or MFI """
v1 = 0.1 * (series - 50) v1 = 0.1 * (series - 50)
@ -37,4 +37,4 @@ def fishers_inverse(series: Series, smoothing: float = 0):
v2 = ta.WMA(v1.values, timeperiod=smoothing) v2 = ta.WMA(v1.values, timeperiod=smoothing)
else: else:
v2 = v1 v2 = v1
return (numpy.exp(2 * v2)-1) / (numpy.exp(2 * v2) + 1) return (np.exp(2 * v2)-1) / (np.exp(2 * v2) + 1)

83
freqtrade/logger.py Normal file
View File

@ -0,0 +1,83 @@
# pragma pylint: disable=too-few-public-methods
"""
This module contains the class for logger and logging messages
"""
import logging
class Logger(object):
"""
Logging class
"""
def __init__(self, name='', level=logging.INFO) -> None:
"""
Init the logger class
:param name: Name of the Logger scope
:param level: Logger level that should be used
:return: None
"""
self.name = name
self.logger = None
if level is None:
level = logging.INFO
self.level = level
self._init_logger()
def _init_logger(self) -> None:
"""
Setup the bot logger configuration
:return: None
"""
logging.basicConfig(
level=self.level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
self.logger = self.get_logger()
self.set_level(self.level)
def get_logger(self) -> logging.RootLogger:
"""
Return the logger instance to use for sending message
:return: the logger instance
"""
return logging.getLogger(self.name)
def set_name(self, name: str) -> logging.RootLogger:
"""
Set the name of the logger
:param name: Name of the logger
:return: None
"""
self.name = name
self.logger = self.get_logger()
return self.logger
def set_level(self, level) -> None:
"""
Set the level of the logger
:param level:
:return: None
"""
self.level = level
self.logger.setLevel(self.level)
def set_format(self, log_format: str, propagate: bool = False) -> None:
"""
Set a new logging format
:return: None
"""
handler = logging.StreamHandler()
len_handlers = len(self.logger.handlers)
if len_handlers:
self.logger.removeHandler(handler)
handler.setFormatter(logging.Formatter(log_format))
self.logger.addHandler(handler)
self.logger.propagate = propagate

View File

@ -1,564 +1,74 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import copy """
import json Main Freqtrade bot script.
Read the documentation to know what cli arguments you need.
"""
import logging import logging
import sys import sys
import time from typing import List
import traceback
from datetime import datetime
from typing import Dict, List, Optional, Any
import arrow from freqtrade import (__version__)
import requests from freqtrade.arguments import Arguments
from cachetools import cached, TTLCache from freqtrade.configuration import Configuration
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.logger import Logger
from freqtrade import (DependencyException, OperationalException, __version__, logger = Logger(name='freqtrade').get_logger()
exchange, persistence, rpc)
from freqtrade.analyze import get_signal
from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.misc import (State, get_state, load_config, parse_args,
throttle, update_state)
from freqtrade.persistence import Trade
from freqtrade.strategy.strategy import Strategy
logger = logging.getLogger('freqtrade')
_CONF: Dict[str, Any] = {}
def refresh_whitelist(whitelist: List[str]) -> List[str]: def main(sysargv: List[str]) -> None:
""" """
Check wallet health and remove pair from whitelist if necessary This function will initiate the bot and start the trading loop.
:param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to trade
:return: the list of pairs the user wants to trade without the one unavailable or black_listed
"""
sanitized_whitelist = whitelist
health = exchange.get_wallet_health()
known_pairs = set()
for status in health:
pair = '{}_{}'.format(_CONF['stake_currency'], status['Currency'])
# pair is not int the generated dynamic market, or in the blacklist ... ignore it
if pair not in whitelist or pair in _CONF['exchange'].get('pair_blacklist', []):
continue
# else the pair is valid
known_pairs.add(pair)
# Market is not active
if not status['IsActive']:
sanitized_whitelist.remove(pair)
logger.info(
'Ignoring %s from whitelist (reason: %s).',
pair, status.get('Notice') or 'wallet is not active'
)
# We need to remove pairs that are unknown
final_list = [x for x in sanitized_whitelist if x in known_pairs]
return final_list
def process_maybe_execute_buy(interval: int) -> bool:
"""
Tries to execute a buy trade in a safe way
:return: True if executed
"""
try:
# Create entity and execute trade
if create_trade(float(_CONF['stake_amount']), interval):
return True
logger.info('Found no buy signals for whitelisted currencies. Trying again..')
return False
except DependencyException as exception:
logger.warning('Unable to create trade: %s', exception)
return False
def process_maybe_execute_sell(trade: Trade, interval: int) -> bool:
"""
Tries to execute a sell trade
:return: True if executed
"""
# Get order details for actual price per unit
if trade.open_order_id:
# Update trade with order values
logger.info('Found open order for %s', trade)
trade.update(exchange.get_order(trade.open_order_id))
if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair
return handle_trade(trade, interval)
return False
def _process(interval: int, nb_assets: Optional[int] = 0) -> bool:
"""
Queries the persistence layer for open trades and handles them,
otherwise a new trade is created.
:param: nb_assets: the maximum number of pairs to be traded at the same time
:return: True if one or more trades has been created or closed, False otherwise
"""
state_changed = False
try:
# Refresh whitelist based on wallet maintenance
sanitized_list = refresh_whitelist(
gen_pair_whitelist(
_CONF['stake_currency']
) if nb_assets else _CONF['exchange']['pair_whitelist']
)
# Keep only the subsets of pairs wanted (up to nb_assets)
final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
_CONF['exchange']['pair_whitelist'] = final_list
# Query trades from persistence layer
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
# First process current opened trades
for trade in trades:
state_changed |= process_maybe_execute_sell(trade, interval)
# Then looking for buy opportunities
if len(trades) < _CONF['max_open_trades']:
state_changed = process_maybe_execute_buy(interval)
if 'unfilledtimeout' in _CONF:
# Check and handle any timed out open orders
check_handle_timedout(_CONF['unfilledtimeout'])
Trade.session.flush()
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
logger.warning('%s, retrying in 30 seconds...', error)
time.sleep(30)
except OperationalException:
rpc.send_msg('*Status:* OperationalException:\n```\n{traceback}```{hint}'.format(
traceback=traceback.format_exc(),
hint='Issue `/start` if you think it is safe to restart.'
))
logger.exception('OperationalException. Stopping trader ...')
update_state(State.STOPPED)
return state_changed
# FIX: 20180110, why is cancel.order unconditionally here, whereas
# it is conditionally called in the
# handle_timedout_limit_sell()?
def handle_timedout_limit_buy(trade: Trade, order: Dict) -> bool:
"""Buy timeout - cancel order
:return: True if order was fully cancelled
"""
exchange.cancel_order(trade.open_order_id)
if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade
Trade.session.delete(trade)
# FIX? do we really need to flush, caller of
# check_handle_timedout will flush afterwards
Trade.session.flush()
logger.info('Buy order timeout for %s.', trade)
rpc.send_msg('*Timeout:* Unfilled buy order for {} cancelled'.format(
trade.pair.replace('_', '/')))
return True
# if trade is partially complete, edit the stake details for the trade
# and close the order
trade.amount = order['amount'] - order['remaining']
trade.stake_amount = trade.amount * trade.open_rate
trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade)
rpc.send_msg('*Timeout:* Remaining buy order for {} cancelled'.format(
trade.pair.replace('_', '/')))
return False
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
def handle_timedout_limit_sell(trade: Trade, order: Dict) -> bool:
"""
Sell timeout - cancel order and update trade
:return: True if order was fully cancelled
"""
if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade
exchange.cancel_order(trade.open_order_id)
trade.close_rate = None
trade.close_profit = None
trade.close_date = None
trade.is_open = True
trade.open_order_id = None
rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format(
trade.pair.replace('_', '/')))
logger.info('Sell order timeout for %s.', trade)
return True
# TODO: figure out how to handle partially complete sell orders
return False
def check_handle_timedout(timeoutvalue: int) -> None:
"""
Check if any orders are timed out and cancel if neccessary
:param timeoutvalue: Number of minutes until order is considered timed out
:return: None :return: None
""" """
timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime arguments = Arguments(
sysargv,
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all(): 'Simple High Frequency Trading Bot for crypto currencies'
try:
order = exchange.get_order(trade.open_order_id)
except requests.exceptions.RequestException:
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
continue
ordertime = arrow.get(order['opened'])
# Check if trade is still actually open
if int(order['remaining']) == 0:
continue
if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold:
handle_timedout_limit_buy(trade, order)
elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold:
handle_timedout_limit_sell(trade, order)
def execute_sell(trade: Trade, limit: float) -> None:
"""
Executes a limit sell for the given trade and limit
:param trade: Trade instance
:param limit: limit rate for the sell order
:return: None
"""
# Execute sell and update trade record
order_id = exchange.sell(str(trade.pair), limit, trade.amount)
trade.open_order_id = order_id
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
profit_trade = trade.calc_profit(rate=limit)
current_rate = exchange.get_ticker(trade.pair, False)['bid']
profit = trade.calc_profit_percent(current_rate)
message = """*{exchange}:* Selling
*Current Pair:* [{pair}]({pair_url})
*Limit:* `{limit}`
*Amount:* `{amount}`
*Open Rate:* `{open_rate:.8f}`
*Current Rate:* `{current_rate:.8f}`
*Profit:* `{profit:.2f}%`
""".format(
exchange=trade.exchange,
pair=trade.pair,
pair_url=exchange.get_pair_detail_url(trade.pair),
limit=limit,
open_rate=trade.open_rate,
current_rate=current_rate,
amount=round(trade.amount, 8),
profit=round(profit * 100, 2),
) )
args = arguments.get_parsed_arg()
# For regular case, when the configuration exists # A subcommand has been issued.
if 'stake_currency' in _CONF and 'fiat_display_currency' in _CONF: # Means if Backtesting or Hyperopt have been called we exit the bot
fiat_converter = CryptoToFiatConverter()
profit_fiat = fiat_converter.convert_amount(
profit_trade,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \
'` / {profit_fiat:.3f} {fiat})`'.format(
gain="profit" if fmt_exp_profit > 0 else "loss",
profit_percent=fmt_exp_profit,
profit_coin=profit_trade,
coin=_CONF['stake_currency'],
profit_fiat=profit_fiat,
fiat=_CONF['fiat_display_currency'],
)
# Because telegram._forcesell does not have the configuration
# Ignore the FIAT value and does not show the stake_currency as well
else:
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f})`'.format(
gain="profit" if fmt_exp_profit > 0 else "loss",
profit_percent=fmt_exp_profit,
profit_coin=profit_trade
)
# Send the message
rpc.send_msg(message)
Trade.session.flush()
def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool:
"""
Based an earlier trade and current price and ROI configuration, decides whether bot should sell
:return True if bot should sell at current rate
"""
strategy = Strategy()
current_profit = trade.calc_profit_percent(current_rate)
if strategy.stoploss is not None and current_profit < strategy.stoploss:
logger.debug('Stop loss hit.')
return True
# Check if time matches and current rate is above threshold
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
for duration, threshold in strategy.minimal_roi.items():
if time_diff <= duration:
return False
if current_profit > threshold:
return True
return False
def should_sell(trade: Trade, rate: float, date: datetime, buy: bool, sell: bool) -> bool:
"""
This function evaluate if on the condition required to trigger a sell has been reached
if the threshold is reached and updates the trade record.
:return: True if trade should be sold, False otherwise
"""
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
if min_roi_reached(trade, rate, date):
logger.debug('Required profit reached. Selling..')
return True
# Experimental: Check if the trade is profitable before selling it (avoid selling at loss)
if _CONF.get('experimental', {}).get('sell_profit_only', False):
logger.debug('Checking if trade is profitable..')
if trade.calc_profit(rate=rate) <= 0:
return False
if sell and not buy and _CONF.get('experimental', {}).get('use_sell_signal', False):
logger.debug('Sell signal received. Selling..')
return True
return False
def handle_trade(trade: Trade, interval: int) -> bool:
"""
Sells the current pair if the threshold is reached and updates the trade record.
:return: True if trade has been sold, False otherwise
"""
if not trade.is_open:
raise ValueError('attempt to handle closed trade: {}'.format(trade))
logger.debug('Handling %s ...', trade)
current_rate = exchange.get_ticker(trade.pair)['bid']
(buy, sell) = (False, False)
if _CONF.get('experimental', {}).get('use_sell_signal'):
(buy, sell) = get_signal(trade.pair, interval)
if should_sell(trade, current_rate, datetime.utcnow(), buy, sell):
execute_sell(trade, current_rate)
return True
return False
def get_target_bid(ticker: Dict[str, float]) -> float:
""" Calculates bid target between current ask price and last price """
if ticker['ask'] < ticker['last']:
return ticker['ask']
balance = _CONF['bid_strategy']['ask_last_balance']
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
def create_trade(stake_amount: float, interval: int) -> bool:
"""
Checks the implemented trading indicator(s) for a randomly picked pair,
if one pair triggers the buy_signal a new trade record gets created
:param stake_amount: amount of btc to spend
:return: True if a trade object has been created and persisted, False otherwise
"""
logger.info(
'Checking buy signals to create a new trade with stake_amount: %f ...',
stake_amount
)
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
# Check if stake_amount is fulfilled
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
raise DependencyException(
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
)
# Remove currently opened and latest pairs from whitelist
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
if trade.pair in whitelist:
whitelist.remove(trade.pair)
logger.debug('Ignoring %s in pair whitelist', trade.pair)
if not whitelist:
raise DependencyException('No currency pairs in whitelist')
# Pick pair based on StochRSI buy signals
for _pair in whitelist:
(buy, sell) = get_signal(_pair, interval)
if buy and not sell:
pair = _pair
break
else:
return False
# Calculate amount
buy_limit = get_target_bid(exchange.get_ticker(pair))
amount = stake_amount / buy_limit
order_id = exchange.buy(pair, buy_limit, amount)
fiat_converter = CryptoToFiatConverter()
stake_amount_fiat = fiat_converter.convert_amount(
stake_amount,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
# Create trade entity and return
rpc.send_msg('*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '.format(
exchange.get_name().upper(),
pair.replace('_', '/'),
exchange.get_pair_detail_url(pair),
buy_limit, stake_amount, _CONF['stake_currency'],
stake_amount_fiat, _CONF['fiat_display_currency']
))
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
trade = Trade(
pair=pair,
stake_amount=stake_amount,
amount=amount,
fee=exchange.get_fee(),
open_rate=buy_limit,
open_date=datetime.utcnow(),
exchange=exchange.get_name().upper(),
open_order_id=order_id
)
Trade.session.add(trade)
Trade.session.flush()
return True
def init(config: dict, db_url: Optional[str] = None) -> None:
"""
Initializes all modules and updates the config
:param config: config as dict
:param db_url: database connector string for sqlalchemy (Optional)
:return: None
"""
# Initialize all modules
rpc.init(config)
persistence.init(config, db_url)
exchange.init(config)
strategy = Strategy()
strategy.init(config)
# Set initial application state
initial_state = config.get('initial_state')
if initial_state:
update_state(State[initial_state.upper()])
else:
update_state(State.STOPPED)
@cached(TTLCache(maxsize=1, ttl=1800))
def gen_pair_whitelist(base_currency: str, key: str = 'BaseVolume') -> List[str]:
"""
Updates the whitelist with with a dynamically generated list
:param base_currency: base currency as str
:param key: sort key (defaults to 'BaseVolume')
:return: List of pairs
"""
summaries = sorted(
(s for s in exchange.get_market_summaries() if s['MarketName'].startswith(base_currency)),
key=lambda s: s.get(key) or 0.0,
reverse=True
)
return [s['MarketName'].replace('-', '_') for s in summaries]
def cleanup() -> None:
"""
Cleanup the application state und finish all pending tasks
:return: None
"""
rpc.send_msg('*Status:* `Stopping trader...`')
logger.info('Stopping trader and cleaning up modules...')
update_state(State.STOPPED)
persistence.cleanup()
rpc.cleanup()
exit(0)
def main(sysargv=sys.argv[1:]) -> int:
"""
Loads and validates the config and handles the main loop
:return: None
"""
global _CONF
args = parse_args(sysargv,
'Simple High Frequency Trading Bot for crypto currencies')
# A subcommand has been issued
if hasattr(args, 'func'): if hasattr(args, 'func'):
args.func(args) args.func(args)
return 0 return 0
# Initialize logger
logging.basicConfig(
level=args.loglevel,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
logger.info( logger.info(
'Starting freqtrade %s (loglevel=%s)', 'Starting freqtrade %s (loglevel=%s)',
__version__, __version__,
logging.getLevelName(args.loglevel) logging.getLevelName(args.loglevel)
) )
# Load and validate configuration
_CONF = load_config(args.config)
# Add the strategy file to use
_CONF.update({'strategy': args.strategy})
# Initialize all modules and start main loop
if args.dynamic_whitelist:
logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)')
# If the user ask for Dry run with a local DB instead of memory
if args.dry_run_db:
if _CONF.get('dry_run', False):
_CONF.update({'dry_run_db': True})
logger.info(
'Dry_run will use the DB file: "tradesv3.dry_run.sqlite". (--dry_run_db detected)'
)
else:
logger.info('Dry run is disabled. (--dry_run_db ignored)')
try: try:
init(_CONF) # Load and validate configuration
old_state = None configuration = Configuration(args)
while True: # Init the bot
new_state = get_state() freqtrade = FreqtradeBot(configuration.get_config())
# Log state transition
if new_state != old_state: state = None
rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower())) while 1:
logger.info('Changing state to: %s', new_state.name) state = freqtrade.worker(old_state=state)
if new_state == State.STOPPED:
time.sleep(1)
elif new_state == State.RUNNING:
throttle(
_process,
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
nb_assets=args.dynamic_whitelist,
interval=int(_CONF.get('ticker_interval', 5))
)
old_state = new_state
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info('SIGINT received, aborting ...') logger.info('SIGINT received, aborting ...')
except BaseException: except BaseException:
logger.exception('Fatal exception!') logger.exception('Fatal exception!')
finally: finally:
cleanup() freqtrade.clean()
return 0 sys.exit(0)
def set_loggers() -> None:
"""
Set the logger level for Third party libs
:return: None
"""
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
logging.getLogger('telegram').setLevel(logging.INFO)
if __name__ == '__main__': if __name__ == '__main__':
set_loggers()
main(sys.argv[1:]) main(sys.argv[1:])

View File

@ -1,30 +1,29 @@
import argparse """
import enum Various tool function for Freqtrade and scripts
"""
import json import json
import logging import logging
import time
import os
import re import re
from datetime import datetime from datetime import datetime
from typing import Any, Callable, Dict, List from typing import Dict
import numpy as np import numpy as np
from jsonschema import Draft4Validator, validate from pandas import DataFrame
from jsonschema.exceptions import ValidationError, best_match
from wrapt import synchronized
from freqtrade import __version__
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class State(enum.Enum): def shorten_date(_date: str) -> str:
RUNNING = 0 """
STOPPED = 1 Trim the date so it fits on small screens
"""
new_date = re.sub('seconds?', 'sec', _date)
# Current application state new_date = re.sub('minutes?', 'min', new_date)
_STATE = State.STOPPED new_date = re.sub('hours?', 'h', new_date)
new_date = re.sub('days?', 'd', new_date)
new_date = re.sub('^an?', '1', new_date)
return new_date
############################################ ############################################
@ -32,8 +31,7 @@ _STATE = State.STOPPED
# Matplotlib doesn't support ::datetime64, # # Matplotlib doesn't support ::datetime64, #
# so we need to convert it into ::datetime # # so we need to convert it into ::datetime #
############################################ ############################################
def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
def datesarray_to_datetimearray(dates):
""" """
Convert an pandas-array of timestamps into Convert an pandas-array of timestamps into
An numpy-array of datetimes An numpy-array of datetimes
@ -41,13 +39,18 @@ def datesarray_to_datetimearray(dates):
""" """
times = [] times = []
dates = dates.astype(datetime) dates = dates.astype(datetime)
for i in range(0, dates.size): for index in range(0, dates.size):
date = dates[i].to_pydatetime() date = dates[index].to_pydatetime()
times.append(date) times.append(date)
return np.array(times) return np.array(times)
def common_datearray(dfs): def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray:
"""
Return dates from Dataframe
:param dfs: Dict with format pair: pair_data
:return: List of dates
"""
alldates = {} alldates = {}
for pair, pair_data in dfs.items(): for pair, pair_data in dfs.items():
dates = datesarray_to_datetimearray(pair_data['date']) dates = datesarray_to_datetimearray(pair_data['date'])
@ -61,375 +64,11 @@ def common_datearray(dfs):
def file_dump_json(filename, data) -> None: def file_dump_json(filename, data) -> None:
with open(filename, 'w') as fp:
json.dump(data, fp)
@synchronized
def update_state(state: State) -> None:
""" """
Updates the application state Dump JSON data into a file
:param state: new state :param filename: file to create
:return: None :param data: JSON Data to save
"""
global _STATE
_STATE = state
@synchronized
def get_state() -> State:
"""
Gets the current application state
:return: :return:
""" """
return _STATE with open(filename, 'w') as fp:
json.dump(data, fp, default=str)
def load_config(path: str) -> Dict:
"""
Loads a config file from the given path
:param path: path as str
:return: configuration as dictionary
"""
with open(path) as file:
conf = json.load(file)
if 'internals' not in conf:
conf['internals'] = {}
logger.info('Validating configuration ...')
try:
validate(conf, CONF_SCHEMA)
return conf
except ValidationError as exception:
logger.fatal('Invalid configuration. See config.json.example. Reason: %s', exception)
raise ValidationError(
best_match(Draft4Validator(CONF_SCHEMA).iter_errors(conf)).message
)
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
def common_args_parser(description: str):
"""
Parses given common arguments and returns them as a parsed object.
"""
parser = argparse.ArgumentParser(
description=description
)
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__),
)
parser.add_argument(
'-c', '--config',
help='specify configuration file (default: %(default)s)',
dest='config',
default='config.json',
type=str,
metavar='PATH',
)
parser.add_argument(
'-d', '--datadir',
help='path to backtest data (default: %(default)s',
dest='datadir',
default=os.path.join('freqtrade', 'tests', 'testdata'),
type=str,
metavar='PATH',
)
parser.add_argument(
'-s', '--strategy',
help='specify strategy file (default: %(default)s)',
dest='strategy',
default='default_strategy',
type=str,
metavar='PATH',
)
return parser
def parse_args(args: List[str], description: str):
"""
Parses given arguments and returns an argparse Namespace instance.
Returns None if a sub command has been selected and executed.
"""
parser = common_args_parser(description)
parser.add_argument(
'--dry-run-db',
help='Force dry run to use a local DB "tradesv3.dry_run.sqlite" \
instead of memory DB. Work only if dry_run is enabled.',
action='store_true',
dest='dry_run_db',
)
parser.add_argument(
'--dynamic-whitelist',
help='dynamically generate and update whitelist \
based on 24h BaseVolume (Default 20 currencies)', # noqa
dest='dynamic_whitelist',
const=20,
type=int,
metavar='INT',
nargs='?',
)
build_subcommands(parser)
return parser.parse_args(args)
def scripts_options(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
'-p', '--pair',
help='Show profits for only this pairs. Pairs are comma-separated.',
dest='pair',
default=None
)
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
'-i', '--ticker-interval',
help='specify ticker interval in minutes (1, 5, 30, 60, 1440)',
dest='ticker_interval',
type=int,
metavar='INT',
)
parser.add_argument(
'--realistic-simulation',
help='uses max_open_trades from config to simulate real world limitations',
action='store_true',
dest='realistic_simulation',
)
parser.add_argument(
'--timerange',
help='Specify what timerange of data to use.',
default=None,
type=str,
dest='timerange',
)
def backtesting_options(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
'-l', '--live',
action='store_true',
dest='live',
help='using live data',
)
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',
)
parser.add_argument(
'--export',
help='Export backtest results, argument are: trades\
Example --export=trades',
type=str,
default=None,
dest='export',
)
def hyperopt_options(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
'-e', '--epochs',
help='specify number of epochs (default: %(default)d)',
dest='epochs',
default=100,
type=int,
metavar='INT',
)
parser.add_argument(
'--use-mongodb',
help='parallelize evaluations with mongodb (requires mongod in PATH)',
dest='mongodb',
action='store_true',
)
parser.add_argument(
'-s', '--spaces',
help='Specify which parameters to hyperopt. Space separate list. \
Default: %(default)s',
choices=['all', 'buy', 'roi', 'stoploss'],
default='all',
nargs='+',
dest='spaces',
)
def parse_timerange(text):
if text is None:
return None
syntax = [(r'^-(\d{8})$', (None, 'date')),
(r'^(\d{8})-$', ('date', None)),
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
(r'^(-\d+)$', (None, 'line')),
(r'^(\d+)-$', ('line', None)),
(r'^(\d+)-(\d+)$', ('index', 'index'))]
for rex, stype in syntax:
# Apply the regular expression to text
match = re.match(rex, text)
if match: # Regex has matched
rvals = match.groups()
index = 0
start = None
stop = None
if stype[0]:
start = rvals[index]
if stype[0] != 'date':
start = int(start)
index += 1
if stype[1]:
stop = rvals[index]
if stype[1] != 'date':
stop = int(stop)
return (stype, start, stop)
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)
optimizer_shared_options(backtesting_cmd)
backtesting_options(backtesting_cmd)
# Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
hyperopt_cmd.set_defaults(func=hyperopt.start)
optimizer_shared_options(hyperopt_cmd)
hyperopt_options(hyperopt_cmd)
# Required json-schema for user specified config
CONF_SCHEMA = {
'type': 'object',
'properties': {
'max_open_trades': {'type': 'integer', 'minimum': 0},
'ticker_interval': {'type': 'integer', 'enum': [1, 5, 30, 60, 1440]},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
'stake_amount': {'type': 'number', 'minimum': 0.0005},
'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF',
'CLP', 'CNY', 'CZK', 'DKK',
'EUR', 'GBP', 'HKD', 'HUF',
'IDR', 'ILS', 'INR', 'JPY',
'KRW', 'MXN', 'MYR', 'NOK',
'NZD', 'PHP', 'PKR', 'PLN',
'RUB', 'SEK', 'SGD', 'THB',
'TRY', 'TWD', 'ZAR', 'USD']},
'dry_run': {'type': 'boolean'},
'minimal_roi': {
'type': 'object',
'patternProperties': {
'^[0-9.]+$': {'type': 'number'}
},
'minProperties': 1
},
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
'unfilledtimeout': {'type': 'integer', 'minimum': 0},
'bid_strategy': {
'type': 'object',
'properties': {
'ask_last_balance': {
'type': 'number',
'minimum': 0,
'maximum': 1,
'exclusiveMaximum': False
},
},
'required': ['ask_last_balance']
},
'exchange': {'$ref': '#/definitions/exchange'},
'experimental': {
'type': 'object',
'properties': {
'use_sell_signal': {'type': 'boolean'},
'sell_profit_only': {'type': 'boolean'}
}
},
'telegram': {
'type': 'object',
'properties': {
'enabled': {'type': 'boolean'},
'token': {'type': 'string'},
'chat_id': {'type': 'string'},
},
'required': ['enabled', 'token', 'chat_id']
},
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
'internals': {
'type': 'object',
'properties': {
'process_throttle_secs': {'type': 'number'},
'interval': {'type': 'integer'}
}
}
},
'definitions': {
'exchange': {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'key': {'type': 'string'},
'secret': {'type': 'string'},
'pair_whitelist': {
'type': 'array',
'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
},
'uniqueItems': True
},
'pair_blacklist': {
'type': 'array',
'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
},
'uniqueItems': True
}
},
'required': ['name', 'key', 'secret', 'pair_whitelist']
}
},
'anyOf': [
{'required': ['exchange']}
],
'required': [
'max_open_trades',
'stake_currency',
'stake_amount',
'fiat_display_currency',
'dry_run',
'bid_strategy',
'telegram'
]
}

View File

@ -1,22 +1,20 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
import logging import gzip
import json import json
import os import os
from typing import Optional, List, Dict from typing import Optional, List, Dict, Tuple
from pandas import DataFrame
from freqtrade.exchange import get_ticker_history
from freqtrade.analyze import populate_indicators, parse_ticker_dataframe
from freqtrade import misc from freqtrade import misc
from freqtrade.exchange import get_ticker_history
from freqtrade.logger import Logger
from user_data.hyperopt_conf import hyperopt_optimize_conf from user_data.hyperopt_conf import hyperopt_optimize_conf
import gzip
logger = logging.getLogger(__name__) logger = Logger(name=__name__).get_logger()
def trim_tickerlist(tickerlist, timerange): def trim_tickerlist(tickerlist: List[Dict], timerange: Tuple[Tuple, int, int]) -> List[Dict]:
(stype, start, stop) = timerange stype, start, stop = timerange
if stype == (None, 'line'): if stype == (None, 'line'):
return tickerlist[stop:] return tickerlist[stop:]
elif stype == ('line', None): elif stype == ('line', None):
@ -27,7 +25,10 @@ def trim_tickerlist(tickerlist, timerange):
return tickerlist return tickerlist
def load_tickerdata_file(datadir, pair, ticker_interval, timerange=None): def load_tickerdata_file(
datadir: str, pair: str,
ticker_interval: int,
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Optional[List[Dict]]:
""" """
Load a pair from file, Load a pair from file,
:return dict OR empty if unsuccesful :return dict OR empty if unsuccesful
@ -57,12 +58,12 @@ def load_tickerdata_file(datadir, pair, ticker_interval, timerange=None):
return pairdata return pairdata
def load_data(datadir: str, ticker_interval: int, pairs: Optional[List[str]] = None, def load_data(datadir: str, ticker_interval: int,
refresh_pairs: Optional[bool] = False, timerange=None) -> Dict[str, List]: pairs: Optional[List[str]] = None,
refresh_pairs: Optional[bool] = False,
timerange: Optional[Tuple[Tuple, int, int]] = 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 pairs: list of pairs
:return: dict :return: dict
""" """
result = {} result = {}
@ -85,21 +86,13 @@ def load_data(datadir: str, ticker_interval: int, pairs: Optional[List[str]] = N
return result return result
def tickerdata_to_dataframe(data):
preprocessed = preprocess(data)
return preprocessed
def preprocess(tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
"""Creates a dataframe and populates indicators for given ticker data"""
return {pair: populate_indicators(parse_ticker_dataframe(pair_data))
for pair, pair_data in tickerdata.items()}
def make_testdata_path(datadir: str) -> str: def make_testdata_path(datadir: str) -> str:
"""Return the path where testdata files are stored""" """Return the path where testdata files are stored"""
return datadir or os.path.abspath(os.path.join(os.path.dirname(__file__), return datadir or os.path.abspath(
'..', 'tests', 'testdata')) os.path.join(
os.path.dirname(__file__), '..', 'tests', 'testdata'
)
)
def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool: def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
@ -108,19 +101,15 @@ def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
try: try:
download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval) download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval)
except BaseException: except BaseException:
logger.info('Failed to download the pair: "{pair}", Interval: {interval} min'.format( logger.info(
pair=pair, 'Failed to download the pair: "%s", Interval: %s min',
interval=ticker_interval, pair,
)) ticker_interval
)
return False return False
return True return True
def file_dump_json(filename, data):
with open(filename, "wt") as fp:
json.dump(data, fp)
# FIX: 20180110, suggest rename interval to tick_interval # FIX: 20180110, suggest rename interval to tick_interval
def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> bool: def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> bool:
""" """
@ -131,10 +120,11 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) ->
""" """
path = make_testdata_path(datadir) path = make_testdata_path(datadir)
logger.info('Download the pair: "{pair}", Interval: {interval} min'.format( logger.info(
pair=pair, 'Download the pair: "%s", Interval: %s min',
interval=interval, pair,
)) interval
)
filepair = pair.replace("-", "_") filepair = pair.replace("-", "_")
filename = os.path.join(path, '{pair}-{interval}.json'.format( filename = os.path.join(path, '{pair}-{interval}.json'.format(
@ -143,10 +133,10 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) ->
)) ))
if os.path.isfile(filename): if os.path.isfile(filename):
with open(filename, "rt") as fp: with open(filename, "rt") as file:
data = json.load(fp) data = json.load(file)
logger.debug("Current Start: {}".format(data[1]['T'])) logger.debug("Current Start: %s", data[1]['T'])
logger.debug("Current End: {}".format(data[-1:][0]['T'])) logger.debug("Current End: %s", data[-1:][0]['T'])
else: else:
data = [] data = []
logger.debug("Current Start: None") logger.debug("Current Start: None")
@ -156,8 +146,8 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) ->
for row in new_data: for row in new_data:
if row not in data: if row not in data:
data.append(row) data.append(row)
logger.debug("New Start: {}".format(data[1]['T'])) logger.debug("New Start: %s", data[1]['T'])
logger.debug("New End: {}".format(data[-1:][0]['T'])) logger.debug("New End: %s", data[-1:][0]['T'])
data = sorted(data, key=lambda data: data['T']) data = sorted(data, key=lambda data: data['T'])
misc.file_dump_json(filename, data) misc.file_dump_json(filename, data)

View File

@ -1,231 +1,312 @@
# pragma pylint: disable=missing-docstring,W0212 # pragma pylint: disable=missing-docstring, W0212, too-many-arguments
import logging """
from typing import Dict, Tuple This module contains the backtesting logic
"""
from argparse import Namespace
from typing import Dict, Tuple, Any, List, Optional
import arrow import arrow
from pandas import DataFrame, Series from pandas import DataFrame, Series
from tabulate import tabulate from tabulate import tabulate
import freqtrade.misc as misc
import freqtrade.optimize as optimize import freqtrade.optimize as optimize
from freqtrade import exchange from freqtrade import exchange
from freqtrade.analyze import populate_buy_trend, populate_sell_trend from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.exchange import Bittrex from freqtrade.exchange import Bittrex
from freqtrade.main import should_sell from freqtrade.logger import Logger
from freqtrade.misc import file_dump_json
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.strategy.strategy import Strategy
logger = logging.getLogger(__name__)
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: class Backtesting(object):
""" """
Get the maximum timeframe for the given backtest data Backtesting class, this class contains all the logic to run a backtest
:param data: dictionary with preprocessed backtesting data
:return: tuple containing min_date, max_date
"""
all_dates = Series([])
for pair_data in data.values():
all_dates = all_dates.append(pair_data['date'])
all_dates.sort_values(inplace=True)
return arrow.get(all_dates.iloc[0]), arrow.get(all_dates.iloc[-1])
To run a backtest:
backtesting = Backtesting(config)
backtesting.start()
"""
def __init__(self, config: Dict[str, Any]) -> None:
def generate_text_table( # Init the logger
data: Dict[str, Dict], results: DataFrame, stake_currency) -> str: self.logging = Logger(name=__name__, level=config['loglevel'])
""" self.logger = self.logging.get_logger()
Generates and returns a text table for the given backtest data and the results dataframe self.config = config
:return: pretty printed table with tabulate as str self.analyze = None
""" self.ticker_interval = None
floatfmt = ('s', 'd', '.2f', '.8f', '.1f') self.tickerdata_to_dataframe = None
tabular_data = [] self.populate_buy_trend = None
headers = ['pair', 'buy count', 'avg profit %', self.populate_sell_trend = None
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss'] self._init()
for pair in data:
result = results[results.currency == pair] def _init(self) -> None:
"""
Init objects required for backtesting
:return: None
"""
self.analyze = Analyze(self.config)
self.ticker_interval = self.analyze.strategy.ticker_interval
self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe
self.populate_buy_trend = self.analyze.populate_buy_trend
self.populate_sell_trend = self.analyze.populate_sell_trend
exchange._API = Bittrex({'key': '', 'secret': ''})
@staticmethod
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
"""
Get the maximum timeframe for the given backtest data
:param data: dictionary with preprocessed backtesting data
:return: tuple containing min_date, max_date
"""
all_dates = Series([])
for pair_data in data.values():
all_dates = all_dates.append(pair_data['date'])
all_dates.sort_values(inplace=True)
return arrow.get(all_dates.iloc[0]), arrow.get(all_dates.iloc[-1])
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str:
"""
Generates and returns a text table for the given backtest data and the results dataframe
:return: pretty printed table with tabulate as str
"""
stake_currency = self.config.get('stake_currency')
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
tabular_data = []
headers = ['pair', 'buy count', 'avg profit %',
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
for pair in data:
result = results[results.currency == pair]
tabular_data.append([
pair,
len(result.index),
result.profit_percent.mean() * 100.0,
result.profit_BTC.sum(),
result.duration.mean(),
len(result[result.profit_BTC > 0]),
len(result[result.profit_BTC < 0])
])
# Append Total
tabular_data.append([ tabular_data.append([
pair, 'TOTAL',
len(result.index), len(results.index),
result.profit_percent.mean() * 100.0, results.profit_percent.mean() * 100.0,
result.profit_BTC.sum(), results.profit_BTC.sum(),
result.duration.mean(), results.duration.mean(),
len(result[result.profit_BTC > 0]), len(results[results.profit_BTC > 0]),
len(result[result.profit_BTC < 0]) len(results[results.profit_BTC < 0])
]) ])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
# Append Total def _get_sell_trade_entry(
tabular_data.append([ self, pair: str, buy_row: DataFrame,
'TOTAL', partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[Tuple]:
len(results.index),
results.profit_percent.mean() * 100.0,
results.profit_BTC.sum(),
results.duration.mean(),
len(results[results.profit_BTC > 0]),
len(results[results.profit_BTC < 0])
])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
stake_amount = args['stake_amount']
max_open_trades = args.get('max_open_trades', 0)
trade = Trade(
open_rate=buy_row.close,
open_date=buy_row.date,
stake_amount=stake_amount,
amount=stake_amount / buy_row.open,
fee=exchange.get_fee()
)
def get_sell_trade_entry(pair, buy_row, partial_ticker, trade_count_lock, args): # calculate win/lose forwards from buy point
stake_amount = args['stake_amount'] for sell_row in partial_ticker:
max_open_trades = args.get('max_open_trades', 0)
trade = Trade(open_rate=buy_row.close,
open_date=buy_row.date,
stake_amount=stake_amount,
amount=stake_amount / buy_row.open,
fee=exchange.get_fee()
)
# calculate win/lose forwards from buy point
for sell_row in partial_ticker:
if max_open_trades > 0:
# Increase trade_count_lock for every iteration
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
buy_signal = sell_row.buy
if should_sell(trade, sell_row.close, sell_row.date, buy_signal, sell_row.sell):
return sell_row, (pair,
trade.calc_profit_percent(rate=sell_row.close),
trade.calc_profit(rate=sell_row.close),
(sell_row.date - buy_row.date).seconds // 60
), sell_row.date
return None
def backtest(args) -> DataFrame:
"""
Implements backtesting functionality
:param args: a dict containing:
stake_amount: btc amount to use for each trade
processed: a processed dictionary with format {pair, data}
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
realistic: do we try to simulate realistic trades? (default: True)
sell_profit_only: sell if profit only
use_sell_signal: act on sell-signal
:return: DataFrame
"""
headers = ['date', 'buy', 'open', 'close', 'sell']
processed = args['processed']
max_open_trades = args.get('max_open_trades', 0)
realistic = args.get('realistic', False)
record = args.get('record', None)
records = []
trades = []
trade_count_lock: dict = {}
exchange._API = Bittrex({'key': '', 'secret': ''})
for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
ticker_data = populate_sell_trend(populate_buy_trend(pair_data))[headers]
ticker = [x for x in ticker_data.itertuples()]
lock_pair_until = None
for index, row in enumerate(ticker):
if row.buy == 0 or row.sell == 1:
continue # skip rows where no buy signal or that would immediately sell off
if realistic:
if lock_pair_until is not None and row.date <= lock_pair_until:
continue
if max_open_trades > 0: if max_open_trades > 0:
# Check if max_open_trades has already been reached for the given date # Increase trade_count_lock for every iteration
if not trade_count_lock.get(row.date, 0) < max_open_trades: trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
continue
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
ret = get_sell_trade_entry(pair, row, ticker[index+1:], trade_count_lock, args) buy_signal = sell_row.buy
if ret: if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal,
row2, trade_entry, next_date = ret sell_row.sell):
lock_pair_until = next_date return \
trades.append(trade_entry) sell_row, \
if record: (
# Note, need to be json.dump friendly pair,
# record a tuple of pair, current_profit_percent, trade.calc_profit_percent(rate=sell_row.close),
# entry-date, duration trade.calc_profit(rate=sell_row.close),
records.append((pair, trade_entry[1], (sell_row.date - buy_row.date).seconds // 60
row.date.strftime('%s'), ), \
row2.date.strftime('%s'), sell_row.date
row.date, trade_entry[3])) return None
# For now export inside backtest(), maybe change so that backtest()
# returns a tuple like: (dataframe, records, logs, etc) def backtest(self, args: Dict) -> DataFrame:
if record and record.find('trades') >= 0: """
logger.info('Dumping backtest results') Implements backtesting functionality
misc.file_dump_json('backtest-result.json', records)
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration'] NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
return DataFrame.from_records(trades, columns=labels) Of course try to not have ugly code. By some accessor are sometime slower than functions.
Avoid, logging on this method
:param args: a dict containing:
stake_amount: btc amount to use for each trade
processed: a processed dictionary with format {pair, data}
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
realistic: do we try to simulate realistic trades? (default: True)
sell_profit_only: sell if profit only
use_sell_signal: act on sell-signal
:return: DataFrame
"""
headers = ['date', 'buy', 'open', 'close', 'sell']
processed = args['processed']
max_open_trades = args.get('max_open_trades', 0)
realistic = args.get('realistic', False)
record = args.get('record', None)
records = []
trades = []
trade_count_lock = {}
for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
ticker_data = self.populate_sell_trend(self.populate_buy_trend(pair_data))[headers]
ticker = [x for x in ticker_data.itertuples()]
lock_pair_until = None
for index, row in enumerate(ticker):
if row.buy == 0 or row.sell == 1:
continue # skip rows where no buy signal or that would immediately sell off
if realistic:
if lock_pair_until is not None and row.date <= lock_pair_until:
continue
if max_open_trades > 0:
# Check if max_open_trades has already been reached for the given date
if not trade_count_lock.get(row.date, 0) < max_open_trades:
continue
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
ret = self._get_sell_trade_entry(pair, row, ticker[index + 1:],
trade_count_lock, args)
if ret:
row2, trade_entry, next_date = ret
lock_pair_until = next_date
trades.append(trade_entry)
if record:
# Note, need to be json.dump friendly
# record a tuple of pair, current_profit_percent,
# entry-date, duration
records.append((pair, trade_entry[1],
row.date.strftime('%s'),
row2.date.strftime('%s'),
row.date, trade_entry[3]))
# For now export inside backtest(), maybe change so that backtest()
# returns a tuple like: (dataframe, records, logs, etc)
if record and record.find('trades') >= 0:
self.logger.info('Dumping backtest results')
file_dump_json('backtest-result.json', records)
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
return DataFrame.from_records(trades, columns=labels)
def start(self) -> None:
"""
Run a backtesting end-to-end
:return: None
"""
data = {}
pairs = self.config['exchange']['pair_whitelist']
self.logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
self.logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
if self.config.get('live'):
self.logger.info('Downloading data for all pairs in whitelist ...')
for pair in pairs:
data[pair] = exchange.get_ticker_history(pair, self.ticker_interval)
else:
self.logger.info('Using local backtesting data (using whitelist in given config) ...')
timerange = Arguments.parse_timerange(self.config.get('timerange'))
data = optimize.load_data(
self.config['datadir'],
pairs=pairs,
ticker_interval=self.ticker_interval,
refresh_pairs=self.config.get('refresh_pairs', False),
timerange=timerange
)
# Ignore max_open_trades in backtesting, except realistic flag was passed
if self.config.get('realistic_simulation', False):
max_open_trades = self.config['max_open_trades']
else:
self.logger.info('Ignoring max_open_trades (realistic_simulation not set) ...')
max_open_trades = 0
preprocessed = self.tickerdata_to_dataframe(data)
# Print timeframe
min_date, max_date = self.get_timeframe(preprocessed)
self.logger.info(
'Measuring data from %s up to %s (%s days)..',
min_date.isoformat(),
max_date.isoformat(),
(max_date - min_date).days
)
# Execute backtest and print results
sell_profit_only = self.config.get('experimental', {}).get('sell_profit_only', False)
use_sell_signal = self.config.get('experimental', {}).get('use_sell_signal', False)
results = self.backtest(
{
'stake_amount': self.config.get('stake_amount'),
'processed': preprocessed,
'max_open_trades': max_open_trades,
'realistic': self.config.get('realistic_simulation', False),
'sell_profit_only': sell_profit_only,
'use_sell_signal': use_sell_signal,
'record': self.config.get('export')
}
)
self.logging.set_format('%(message)s')
self.logger.info(
'\n==================================== '
'BACKTESTING REPORT'
' ====================================\n'
'%s',
self._generate_text_table(
data,
results
)
)
def start(args): def setup_configuration(args: Namespace) -> Dict[str, Any]:
"""
Prepare the configuration for the backtesting
:param args: Cli args from Arguments()
:return: Configuration
"""
configuration = Configuration(args)
config = configuration.get_config()
# Ensure we do not use Exchange credentials
config['exchange']['key'] = ''
config['exchange']['secret'] = ''
return config
def start(args: Namespace) -> None:
"""
Start Backtesting script
:param args: Cli args from Arguments()
:return: None
"""
# Initialize logger # Initialize logger
logging.basicConfig( logger = Logger(name=__name__).get_logger()
level=args.loglevel, logger.info('Starting freqtrade in Backtesting mode')
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
exchange._API = Bittrex({'key': '', 'secret': ''}) # Initialize configuration
config = setup_configuration(args)
logger.info('Using config: %s ...', args.config) # Initialize backtesting object
config = misc.load_config(args.config) backtesting = Backtesting(config)
backtesting.start()
# If -i/--ticker-interval is use we override the configuration parameter
# (that will override the strategy configuration)
if args.ticker_interval:
config.update({'ticker_interval': args.ticker_interval})
# init the strategy to use
config.update({'strategy': args.strategy})
strategy = Strategy()
strategy.init(config)
logger.info('Using ticker_interval: %d ...', strategy.ticker_interval)
data = {}
pairs = config['exchange']['pair_whitelist']
logger.info('Using stake_currency: %s ...', config['stake_currency'])
logger.info('Using stake_amount: %s ...', config['stake_amount'])
if args.live:
logger.info('Downloading data for all pairs in whitelist ...')
for pair in pairs:
data[pair] = exchange.get_ticker_history(pair, strategy.ticker_interval)
else:
logger.info('Using local backtesting data (using whitelist in given config) ...')
timerange = misc.parse_timerange(args.timerange)
data = optimize.load_data(args.datadir,
pairs=pairs,
ticker_interval=strategy.ticker_interval,
refresh_pairs=args.refresh_pairs,
timerange=timerange)
max_open_trades = 0
if args.realistic_simulation:
logger.info('Using max_open_trades: %s ...', config['max_open_trades'])
max_open_trades = config['max_open_trades']
# Monkey patch config
from freqtrade import main
main._CONF = config
preprocessed = optimize.tickerdata_to_dataframe(data)
# Print timeframe
min_date, max_date = get_timeframe(preprocessed)
logger.info('Measuring data from %s up to %s (%s days)..',
min_date.isoformat(),
max_date.isoformat(),
(max_date-min_date).days)
# Execute backtest and print results
sell_profit_only = config.get('experimental', {}).get('sell_profit_only', False)
use_sell_signal = config.get('experimental', {}).get('use_sell_signal', False)
results = backtest({'stake_amount': config['stake_amount'],
'processed': preprocessed,
'max_open_trades': max_open_trades,
'realistic': args.realistic_simulation,
'sell_profit_only': sell_profit_only,
'use_sell_signal': use_sell_signal,
'record': args.export
})
logger.info(
'\n==================================== BACKTESTING REPORT ====================================\n%s', # noqa
generate_text_table(data, results, config['stake_currency'])
)

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,7 @@
"""
This module contains the class to persist trades into SQLite
"""
import logging import logging
from datetime import datetime from datetime import datetime
from decimal import Decimal, getcontext from decimal import Decimal, getcontext
@ -72,6 +76,9 @@ def clean_dry_run_db() -> None:
class Trade(_DECL_BASE): class Trade(_DECL_BASE):
"""
Class used to define a trade structure
"""
__tablename__ = 'trades' __tablename__ = 'trades'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
@ -200,6 +207,7 @@ class Trade(_DECL_BASE):
Calculates the profit in percentage (including fee). Calculates the profit in percentage (including fee).
:param rate: rate to compare with (optional). :param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used If rate is not set self.close_rate will be used
:param fee: fee to use on the close rate (optional).
:return: profit in percentage as float :return: profit in percentage as float
""" """
getcontext().prec = 8 getcontext().prec = 8

View File

@ -1,415 +0,0 @@
import logging
import re
import arrow
from decimal import Decimal
from datetime import datetime, timedelta
from pandas import DataFrame
import sqlalchemy as sql
# from sqlalchemy import and_, func, text
from freqtrade.persistence import Trade
from freqtrade.misc import State, get_state, update_state
from freqtrade import exchange
from freqtrade.fiat_convert import CryptoToFiatConverter
from . import telegram
logger = logging.getLogger(__name__)
_FIAT_CONVERT = CryptoToFiatConverter()
REGISTERED_MODULES = []
def init(config: dict) -> None:
"""
Initializes all enabled rpc modules
:param config: config to use
:return: None
"""
if config['telegram'].get('enabled', False):
logger.info('Enabling rpc.telegram ...')
REGISTERED_MODULES.append('telegram')
telegram.init(config)
def cleanup() -> None:
"""
Stops all enabled rpc modules
:return: None
"""
if 'telegram' in REGISTERED_MODULES:
logger.debug('Cleaning up rpc.telegram ...')
telegram.cleanup()
def send_msg(msg: str) -> None:
"""
Send given markdown message to all registered rpc modules
:param msg: message
:return: None
"""
logger.info(msg)
if 'telegram' in REGISTERED_MODULES:
telegram.send_msg(msg)
def shorten_date(_date):
"""
Trim the date so it fits on small screens
"""
new_date = re.sub('seconds?', 'sec', _date)
new_date = re.sub('minutes?', 'min', new_date)
new_date = re.sub('hours?', 'h', new_date)
new_date = re.sub('days?', 'd', new_date)
new_date = re.sub('^an?', '1', new_date)
return new_date
#
# Below follows the RPC backend
# it is prefixed with rpc_
# to raise awareness that it is
# a remotely exposed function
def rpc_trade_status():
# Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if get_state() != State.RUNNING:
return (True, '*Status:* `trader is not running`')
elif not trades:
return (True, '*Status:* `no active trade`')
else:
result = []
for trade in trades:
order = None
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id)
# calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair, False)['bid']
current_profit = trade.calc_profit_percent(current_rate)
fmt_close_profit = '{:.2f}%'.format(
round(trade.close_profit * 100, 2)
) if trade.close_profit else None
message = """
*Trade ID:* `{trade_id}`
*Current Pair:* [{pair}]({market_url})
*Open Since:* `{date}`
*Amount:* `{amount}`
*Open Rate:* `{open_rate:.8f}`
*Close Rate:* `{close_rate}`
*Current Rate:* `{current_rate:.8f}`
*Close Profit:* `{close_profit}`
*Current Profit:* `{current_profit:.2f}%`
*Open Order:* `{open_order}`
""".format(
trade_id=trade.id,
pair=trade.pair,
market_url=exchange.get_pair_detail_url(trade.pair),
date=arrow.get(trade.open_date).humanize(),
open_rate=trade.open_rate,
close_rate=trade.close_rate,
current_rate=current_rate,
amount=round(trade.amount, 8),
close_profit=fmt_close_profit,
current_profit=round(current_profit * 100, 2),
open_order='({} rem={:.8f})'.format(
order['type'], order['remaining']
) if order else None,
)
result.append(message)
return (False, result)
def rpc_status_table():
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if get_state() != State.RUNNING:
return (True, '*Status:* `trader is not running`')
elif not trades:
return (True, '*Status:* `no active order`')
else:
trades_list = []
for trade in trades:
# calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair, False)['bid']
trades_list.append([
trade.id,
trade.pair,
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate))
])
columns = ['ID', 'Pair', 'Since', 'Profit']
df_statuses = DataFrame.from_records(trades_list, columns=columns)
df_statuses = df_statuses.set_index(columns[0])
# The style used throughout is to return a tuple
# consisting of (error_occured?, result)
# Another approach would be to just return the
# result, or raise error
return (False, df_statuses)
def rpc_daily_profit(timescale, stake_currency, fiat_display_currency):
today = datetime.utcnow().date()
profit_days = {}
if not (isinstance(timescale, int) and timescale > 0):
return (True, '*Daily [n]:* `must be an integer greater than 0`')
fiat = _FIAT_CONVERT
for day in range(0, timescale):
profitday = today - timedelta(days=day)
trades = Trade.query \
.filter(Trade.is_open.is_(False)) \
.filter(Trade.close_date >= profitday)\
.filter(Trade.close_date < (profitday + timedelta(days=1)))\
.order_by(Trade.close_date)\
.all()
curdayprofit = sum(trade.calc_profit() for trade in trades)
profit_days[profitday] = {
'amount': format(curdayprofit, '.8f'),
'trades': len(trades)
}
stats = [
[
key,
'{value:.8f} {symbol}'.format(
value=float(value['amount']),
symbol=stake_currency
),
'{value:.3f} {symbol}'.format(
value=fiat.convert_amount(
value['amount'],
stake_currency,
fiat_display_currency
),
symbol=fiat_display_currency
),
'{value} trade{s}'.format(value=value['trades'], s='' if value['trades'] < 2 else 's'),
]
for key, value in profit_days.items()
]
return (False, stats)
def rpc_trade_statistics(stake_currency, fiat_display_currency) -> None:
"""
:return: cumulative profit statistics.
"""
trades = Trade.query.order_by(Trade.id).all()
profit_all_coin = []
profit_all_percent = []
profit_closed_coin = []
profit_closed_percent = []
durations = []
for trade in trades:
current_rate = None
if not trade.open_rate:
continue
if trade.close_date:
durations.append((trade.close_date - trade.open_date).total_seconds())
if not trade.is_open:
profit_percent = trade.calc_profit_percent()
profit_closed_coin.append(trade.calc_profit())
profit_closed_percent.append(profit_percent)
else:
# Get current rate
current_rate = exchange.get_ticker(trade.pair, False)['bid']
profit_percent = trade.calc_profit_percent(rate=current_rate)
profit_all_coin.append(trade.calc_profit(rate=Decimal(trade.close_rate or current_rate)))
profit_all_percent.append(profit_percent)
best_pair = Trade.session.query(Trade.pair,
sql.func.sum(Trade.close_profit).label('profit_sum')) \
.filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(sql.text('profit_sum DESC')) \
.first()
if not best_pair:
return (True, '*Status:* `no closed trade`')
bp_pair, bp_rate = best_pair
# FIX: we want to keep fiatconverter in a state/environment,
# doing this will utilize its caching functionallity, instead we reinitialize it here
fiat = _FIAT_CONVERT
# Prepare data to display
profit_closed_coin = round(sum(profit_closed_coin), 8)
profit_closed_percent = round(sum(profit_closed_percent) * 100, 2)
profit_closed_fiat = fiat.convert_amount(
profit_closed_coin,
stake_currency,
fiat_display_currency
)
profit_all_coin = round(sum(profit_all_coin), 8)
profit_all_percent = round(sum(profit_all_percent) * 100, 2)
profit_all_fiat = fiat.convert_amount(
profit_all_coin,
stake_currency,
fiat_display_currency
)
num = float(len(durations) or 1)
return (False,
{'profit_closed_coin': profit_closed_coin,
'profit_closed_percent': profit_closed_percent,
'profit_closed_fiat': profit_closed_fiat,
'profit_all_coin': profit_all_coin,
'profit_all_percent': profit_all_percent,
'profit_all_fiat': profit_all_fiat,
'trade_count': len(trades),
'first_trade_date': arrow.get(trades[0].open_date).humanize(),
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
'avg_duration': str(timedelta(seconds=sum(durations) /
num)).split('.')[0],
'best_pair': bp_pair,
'best_rate': round(bp_rate * 100, 2)
})
def rpc_balance(fiat_display_currency):
"""
:return: current account balance per crypto
"""
balances = [
c for c in exchange.get_balances()
if c['Balance'] or c['Available'] or c['Pending']
]
if not balances:
return (True, '`All balances are zero.`')
output = []
total = 0.0
for currency in balances:
coin = currency['Currency']
if coin == 'BTC':
currency["Rate"] = 1.0
else:
if coin == 'USDT':
currency["Rate"] = 1.0 / exchange.get_ticker('USDT_BTC', False)['bid']
else:
currency["Rate"] = exchange.get_ticker('BTC_' + coin, False)['bid']
currency['BTC'] = currency["Rate"] * currency["Balance"]
total = total + currency['BTC']
output.append({'currency': currency['Currency'],
'available': currency['Available'],
'balance': currency['Balance'],
'pending': currency['Pending'],
'est_btc': currency['BTC']
})
fiat = _FIAT_CONVERT
symbol = fiat_display_currency
value = fiat.convert_amount(total, 'BTC', symbol)
return (False, (output, total, symbol, value))
def rpc_start():
"""
Handler for start.
"""
if get_state() == State.RUNNING:
return (True, '*Status:* `already running`')
else:
update_state(State.RUNNING)
def rpc_stop():
"""
Handler for stop.
"""
if get_state() == State.RUNNING:
update_state(State.STOPPED)
return (False, '`Stopping trader ...`')
else:
return (True, '*Status:* `already stopped`')
# FIX: no test for this!!!!
def rpc_forcesell(trade_id) -> None:
"""
Handler for forcesell <id>.
Sells the given trade at current price
:return: error or None
"""
def _exec_forcesell(trade: Trade) -> str:
# Check if there is there is an open order
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id)
# Cancel open LIMIT_BUY orders and close trade
if order and not order['closed'] and order['type'] == 'LIMIT_BUY':
exchange.cancel_order(trade.open_order_id)
trade.close(order.get('rate') or trade.open_rate)
# TODO: sell amount which has been bought already
return
# Ignore trades with an attached LIMIT_SELL order
if order and not order['closed'] and order['type'] == 'LIMIT_SELL':
return
# Get current rate and execute sell
current_rate = exchange.get_ticker(trade.pair, False)['bid']
from freqtrade.main import execute_sell
execute_sell(trade, current_rate)
# ---- EOF def _exec_forcesell ----
if get_state() != State.RUNNING:
return (True, '`trader is not running`')
if trade_id == 'all':
# Execute sell for all open orders
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
_exec_forcesell(trade)
return (False, '')
# Query for trade
trade = Trade.query.filter(sql.and_(
Trade.id == trade_id,
Trade.is_open.is_(True)
)).first()
if not trade:
logger.warning('forcesell: Invalid argument received')
return (True, 'Invalid argument.')
_exec_forcesell(trade)
return (False, '')
def rpc_performance() -> None:
"""
Handler for performance.
Shows a performance statistic from finished trades
"""
if get_state() != State.RUNNING:
return (True, '`trader is not running`')
pair_rates = Trade.session.query(Trade.pair,
sql.func.sum(Trade.close_profit).label('profit_sum'),
sql.func.count(Trade.pair).label('count')) \
.filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(sql.text('profit_sum DESC')) \
.all()
trades = []
for (pair, rate, count) in pair_rates:
trades.append({'pair': pair, 'profit': round(rate * 100, 2), 'count': count})
return (False, trades)
def rpc_count() -> None:
"""
Returns the number of trades running
:return: None
"""
if get_state() != State.RUNNING:
return (True, '`trader is not running`')
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
return (False, trades)

385
freqtrade/rpc/rpc.py Normal file
View File

@ -0,0 +1,385 @@
"""
This module contains class to define a RPC communications
"""
from datetime import datetime, timedelta
from decimal import Decimal
from typing import Tuple, Any
import arrow
import sqlalchemy as sql
from pandas import DataFrame
from freqtrade import exchange
from freqtrade.logger import Logger
from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade
from freqtrade.state import State
class RPC(object):
"""
RPC class can be used to have extra feature, like bot data, and access to DB data
"""
def __init__(self, freqtrade) -> None:
"""
Initializes all enabled rpc modules
:param freqtrade: Instance of a freqtrade bot
:return: None
"""
self.freqtrade = freqtrade
self.logger = Logger(
name=__name__,
level=self.freqtrade.config.get('loglevel')
).get_logger()
def rpc_trade_status(self) -> Tuple[bool, Any]:
"""
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
a remotely exposed function
:return:
"""
# Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if self.freqtrade.get_state() != State.RUNNING:
return True, '*Status:* `trader is not running`'
elif not trades:
return True, '*Status:* `no active trade`'
else:
result = []
for trade in trades:
order = None
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id)
# calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair, False)['bid']
current_profit = trade.calc_profit_percent(current_rate)
fmt_close_profit = '{:.2f}%'.format(
round(trade.close_profit * 100, 2)
) if trade.close_profit else None
message = "*Trade ID:* `{trade_id}`\n" \
"*Current Pair:* [{pair}]({market_url})\n" \
"*Open Since:* `{date}`\n" \
"*Amount:* `{amount}`\n" \
"*Open Rate:* `{open_rate:.8f}`\n" \
"*Close Rate:* `{close_rate}`\n" \
"*Current Rate:* `{current_rate:.8f}`\n" \
"*Close Profit:* `{close_profit}`\n" \
"*Current Profit:* `{current_profit:.2f}%`\n" \
"*Open Order:* `{open_order}`"\
.format(
trade_id=trade.id,
pair=trade.pair,
market_url=exchange.get_pair_detail_url(trade.pair),
date=arrow.get(trade.open_date).humanize(),
open_rate=trade.open_rate,
close_rate=trade.close_rate,
current_rate=current_rate,
amount=round(trade.amount, 8),
close_profit=fmt_close_profit,
current_profit=round(current_profit * 100, 2),
open_order='({} rem={:.8f})'.format(
order['type'], order['remaining']
) if order else None,
)
result.append(message)
return False, result
def rpc_status_table(self) -> Tuple[bool, Any]:
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if self.freqtrade.get_state() != State.RUNNING:
return True, '*Status:* `trader is not running`'
elif not trades:
return True, '*Status:* `no active order`'
else:
trades_list = []
for trade in trades:
# calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair, False)['bid']
trades_list.append([
trade.id,
trade.pair,
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate))
])
columns = ['ID', 'Pair', 'Since', 'Profit']
df_statuses = DataFrame.from_records(trades_list, columns=columns)
df_statuses = df_statuses.set_index(columns[0])
# The style used throughout is to return a tuple
# consisting of (error_occured?, result)
# Another approach would be to just return the
# result, or raise error
return False, df_statuses
def rpc_daily_profit(
self, timescale: int,
stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
today = datetime.utcnow().date()
profit_days = {}
if not (isinstance(timescale, int) and timescale > 0):
return True, '*Daily [n]:* `must be an integer greater than 0`'
fiat = self.freqtrade.fiat_converter
for day in range(0, timescale):
profitday = today - timedelta(days=day)
trades = Trade.query \
.filter(Trade.is_open.is_(False)) \
.filter(Trade.close_date >= profitday)\
.filter(Trade.close_date < (profitday + timedelta(days=1)))\
.order_by(Trade.close_date)\
.all()
curdayprofit = sum(trade.calc_profit() for trade in trades)
profit_days[profitday] = {
'amount': format(curdayprofit, '.8f'),
'trades': len(trades)
}
stats = [
[
key,
'{value:.8f} {symbol}'.format(
value=float(value['amount']),
symbol=stake_currency
),
'{value:.3f} {symbol}'.format(
value=fiat.convert_amount(
value['amount'],
stake_currency,
fiat_display_currency
),
symbol=fiat_display_currency
),
'{value} trade{s}'.format(
value=value['trades'],
s='' if value['trades'] < 2 else 's'
),
]
for key, value in profit_days.items()
]
return False, stats
def rpc_trade_statistics(
self, stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
"""
:return: cumulative profit statistics.
"""
trades = Trade.query.order_by(Trade.id).all()
profit_all_coin = []
profit_all_percent = []
profit_closed_coin = []
profit_closed_percent = []
durations = []
for trade in trades:
current_rate = None
if not trade.open_rate:
continue
if trade.close_date:
durations.append((trade.close_date - trade.open_date).total_seconds())
if not trade.is_open:
profit_percent = trade.calc_profit_percent()
profit_closed_coin.append(trade.calc_profit())
profit_closed_percent.append(profit_percent)
else:
# Get current rate
current_rate = exchange.get_ticker(trade.pair, False)['bid']
profit_percent = trade.calc_profit_percent(rate=current_rate)
profit_all_coin.append(
trade.calc_profit(rate=Decimal(trade.close_rate or current_rate))
)
profit_all_percent.append(profit_percent)
best_pair = Trade.session.query(
Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum')
).filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(sql.text('profit_sum DESC')).first()
if not best_pair:
return True, '*Status:* `no closed trade`'
bp_pair, bp_rate = best_pair
# FIX: we want to keep fiatconverter in a state/environment,
# doing this will utilize its caching functionallity, instead we reinitialize it here
fiat = self.freqtrade.fiat_converter
# Prepare data to display
profit_closed_coin = round(sum(profit_closed_coin), 8)
profit_closed_percent = round(sum(profit_closed_percent) * 100, 2)
profit_closed_fiat = fiat.convert_amount(
profit_closed_coin,
stake_currency,
fiat_display_currency
)
profit_all_coin = round(sum(profit_all_coin), 8)
profit_all_percent = round(sum(profit_all_percent) * 100, 2)
profit_all_fiat = fiat.convert_amount(
profit_all_coin,
stake_currency,
fiat_display_currency
)
num = float(len(durations) or 1)
return (
False,
{
'profit_closed_coin': profit_closed_coin,
'profit_closed_percent': profit_closed_percent,
'profit_closed_fiat': profit_closed_fiat,
'profit_all_coin': profit_all_coin,
'profit_all_percent': profit_all_percent,
'profit_all_fiat': profit_all_fiat,
'trade_count': len(trades),
'first_trade_date': arrow.get(trades[0].open_date).humanize(),
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
'best_pair': bp_pair,
'best_rate': round(bp_rate * 100, 2)
}
)
def rpc_balance(self, fiat_display_currency: str) -> Tuple[bool, Any]:
"""
:return: current account balance per crypto
"""
balances = [
c for c in exchange.get_balances()
if c['Balance'] or c['Available'] or c['Pending']
]
if not balances:
return True, '`All balances are zero.`'
output = []
total = 0.0
for currency in balances:
coin = currency['Currency']
if coin == 'BTC':
currency["Rate"] = 1.0
else:
if coin == 'USDT':
currency["Rate"] = 1.0 / exchange.get_ticker('USDT_BTC', False)['bid']
else:
currency["Rate"] = exchange.get_ticker('BTC_' + coin, False)['bid']
currency['BTC'] = currency["Rate"] * currency["Balance"]
total = total + currency['BTC']
output.append(
{
'currency': currency['Currency'],
'available': currency['Available'],
'balance': currency['Balance'],
'pending': currency['Pending'],
'est_btc': currency['BTC']
}
)
fiat = self.freqtrade.fiat_converter
symbol = fiat_display_currency
value = fiat.convert_amount(total, 'BTC', symbol)
return False, (output, total, symbol, value)
def rpc_start(self) -> (bool, str):
"""
Handler for start.
"""
if self.freqtrade.get_state() == State.RUNNING:
return True, '*Status:* `already running`'
self.freqtrade.update_state(State.RUNNING)
return False, '`Starting trader ...`'
def rpc_stop(self) -> (bool, str):
"""
Handler for stop.
"""
if self.freqtrade.get_state() == State.RUNNING:
self.freqtrade.update_state(State.STOPPED)
return False, '`Stopping trader ...`'
return True, '*Status:* `already stopped`'
# FIX: no test for this!!!!
def rpc_forcesell(self, trade_id) -> Tuple[bool, Any]:
"""
Handler for forcesell <id>.
Sells the given trade at current price
:return: error or None
"""
def _exec_forcesell(trade: Trade) -> None:
# Check if there is there is an open order
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id)
# Cancel open LIMIT_BUY orders and close trade
if order and not order['closed'] and order['type'] == 'LIMIT_BUY':
exchange.cancel_order(trade.open_order_id)
trade.close(order.get('rate') or trade.open_rate)
# TODO: sell amount which has been bought already
return
# Ignore trades with an attached LIMIT_SELL order
if order and not order['closed'] and order['type'] == 'LIMIT_SELL':
return
# Get current rate and execute sell
current_rate = exchange.get_ticker(trade.pair, False)['bid']
self.freqtrade.execute_sell(trade, current_rate)
# ---- EOF def _exec_forcesell ----
if self.freqtrade.get_state() != State.RUNNING:
return True, '`trader is not running`'
if trade_id == 'all':
# Execute sell for all open orders
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
_exec_forcesell(trade)
return False, ''
# Query for trade
trade = Trade.query.filter(
sql.and_(
Trade.id == trade_id,
Trade.is_open.is_(True)
)
).first()
if not trade:
self.logger.warning('forcesell: Invalid argument received')
return True, 'Invalid argument.'
_exec_forcesell(trade)
return False, ''
def rpc_performance(self) -> Tuple[bool, Any]:
"""
Handler for performance.
Shows a performance statistic from finished trades
"""
if self.freqtrade.get_state() != State.RUNNING:
return True, '`trader is not running`'
pair_rates = Trade.session.query(Trade.pair,
sql.func.sum(Trade.close_profit).label('profit_sum'),
sql.func.count(Trade.pair).label('count')) \
.filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(sql.text('profit_sum DESC')) \
.all()
trades = []
for (pair, rate, count) in pair_rates:
trades.append({'pair': pair, 'profit': round(rate * 100, 2), 'count': count})
return False, trades
def rpc_count(self) -> Tuple[bool, Any]:
"""
Returns the number of trades running
:return: None
"""
if self.freqtrade.get_state() != State.RUNNING:
return True, '`trader is not running`'
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
return False, trades

View File

@ -0,0 +1,59 @@
"""
This module contains class to manage RPC communications (Telegram, Slack, ...)
"""
from freqtrade.logger import Logger
from freqtrade.rpc.telegram import Telegram
class RPCManager(object):
"""
Class to manage RPC objects (Telegram, Slack, ...)
"""
def __init__(self, freqtrade) -> None:
"""
Initializes all enabled rpc modules
:param config: config to use
:return: None
"""
self.freqtrade = freqtrade
# Init the logger
self.logger = Logger(
name=__name__,
level=self.freqtrade.config.get('loglevel')
).get_logger()
self.registered_modules = []
self.telegram = None
self._init()
def _init(self) -> None:
"""
Init RPC modules
:return:
"""
if self.freqtrade.config['telegram'].get('enabled', False):
self.logger.info('Enabling rpc.telegram ...')
self.registered_modules.append('telegram')
self.telegram = Telegram(self.freqtrade)
def cleanup(self) -> None:
"""
Stops all enabled rpc modules
:return: None
"""
if 'telegram' in self.registered_modules:
self.logger.info('Cleaning up rpc.telegram ...')
self.registered_modules.remove('telegram')
self.telegram.cleanup()
def send_msg(self, msg: str) -> None:
"""
Send given markdown message to all registered rpc modules
:param msg: message
:return: None
"""
self.logger.info(msg)
if 'telegram' in self.registered_modules:
self.telegram.send_msg(msg)

View File

@ -1,4 +1,9 @@
import logging # pragma pylint: disable=unused-argument, unused-variable, protected-access, invalid-name
"""
This module manage Telegram communication
"""
from typing import Any, Callable from typing import Any, Callable
from tabulate import tabulate from tabulate import tabulate
@ -6,89 +11,8 @@ from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update
from telegram.error import NetworkError, TelegramError from telegram.error import NetworkError, TelegramError
from telegram.ext import CommandHandler, Updater from telegram.ext import CommandHandler, Updater
from freqtrade.rpc.__init__ import (rpc_status_table, from freqtrade.__init__ import __version__
rpc_trade_status, from freqtrade.rpc.rpc import RPC
rpc_daily_profit,
rpc_trade_statistics,
rpc_balance,
rpc_start,
rpc_stop,
rpc_forcesell,
rpc_performance,
rpc_count,
)
from freqtrade import __version__
# Remove noisy log messages
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
logging.getLogger('telegram').setLevel(logging.INFO)
logger = logging.getLogger(__name__)
_UPDATER: Updater = None
_CONF = {}
def init(config: dict) -> None:
"""
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
:param config: config to use
:return: None
"""
global _UPDATER
_CONF.update(config)
if not is_enabled():
return
_UPDATER = Updater(token=config['telegram']['token'], workers=0)
# Register command handler and start telegram message polling
handles = [
CommandHandler('status', _status),
CommandHandler('profit', _profit),
CommandHandler('balance', _balance),
CommandHandler('start', _start),
CommandHandler('stop', _stop),
CommandHandler('forcesell', _forcesell),
CommandHandler('performance', _performance),
CommandHandler('daily', _daily),
CommandHandler('count', _count),
CommandHandler('help', _help),
CommandHandler('version', _version),
]
for handle in handles:
_UPDATER.dispatcher.add_handler(handle)
_UPDATER.start_polling(
clean=True,
bootstrap_retries=-1,
timeout=30,
read_latency=60,
)
logger.info(
'rpc.telegram is listening for following commands: %s',
[h.command for h in handles]
)
def cleanup() -> None:
"""
Stops all running telegram threads.
:return: None
"""
if not is_enabled():
return
_UPDATER.stop()
def is_enabled() -> bool:
"""
Returns True if the telegram module is activated, False otherwise
"""
return bool(_CONF['telegram'].get('enabled', False))
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]: def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
@ -97,340 +21,425 @@ def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[
:param command_handler: Telegram CommandHandler :param command_handler: Telegram CommandHandler
:return: decorated function :return: decorated function
""" """
def wrapper(*args, **kwargs): def wrapper(self, *args, **kwargs):
"""
Decorator logic
"""
update = kwargs.get('update') or args[1] update = kwargs.get('update') or args[1]
# Reject unauthorized messages # Reject unauthorized messages
chat_id = int(_CONF['telegram']['chat_id']) chat_id = int(self._config['telegram']['chat_id'])
if int(update.message.chat_id) != chat_id: if int(update.message.chat_id) != chat_id:
logger.info('Rejected unauthorized message from: %s', update.message.chat_id) self.logger.info(
'Rejected unauthorized message from: %s',
update.message.chat_id
)
return wrapper return wrapper
logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id) self.logger.info(
'Executing handler: %s for chat_id: %s',
command_handler.__name__,
chat_id
)
try: try:
return command_handler(*args, **kwargs) return command_handler(self, *args, **kwargs)
except BaseException: except BaseException:
logger.exception('Exception occurred within Telegram module') self.logger.exception('Exception occurred within Telegram module')
return wrapper return wrapper
@authorized_only class Telegram(RPC):
def _status(bot: Bot, update: Update) -> None:
""" """
Handler for /status. Telegram, this class send messages to Telegram
Returns the current TradeThread status
:param bot: telegram bot
:param update: message update
:return: None
""" """
def __init__(self, freqtrade) -> None:
"""
Init the Telegram call, and init the super class RPC
:param freqtrade: Instance of a freqtrade bot
:return: None
"""
super().__init__(freqtrade)
# Check if additional parameters are passed self._updater = None
params = update.message.text.replace('/status', '').split(' ') \ self._config = freqtrade.config
if update.message.text else [] self._init()
if 'table' in params:
_status_table(bot, update)
return
# Fetch open trade def _init(self) -> None:
(error, trades) = rpc_trade_status() """
if error: Initializes this module with the given config,
send_msg(trades, bot=bot) registers all known command handlers
else: and starts polling for message updates
for trademsg in trades: :param config: config to use
send_msg(trademsg, bot=bot) :return: None
"""
if not self.is_enabled():
return
self._updater = Updater(token=self._config['telegram']['token'], workers=0)
@authorized_only # Register command handler and start telegram message polling
def _status_table(bot: Bot, update: Update) -> None: handles = [
""" CommandHandler('status', self._status),
Handler for /status table. CommandHandler('profit', self._profit),
Returns the current TradeThread status in table format CommandHandler('balance', self._balance),
:param bot: telegram bot CommandHandler('start', self._start),
:param update: message update CommandHandler('stop', self._stop),
:return: None CommandHandler('forcesell', self._forcesell),
""" CommandHandler('performance', self._performance),
# Fetch open trade CommandHandler('daily', self._daily),
(err, df_statuses) = rpc_status_table() CommandHandler('count', self._count),
if err: CommandHandler('help', self._help),
send_msg(df_statuses, bot=bot) CommandHandler('version', self._version),
else: ]
message = tabulate(df_statuses, headers='keys', tablefmt='simple') for handle in handles:
message = "<pre>{}</pre>".format(message) self._updater.dispatcher.add_handler(handle)
self._updater.start_polling(
clean=True,
bootstrap_retries=-1,
timeout=30,
read_latency=60,
)
self.logger.info(
'rpc.telegram is listening for following commands: %s',
[h.command for h in handles]
)
send_msg(message, parse_mode=ParseMode.HTML) def cleanup(self) -> None:
"""
Stops all running telegram threads.
:return: None
"""
if not self.is_enabled():
return
self._updater.stop()
@authorized_only def is_enabled(self) -> bool:
def _daily(bot: Bot, update: Update) -> None: """
""" Returns True if the telegram module is activated, False otherwise
Handler for /daily <n> """
Returns a daily profit (in BTC) over the last n days. return bool(self._config.get('telegram', {}).get('enabled', False))
:param bot: telegram bot
:param update: message update
:return: None
"""
try:
timescale = int(update.message.text.replace('/daily', '').strip())
except (TypeError, ValueError):
timescale = 7
(error, stats) = rpc_daily_profit(timescale,
_CONF['stake_currency'],
_CONF['fiat_display_currency'])
if error:
send_msg(stats, bot=bot)
else:
stats = tabulate(stats,
headers=[
'Day',
'Profit {}'.format(_CONF['stake_currency']),
'Profit {}'.format(_CONF['fiat_display_currency'])
],
tablefmt='simple')
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'.format(
timescale, stats)
send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
@authorized_only
def _status(self, bot: Bot, update: Update) -> None:
"""
Handler for /status.
Returns the current TradeThread status
:param bot: telegram bot
:param update: message update
:return: None
"""
@authorized_only # Check if additional parameters are passed
def _profit(bot: Bot, update: Update) -> None: params = update.message.text.replace('/status', '').split(' ') \
""" if update.message.text else []
Handler for /profit. if 'table' in params:
Returns a cumulative profit statistics. self._status_table(bot, update)
:param bot: telegram bot return
:param update: message update
:return: None
"""
(error, stats) = rpc_trade_statistics(_CONF['stake_currency'],
_CONF['fiat_display_currency'])
if error:
send_msg(stats, bot=bot)
return
# Message to display # Fetch open trade
markdown_msg = """ (error, trades) = self.rpc_trade_status()
*ROI:* Close trades if error:
`{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)` self.send_msg(trades, bot=bot)
`{profit_closed_fiat:.3f} {fiat}` else:
*ROI:* All trades for trademsg in trades:
`{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)` self.send_msg(trademsg, bot=bot)
`{profit_all_fiat:.3f} {fiat}`
*Total Trade Count:* `{trade_count}` @authorized_only
*First Trade opened:* `{first_trade_date}` def _status_table(self, bot: Bot, update: Update) -> None:
*Latest Trade opened:* `{latest_trade_date}` """
*Avg. Duration:* `{avg_duration}` Handler for /status table.
*Best Performing:* `{best_pair}: {best_rate:.2f}%` Returns the current TradeThread status in table format
""".format( :param bot: telegram bot
coin=_CONF['stake_currency'], :param update: message update
fiat=_CONF['fiat_display_currency'], :return: None
profit_closed_coin=stats['profit_closed_coin'], """
profit_closed_percent=stats['profit_closed_percent'], # Fetch open trade
profit_closed_fiat=stats['profit_closed_fiat'], (err, df_statuses) = self.rpc_status_table()
profit_all_coin=stats['profit_all_coin'], if err:
profit_all_percent=stats['profit_all_percent'], self.send_msg(df_statuses, bot=bot)
profit_all_fiat=stats['profit_all_fiat'], else:
trade_count=stats['trade_count'], message = tabulate(df_statuses, headers='keys', tablefmt='simple')
first_trade_date=stats['first_trade_date'], message = "<pre>{}</pre>".format(message)
latest_trade_date=stats['latest_trade_date'],
avg_duration=stats['avg_duration'],
best_pair=stats['best_pair'],
best_rate=stats['best_rate']
)
send_msg(markdown_msg, bot=bot)
self.send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only @authorized_only
def _balance(bot: Bot, update: Update) -> None: def _daily(self, bot: Bot, update: Update) -> None:
""" """
Handler for /balance Handler for /daily <n>
""" Returns a daily profit (in BTC) over the last n days.
(error, result) = rpc_balance(_CONF['fiat_display_currency']) :param bot: telegram bot
if error: :param update: message update
send_msg('`All balances are zero.`') :return: None
return """
(currencys, total, symbol, value) = result
output = ''
for currency in currencys:
output += """*Currency*: {currency}
*Available*: {available}
*Balance*: {balance}
*Pending*: {pending}
*Est. BTC*: {est_btc: .8f}
""".format(**currency)
output += """*Estimated Value*:
*BTC*: {0: .8f}
*{1}*: {2: .2f}
""".format(total, symbol, value)
send_msg(output)
@authorized_only
def _start(bot: Bot, update: Update) -> None:
"""
Handler for /start.
Starts TradeThread
:param bot: telegram bot
:param update: message update
:return: None
"""
(error, msg) = rpc_start()
if error:
send_msg(msg, bot=bot)
@authorized_only
def _stop(bot: Bot, update: Update) -> None:
"""
Handler for /stop.
Stops TradeThread
:param bot: telegram bot
:param update: message update
:return: None
"""
(error, msg) = rpc_stop()
send_msg(msg, bot=bot)
# FIX: no test for this!!!!
@authorized_only
def _forcesell(bot: Bot, update: Update) -> None:
"""
Handler for /forcesell <id>.
Sells the given trade at current price
:param bot: telegram bot
:param update: message update
:return: None
"""
trade_id = update.message.text.replace('/forcesell', '').strip()
(error, message) = rpc_forcesell(trade_id)
if error:
send_msg(message, bot=bot)
return
@authorized_only
def _performance(bot: Bot, update: Update) -> None:
"""
Handler for /performance.
Shows a performance statistic from finished trades
:param bot: telegram bot
:param update: message update
:return: None
"""
(error, trades) = rpc_performance()
if error:
send_msg(trades, bot=bot)
return
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
index=i + 1,
pair=trade['pair'],
profit=trade['profit'],
count=trade['count']
) for i, trade in enumerate(trades))
message = '<b>Performance:</b>\n{}'.format(stats)
send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _count(bot: Bot, update: Update) -> None:
"""
Handler for /count.
Returns the number of trades running
:param bot: telegram bot
:param update: message update
:return: None
"""
(error, trades) = rpc_count()
if error:
send_msg(trades, bot=bot)
return
message = tabulate({
'current': [len(trades)],
'max': [_CONF['max_open_trades']]
}, headers=['current', 'max'], tablefmt='simple')
message = "<pre>{}</pre>".format(message)
logger.debug(message)
send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _help(bot: Bot, update: Update) -> None:
"""
Handler for /help.
Show commands of the bot
:param bot: telegram bot
:param update: message update
:return: None
"""
message = """
*/start:* `Starts the trader`
*/stop:* `Stops the trader`
*/status [table]:* `Lists all open trades`
*table :* `will display trades in a table`
*/profit:* `Lists cumulative profit from all finished trades`
*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, regardless of profit`
*/performance:* `Show performance of each finished trade grouped by pair`
*/daily <n>:* `Shows profit or loss per day, over the last n days`
*/count:* `Show number of trades running compared to allowed number of trades`
*/balance:* `Show account balance per currency`
*/help:* `This help message`
*/version:* `Show version`
"""
send_msg(message, bot=bot)
@authorized_only
def _version(bot: Bot, update: Update) -> None:
"""
Handler for /version.
Show version information
:param bot: telegram bot
:param update: message update
:return: None
"""
send_msg('*Version:* `{}`'.format(__version__), bot=bot)
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
"""
Send given markdown message
:param msg: message
:param bot: alternative bot
:param parse_mode: telegram parse mode
:return: None
"""
if not is_enabled():
return
bot = bot or _UPDATER.bot
keyboard = [['/daily', '/profit', '/balance'],
['/status', '/status table', '/performance'],
['/count', '/start', '/stop', '/help']]
reply_markup = ReplyKeyboardMarkup(keyboard)
try:
try: try:
bot.send_message( timescale = int(update.message.text.replace('/daily', '').strip())
_CONF['telegram']['chat_id'], msg, except (TypeError, ValueError):
parse_mode=parse_mode, reply_markup=reply_markup timescale = 7
(error, stats) = self.rpc_daily_profit(
timescale,
self._config['stake_currency'],
self._config['fiat_display_currency']
)
if error:
self.send_msg(stats, bot=bot)
else:
stats = tabulate(stats,
headers=[
'Day',
'Profit {}'.format(self._config['stake_currency']),
'Profit {}'.format(self._config['fiat_display_currency'])
],
tablefmt='simple')
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\
.format(
timescale,
stats
)
self.send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
@authorized_only
def _profit(self, bot: Bot, update: Update) -> None:
"""
Handler for /profit.
Returns a cumulative profit statistics.
:param bot: telegram bot
:param update: message update
:return: None
"""
(error, stats) = self.rpc_trade_statistics(
self._config['stake_currency'],
self._config['fiat_display_currency']
)
if error:
self.send_msg(stats, bot=bot)
return
# Message to display
markdown_msg = "*ROI:* Close trades\n" \
"∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \
"∙ `{profit_closed_fiat:.3f} {fiat}`\n" \
"*ROI:* All trades\n" \
"∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \
"∙ `{profit_all_fiat:.3f} {fiat}`\n" \
"*Total Trade Count:* `{trade_count}`\n" \
"*First Trade opened:* `{first_trade_date}`\n" \
"*Latest Trade opened:* `{latest_trade_date}`\n" \
"*Avg. Duration:* `{avg_duration}`\n" \
"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\
.format(
coin=self._config['stake_currency'],
fiat=self._config['fiat_display_currency'],
profit_closed_coin=stats['profit_closed_coin'],
profit_closed_percent=stats['profit_closed_percent'],
profit_closed_fiat=stats['profit_closed_fiat'],
profit_all_coin=stats['profit_all_coin'],
profit_all_percent=stats['profit_all_percent'],
profit_all_fiat=stats['profit_all_fiat'],
trade_count=stats['trade_count'],
first_trade_date=stats['first_trade_date'],
latest_trade_date=stats['latest_trade_date'],
avg_duration=stats['avg_duration'],
best_pair=stats['best_pair'],
best_rate=stats['best_rate']
)
self.send_msg(markdown_msg, bot=bot)
@authorized_only
def _balance(self, bot: Bot, update: Update) -> None:
"""
Handler for /balance
"""
(error, result) = self.rpc_balance(self._config['fiat_display_currency'])
if error:
self.send_msg('`All balances are zero.`')
return
(currencys, total, symbol, value) = result
output = ''
for currency in currencys:
output += """*Currency*: {currency}
*Available*: {available}
*Balance*: {balance}
*Pending*: {pending}
*Est. BTC*: {est_btc: .8f}
""".format(**currency)
output += """*Estimated Value*:
*BTC*: {0: .8f}
*{1}*: {2: .2f}
""".format(total, symbol, value)
self.send_msg(output)
@authorized_only
def _start(self, bot: Bot, update: Update) -> None:
"""
Handler for /start.
Starts TradeThread
:param bot: telegram bot
:param update: message update
:return: None
"""
(error, msg) = self.rpc_start()
if error:
self.send_msg(msg, bot=bot)
@authorized_only
def _stop(self, bot: Bot, update: Update) -> None:
"""
Handler for /stop.
Stops TradeThread
:param bot: telegram bot
:param update: message update
:return: None
"""
(error, msg) = self.rpc_stop()
self.send_msg(msg, bot=bot)
@authorized_only
def _forcesell(self, bot: Bot, update: Update) -> None:
"""
Handler for /forcesell <id>.
Sells the given trade at current price
:param bot: telegram bot
:param update: message update
:return: None
"""
trade_id = update.message.text.replace('/forcesell', '').strip()
(error, message) = self.rpc_forcesell(trade_id)
if error:
self.send_msg(message, bot=bot)
return
@authorized_only
def _performance(self, bot: Bot, update: Update) -> None:
"""
Handler for /performance.
Shows a performance statistic from finished trades
:param bot: telegram bot
:param update: message update
:return: None
"""
(error, trades) = self.rpc_performance()
if error:
self.send_msg(trades, bot=bot)
return
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
index=i + 1,
pair=trade['pair'],
profit=trade['profit'],
count=trade['count']
) for i, trade in enumerate(trades))
message = '<b>Performance:</b>\n{}'.format(stats)
self.send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _count(self, bot: Bot, update: Update) -> None:
"""
Handler for /count.
Returns the number of trades running
:param bot: telegram bot
:param update: message update
:return: None
"""
(error, trades) = self.rpc_count()
if error:
self.send_msg(trades, bot=bot)
return
message = tabulate({
'current': [len(trades)],
'max': [self._config['max_open_trades']]
}, headers=['current', 'max'], tablefmt='simple')
message = "<pre>{}</pre>".format(message)
self.logger.debug(message)
self.send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _help(self, bot: Bot, update: Update) -> None:
"""
Handler for /help.
Show commands of the bot
:param bot: telegram bot
:param update: message update
:return: None
"""
message = "*/start:* `Starts the trader`\n" \
"*/stop:* `Stops the trader`\n" \
"*/status [table]:* `Lists all open trades`\n" \
" *table :* `will display trades in a table`\n" \
"*/profit:* `Lists cumulative profit from all finished trades`\n" \
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, " \
"regardless of profit`\n" \
"*/performance:* `Show performance of each finished trade grouped by pair`\n" \
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n" \
"*/count:* `Show number of trades running compared to allowed number of trades`" \
"\n" \
"*/balance:* `Show account balance per currency`\n" \
"*/help:* `This help message`\n" \
"*/version:* `Show version`"
self.send_msg(message, bot=bot)
@authorized_only
def _version(self, bot: Bot, update: Update) -> None:
"""
Handler for /version.
Show version information
:param bot: telegram bot
:param update: message update
:return: None
"""
self.send_msg('*Version:* `{}`'.format(__version__), bot=bot)
def send_msg(self, msg: str, bot: Bot = None,
parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
"""
Send given markdown message
:param msg: message
:param bot: alternative bot
:param parse_mode: telegram parse mode
:return: None
"""
if not self.is_enabled():
return
bot = bot or self._updater.bot
keyboard = [['/daily', '/profit', '/balance'],
['/status', '/status table', '/performance'],
['/count', '/start', '/stop', '/help']]
reply_markup = ReplyKeyboardMarkup(keyboard)
try:
try:
bot.send_message(
self._config['telegram']['chat_id'],
text=msg,
parse_mode=parse_mode,
reply_markup=reply_markup
)
except NetworkError as network_err:
# Sometimes the telegram server resets the current connection,
# if this is the case we send the message again.
self.logger.warning(
'Telegram NetworkError: %s! Trying one more time.',
network_err.message
)
bot.send_message(
self._config['telegram']['chat_id'],
text=msg,
parse_mode=parse_mode,
reply_markup=reply_markup
)
except TelegramError as telegram_err:
self.logger.warning(
'TelegramError: %s! Giving up on that message.',
telegram_err.message
) )
except NetworkError as network_err:
# Sometimes the telegram server resets the current connection,
# if this is the case we send the message again.
logger.warning(
'Telegram NetworkError: %s! Trying one more time.',
network_err.message
)
bot.send_message(
_CONF['telegram']['chat_id'], msg,
parse_mode=parse_mode, reply_markup=reply_markup
)
except TelegramError as telegram_err:
logger.warning('TelegramError: %s! Giving up on that message.', telegram_err.message)

14
freqtrade/state.py Normal file
View File

@ -0,0 +1,14 @@
# pragma pylint: disable=too-few-public-methods
"""
Bot state constant
"""
import enum
class State(enum.Enum):
"""
Bot running states
"""
RUNNING = 0
STOPPED = 1

View File

@ -2,9 +2,10 @@
import talib.abstract as ta import talib.abstract as ta
from pandas import DataFrame from pandas import DataFrame
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.strategy.interface import IStrategy
from freqtrade.indicator_helpers import fishers_inverse from freqtrade.indicator_helpers import fishers_inverse
from freqtrade.strategy.interface import IStrategy
class_name = 'DefaultStrategy' class_name = 'DefaultStrategy'

View File

@ -4,6 +4,7 @@ This module defines the interface to apply for strategies
""" """
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pandas import DataFrame from pandas import DataFrame

View File

@ -3,15 +3,16 @@
""" """
This module load custom strategies This module load custom strategies
""" """
import importlib
import os import os
import sys import sys
import logging
import importlib
from collections import OrderedDict from collections import OrderedDict
from pandas import DataFrame from pandas import DataFrame
from freqtrade.strategy.interface import IStrategy
from freqtrade.constants import Constants
from freqtrade.logger import Logger
from freqtrade.strategy.interface import IStrategy
sys.path.insert(0, r'../../user_data/strategies') sys.path.insert(0, r'../../user_data/strategies')
@ -20,32 +21,19 @@ class Strategy(object):
""" """
This class contains all the logic to load custom strategy class This class contains all the logic to load custom strategy class
""" """
__instance = None def __init__(self, config: dict = {}) -> None:
DEFAULT_STRATEGY = 'default_strategy'
def __new__(cls) -> object:
"""
Used to create the Singleton
:return: Strategy object
"""
if Strategy.__instance is None:
Strategy.__instance = object.__new__(cls)
return Strategy.__instance
def init(self, config: dict) -> None:
""" """
Load the custom class from config parameter Load the custom class from config parameter
:param config: :param config:
:return: :return:
""" """
self.logger = logging.getLogger(__name__) self.logger = Logger(name=__name__).get_logger()
# Verify the strategy is in the configuration, otherwise fallback to the default strategy # Verify the strategy is in the configuration, otherwise fallback to the default strategy
if 'strategy' in config: if 'strategy' in config:
strategy = config['strategy'] strategy = config['strategy']
else: else:
strategy = self.DEFAULT_STRATEGY strategy = Constants.DEFAULT_STRATEGY
# Load the strategy # Load the strategy
self._load_strategy(strategy) self._load_strategy(strategy)
@ -72,12 +60,12 @@ class Strategy(object):
# Minimal ROI designed for the strategy # Minimal ROI designed for the strategy
self.minimal_roi = OrderedDict(sorted( self.minimal_roi = OrderedDict(sorted(
{int(key): value for (key, value) in self.custom_strategy.minimal_roi.items()}.items(), {int(key): value for (key, value) in self.custom_strategy.minimal_roi.items()}.items(),
key=lambda tuple: tuple[0])) # sort after converting to number key=lambda t: t[0])) # sort after converting to number
# Optimal stoploss designed for the strategy # Optimal stoploss designed for the strategy
self.stoploss = float(self.custom_strategy.stoploss) self.stoploss = float(self.custom_strategy.stoploss)
self.ticker_interval = self.custom_strategy.ticker_interval self.ticker_interval = int(self.custom_strategy.ticker_interval)
def _load_strategy(self, strategy_name: str) -> None: def _load_strategy(self, strategy_name: str) -> None:
""" """

View File

@ -1,17 +1,21 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
from datetime import datetime
from unittest.mock import MagicMock
from functools import reduce
import json import json
import logging
from datetime import datetime
from functools import reduce
from unittest.mock import MagicMock
import arrow import arrow
import pytest import pytest
from jsonschema import validate from jsonschema import validate
from sqlalchemy import create_engine
from telegram import Chat, Message, Update from telegram import Chat, Message, Update
from freqtrade.analyze import parse_ticker_dataframe
from freqtrade.strategy.strategy import Strategy
from freqtrade.misc import CONF_SCHEMA from freqtrade.analyze import Analyze
from freqtrade.constants import Constants
from freqtrade.freqtradebot import FreqtradeBot
logging.getLogger('').setLevel(logging.INFO)
def log_has(line, logs): def log_has(line, logs):
@ -22,6 +26,26 @@ def log_has(line, logs):
False) False)
# Functions for recurrent object patching
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
"""
This function patch _init_modules() to not call dependencies
:param mocker: a Mocker object to apply patches
:param config: Config to pass to the bot
:return: None
"""
mocker.patch('freqtrade.fiat_convert.Market', {'price_usd': 12345.0})
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
mocker.patch('freqtrade.freqtradebot.Analyze.get_signal', MagicMock())
return FreqtradeBot(config, create_engine('sqlite://'))
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def default_conf(): def default_conf():
""" Returns validated configuration suitable for most tests """ """ Returns validated configuration suitable for most tests """
@ -61,9 +85,10 @@ def default_conf():
"token": "token", "token": "token",
"chat_id": "0" "chat_id": "0"
}, },
"initial_state": "running" "initial_state": "running",
"loglevel": logging.DEBUG
} }
validate(configuration, CONF_SCHEMA) validate(configuration, Constants.CONF_SCHEMA)
return configuration return configuration
@ -265,14 +290,7 @@ def ticker_history_without_bv():
@pytest.fixture @pytest.fixture
def result(): def result():
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file: with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
return parse_ticker_dataframe(json.load(data_file)) return Analyze.parse_ticker_dataframe(json.load(data_file))
@pytest.fixture
def default_strategy():
strategy = Strategy()
strategy.init({'strategy': 'default_strategy'})
return strategy
# FIX: # FIX:
@ -280,3 +298,134 @@ def default_strategy():
# that inserts a trade of some type and open-status # that inserts a trade of some type and open-status
# return the open-order-id # return the open-order-id
# See tests in rpc/main that could use this # See tests in rpc/main that could use this
@pytest.fixture
def get_market_summaries_data():
"""
This fixture is a real result from exchange.get_market_summaries() but reduced to only
8 entries. 4 BTC, 4 USTD
:return: JSON market summaries
"""
return [
{
'Ask': 1.316e-05,
'BaseVolume': 5.72599471,
'Bid': 1.3e-05,
'Created': '2014-04-14T00:00:00',
'High': 1.414e-05,
'Last': 1.298e-05,
'Low': 1.282e-05,
'MarketName': 'BTC-XWC',
'OpenBuyOrders': 2000,
'OpenSellOrders': 1484,
'PrevDay': 1.376e-05,
'TimeStamp': '2018-02-05T01:32:40.493',
'Volume': 424041.21418375
},
{
'Ask': 0.00627051,
'BaseVolume': 93.23302388,
'Bid': 0.00618192,
'Created': '2016-10-20T04:48:30.387',
'High': 0.00669897,
'Last': 0.00618192,
'Low': 0.006,
'MarketName': 'BTC-XZC',
'OpenBuyOrders': 343,
'OpenSellOrders': 2037,
'PrevDay': 0.00668229,
'TimeStamp': '2018-02-05T01:32:43.383',
'Volume': 14863.60730702
},
{
'Ask': 0.01137247,
'BaseVolume': 383.55922657,
'Bid': 0.01136006,
'Created': '2016-11-15T20:29:59.73',
'High': 0.012,
'Last': 0.01137247,
'Low': 0.01119883,
'MarketName': 'BTC-ZCL',
'OpenBuyOrders': 1332,
'OpenSellOrders': 5317,
'PrevDay': 0.01179603,
'TimeStamp': '2018-02-05T01:32:42.773',
'Volume': 33308.07358285
},
{
'Ask': 0.04155821,
'BaseVolume': 274.75369074,
'Bid': 0.04130002,
'Created': '2016-10-28T17:13:10.833',
'High': 0.04354429,
'Last': 0.041585,
'Low': 0.0413,
'MarketName': 'BTC-ZEC',
'OpenBuyOrders': 863,
'OpenSellOrders': 5579,
'PrevDay': 0.0429,
'TimeStamp': '2018-02-05T01:32:43.21',
'Volume': 6479.84033259
},
{
'Ask': 210.99999999,
'BaseVolume': 615132.70989532,
'Bid': 210.05503736,
'Created': '2017-07-21T01:08:49.397',
'High': 257.396,
'Last': 211.0,
'Low': 209.05333589,
'MarketName': 'USDT-XMR',
'OpenBuyOrders': 180,
'OpenSellOrders': 1203,
'PrevDay': 247.93528899,
'TimeStamp': '2018-02-05T01:32:43.117',
'Volume': 2688.17410793
},
{
'Ask': 0.79589979,
'BaseVolume': 9349557.01853031,
'Bid': 0.789226,
'Created': '2017-07-14T17:10:10.737',
'High': 0.977,
'Last': 0.79589979,
'Low': 0.781,
'MarketName': 'USDT-XRP',
'OpenBuyOrders': 1075,
'OpenSellOrders': 6508,
'PrevDay': 0.93300218,
'TimeStamp': '2018-02-05T01:32:42.383',
'Volume': 10801663.00788851
},
{
'Ask': 0.05154982,
'BaseVolume': 2311087.71232136,
'Bid': 0.05040107,
'Created': '2017-12-29T19:29:18.357',
'High': 0.06668561,
'Last': 0.0508,
'Low': 0.05006731,
'MarketName': 'USDT-XVG',
'OpenBuyOrders': 655,
'OpenSellOrders': 5544,
'PrevDay': 0.0627,
'TimeStamp': '2018-02-05T01:32:41.507',
'Volume': 40031424.2152716
},
{
'Ask': 332.65500022,
'BaseVolume': 562911.87455665,
'Bid': 330.00000001,
'Created': '2017-07-14T17:10:10.673',
'High': 401.59999999,
'Last': 332.65500019,
'Low': 330.0,
'MarketName': 'USDT-ZEC',
'OpenBuyOrders': 161,
'OpenSellOrders': 1731,
'PrevDay': 391.42,
'TimeStamp': '2018-02-05T01:32:42.947',
'Volume': 1571.09647946
}
]

View File

@ -1,15 +1,16 @@
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement # pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
# pragma pylint: disable=protected-access # pragma pylint: disable=protected-access
from unittest.mock import MagicMock
from random import randint
import logging import logging
from requests.exceptions import RequestException from random import randint
import pytest from unittest.mock import MagicMock
import pytest
from requests.exceptions import RequestException
import freqtrade.exchange as exchange
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \ from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \
get_ticker, get_ticker_history, cancel_order, get_name, get_fee get_ticker, get_ticker_history, cancel_order, get_name, get_fee
import freqtrade.exchange as exchange
from freqtrade.tests.conftest import log_has from freqtrade.tests.conftest import log_has
API_INIT = False API_INIT = False

View File

@ -1,10 +1,12 @@
# pragma pylint: disable=missing-docstring, C0103, protected-access, unused-argument # pragma pylint: disable=missing-docstring, C0103, protected-access, unused-argument
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from requests.exceptions import ContentDecodingError from requests.exceptions import ContentDecodingError
from freqtrade.exchange.bittrex import Bittrex
import freqtrade.exchange.bittrex as btx import freqtrade.exchange.bittrex as btx
from freqtrade.exchange.bittrex import Bittrex
# Eat this flake8 # Eat this flake8

View File

@ -1,16 +1,28 @@
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103 # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument
import random
import logging import json
import math import math
import random
from copy import deepcopy
from typing import List
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pandas as pd
import numpy as np import numpy as np
from freqtrade import exchange, optimize import pandas as pd
from freqtrade.exchange import Bittrex from arrow import Arrow
from freqtrade.optimize import preprocess
from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe from freqtrade import optimize
import freqtrade.optimize.backtesting as backtesting from freqtrade.analyze import Analyze
from freqtrade.tests.conftest import log_has from freqtrade.arguments import Arguments
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
from freqtrade.tests.conftest import default_conf, log_has
# Avoid to reinit the same object again and again
_BACKTESTING = Backtesting(default_conf())
def get_args(args) -> List[str]:
return Arguments(args, '').get_parsed_arg()
def trim_dictlist(dict_list, num): def trim_dictlist(dict_list, num):
@ -20,124 +32,6 @@ def trim_dictlist(dict_list, num):
return new return new
# use for mock freqtrade.exchange.get_ticker_history'
def _load_pair_as_ticks(pair, tickfreq):
ticks = optimize.load_data(None, ticker_interval=8, pairs=[pair])
ticks = trim_dictlist(ticks, -200)
return ticks[pair]
# FIX: fixturize this?
def _make_backtest_conf(conf=None,
pair='BTC_UNITEST',
record=None):
data = optimize.load_data(None, ticker_interval=8, pairs=[pair])
data = trim_dictlist(data, -200)
return {'stake_amount': conf['stake_amount'],
'processed': optimize.preprocess(data),
'max_open_trades': 10,
'realistic': True,
'record': record}
def _trend(signals, buy_value, sell_value):
n = len(signals['low'])
buy = np.zeros(n)
sell = np.zeros(n)
for i in range(0, len(signals['buy'])):
if random.random() > 0.5: # Both buy and sell signals at same timeframe
buy[i] = buy_value
sell[i] = sell_value
signals['buy'] = buy
signals['sell'] = sell
return signals
def _trend_alternate(dataframe=None):
signals = dataframe
low = signals['low']
n = len(low)
buy = np.zeros(n)
sell = np.zeros(n)
for i in range(0, len(buy)):
if i % 2 == 0:
buy[i] = 1
else:
sell[i] = 1
signals['buy'] = buy
signals['sell'] = sell
return dataframe
def _run_backtest_1(strategy, fun, backtest_conf):
# strategy is a global (hidden as a singleton), so we
# emulate strategy being pure, by override/restore here
# if we dont do this, the override in strategy will carry over
# to other tests
old_buy = strategy.populate_buy_trend
old_sell = strategy.populate_sell_trend
strategy.populate_buy_trend = fun # Override
strategy.populate_sell_trend = fun # Override
results = backtest(backtest_conf)
strategy.populate_buy_trend = old_buy # restore override
strategy.populate_sell_trend = old_sell # restore override
return results
def test_generate_text_table():
results = pd.DataFrame(
{
'currency': ['BTC_ETH', 'BTC_ETH'],
'profit_percent': [0.1, 0.2],
'profit_BTC': [0.2, 0.4],
'duration': [10, 30],
'profit': [2, 0],
'loss': [0, 0]
}
)
print(generate_text_table({'BTC_ETH': {}}, results, 'BTC'))
assert generate_text_table({'BTC_ETH': {}}, results, 'BTC') == (
'pair buy count avg profit % total profit BTC avg duration profit loss\n' # noqa
'------- ----------- -------------- ------------------ -------------- -------- ------\n' # noqa
'BTC_ETH 2 15.00 0.60000000 20.0 2 0\n' # noqa
'TOTAL 2 15.00 0.60000000 20.0 2 0') # noqa
def test_get_timeframe(default_strategy):
data = preprocess(optimize.load_data(
None, ticker_interval=1, pairs=['BTC_UNITEST']))
min_date, max_date = get_timeframe(data)
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
def test_backtest(default_strategy, default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
data = trim_dictlist(data, -200)
results = backtest({'stake_amount': default_conf['stake_amount'],
'processed': optimize.preprocess(data),
'max_open_trades': 10,
'realistic': True})
assert not results.empty
def test_backtest_1min_ticker_interval(default_strategy, default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
# Run a backtesting for an exiting 5min ticker_interval
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
data = trim_dictlist(data, -200)
results = backtest({'stake_amount': default_conf['stake_amount'],
'processed': optimize.preprocess(data),
'max_open_trades': 1,
'realistic': True})
assert not results.empty
def load_data_test(what): def load_data_test(what):
timerange = ((None, 'line'), None, -100) timerange = ((None, 'line'), None, -100)
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'], timerange=timerange) data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'], timerange=timerange)
@ -181,34 +75,429 @@ def load_data_test(what):
return data return data
def simple_backtest(config, contour, num_results): def simple_backtest(config, contour, num_results) -> None:
backtesting = _BACKTESTING
data = load_data_test(contour) data = load_data_test(contour)
processed = optimize.preprocess(data) processed = backtesting.tickerdata_to_dataframe(data)
assert isinstance(processed, dict) assert isinstance(processed, dict)
results = backtest({'stake_amount': config['stake_amount'], results = backtesting.backtest(
'processed': processed, {
'max_open_trades': 1, 'stake_amount': config['stake_amount'],
'realistic': True}) 'processed': processed,
'max_open_trades': 1,
'realistic': True
}
)
# results :: <class 'pandas.core.frame.DataFrame'> # results :: <class 'pandas.core.frame.DataFrame'>
assert len(results) == num_results assert len(results) == num_results
def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False, timerange=None):
tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1, timerange=timerange)
pairdata = {'BTC_UNITEST': tickerdata}
return pairdata
# use for mock freqtrade.exchange.get_ticker_history'
def _load_pair_as_ticks(pair, tickfreq):
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
ticks = trim_dictlist(ticks, -200)
return ticks[pair]
# FIX: fixturize this?
def _make_backtest_conf(conf=None, pair='BTC_UNITEST', record=None):
data = optimize.load_data(None, ticker_interval=8, pairs=[pair])
data = trim_dictlist(data, -200)
return {
'stake_amount': conf['stake_amount'],
'processed': _BACKTESTING.tickerdata_to_dataframe(data),
'max_open_trades': 10,
'realistic': True,
'record': record
}
def _trend(signals, buy_value, sell_value):
n = len(signals['low'])
buy = np.zeros(n)
sell = np.zeros(n)
for i in range(0, len(signals['buy'])):
if random.random() > 0.5: # Both buy and sell signals at same timeframe
buy[i] = buy_value
sell[i] = sell_value
signals['buy'] = buy
signals['sell'] = sell
return signals
def _trend_alternate(dataframe=None):
signals = dataframe
low = signals['low']
n = len(low)
buy = np.zeros(n)
sell = np.zeros(n)
for i in range(0, len(buy)):
if i % 2 == 0:
buy[i] = 1
else:
sell[i] = 1
signals['buy'] = buy
signals['sell'] = sell
return dataframe
def _run_backtest_1(fun, backtest_conf):
# strategy is a global (hidden as a singleton), so we
# emulate strategy being pure, by override/restore here
# if we dont do this, the override in strategy will carry over
# to other tests
old_buy = _BACKTESTING.populate_buy_trend
old_sell = _BACKTESTING.populate_sell_trend
_BACKTESTING.populate_buy_trend = fun # Override
_BACKTESTING.populate_sell_trend = fun # Override
results = _BACKTESTING.backtest(backtest_conf)
_BACKTESTING.populate_buy_trend = old_buy # restore override
_BACKTESTING.populate_sell_trend = old_sell # restore override
return results
# Unit tests
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'--config', 'config.json',
'--strategy', 'default_strategy',
'backtesting'
]
config = setup_configuration(get_args(args))
assert 'max_open_trades' in config
assert 'stake_currency' in config
assert 'stake_amount' in config
assert 'exchange' in config
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
assert not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
assert 'live' not in config
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation' not in config
assert not log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
assert 'refresh_pairs' not in config
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
assert 'timerange' not in config
assert 'export' not in config
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'--config', 'config.json',
'--strategy', 'default_strategy',
'--datadir', '/foo/bar',
'backtesting',
'--ticker-interval', '1',
'--live',
'--realistic-simulation',
'--refresh-pairs-cached',
'--timerange', ':100',
'--export', '/bar/foo'
]
config = setup_configuration(get_args(args))
assert 'max_open_trades' in config
assert 'stake_currency' in config
assert 'stake_amount' in config
assert 'exchange' in config
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
assert log_has(
'Using ticker_interval: 1 ...',
caplog.record_tuples
)
assert 'live' in config
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation'in config
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
assert 'refresh_pairs'in config
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
assert 'timerange' in config
assert log_has(
'Parameter --timerange detected: {} ...'.format(config['timerange']),
caplog.record_tuples
)
assert 'export' in config
assert log_has(
'Parameter --export detected: {} ...'.format(config['export']),
caplog.record_tuples
)
def test_start(mocker, default_conf, caplog) -> None:
"""
Test start() function
"""
start_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'--config', 'config.json',
'--strategy', 'default_strategy',
'backtesting'
]
args = get_args(args)
start(args)
assert log_has(
'Starting freqtrade in Backtesting mode',
caplog.record_tuples
)
assert start_mock.call_count == 1
def test_backtesting__init__(mocker, default_conf) -> None:
"""
Test Backtesting.__init__() method
"""
init_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.Backtesting._init', init_mock)
backtesting = Backtesting(default_conf)
assert backtesting.config == default_conf
assert backtesting.analyze is None
assert backtesting.ticker_interval is None
assert backtesting.tickerdata_to_dataframe is None
assert backtesting.populate_buy_trend is None
assert backtesting.populate_sell_trend is None
assert init_mock.call_count == 1
def test_backtesting_init(default_conf) -> None:
"""
Test Backtesting._init() method
"""
backtesting = Backtesting(default_conf)
assert backtesting.config == default_conf
assert isinstance(backtesting.analyze, Analyze)
assert backtesting.ticker_interval == 5
assert callable(backtesting.tickerdata_to_dataframe)
assert callable(backtesting.populate_buy_trend)
assert callable(backtesting.populate_sell_trend)
def test_tickerdata_to_dataframe(default_conf) -> None:
"""
Test Backtesting.tickerdata_to_dataframe() method
"""
timerange = ((None, 'line'), None, -100)
tick = optimize.load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
tickerlist = {'BTC_UNITEST': tick}
backtesting = _BACKTESTING
data = backtesting.tickerdata_to_dataframe(tickerlist)
assert len(data['BTC_UNITEST']) == 100
# Load Analyze to compare the result between Backtesting function and Analyze are the same
analyze = Analyze(default_conf)
data2 = analyze.tickerdata_to_dataframe(tickerlist)
assert data['BTC_UNITEST'].equals(data2['BTC_UNITEST'])
def test_get_timeframe() -> None:
"""
Test Backtesting.get_timeframe() method
"""
backtesting = _BACKTESTING
data = backtesting.tickerdata_to_dataframe(
optimize.load_data(
None,
ticker_interval=1,
pairs=['BTC_UNITEST']
)
)
min_date, max_date = backtesting.get_timeframe(data)
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
def test_generate_text_table():
"""
Test Backtesting.generate_text_table() method
"""
backtesting = _BACKTESTING
results = pd.DataFrame(
{
'currency': ['BTC_ETH', 'BTC_ETH'],
'profit_percent': [0.1, 0.2],
'profit_BTC': [0.2, 0.4],
'duration': [10, 30],
'profit': [2, 0],
'loss': [0, 0]
}
)
result_str = (
'pair buy count avg profit % '
'total profit BTC avg duration profit loss\n'
'------- ----------- -------------- '
'------------------ -------------- -------- ------\n'
'BTC_ETH 2 15.00 '
'0.60000000 20.0 2 0\n'
'TOTAL 2 15.00 '
'0.60000000 20.0 2 0'
)
assert backtesting._generate_text_table(data={'BTC_ETH': {}}, results=results) == result_str
def test_backtesting_start(default_conf, mocker, caplog) -> None:
"""
Test Backtesting.start() method
"""
def get_timeframe(input1, input2):
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
mocker.patch('freqtrade.optimize.load_data', mocked_load_data)
mocker.patch('freqtrade.exchange.get_ticker_history')
mocker.patch.multiple(
'freqtrade.optimize.backtesting.Backtesting',
backtest=MagicMock(),
_generate_text_table=MagicMock(return_value='1'),
get_timeframe=get_timeframe,
)
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
conf['ticker_interval'] = 1
conf['live'] = False
conf['datadir'] = None
conf['export'] = None
conf['timerange'] = '-100'
backtesting = Backtesting(conf)
backtesting.start()
# check the logs, that will contain the backtest result
exists = [
'Using local backtesting data (using whitelist in given config) ...',
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Measuring data from 2017-11-14T21:17:00+00:00 '
'up to 2017-11-14T22:59:00+00:00 (0 days)..'
]
for line in exists:
assert log_has(line, caplog.record_tuples)
def test_backtest(default_conf) -> None:
"""
Test Backtesting.backtest() method
"""
backtesting = _BACKTESTING
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
data = trim_dictlist(data, -200)
results = backtesting.backtest(
{
'stake_amount': default_conf['stake_amount'],
'processed': backtesting.tickerdata_to_dataframe(data),
'max_open_trades': 10,
'realistic': True
}
)
assert not results.empty
def test_backtest_1min_ticker_interval(default_conf) -> None:
"""
Test Backtesting.backtest() method with 1 min ticker
"""
backtesting = _BACKTESTING
# Run a backtesting for an exiting 5min ticker_interval
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
data = trim_dictlist(data, -200)
results = backtesting.backtest(
{
'stake_amount': default_conf['stake_amount'],
'processed': backtesting.tickerdata_to_dataframe(data),
'max_open_trades': 1,
'realistic': True
}
)
assert not results.empty
def test_processed() -> None:
"""
Test Backtesting.backtest() method with offline data
"""
backtesting = _BACKTESTING
dict_of_tickerrows = load_data_test('raise')
dataframes = backtesting.tickerdata_to_dataframe(dict_of_tickerrows)
dataframe = dataframes['BTC_UNITEST']
cols = dataframe.columns
# assert the dataframe got some of the indicator columns
for col in ['close', 'high', 'low', 'open', 'date',
'ema50', 'ao', 'macd', 'plus_dm']:
assert col in cols
def test_backtest_pricecontours(default_conf) -> None:
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
for [contour, numres] in tests:
simple_backtest(default_conf, contour, numres)
# Test backtest using offline data (testdata directory) # Test backtest using offline data (testdata directory)
def test_backtest_ticks(default_conf):
def test_backtest_ticks(default_conf, mocker, default_strategy):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
ticks = [1, 5] ticks = [1, 5]
fun = default_strategy.populate_buy_trend fun = _BACKTESTING.populate_buy_trend
for tick in ticks: for tick in ticks:
backtest_conf = _make_backtest_conf(conf=default_conf) backtest_conf = _make_backtest_conf(conf=default_conf)
results = _run_backtest_1(default_strategy, fun, backtest_conf) results = _run_backtest_1(fun, backtest_conf)
assert not results.empty assert not results.empty
def test_backtest_clash_buy_sell(default_conf, mocker, default_strategy): def test_backtest_clash_buy_sell(default_conf):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
# Override the default buy trend function in our default_strategy # Override the default buy trend function in our default_strategy
def fun(dataframe=None): def fun(dataframe=None):
buy_value = 1 buy_value = 1
@ -216,13 +505,11 @@ def test_backtest_clash_buy_sell(default_conf, mocker, default_strategy):
return _trend(dataframe, buy_value, sell_value) return _trend(dataframe, buy_value, sell_value)
backtest_conf = _make_backtest_conf(conf=default_conf) backtest_conf = _make_backtest_conf(conf=default_conf)
results = _run_backtest_1(default_strategy, fun, backtest_conf) results = _run_backtest_1(fun, backtest_conf)
assert results.empty assert results.empty
def test_backtest_only_sell(default_conf, mocker, default_strategy): def test_backtest_only_sell(default_conf):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
# Override the default buy trend function in our default_strategy # Override the default buy trend function in our default_strategy
def fun(dataframe=None): def fun(dataframe=None):
buy_value = 0 buy_value = 0
@ -230,31 +517,29 @@ def test_backtest_only_sell(default_conf, mocker, default_strategy):
return _trend(dataframe, buy_value, sell_value) return _trend(dataframe, buy_value, sell_value)
backtest_conf = _make_backtest_conf(conf=default_conf) backtest_conf = _make_backtest_conf(conf=default_conf)
results = _run_backtest_1(default_strategy, fun, backtest_conf) results = _run_backtest_1(fun, backtest_conf)
assert results.empty assert results.empty
def test_backtest_alternate_buy_sell(default_conf, mocker, default_strategy): def test_backtest_alternate_buy_sell(default_conf):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
backtest_conf = _make_backtest_conf(conf=default_conf, pair='BTC_UNITEST') backtest_conf = _make_backtest_conf(conf=default_conf, pair='BTC_UNITEST')
results = _run_backtest_1(default_strategy, _trend_alternate, results = _run_backtest_1(_trend_alternate, backtest_conf)
backtest_conf)
assert len(results) == 3 assert len(results) == 3
def test_backtest_record(default_conf, mocker, default_strategy): def test_backtest_record(default_conf, mocker):
names = [] names = []
records = [] records = []
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch(
mocker.patch('freqtrade.misc.file_dump_json', 'freqtrade.optimize.backtesting.file_dump_json',
new=lambda n, r: (names.append(n), records.append(r))) new=lambda n, r: (names.append(n), records.append(r))
)
backtest_conf = _make_backtest_conf( backtest_conf = _make_backtest_conf(
conf=default_conf, conf=default_conf,
pair='BTC_UNITEST', pair='BTC_UNITEST',
record="trades" record="trades"
) )
results = _run_backtest_1(default_strategy, _trend_alternate, results = _run_backtest_1(_trend_alternate, backtest_conf)
backtest_conf)
assert len(results) == 3 assert len(results) == 3
# Assert file_dump_json was only called once # Assert file_dump_json was only called once
assert names == ['backtest-result.json'] assert names == ['backtest-result.json']
@ -277,74 +562,48 @@ def test_backtest_record(default_conf, mocker, default_strategy):
assert dur > 0 assert dur > 0
def test_processed(default_conf, mocker, default_strategy): def test_backtest_start_live(default_conf, mocker, caplog):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
dict_of_tickerrows = load_data_test('raise')
dataframes = optimize.preprocess(dict_of_tickerrows)
dataframe = dataframes['BTC_UNITEST']
cols = dataframe.columns
# assert the dataframe got some of the indicator columns
for col in ['close', 'high', 'low', 'open', 'date',
'ema50', 'ao', 'macd', 'plus_dm']:
assert col in cols
def test_backtest_pricecontours(default_conf, mocker, default_strategy):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
for [contour, numres] in tests:
simple_backtest(default_conf, contour, numres)
def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False, timerange=None):
tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1, timerange=timerange)
pairdata = {'BTC_UNITEST': tickerdata}
return pairdata
def test_backtest_start(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.misc.load_config', new=lambda s: default_conf)
mocker.patch.multiple('freqtrade.optimize',
load_data=mocked_load_data)
args = MagicMock()
args.ticker_interval = 1
args.level = 10
args.live = False
args.datadir = None
args.export = None
args.timerange = '-100' # needed due to MagicMock malleability
backtesting.start(args)
# check the logs, that will contain the backtest result
exists = ['Using max_open_trades: 1 ...',
'Using stake_amount: 0.001 ...',
'Measuring data from 2017-11-14T21:17:00+00:00 '
'up to 2017-11-14T22:59:00+00:00 (0 days)..']
for line in exists:
assert log_has(line, caplog.record_tuples)
def test_backtest_start_live(default_strategy, default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST'] default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
mocker.patch('freqtrade.exchange.get_ticker_history', mocker.patch('freqtrade.exchange.get_ticker_history',
new=lambda n, i: _load_pair_as_ticks(n, i)) new=lambda n, i: _load_pair_as_ticks(n, i))
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
mocker.patch('freqtrade.misc.load_config', new=lambda s: default_conf) mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = MagicMock() args = MagicMock()
args.ticker_interval = 1 args.ticker_interval = 1
args.level = 10 args.level = 10
args.live = True args.live = True
args.datadir = None args.datadir = None
args.export = None args.export = None
args.strategy = 'default_strategy'
args.timerange = '-100' # needed due to MagicMock malleability args.timerange = '-100' # needed due to MagicMock malleability
backtesting.start(args)
args = [
'--config', 'config.json',
'--strategy', 'default_strategy',
'backtesting',
'--ticker-interval', '1',
'--live',
'--timerange', '-100'
]
args = get_args(args)
start(args)
# check the logs, that will contain the backtest result # check the logs, that will contain the backtest result
exists = ['Using max_open_trades: 1 ...', exists = [
'Using stake_amount: 0.001 ...', 'Parameter -i/--ticker-interval detected ...',
'Measuring data from 2017-11-14T19:32:00+00:00 ' 'Using ticker_interval: 1 ...',
'up to 2017-11-14T22:59:00+00:00 (0 days)..'] 'Parameter -l/--live detected ...',
'Using max_open_trades: 1 ...',
'Parameter --timerange detected: -100 ..',
'Parameter --datadir detected: freqtrade/tests/testdata ...',
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Downloading data for all pairs in whitelist ...',
'Measuring data from 2017-11-14T19:32:00+00:00 up to 2017-11-14T22:59:00+00:00 (0 days)..'
]
for line in exists: for line in exists:
assert log_has(line, caplog.record_tuples) log_has(line, caplog.record_tuples)

View File

@ -1,125 +1,141 @@
# pragma pylint: disable=missing-docstring,W0212,C0103 # pragma pylint: disable=missing-docstring,W0212,C0103
import logging import json
import os
from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pandas as pd import pandas as pd
from freqtrade.optimize.hyperopt import calculate_loss, TARGET_TRADES, EXPECTED_MAX_PROFIT, start, \ from freqtrade.optimize.__init__ import load_tickerdata_file
log_results, save_trials, read_trials, generate_roi_table, has_space from freqtrade.optimize.hyperopt import Hyperopt, start
from freqtrade.strategy.strategy import Strategy from freqtrade.strategy.strategy import Strategy
import freqtrade.optimize.hyperopt as hyperopt from freqtrade.tests.conftest import default_conf, log_has
from freqtrade.tests.optimize.test_backtesting import get_args
def test_loss_calculation_prefer_correct_trade_count(): # Avoid to reinit the same object again and again
correct = calculate_loss(1, TARGET_TRADES, 20) _HYPEROPT = Hyperopt(default_conf())
over = calculate_loss(1, TARGET_TRADES + 100, 20)
under = calculate_loss(1, TARGET_TRADES - 100, 20)
assert over > correct
assert under > correct
def test_loss_calculation_prefer_shorter_trades(): # Functions for recurrent object patching
shorter = calculate_loss(1, 100, 20) def create_trials(mocker) -> None:
longer = calculate_loss(1, 100, 30)
assert shorter < longer
def test_loss_calculation_has_limited_profit():
correct = calculate_loss(EXPECTED_MAX_PROFIT, TARGET_TRADES, 20)
over = calculate_loss(EXPECTED_MAX_PROFIT * 2, TARGET_TRADES, 20)
under = calculate_loss(EXPECTED_MAX_PROFIT / 2, TARGET_TRADES, 20)
assert over == correct
assert under > correct
def create_trials(mocker):
""" """
When creating trials, mock the hyperopt Trials so that *by default* When creating trials, mock the hyperopt Trials so that *by default*
- we don't create any pickle'd files in the filesystem - we don't create any pickle'd files in the filesystem
- we might have a pickle'd file so make sure that we return - we might have a pickle'd file so make sure that we return
false when looking for it false when looking for it
""" """
mocker.patch('freqtrade.optimize.hyperopt.TRIALS_FILE', _HYPEROPT.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
return_value='freqtrade/tests/optimize/ut_trials.pickle')
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False)
return_value=False) mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1)
mocker.patch('freqtrade.optimize.hyperopt.save_trials', mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True)
return_value=None) mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None)
mocker.patch('freqtrade.optimize.hyperopt.read_trials',
return_value=None)
mocker.patch('freqtrade.optimize.hyperopt.os.remove',
return_value=True)
return mocker.Mock( return mocker.Mock(
results=[{ results=[
'loss': 1, {
'result': 'foo', 'loss': 1,
'status': 'ok' 'result': 'foo',
}], 'status': 'ok'
}
],
best_trial={'misc': {'vals': {'adx': 999}}} best_trial={'misc': {'vals': {'adx': 999}}}
) )
def test_start_calls_fmin(mocker): # Unit tests
trials = create_trials(mocker) def test_start(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.optimize.tickerdata_to_dataframe') """
mocker.patch('freqtrade.optimize.hyperopt.TRIALS', return_value=trials) Test start() function
mocker.patch('freqtrade.optimize.hyperopt.sorted', """
return_value=trials.results) start_mock = MagicMock()
mocker.patch('freqtrade.optimize.preprocess') mocker.patch('freqtrade.logger.Logger.set_format', MagicMock())
mocker.patch('freqtrade.optimize.load_data') mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={}) mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=False, ))
timerange=None, spaces='all') args = [
Strategy().init({'strategy': 'default_strategy'}) '--config', 'config.json',
'--strategy', 'default_strategy',
'hyperopt',
'--epochs', '5'
]
args = get_args(args)
Strategy({'strategy': 'default_strategy'})
start(args) start(args)
mock_fmin.assert_called_once() import pprint
pprint.pprint(caplog.record_tuples)
assert log_has(
'Starting freqtrade in Hyperopt mode',
caplog.record_tuples
)
assert start_mock.call_count == 1
def test_start_uses_mongotrials(mocker): def test_loss_calculation_prefer_correct_trade_count() -> None:
mock_mongotrials = mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', """
return_value=create_trials(mocker)) Test Hyperopt.calculate_loss()
mocker.patch('freqtrade.optimize.tickerdata_to_dataframe') """
mocker.patch('freqtrade.optimize.load_data') hyperopt = _HYPEROPT
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={}) Strategy({'strategy': 'default_strategy'})
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=True, correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20)
timerange=None, spaces='all') over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20)
Strategy().init({'strategy': 'default_strategy'}) under = hyperopt.calculate_loss(1, hyperopt.target_trades - 100, 20)
start(args) assert over > correct
assert under > correct
mock_mongotrials.assert_called_once()
def test_log_results_if_loss_improves(mocker): def test_loss_calculation_prefer_shorter_trades() -> None:
logger = mocker.patch('freqtrade.optimize.hyperopt.logger.info') """
global CURRENT_BEST_LOSS Test Hyperopt.calculate_loss()
CURRENT_BEST_LOSS = 2 """
log_results({ hyperopt = _HYPEROPT
'loss': 1,
'current_tries': 1,
'total_tries': 2,
'result': 'foo'
})
logger.assert_called_once() shorter = hyperopt.calculate_loss(1, 100, 20)
longer = hyperopt.calculate_loss(1, 100, 30)
assert shorter < longer
def test_no_log_if_loss_does_not_improve(mocker): def test_loss_calculation_has_limited_profit() -> None:
logger = mocker.patch('freqtrade.optimize.hyperopt.logger.info') hyperopt = _HYPEROPT
global CURRENT_BEST_LOSS
CURRENT_BEST_LOSS = 2
log_results({
'loss': 3,
})
assert not logger.called correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20)
over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20)
under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20)
assert over == correct
assert under > correct
def test_fmin_best_results(mocker, caplog): def test_log_results_if_loss_improves(caplog) -> None:
caplog.set_level(logging.INFO) hyperopt = _HYPEROPT
hyperopt.current_best_loss = 2
hyperopt.log_results(
{
'loss': 1,
'current_tries': 1,
'total_tries': 2,
'result': 'foo'
}
)
assert log_has(' 1/2: foo. Loss 1.00000', caplog.record_tuples)
def test_no_log_if_loss_does_not_improve(caplog) -> None:
hyperopt = _HYPEROPT
hyperopt.current_best_loss = 2
hyperopt.log_results(
{
'loss': 3,
}
)
assert caplog.record_tuples == []
def test_fmin_best_results(mocker, default_conf, caplog) -> None:
fmin_result = { fmin_result = {
"macd_below_zero": 0, "macd_below_zero": 0,
"adx": 1, "adx": 1,
@ -144,41 +160,67 @@ def test_fmin_best_results(mocker, caplog):
"roi_p3": 3, "roi_p3": 3,
} }
mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker)) conf = deepcopy(default_conf)
mocker.patch('freqtrade.optimize.tickerdata_to_dataframe') conf.update({'config': 'config.json.example'})
mocker.patch('freqtrade.optimize.load_data') conf.update({'epochs': 1})
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result) conf.update({'timerange': None})
conf.update({'spaces': 'all'})
args = mocker.Mock(epochs=1, config='config.json.example', mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
timerange=None, spaces='all') mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
Strategy().init({'strategy': 'default_strategy'}) mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
start(args) mocker.patch('freqtrade.logger.Logger.set_format', MagicMock())
Strategy({'strategy': 'default_strategy'})
hyperopt = Hyperopt(conf)
hyperopt.trials = create_trials(mocker)
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
exists = [ exists = [
'Best parameters', 'Best parameters:',
'"adx": {\n "enabled": true,\n "value": 15.0\n },', '"adx": {\n "enabled": true,\n "value": 15.0\n },',
'"fastd": {\n "enabled": true,\n "value": 40.0\n },',
'"green_candle": {\n "enabled": true\n },', '"green_candle": {\n "enabled": true\n },',
'"macd_below_zero": {\n "enabled": false\n },',
'"mfi": {\n "enabled": false\n },', '"mfi": {\n "enabled": false\n },',
'"over_sar": {\n "enabled": false\n },',
'"roi_p1": 1.0,',
'"roi_p2": 2.0,',
'"roi_p3": 3.0,',
'"roi_t1": 1.0,',
'"roi_t2": 2.0,',
'"roi_t3": 3.0,',
'"rsi": {\n "enabled": true,\n "value": 37.0\n },',
'"stoploss": -0.1,',
'"trigger": {\n "type": "faststoch10"\n },', '"trigger": {\n "type": "faststoch10"\n },',
'"stoploss": -0.1', '"uptrend_long_ema": {\n "enabled": true\n },',
'"uptrend_short_ema": {\n "enabled": false\n },',
'"uptrend_sma": {\n "enabled": false\n }',
'ROI table:\n{0: 6.0, 3.0: 3.0, 5.0: 1.0, 6.0: 0}',
'Best Result:\nfoo'
] ]
for line in exists: for line in exists:
assert line in caplog.text assert line in caplog.text
def test_fmin_throw_value_error(mocker, caplog): def test_fmin_throw_value_error(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.INFO) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
Strategy().init({'strategy': 'default_strategy'})
mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker))
mocker.patch('freqtrade.optimize.tickerdata_to_dataframe')
mocker.patch('freqtrade.optimize.load_data')
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError()) mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
args = mocker.Mock(epochs=1, config='config.json.example', conf = deepcopy(default_conf)
timerange=None, spaces='all') conf.update({'config': 'config.json.example'})
Strategy().init({'strategy': 'default_strategy'}) conf.update({'epochs': 1})
start(args) conf.update({'timerange': None})
conf.update({'spaces': 'all'})
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
mocker.patch('freqtrade.logger.Logger.set_format', MagicMock())
Strategy({'strategy': 'default_strategy'})
hyperopt = Hyperopt(conf)
hyperopt.trials = create_trials(mocker)
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
exists = [ exists = [
'Best Result:', 'Best Result:',
@ -190,69 +232,82 @@ def test_fmin_throw_value_error(mocker, caplog):
assert line in caplog.text assert line in caplog.text
def test_resuming_previous_hyperopt_results_succeeds(mocker): def test_resuming_previous_hyperopt_results_succeeds(mocker, default_conf) -> None:
import freqtrade.optimize.hyperopt as hyperopt
trials = create_trials(mocker) trials = create_trials(mocker)
mocker.patch('freqtrade.optimize.hyperopt.TRIALS',
return_value=trials) conf = deepcopy(default_conf)
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', conf.update({'config': 'config.json.example'})
return_value=True) conf.update({'epochs': 1})
mocker.patch('freqtrade.optimize.hyperopt.len', conf.update({'mongodb': False})
return_value=len(trials.results)) conf.update({'timerange': None})
mock_read = mocker.patch('freqtrade.optimize.hyperopt.read_trials', conf.update({'spaces': 'all'})
return_value=trials)
mock_save = mocker.patch('freqtrade.optimize.hyperopt.save_trials', mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=True)
return_value=None) mocker.patch('freqtrade.optimize.hyperopt.len', return_value=len(trials.results))
mocker.patch('freqtrade.optimize.hyperopt.sorted', mock_read = mocker.patch(
return_value=trials.results) 'freqtrade.optimize.hyperopt.Hyperopt.read_trials',
mocker.patch('freqtrade.optimize.preprocess') return_value=trials
mocker.patch('freqtrade.optimize.load_data') )
mocker.patch('freqtrade.optimize.hyperopt.fmin', mock_save = mocker.patch(
return_value={}) 'freqtrade.optimize.hyperopt.Hyperopt.save_trials',
args = mocker.Mock(epochs=1, return_value=None
config='config.json.example', )
mongodb=False, mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
timerange=None, mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
spaces='all') mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
Strategy().init({'strategy': 'default_strategy'}) mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
start(args) mocker.patch('freqtrade.logger.Logger.set_format', MagicMock())
Strategy({'strategy': 'default_strategy'})
hyperopt = Hyperopt(conf)
hyperopt.trials = trials
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
mock_read.assert_called_once() mock_read.assert_called_once()
mock_save.assert_called_once() mock_save.assert_called_once()
current_tries = hyperopt._CURRENT_TRIES current_tries = hyperopt.current_tries
total_tries = hyperopt.TOTAL_TRIES total_tries = hyperopt.total_tries
assert current_tries == len(trials.results) assert current_tries == len(trials.results)
assert total_tries == (current_tries + len(trials.results)) assert total_tries == (current_tries + len(trials.results))
def test_save_trials_saves_trials(mocker): def test_save_trials_saves_trials(mocker, caplog) -> None:
create_trials(mocker)
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None)
hyperopt = _HYPEROPT
mocker.patch('freqtrade.optimize.hyperopt.open', return_value=hyperopt.trials_file)
hyperopt.save_trials()
assert log_has(
'Saving Trials to \'freqtrade/tests/optimize/ut_trials.pickle\'',
caplog.record_tuples
)
mock_dump.assert_called_once()
def test_read_trials_returns_trials_file(mocker, caplog) -> None:
trials = create_trials(mocker) trials = create_trials(mocker)
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load', return_value=trials)
return_value=None) mock_open = mocker.patch('freqtrade.optimize.hyperopt.open', return_value=mock_load)
trials_path = mocker.patch('freqtrade.optimize.hyperopt.TRIALS_FILE',
return_value='ut_trials.pickle')
mocker.patch('freqtrade.optimize.hyperopt.open',
return_value=trials_path)
save_trials(trials, trials_path)
mock_dump.assert_called_once_with(trials, trials_path) hyperopt = _HYPEROPT
hyperopt_trial = hyperopt.read_trials()
assert log_has(
def test_read_trials_returns_trials_file(mocker): 'Reading Trials from \'freqtrade/tests/optimize/ut_trials.pickle\'',
trials = create_trials(mocker) caplog.record_tuples
mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load', )
return_value=trials) assert hyperopt_trial == trials
mock_open = mocker.patch('freqtrade.optimize.hyperopt.open',
return_value=mock_load)
assert read_trials() == trials
mock_open.assert_called_once() mock_open.assert_called_once()
mock_load.assert_called_once() mock_load.assert_called_once()
def test_roi_table_generation(): def test_roi_table_generation() -> None:
params = { params = {
'roi_t1': 5, 'roi_t1': 5,
'roi_t2': 10, 'roi_t2': 10,
@ -261,7 +316,54 @@ def test_roi_table_generation():
'roi_p2': 2, 'roi_p2': 2,
'roi_p3': 3, 'roi_p3': 3,
} }
assert generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
hyperopt = _HYPEROPT
assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
def test_start_calls_fmin(mocker, default_conf) -> None:
trials = create_trials(mocker)
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1})
conf.update({'mongodb': False})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
hyperopt = Hyperopt(conf)
hyperopt.trials = trials
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
mock_fmin.assert_called_once()
def test_start_uses_mongotrials(mocker, default_conf) -> None:
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
mock_mongotrials = mocker.patch(
'freqtrade.optimize.hyperopt.MongoTrials',
return_value=create_trials(mocker)
)
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1})
conf.update({'mongodb': True})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
hyperopt = Hyperopt(conf)
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
mock_mongotrials.assert_called_once()
mock_fmin.assert_called_once()
# test log_trials_result # test log_trials_result
@ -269,26 +371,167 @@ def test_roi_table_generation():
# test optimizer if 'ro_t1' in params # test optimizer if 'ro_t1' in params
def test_format_results(): def test_format_results():
trades = [('BTC_ETH', 2, 2, 123), """
('BTC_LTC', 1, 1, 123), Test Hyperopt.format_results()
('BTC_XRP', -1, -2, -246)] """
trades = [
('BTC_ETH', 2, 2, 123),
('BTC_LTC', 1, 1, 123),
('BTC_XRP', -1, -2, -246)
]
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration'] labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
df = pd.DataFrame.from_records(trades, columns=labels) df = pd.DataFrame.from_records(trades, columns=labels)
x = hyperopt.format_results(df) x = Hyperopt.format_results(df)
assert x.find(' 66.67%') assert x.find(' 66.67%')
def test_signal_handler(mocker): def test_signal_handler(mocker):
"""
Test Hyperopt.signal_handler()
"""
m = MagicMock() m = MagicMock()
mocker.patch('sys.exit', m) mocker.patch('sys.exit', m)
mocker.patch('freqtrade.optimize.hyperopt.save_trials', m) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.save_trials', m)
mocker.patch('freqtrade.optimize.hyperopt.log_trials_result', m) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.log_trials_result', m)
hyperopt = _HYPEROPT
hyperopt.signal_handler(9, None) hyperopt.signal_handler(9, None)
assert m.call_count == 3 assert m.call_count == 3
def test_has_space(): def test_has_space():
assert has_space(['buy', 'roi'], 'roi') """
assert has_space(['buy', 'roi'], 'buy') Test Hyperopt.has_space() method
assert not has_space(['buy', 'roi'], 'stoploss') """
assert has_space(['all'], 'buy') _HYPEROPT.config.update({'spaces': ['buy', 'roi']})
assert _HYPEROPT.has_space('roi')
assert _HYPEROPT.has_space('buy')
assert not _HYPEROPT.has_space('stoploss')
_HYPEROPT.config.update({'spaces': ['all']})
assert _HYPEROPT.has_space('buy')
def test_populate_indicators() -> None:
"""
Test Hyperopt.populate_indicators()
"""
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
tickerlist = {'BTC_UNITEST': tick}
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
dataframe = _HYPEROPT.populate_indicators(dataframes['BTC_UNITEST'])
# Check if some indicators are generated. We will not test all of them
assert 'adx' in dataframe
assert 'ao' in dataframe
assert 'cci' in dataframe
def test_buy_strategy_generator() -> None:
"""
Test Hyperopt.buy_strategy_generator()
"""
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
tickerlist = {'BTC_UNITEST': tick}
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
dataframe = _HYPEROPT.populate_indicators(dataframes['BTC_UNITEST'])
populate_buy_trend = _HYPEROPT.buy_strategy_generator(
{
'uptrend_long_ema': {
'enabled': True
},
'macd_below_zero': {
'enabled': True
},
'uptrend_short_ema': {
'enabled': True
},
'mfi': {
'enabled': True,
'value': 20
},
'fastd': {
'enabled': True,
'value': 20
},
'adx': {
'enabled': True,
'value': 20
},
'rsi': {
'enabled': True,
'value': 20
},
'over_sar': {
'enabled': True,
},
'green_candle': {
'enabled': True,
},
'uptrend_sma': {
'enabled': True,
},
'trigger': {
'type': 'lower_bb'
}
}
)
result = populate_buy_trend(dataframe)
# Check if some indicators are generated. We will not test all of them
assert 'buy' in result
assert 1 in result['buy']
def test_generate_optimizer(mocker, default_conf) -> None:
"""
Test Hyperopt.generate_optimizer() function
"""
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
trades = [
('BTC_POWR', 0.023117, 0.000233, 100)
]
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.backtest',
MagicMock(return_value=backtest_result)
)
optimizer_param = {
'adx': {'enabled': False},
'fastd': {'enabled': True, 'value': 35.0},
'green_candle': {'enabled': True},
'macd_below_zero': {'enabled': True},
'mfi': {'enabled': False},
'over_sar': {'enabled': False},
'roi_p1': 0.01,
'roi_p2': 0.01,
'roi_p3': 0.1,
'roi_t1': 60.0,
'roi_t2': 30.0,
'roi_t3': 20.0,
'rsi': {'enabled': False},
'stoploss': -0.4,
'trigger': {'type': 'macd_cross_signal'},
'uptrend_long_ema': {'enabled': False},
'uptrend_short_ema': {'enabled': True},
'uptrend_sma': {'enabled': True}
}
response_expected = {
'loss': 1.9840569076926293,
'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC '
'(0.0231Σ%). Avg duration 100.0 mins.',
'status': 'ok'
}
hyperopt = Hyperopt(conf)
generate_optimizer_value = hyperopt.generate_optimizer(optimizer_param)
assert generate_optimizer_value == response_expected

View File

@ -1,16 +1,15 @@
# pragma pylint: disable=missing-docstring, protected-access, C0103 # pragma pylint: disable=missing-docstring, protected-access, C0103
import os
import json import json
import logging import os
import uuid import uuid
from shutil import copyfile from shutil import copyfile
from freqtrade import exchange, optimize
from freqtrade.exchange import Bittrex from freqtrade import optimize
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs,\ from freqtrade.misc import file_dump_json
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist, file_dump_json from freqtrade.optimize.__init__ import make_testdata_path, download_pairs, \
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist
from freqtrade.tests.conftest import log_has from freqtrade.tests.conftest import log_has
from freqtrade.strategy.strategy import Strategy
# Change this if modifying BTC_UNITEST testdatafile # Change this if modifying BTC_UNITEST testdatafile
_BTC_UNITTEST_LENGTH = 13681 _BTC_UNITTEST_LENGTH = 13681
@ -47,12 +46,11 @@ 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): def test_load_data_30min_ticker(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.INFO) """
Test load_data() with 30 min ticker
"""
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)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = 'freqtrade/tests/testdata/BTC_UNITTEST-30.json' file = 'freqtrade/tests/testdata/BTC_UNITTEST-30.json'
_backup_file(file, copy_file=True) _backup_file(file, copy_file=True)
@ -62,12 +60,11 @@ def test_load_data_30min_ticker(default_conf, ticker_history, mocker, caplog):
_clean_test_file(file) _clean_test_file(file)
def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog): def test_load_data_5min_ticker(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.INFO) """
Test load_data() with 5 min ticker
"""
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)
exchange._API = Bittrex({'key': '', 'secret': ''})
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)
@ -77,12 +74,11 @@ def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog):
_clean_test_file(file) _clean_test_file(file)
def test_load_data_1min_ticker(default_conf, ticker_history, mocker, caplog): def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.INFO) """
Test load_data() with 1 min ticker
"""
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)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = 'freqtrade/tests/testdata/BTC_ETH-1.json' file = 'freqtrade/tests/testdata/BTC_ETH-1.json'
_backup_file(file, copy_file=True) _backup_file(file, copy_file=True)
@ -92,12 +88,11 @@ def test_load_data_1min_ticker(default_conf, ticker_history, mocker, caplog):
_clean_test_file(file) _clean_test_file(file)
def test_load_data_with_new_pair_1min(default_conf, ticker_history, mocker, caplog): def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.INFO) """
Test load_data() with 1 min ticker
"""
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)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = 'freqtrade/tests/testdata/BTC_MEME-1.json' file = 'freqtrade/tests/testdata/BTC_MEME-1.json'
_backup_file(file) _backup_file(file)
@ -107,14 +102,12 @@ def test_load_data_with_new_pair_1min(default_conf, ticker_history, mocker, capl
_clean_test_file(file) _clean_test_file(file)
def test_testdata_path(): def test_testdata_path() -> None:
assert os.path.join('freqtrade', 'tests', 'testdata') in make_testdata_path(None) assert os.path.join('freqtrade', 'tests', 'testdata') in make_testdata_path(None)
def test_download_pairs(default_conf, ticker_history, mocker): def test_download_pairs(ticker_history, mocker) -> None:
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json' file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json' file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
@ -151,13 +144,10 @@ def test_download_pairs(default_conf, ticker_history, mocker):
_clean_test_file(file2_5) _clean_test_file(file2_5)
def test_download_pairs_exception(default_conf, ticker_history, mocker, caplog): def test_download_pairs_exception(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.INFO)
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata', mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
side_effect=BaseException('File Error')) side_effect=BaseException('File Error'))
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json' file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json' file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
@ -171,10 +161,8 @@ def test_download_pairs_exception(default_conf, ticker_history, mocker, caplog):
assert log_has('Failed to download the pair: "BTC-MEME", Interval: 1 min', caplog.record_tuples) assert log_has('Failed to download the pair: "BTC-MEME", Interval: 1 min', caplog.record_tuples)
def test_download_backtesting_testdata(default_conf, ticker_history, mocker): def test_download_backtesting_testdata(ticker_history, mocker) -> None:
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
# Download a 1 min ticker file # Download a 1 min ticker file
file1 = 'freqtrade/tests/testdata/BTC_XEL-1.json' file1 = 'freqtrade/tests/testdata/BTC_XEL-1.json'
@ -192,7 +180,7 @@ def test_download_backtesting_testdata(default_conf, ticker_history, mocker):
_clean_test_file(file2) _clean_test_file(file2)
def test_download_backtesting_testdata2(mocker): def test_download_backtesting_testdata2(mocker) -> None:
tick = [{'T': 'bar'}, {'T': 'foo'}] tick = [{'T': 'bar'}, {'T': 'foo'}]
mocker.patch('freqtrade.misc.file_dump_json', return_value=None) mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=tick) mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=tick)
@ -200,7 +188,7 @@ def test_download_backtesting_testdata2(mocker):
assert download_backtesting_testdata(None, pair="BTC-UNITEST", interval=3) assert download_backtesting_testdata(None, pair="BTC-UNITEST", interval=3)
def test_load_tickerdata_file(): def test_load_tickerdata_file() -> None:
# 7 does not exist in either format. # 7 does not exist in either format.
assert not load_tickerdata_file(None, 'BTC_UNITEST', 7) assert not load_tickerdata_file(None, 'BTC_UNITEST', 7)
# 1 exists only as a .json # 1 exists only as a .json
@ -211,23 +199,18 @@ def test_load_tickerdata_file():
assert _BTC_UNITTEST_LENGTH == len(tickerdata) assert _BTC_UNITTEST_LENGTH == len(tickerdata)
def test_init(default_conf, mocker): def test_init(default_conf, mocker) -> None:
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(
ticker_interval=int(default_conf['ticker_interval'])) '',
pairs=[],
refresh_pairs=True,
ticker_interval=int(default_conf['ticker_interval'])
)
def test_tickerdata_to_dataframe(): def test_trim_tickerlist() -> None:
Strategy().init({'strategy': 'default_strategy'})
timerange = ((None, 'line'), None, -100)
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
tickerlist = {'BTC_UNITEST': tick}
data = optimize.tickerdata_to_dataframe(tickerlist)
assert len(data['BTC_UNITEST']) == 100
def test_trim_tickerlist():
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file: with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
ticker_list = json.load(data_file) ticker_list = json.load(data_file)
ticker_list_len = len(ticker_list) ticker_list_len = len(ticker_list)
@ -272,7 +255,11 @@ def test_trim_tickerlist():
assert ticker_list_len == ticker_len assert ticker_list_len == ticker_len
def test_file_dump_json(): def test_file_dump_json() -> None:
"""
Test file_dump_json()
:return: None
"""
file = 'freqtrade/tests/testdata/test_{id}.json'.format(id=str(uuid.uuid4())) file = 'freqtrade/tests/testdata/test_{id}.json'.format(id=str(uuid.uuid4()))
data = {'bar': 'foo'} data = {'bar': 'foo'}

View File

@ -1,203 +1,133 @@
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103 # pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments
"""
Unit test file for rpc/rpc.py
"""
from datetime import datetime from datetime import datetime
from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
from sqlalchemy import create_engine from sqlalchemy import create_engine
from freqtrade.rpc import init, cleanup, send_msg from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
import freqtrade.main as main from freqtrade.rpc.rpc import RPC
import freqtrade.misc as misc from freqtrade.state import State
import freqtrade.rpc as rpc from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
def prec_satoshi(a, b): # Functions for recurrent object patching
def prec_satoshi(a, b) -> float:
""" """
:return: True if A and B differs less than one satoshi. :return: True if A and B differs less than one satoshi.
""" """
return abs(a - b) < 0.00000001 return abs(a - b) < 0.00000001
def test_init_telegram_enabled(default_conf, mocker): # Unit tests
module_list = [] def test_rpc_trade_status(default_conf, ticker, mocker) -> None:
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list) """
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock()) Test rpc_trade_status() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
)
init(default_conf) freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
rpc = RPC(freqtradebot)
assert telegram_mock.call_count == 1 freqtradebot.update_state(State.STOPPED)
assert 'telegram' in module_list
def test_init_telegram_disabled(default_conf, mocker):
module_list = []
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list)
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock())
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
init(conf)
assert telegram_mock.call_count == 0
assert 'telegram' not in module_list
def test_cleanup_telegram_enabled(mocker):
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram'])
telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock())
cleanup()
assert telegram_mock.call_count == 1
def test_cleanup_telegram_disabled(mocker):
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', [])
telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock())
cleanup()
assert telegram_mock.call_count == 0
def test_send_msg_telegram_enabled(mocker):
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram'])
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
send_msg('test')
assert telegram_mock.call_count == 1
def test_send_msg_telegram_disabled(mocker):
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', [])
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
send_msg('test')
assert telegram_mock.call_count == 0
def test_rpc_forcesell(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock())
cancel_order_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
cancel_order=cancel_order_mock,
get_order=MagicMock(return_value={
'closed': True,
'type': 'LIMIT_BUY',
}))
main.init(default_conf, create_engine('sqlite://'))
misc.update_state(misc.State.STOPPED)
(error, res) = rpc.rpc_forcesell(None)
assert error
assert res == '`trader is not running`'
misc.update_state(misc.State.RUNNING)
(error, res) = rpc.rpc_forcesell(None)
assert error
assert res == 'Invalid argument.'
(error, res) = rpc.rpc_forcesell('all')
assert not error
assert res == ''
main.create_trade(0.001, 5)
(error, res) = rpc.rpc_forcesell('all')
assert not error
assert res == ''
(error, res) = rpc.rpc_forcesell('1')
assert not error
assert res == ''
misc.update_state(misc.State.STOPPED)
(error, res) = rpc.rpc_forcesell(None)
assert error
assert res == '`trader is not running`'
(error, res) = rpc.rpc_forcesell('all')
assert error
assert res == '`trader is not running`'
misc.update_state(misc.State.RUNNING)
assert cancel_order_mock.call_count == 0
# make an limit-buy open trade
mocker.patch.multiple('freqtrade.exchange',
get_order=MagicMock(return_value={
'closed': None,
'type': 'LIMIT_BUY'
}))
# check that the trade is called, which is done
# by ensuring exchange.cancel_order is called
(error, res) = rpc.rpc_forcesell('1')
assert not error
assert res == ''
assert cancel_order_mock.call_count == 1
main.create_trade(0.001, 5)
# make an limit-sell open trade
mocker.patch.multiple('freqtrade.exchange',
get_order=MagicMock(return_value={
'closed': None,
'type': 'LIMIT_SELL'
}))
(error, res) = rpc.rpc_forcesell('2')
assert not error
assert res == ''
# status quo, no exchange calls
assert cancel_order_mock.call_count == 1
def test_rpc_trade_status(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
main.init(default_conf, create_engine('sqlite://'))
misc.update_state(misc.State.STOPPED)
(error, result) = rpc.rpc_trade_status() (error, result) = rpc.rpc_trade_status()
assert error assert error
assert result.find('trader is not running') >= 0 assert 'trader is not running' in result
misc.update_state(misc.State.RUNNING) freqtradebot.update_state(State.RUNNING)
(error, result) = rpc.rpc_trade_status() (error, result) = rpc.rpc_trade_status()
assert error assert error
assert result.find('no active trade') >= 0 assert 'no active trade' in result
main.create_trade(0.001, 5) freqtradebot.create_trade()
(error, result) = rpc.rpc_trade_status() (error, result) = rpc.rpc_trade_status()
assert not error assert not error
trade = result[0] trade = result[0]
result_message = [
'*Trade ID:* `1`\n'
'*Current Pair:* '
'[BTC_ETH](https://www.bittrex.com/Market/Index?MarketName=BTC-ETH)\n'
'*Open Since:* `just now`\n'
'*Amount:* `90.99181074`\n'
'*Open Rate:* `0.00001099`\n'
'*Close Rate:* `None`\n'
'*Current Rate:* `0.00001098`\n'
'*Close Profit:* `None`\n'
'*Current Profit:* `-0.59%`\n'
'*Open Order:* `(LIMIT_BUY rem=0.00000000)`'
]
assert result == result_message
assert trade.find('[BTC_ETH]') >= 0 assert trade.find('[BTC_ETH]') >= 0
def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): def test_rpc_status_table(default_conf, ticker, mocker) -> None:
mocker.patch.dict('freqtrade.main._CONF', default_conf) """
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) Test rpc_status_table() method
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) """
mocker.patch.multiple('freqtrade.rpc.telegram', patch_get_signal(mocker, (True, False))
_CONF=default_conf, patch_coinmarketcap(mocker)
init=MagicMock()) mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple(
validate_pairs=MagicMock(), 'freqtrade.freqtradebot.exchange',
get_ticker=ticker) validate_pairs=MagicMock(),
mocker.patch.multiple('freqtrade.fiat_convert.Market', get_ticker=ticker
ticker=MagicMock(return_value={'price_usd': 15000.0})) )
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
main.init(default_conf, create_engine('sqlite://')) freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
rpc = RPC(freqtradebot)
freqtradebot.update_state(State.STOPPED)
(error, result) = rpc.rpc_status_table()
assert error
assert '*Status:* `trader is not running`' in result
freqtradebot.update_state(State.RUNNING)
(error, result) = rpc.rpc_status_table()
assert error
assert '*Status:* `no active order`' in result
freqtradebot.create_trade()
(error, result) = rpc.rpc_status_table()
assert 'just now' in result['Since'].all()
assert 'BTC_ETH' in result['Pair'].all()
assert '-0.59%' in result['Profit'].all()
def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker)\
-> None:
"""
Test rpc_daily_profit() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
stake_currency = default_conf['stake_currency'] stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency'] fiat_display_currency = default_conf['fiat_display_currency']
rpc = RPC(freqtradebot)
# Create some test data # Create some test data
main.create_trade(0.001, 5) freqtradebot.create_trade()
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -209,8 +139,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_s
# Try valid data # Try valid data
update.message.text = '/daily 2' update.message.text = '/daily 2'
(error, days) = rpc.rpc_daily_profit(7, stake_currency, (error, days) = rpc.rpc_daily_profit(7, stake_currency, fiat_display_currency)
fiat_display_currency)
assert not error assert not error
assert len(days) == 7 assert len(days) == 7
for day in days: for day in days:
@ -224,50 +153,56 @@ def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_s
assert str(days[0][0]) == str(datetime.utcnow().date()) assert str(days[0][0]) == str(datetime.utcnow().date())
# Try invalid data # Try invalid data
(error, days) = rpc.rpc_daily_profit(0, stake_currency, (error, days) = rpc.rpc_daily_profit(0, stake_currency, fiat_display_currency)
fiat_display_currency)
assert error assert error
assert days.find('must be an integer greater than 0') >= 0 assert days.find('must be an integer greater than 0') >= 0
def test_rpc_trade_statistics( def test_rpc_trade_statistics(
default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker): default_conf, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker) -> None:
mocker.patch.dict('freqtrade.main._CONF', default_conf) """
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) Test rpc_trade_statistics() method
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) """
mocker.patch.multiple('freqtrade.rpc.telegram', patch_get_signal(mocker, (True, False))
_CONF=default_conf, mocker.patch.multiple(
init=MagicMock()) 'freqtrade.fiat_convert.Market',
mocker.patch.multiple('freqtrade.main.exchange', ticker=MagicMock(return_value={'price_usd': 15000.0}),
validate_pairs=MagicMock(), )
get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Market',
ticker=MagicMock(return_value={'price_usd': 15000.0}))
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
main.init(default_conf, create_engine('sqlite://')) mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
stake_currency = default_conf['stake_currency'] stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency'] fiat_display_currency = default_conf['fiat_display_currency']
(error, stats) = rpc.rpc_trade_statistics(stake_currency, rpc = RPC(freqtradebot)
fiat_display_currency)
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
assert error assert error
assert stats.find('no closed trade') >= 0 assert stats.find('no closed trade') >= 0
# Create some test data # Create some test data
main.create_trade(0.001, 5) freqtradebot.create_trade()
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) trade.update(limit_buy_order)
# Update the ticker with a market going up # Update the ticker with a market going up
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple(
validate_pairs=MagicMock(), 'freqtrade.freqtradebot.exchange',
get_ticker=ticker_sell_up) validate_pairs=MagicMock(),
get_ticker=ticker_sell_up
)
trade.update(limit_sell_order) trade.update(limit_sell_order)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
(error, stats) = rpc.rpc_trade_statistics(stake_currency, (error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
fiat_display_currency)
assert not error assert not error
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05) assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
assert prec_satoshi(stats['profit_closed_percent'], 6.2) assert prec_satoshi(stats['profit_closed_percent'], 6.2)
@ -285,33 +220,41 @@ def test_rpc_trade_statistics(
# Test that rpc_trade_statistics can handle trades that lacks # Test that rpc_trade_statistics can handle trades that lacks
# trade.open_rate (it is set to None) # trade.open_rate (it is set to None)
def test_rpc_trade_statistics_closed( def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_up, limit_buy_order,
default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker): limit_sell_order):
mocker.patch.dict('freqtrade.main._CONF', default_conf) """
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) Test rpc_trade_statistics() method
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) """
mocker.patch.multiple('freqtrade.rpc.telegram', patch_get_signal(mocker, (True, False))
_CONF=default_conf, mocker.patch.multiple(
init=MagicMock()) 'freqtrade.fiat_convert.Market',
mocker.patch.multiple('freqtrade.main.exchange', ticker=MagicMock(return_value={'price_usd': 15000.0}),
validate_pairs=MagicMock(), )
get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Market',
ticker=MagicMock(return_value={'price_usd': 15000.0}))
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
main.init(default_conf, create_engine('sqlite://')) mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
stake_currency = default_conf['stake_currency'] stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency'] fiat_display_currency = default_conf['fiat_display_currency']
rpc = RPC(freqtradebot)
# Create some test data # Create some test data
main.create_trade(0.001, 5) freqtradebot.create_trade()
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) trade.update(limit_buy_order)
# Update the ticker with a market going up # Update the ticker with a market going up
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple(
validate_pairs=MagicMock(), 'freqtrade.freqtradebot.exchange',
get_ticker=ticker_sell_up) validate_pairs=MagicMock(),
get_ticker=ticker_sell_up
)
trade.update(limit_sell_order) trade.update(limit_sell_order)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -319,8 +262,7 @@ def test_rpc_trade_statistics_closed(
for trade in Trade.query.order_by(Trade.id).all(): for trade in Trade.query.order_by(Trade.id).all():
trade.open_rate = None trade.open_rate = None
(error, stats) = rpc.rpc_trade_statistics(stake_currency, (error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
fiat_display_currency)
assert not error assert not error
assert prec_satoshi(stats['profit_closed_coin'], 0) assert prec_satoshi(stats['profit_closed_coin'], 0)
assert prec_satoshi(stats['profit_closed_percent'], 0) assert prec_satoshi(stats['profit_closed_percent'], 0)
@ -336,57 +278,223 @@ def test_rpc_trade_statistics_closed(
assert prec_satoshi(stats['best_rate'], 6.2) assert prec_satoshi(stats['best_rate'], 6.2)
def test_rpc_balance_handle(default_conf, update, mocker): def test_rpc_balance_handle(default_conf, mocker):
mock_balance = [{ """
'Currency': 'BTC', Test rpc_balance() method
'Balance': 10.0, """
'Available': 12.0, mock_balance = [
'Pending': 0.0, {
'CryptoAddress': 'XXXX', 'Currency': 'BTC',
}, { 'Balance': 10.0,
'Currency': 'ETH', 'Available': 12.0,
'Balance': 0.0, 'Pending': 0.0,
'Available': 0.0, 'CryptoAddress': 'XXXX',
'Pending': 0.0, },
'CryptoAddress': 'XXXX', {
}] 'Currency': 'ETH',
mocker.patch.dict('freqtrade.main._CONF', default_conf) 'Balance': 0.0,
mocker.patch.multiple('freqtrade.main.exchange', 'Available': 0.0,
get_balances=MagicMock(return_value=mock_balance)) 'Pending': 0.0,
mocker.patch.multiple('freqtrade.fiat_convert.Market', 'CryptoAddress': 'XXXX',
ticker=MagicMock(return_value={'price_usd': 15000.0})) }
]
patch_get_signal(mocker, (True, False))
mocker.patch.multiple(
'freqtrade.fiat_convert.Market',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_balances=MagicMock(return_value=mock_balance)
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
rpc = RPC(freqtradebot)
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency']) (error, res) = rpc.rpc_balance(default_conf['fiat_display_currency'])
assert not error assert not error
(trade, x, y, z) = res (trade, x, y, z) = res
assert prec_satoshi(x, 10) assert prec_satoshi(x, 10)
assert prec_satoshi(z, 150000) assert prec_satoshi(z, 150000)
assert y == 'USD' assert 'USD' in y
assert len(trade) == 1 assert len(trade) == 1
assert trade[0]['currency'] == 'BTC' assert 'BTC' in trade[0]['currency']
assert prec_satoshi(trade[0]['available'], 12) assert prec_satoshi(trade[0]['available'], 12)
assert prec_satoshi(trade[0]['balance'], 10) assert prec_satoshi(trade[0]['balance'], 10)
assert prec_satoshi(trade[0]['pending'], 0) assert prec_satoshi(trade[0]['pending'], 0)
assert prec_satoshi(trade[0]['est_btc'], 10) assert prec_satoshi(trade[0]['est_btc'], 10)
def test_performance_handle( def test_rpc_start(mocker, default_conf) -> None:
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): """
mocker.patch.dict('freqtrade.main._CONF', default_conf) Test rpc_start() method
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) """
msg_mock = MagicMock() patch_get_signal(mocker, (True, False))
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) patch_coinmarketcap(mocker)
mocker.patch.multiple('freqtrade.rpc.telegram', mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
_CONF=default_conf, mocker.patch.multiple(
init=MagicMock(), 'freqtrade.freqtradebot.exchange',
send_msg=msg_mock) validate_pairs=MagicMock(),
mocker.patch.multiple('freqtrade.main.exchange', get_ticker=MagicMock()
validate_pairs=MagicMock(), )
get_ticker=ticker)
main.init(default_conf, create_engine('sqlite://')) freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
rpc = RPC(freqtradebot)
freqtradebot.update_state(State.STOPPED)
(error, result) = rpc.rpc_start()
assert not error
assert '`Starting trader ...`' in result
assert freqtradebot.get_state() == State.RUNNING
(error, result) = rpc.rpc_start()
assert error
assert '*Status:* `already running`' in result
assert freqtradebot.get_state() == State.RUNNING
def test_rpc_stop(mocker, default_conf) -> None:
"""
Test rpc_stop() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock()
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
rpc = RPC(freqtradebot)
freqtradebot.update_state(State.RUNNING)
(error, result) = rpc.rpc_stop()
assert not error
assert '`Stopping trader ...`' in result
assert freqtradebot.get_state() == State.STOPPED
(error, result) = rpc.rpc_stop()
assert error
assert '*Status:* `already stopped`' in result
assert freqtradebot.get_state() == State.STOPPED
def test_rpc_forcesell(default_conf, ticker, mocker) -> None:
"""
Test rpc_forcesell() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
cancel_order_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
cancel_order=cancel_order_mock,
get_order=MagicMock(
return_value={
'closed': True,
'type': 'LIMIT_BUY',
}
)
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
rpc = RPC(freqtradebot)
freqtradebot.update_state(State.STOPPED)
(error, res) = rpc.rpc_forcesell(None)
assert error
assert res == '`trader is not running`'
freqtradebot.update_state(State.RUNNING)
(error, res) = rpc.rpc_forcesell(None)
assert error
assert res == 'Invalid argument.'
(error, res) = rpc.rpc_forcesell('all')
assert not error
assert res == ''
freqtradebot.create_trade()
(error, res) = rpc.rpc_forcesell('all')
assert not error
assert res == ''
(error, res) = rpc.rpc_forcesell('1')
assert not error
assert res == ''
freqtradebot.update_state(State.STOPPED)
(error, res) = rpc.rpc_forcesell(None)
assert error
assert res == '`trader is not running`'
(error, res) = rpc.rpc_forcesell('all')
assert error
assert res == '`trader is not running`'
freqtradebot.update_state(State.RUNNING)
assert cancel_order_mock.call_count == 0
# make an limit-buy open trade
mocker.patch(
'freqtrade.freqtradebot.exchange.get_order',
return_value={
'closed': None,
'type': 'LIMIT_BUY'
}
)
# check that the trade is called, which is done
# by ensuring exchange.cancel_order is called
(error, res) = rpc.rpc_forcesell('1')
assert not error
assert res == ''
assert cancel_order_mock.call_count == 1
freqtradebot.create_trade()
# make an limit-sell open trade
mocker.patch(
'freqtrade.freqtradebot.exchange.get_order',
return_value={
'closed': None,
'type': 'LIMIT_SELL'
}
)
(error, res) = rpc.rpc_forcesell('2')
assert not error
assert res == ''
# status quo, no exchange calls
assert cancel_order_mock.call_count == 1
def test_performance_handle(default_conf, ticker, limit_buy_order,
limit_sell_order, mocker) -> None:
"""
Test rpc_performance() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_balances=MagicMock(return_value=ticker),
get_ticker=ticker
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
rpc = RPC(freqtradebot)
# Create some test data # Create some test data
main.create_trade(0.001, int(default_conf['ticker_interval'])) freqtradebot.create_trade()
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -404,3 +512,33 @@ def test_performance_handle(
assert res[0]['pair'] == 'BTC_ETH' assert res[0]['pair'] == 'BTC_ETH'
assert res[0]['count'] == 1 assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit'], 6.2) assert prec_satoshi(res[0]['profit'], 6.2)
def test_rpc_count(mocker, default_conf, ticker) -> None:
"""
Test rpc_count() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_balances=MagicMock(return_value=ticker),
get_ticker=ticker
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
rpc = RPC(freqtradebot)
(error, trades) = rpc.rpc_count()
nb_trades = len(trades)
assert not error
assert nb_trades == 0
# Create some test data
freqtradebot.create_trade()
(error, trades) = rpc.rpc_count()
nb_trades = len(trades)
assert not error
assert nb_trades == 1

View File

@ -0,0 +1,139 @@
"""
Unit test file for rpc/rpc_manager.py
"""
import logging
from copy import deepcopy
from unittest.mock import MagicMock
from freqtrade.rpc.rpc_manager import RPCManager
from freqtrade.rpc.telegram import Telegram
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
def test_rpc_manager_object() -> None:
"""
Test the Arguments object has the mandatory methods
:return: None
"""
assert hasattr(RPCManager, '_init')
assert hasattr(RPCManager, 'send_msg')
assert hasattr(RPCManager, 'cleanup')
def test__init__(mocker, default_conf) -> None:
"""
Test __init__() method
"""
init_mock = mocker.patch('freqtrade.rpc.rpc_manager.RPCManager._init', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot)
assert rpc_manager.freqtrade == freqtradebot
assert rpc_manager.registered_modules == []
assert rpc_manager.telegram is None
assert init_mock.call_count == 1
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
"""
Test _init() method with Telegram disabled
"""
caplog.set_level(logging.DEBUG)
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
freqtradebot = get_patched_freqtradebot(mocker, conf)
rpc_manager = RPCManager(freqtradebot)
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
assert rpc_manager.registered_modules == []
assert rpc_manager.telegram is None
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
"""
Test _init() method with Telegram enabled
"""
caplog.set_level(logging.DEBUG)
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot)
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
len_modules = len(rpc_manager.registered_modules)
assert len_modules == 1
assert 'telegram' in rpc_manager.registered_modules
assert isinstance(rpc_manager.telegram, Telegram)
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
"""
Test cleanup() method with Telegram disabled
"""
caplog.set_level(logging.DEBUG)
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
freqtradebot = get_patched_freqtradebot(mocker, conf)
rpc_manager = RPCManager(freqtradebot)
rpc_manager.cleanup()
assert not log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
assert telegram_mock.call_count == 0
def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
"""
Test cleanup() method with Telegram enabled
"""
caplog.set_level(logging.DEBUG)
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot)
# Check we have Telegram as a registered modules
assert 'telegram' in rpc_manager.registered_modules
rpc_manager.cleanup()
assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
assert 'telegram' not in rpc_manager.registered_modules
assert telegram_mock.call_count == 1
def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
"""
Test send_msg() method with Telegram disabled
"""
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
freqtradebot = get_patched_freqtradebot(mocker, conf)
rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg('test')
assert log_has('test', caplog.record_tuples)
assert telegram_mock.call_count == 0
def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
"""
Test send_msg() method with Telegram disabled
"""
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg('test')
assert log_has('test', caplog.record_tuples)
assert telegram_mock.call_count == 1

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,16 @@
import json import json
import pytest import pytest
from pandas import DataFrame from pandas import DataFrame
from freqtrade.analyze import Analyze
from freqtrade.strategy.default_strategy import DefaultStrategy, class_name from freqtrade.strategy.default_strategy import DefaultStrategy, class_name
from freqtrade.analyze import parse_ticker_dataframe
@pytest.fixture @pytest.fixture
def result(): def result():
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file: with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
return parse_ticker_dataframe(json.load(data_file)) return Analyze.parse_ticker_dataframe(json.load(data_file))
def test_default_strategy_class_name(): def test_default_strategy_class_name():

View File

@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring, protected-access, C0103 # pragma pylint: disable=missing-docstring, protected-access, C0103
import logging import logging
from freqtrade.strategy.strategy import Strategy from freqtrade.strategy.strategy import Strategy
@ -21,7 +22,6 @@ def test_search_strategy():
def test_strategy_structure(): def test_strategy_structure():
assert hasattr(Strategy, 'init')
assert hasattr(Strategy, 'populate_indicators') assert hasattr(Strategy, 'populate_indicators')
assert hasattr(Strategy, 'populate_buy_trend') assert hasattr(Strategy, 'populate_buy_trend')
assert hasattr(Strategy, 'populate_sell_trend') assert hasattr(Strategy, 'populate_sell_trend')
@ -53,8 +53,7 @@ def test_load_not_found_strategy(caplog):
def test_strategy(result): def test_strategy(result):
strategy = Strategy() strategy = Strategy({'strategy': 'default_strategy'})
strategy.init({'strategy': 'default_strategy'})
assert hasattr(strategy.custom_strategy, 'minimal_roi') assert hasattr(strategy.custom_strategy, 'minimal_roi')
assert strategy.minimal_roi[0] == 0.04 assert strategy.minimal_roi[0] == 0.04
@ -82,8 +81,7 @@ def test_strategy_override_minimal_roi(caplog):
"0": 0.5 "0": 0.5
} }
} }
strategy = Strategy() strategy = Strategy(config)
strategy.init(config)
assert hasattr(strategy.custom_strategy, 'minimal_roi') assert hasattr(strategy.custom_strategy, 'minimal_roi')
assert strategy.minimal_roi[0] == 0.5 assert strategy.minimal_roi[0] == 0.5
@ -99,8 +97,7 @@ def test_strategy_override_stoploss(caplog):
'strategy': 'default_strategy', 'strategy': 'default_strategy',
'stoploss': -0.5 'stoploss': -0.5
} }
strategy = Strategy() strategy = Strategy(config)
strategy.init(config)
assert hasattr(strategy.custom_strategy, 'stoploss') assert hasattr(strategy.custom_strategy, 'stoploss')
assert strategy.stoploss == -0.5 assert strategy.stoploss == -0.5
@ -117,8 +114,7 @@ def test_strategy_override_ticker_interval(caplog):
'strategy': 'default_strategy', 'strategy': 'default_strategy',
'ticker_interval': 60 'ticker_interval': 60
} }
strategy = Strategy() strategy = Strategy(config)
strategy.init(config)
assert hasattr(strategy.custom_strategy, 'ticker_interval') assert hasattr(strategy.custom_strategy, 'ticker_interval')
assert strategy.ticker_interval == 60 assert strategy.ticker_interval == 60
@ -138,8 +134,7 @@ def test_strategy_fallback_default_strategy():
def test_strategy_singleton(): def test_strategy_singleton():
strategy1 = Strategy() strategy1 = Strategy({'strategy': 'default_strategy'})
strategy1.init({'strategy': 'default_strategy'})
assert hasattr(strategy1.custom_strategy, 'minimal_roi') assert hasattr(strategy1.custom_strategy, 'minimal_roi')
assert strategy1.minimal_roi[0] == 0.04 assert strategy1.minimal_roi[0] == 0.04

View File

@ -1,6 +1,6 @@
# pragma pylint: disable=missing-docstring,C0103 # pragma pylint: disable=missing-docstring,C0103,protected-access
from freqtrade.main import refresh_whitelist, gen_pair_whitelist import freqtrade.tests.conftest as tt # test tools
# whitelist, blacklist, filtering, all of that will # whitelist, blacklist, filtering, all of that will
# eventually become some rules to run on a generic ACL engine # eventually become some rules to run on a generic ACL engine
@ -8,21 +8,22 @@ from freqtrade.main import refresh_whitelist, gen_pair_whitelist
def whitelist_conf(): def whitelist_conf():
return { config = tt.default_conf()
'stake_currency': 'BTC',
'exchange': { config['stake_currency'] = 'BTC'
'pair_whitelist': [ config['exchange']['pair_whitelist'] = [
'BTC_ETH', 'BTC_ETH',
'BTC_TKN', 'BTC_TKN',
'BTC_TRST', 'BTC_TRST',
'BTC_SWT', 'BTC_SWT',
'BTC_BCC' 'BTC_BCC'
], ]
'pair_blacklist': [
'BTC_BLK' config['exchange']['pair_blacklist'] = [
], 'BTC_BLK'
}, ]
}
return config
def get_market_summaries(): def get_market_summaries():
@ -86,11 +87,13 @@ def get_health_empty():
def test_refresh_market_pair_not_in_whitelist(mocker): def test_refresh_market_pair_not_in_whitelist(mocker):
conf = whitelist_conf() conf = whitelist_conf()
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch.multiple('freqtrade.main.exchange', freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
get_wallet_health=get_health)
refreshedwhitelist = refresh_whitelist( mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health)
conf['exchange']['pair_whitelist'] + ['BTC_XXX']) refreshedwhitelist = freqtradebot._refresh_whitelist(
conf['exchange']['pair_whitelist'] + ['BTC_XXX']
)
# List ordered by BaseVolume # List ordered by BaseVolume
whitelist = ['BTC_ETH', 'BTC_TKN'] whitelist = ['BTC_ETH', 'BTC_TKN']
# Ensure all except those in whitelist are removed # Ensure all except those in whitelist are removed
@ -99,10 +102,11 @@ def test_refresh_market_pair_not_in_whitelist(mocker):
def test_refresh_whitelist(mocker): def test_refresh_whitelist(mocker):
conf = whitelist_conf() conf = whitelist_conf()
mocker.patch.dict('freqtrade.main._CONF', conf) freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch.multiple('freqtrade.main.exchange',
get_wallet_health=get_health) mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health)
refreshedwhitelist = refresh_whitelist(conf['exchange']['pair_whitelist']) refreshedwhitelist = freqtradebot._refresh_whitelist(conf['exchange']['pair_whitelist'])
# List ordered by BaseVolume # List ordered by BaseVolume
whitelist = ['BTC_ETH', 'BTC_TKN'] whitelist = ['BTC_ETH', 'BTC_TKN']
# Ensure all except those in whitelist are removed # Ensure all except those in whitelist are removed
@ -111,26 +115,32 @@ def test_refresh_whitelist(mocker):
def test_refresh_whitelist_dynamic(mocker): def test_refresh_whitelist_dynamic(mocker):
conf = whitelist_conf() conf = whitelist_conf()
mocker.patch.dict('freqtrade.main._CONF', conf) freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple(
get_wallet_health=get_health) 'freqtrade.freqtradebot.exchange',
mocker.patch.multiple('freqtrade.main.exchange', get_wallet_health=get_health,
get_market_summaries=get_market_summaries) get_market_summaries=get_market_summaries
)
# argument: use the whitelist dynamically by exchange-volume # argument: use the whitelist dynamically by exchange-volume
whitelist = ['BTC_TKN', 'BTC_ETH'] whitelist = ['BTC_TKN', 'BTC_ETH']
refreshedwhitelist = refresh_whitelist(
gen_pair_whitelist(conf['stake_currency'])) refreshedwhitelist = freqtradebot._refresh_whitelist(
freqtradebot._gen_pair_whitelist(conf['stake_currency'])
)
assert whitelist == refreshedwhitelist assert whitelist == refreshedwhitelist
def test_refresh_whitelist_dynamic_empty(mocker): def test_refresh_whitelist_dynamic_empty(mocker):
conf = whitelist_conf() conf = whitelist_conf()
mocker.patch.dict('freqtrade.main._CONF', conf) freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health_empty)
get_wallet_health=get_health_empty)
# argument: use the whitelist dynamically by exchange-volume # argument: use the whitelist dynamically by exchange-volume
whitelist = [] whitelist = []
conf['exchange']['pair_whitelist'] = [] conf['exchange']['pair_whitelist'] = []
refresh_whitelist(whitelist) freqtradebot._refresh_whitelist(whitelist)
pairslist = conf['exchange']['pair_whitelist'] pairslist = conf['exchange']['pair_whitelist']
assert set(whitelist) == set(pairslist) assert set(whitelist) == set(pairslist)

View File

@ -1,16 +1,51 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
"""
Unit test file for analyse.py
"""
import datetime import datetime
import logging
from unittest.mock import MagicMock from unittest.mock import MagicMock
import arrow import arrow
import logging
from pandas import DataFrame from pandas import DataFrame
from freqtrade.analyze import Analyze, SignalType
from freqtrade.optimize.__init__ import load_tickerdata_file
from freqtrade.tests.conftest import log_has from freqtrade.tests.conftest import log_has
from freqtrade.analyze import (get_signal, parse_ticker_dataframe,
populate_buy_trend, populate_indicators, # Avoid to reinit the same object again and again
populate_sell_trend) _ANALYZE = Analyze({'strategy': 'default_strategy'})
from freqtrade.strategy.strategy import Strategy
def test_signaltype_object() -> None:
"""
Test the SignalType object has the mandatory Constants
:return: None
"""
assert hasattr(SignalType, 'BUY')
assert hasattr(SignalType, 'SELL')
def test_analyze_object() -> None:
"""
Test the Analyze object has the mandatory methods
:return: None
"""
assert hasattr(Analyze, 'parse_ticker_dataframe')
assert hasattr(Analyze, 'populate_indicators')
assert hasattr(Analyze, 'populate_buy_trend')
assert hasattr(Analyze, 'populate_sell_trend')
assert hasattr(Analyze, 'analyze_ticker')
assert hasattr(Analyze, 'get_signal')
assert hasattr(Analyze, 'should_sell')
assert hasattr(Analyze, 'min_roi_reached')
def test_dataframe_correct_length(result):
dataframe = Analyze.parse_ticker_dataframe(result)
assert len(result.index) == len(dataframe.index)
def test_dataframe_correct_columns(result): def test_dataframe_correct_columns(result):
@ -18,78 +53,88 @@ def test_dataframe_correct_columns(result):
['close', 'high', 'low', 'open', 'date', 'volume'] ['close', 'high', 'low', 'open', 'date', 'volume']
def test_dataframe_correct_length(result):
dataframe = parse_ticker_dataframe(result)
assert len(result.index) == len(dataframe.index)
def test_populates_buy_trend(result): def test_populates_buy_trend(result):
# Load the default strategy for the unit test, because this logic is done in main.py # Load the default strategy for the unit test, because this logic is done in main.py
Strategy().init({'strategy': 'default_strategy'}) dataframe = _ANALYZE.populate_buy_trend(_ANALYZE.populate_indicators(result))
dataframe = populate_buy_trend(populate_indicators(result))
assert 'buy' in dataframe.columns assert 'buy' in dataframe.columns
def test_populates_sell_trend(result): def test_populates_sell_trend(result):
# Load the default strategy for the unit test, because this logic is done in main.py # Load the default strategy for the unit test, because this logic is done in main.py
Strategy().init({'strategy': 'default_strategy'}) dataframe = _ANALYZE.populate_sell_trend(_ANALYZE.populate_indicators(result))
dataframe = populate_sell_trend(populate_indicators(result))
assert 'sell' in dataframe.columns assert 'sell' in dataframe.columns
def test_returns_latest_buy_signal(mocker): def test_returns_latest_buy_signal(mocker):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock()) mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
mocker.patch(
'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
)
assert get_signal('BTC-ETH', 5) == (True, False)
mocker.patch( mocker.patch.multiple(
'freqtrade.analyze.analyze_ticker', 'freqtrade.analyze.Analyze',
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}]) analyze_ticker=MagicMock(
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
)
) )
assert get_signal('BTC-ETH', 5) == (False, True) assert _ANALYZE.get_signal('BTC-ETH', 5) == (True, False)
mocker.patch.multiple(
'freqtrade.analyze.Analyze',
analyze_ticker=MagicMock(
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
)
)
assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, True)
def test_returns_latest_sell_signal(mocker): def test_returns_latest_sell_signal(mocker):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock()) mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
mocker.patch( mocker.patch.multiple(
'freqtrade.analyze.analyze_ticker', 'freqtrade.analyze.Analyze',
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}]) analyze_ticker=MagicMock(
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
)
) )
assert get_signal('BTC-ETH', 5) == (False, True)
mocker.patch( assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, True)
'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}]) mocker.patch.multiple(
'freqtrade.analyze.Analyze',
analyze_ticker=MagicMock(
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
)
) )
assert get_signal('BTC-ETH', 5) == (True, False) assert _ANALYZE.get_signal('BTC-ETH', 5) == (True, False)
def test_get_signal_empty(default_conf, mocker, caplog): def test_get_signal_empty(default_conf, mocker, caplog):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
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', int(default_conf['ticker_interval'])) assert (False, False) == _ANALYZE.get_signal('foo', int(default_conf['ticker_interval']))
assert log_has('Empty ticker history for pair foo', caplog.record_tuples) assert log_has('Empty ticker history for pair foo', caplog.record_tuples)
def test_get_signal_exception_valueerror(default_conf, mocker, caplog): def test_get_signal_exception_valueerror(default_conf, mocker, caplog):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
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.multiple(
side_effect=ValueError('xyz')) 'freqtrade.analyze.Analyze',
assert (False, False) == get_signal('foo', int(default_conf['ticker_interval'])) analyze_ticker=MagicMock(
side_effect=ValueError('xyz')
)
)
assert (False, False) == _ANALYZE.get_signal('foo', int(default_conf['ticker_interval']))
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples) assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples)
def test_get_signal_empty_dataframe(default_conf, mocker, caplog): def test_get_signal_empty_dataframe(default_conf, mocker, caplog):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
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.multiple(
assert (False, False) == get_signal('xyz', int(default_conf['ticker_interval'])) 'freqtrade.analyze.Analyze',
analyze_ticker=MagicMock(
return_value=DataFrame([])
)
)
assert (False, False) == _ANALYZE.get_signal('xyz', int(default_conf['ticker_interval']))
assert log_has('Empty dataframe for pair xyz', caplog.record_tuples) assert log_has('Empty dataframe for pair xyz', caplog.record_tuples)
@ -99,27 +144,51 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog):
# 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.multiple(
assert (False, False) == get_signal('xyz', int(default_conf['ticker_interval'])) 'freqtrade.analyze.Analyze',
assert log_has('Outdated history for pair xyz. Last tick is 11 minutes old', analyze_ticker=MagicMock(
caplog.record_tuples) return_value=DataFrame(ticks)
)
)
assert (False, False) == _ANALYZE.get_signal('xyz', int(default_conf['ticker_interval']))
assert log_has(
'Outdated history for pair xyz. Last tick is 11 minutes old',
caplog.record_tuples
)
def test_get_signal_handles_exceptions(mocker): def test_get_signal_handles_exceptions(mocker):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock()) mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
mocker.patch('freqtrade.analyze.analyze_ticker', mocker.patch.multiple(
side_effect=Exception('invalid ticker history ')) 'freqtrade.analyze.Analyze',
analyze_ticker=MagicMock(
side_effect=Exception('invalid ticker history ')
)
)
assert get_signal('BTC-ETH', 5) == (False, False) assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, False)
def test_parse_ticker_dataframe(ticker_history, ticker_history_without_bv): def test_parse_ticker_dataframe(ticker_history, ticker_history_without_bv):
columns = ['close', 'high', 'low', 'open', 'date', 'volume'] columns = ['close', 'high', 'low', 'open', 'date', 'volume']
# Test file with BV data # Test file with BV data
dataframe = parse_ticker_dataframe(ticker_history) dataframe = Analyze.parse_ticker_dataframe(ticker_history)
assert dataframe.columns.tolist() == columns assert dataframe.columns.tolist() == columns
# Test file without BV data # Test file without BV data
dataframe = parse_ticker_dataframe(ticker_history_without_bv) dataframe = Analyze.parse_ticker_dataframe(ticker_history_without_bv)
assert dataframe.columns.tolist() == columns assert dataframe.columns.tolist() == columns
def test_tickerdata_to_dataframe(default_conf) -> None:
"""
Test Analyze.tickerdata_to_dataframe() method
"""
analyze = Analyze(default_conf)
timerange = ((None, 'line'), None, -100)
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
tickerlist = {'BTC_UNITEST': tick}
data = analyze.tickerdata_to_dataframe(tickerlist)
assert len(data['BTC_UNITEST']) == 100

View File

@ -0,0 +1,134 @@
# pragma pylint: disable=missing-docstring, C0103
"""
Unit test file for arguments.py
"""
import argparse
import logging
import pytest
from freqtrade.arguments import Arguments
def test_arguments_object() -> None:
"""
Test the Arguments object has the mandatory methods
:return: None
"""
assert hasattr(Arguments, 'get_parsed_arg')
assert hasattr(Arguments, 'parse_args')
assert hasattr(Arguments, 'parse_timerange')
assert hasattr(Arguments, 'scripts_options')
# Parse common command-line-arguments. Used for all tools
def test_parse_args_none() -> None:
arguments = Arguments([], '')
assert isinstance(arguments, Arguments)
assert isinstance(arguments.parser, argparse.ArgumentParser)
assert isinstance(arguments.parser, argparse.ArgumentParser)
def test_parse_args_defaults() -> None:
args = Arguments([], '').get_parsed_arg()
assert args.config == 'config.json'
assert args.dynamic_whitelist is None
assert args.loglevel == logging.INFO
def test_parse_args_config() -> None:
args = Arguments(['-c', '/dev/null'], '').get_parsed_arg()
assert args.config == '/dev/null'
args = Arguments(['--config', '/dev/null'], '').get_parsed_arg()
assert args.config == '/dev/null'
def test_parse_args_verbose() -> None:
args = Arguments(['-v'], '').get_parsed_arg()
assert args.loglevel == logging.DEBUG
args = Arguments(['--verbose'], '').get_parsed_arg()
assert args.loglevel == logging.DEBUG
def test_scripts_options() -> None:
arguments = Arguments(['-p', 'BTC_ETH'], '')
arguments.scripts_options()
args = arguments.get_parsed_arg()
assert args.pair == 'BTC_ETH'
def test_parse_args_version() -> None:
with pytest.raises(SystemExit, match=r'0'):
Arguments(['--version'], '').get_parsed_arg()
def test_parse_args_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'):
Arguments(['-c'], '').get_parsed_arg()
def test_parse_args_dynamic_whitelist() -> None:
args = Arguments(['--dynamic-whitelist'], '').get_parsed_arg()
assert args.dynamic_whitelist == 20
def test_parse_args_dynamic_whitelist_10() -> None:
args = Arguments(['--dynamic-whitelist', '10'], '').get_parsed_arg()
assert args.dynamic_whitelist == 10
def test_parse_args_dynamic_whitelist_invalid_values() -> None:
with pytest.raises(SystemExit, match=r'2'):
Arguments(['--dynamic-whitelist', 'abc'], '').get_parsed_arg()
def test_parse_timerange_incorrect() -> None:
assert ((None, 'line'), None, -200) == Arguments.parse_timerange('-200')
assert (('line', None), 200, None) == Arguments.parse_timerange('200-')
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
Arguments.parse_timerange('-')
def test_parse_args_backtesting_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'):
Arguments(['backtesting --ticker-interval'], '').get_parsed_arg()
with pytest.raises(SystemExit, match=r'2'):
Arguments(['backtesting --ticker-interval', 'abc'], '').get_parsed_arg()
def test_parse_args_backtesting_custom() -> None:
args = [
'-c', 'test_conf.json',
'backtesting',
'--live',
'--ticker-interval', '1',
'--refresh-pairs-cached']
call_args = Arguments(args, '').get_parsed_arg()
assert call_args.config == 'test_conf.json'
assert call_args.live is True
assert call_args.loglevel == logging.INFO
assert call_args.subparser == 'backtesting'
assert call_args.func is not None
assert call_args.ticker_interval == 1
assert call_args.refresh_pairs is True
def test_parse_args_hyperopt_custom() -> None:
args = [
'-c', 'test_conf.json',
'hyperopt',
'--epochs', '20',
'--spaces', 'buy'
]
call_args = Arguments(args, '').get_parsed_arg()
assert call_args.config == 'test_conf.json'
assert call_args.epochs == 20
assert call_args.loglevel == logging.INFO
assert call_args.subparser == 'hyperopt'
assert call_args.spaces == ['buy']
assert call_args.func is not None

View File

@ -0,0 +1,316 @@
# pragma pylint: disable=protected-access, invalid-name
"""
Unit test file for configuration.py
"""
import json
from copy import deepcopy
from unittest.mock import MagicMock
import pytest
from jsonschema import ValidationError
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.tests.conftest import log_has
def test_configuration_object() -> None:
"""
Test the Constants object has the mandatory Constants
"""
assert hasattr(Configuration, 'load_config')
assert hasattr(Configuration, '_load_config_file')
assert hasattr(Configuration, '_validate_config')
assert hasattr(Configuration, '_load_common_config')
assert hasattr(Configuration, '_load_backtesting_config')
assert hasattr(Configuration, '_load_hyperopt_config')
assert hasattr(Configuration, 'get_config')
def test_load_config_invalid_pair(default_conf, mocker) -> None:
"""
Test the configuration validator with an invalid PAIR format
"""
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'].append('BTC-ETH')
with pytest.raises(ValidationError, match=r'.*does not match.*'):
configuration = Configuration([])
configuration._validate_config(conf)
def test_load_config_missing_attributes(default_conf, mocker) -> None:
"""
Test the configuration validator with a missing attribute
"""
conf = deepcopy(default_conf)
conf.pop('exchange')
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
configuration = Configuration([])
configuration._validate_config(conf)
def test_load_config_file(default_conf, mocker, caplog) -> None:
"""
Test Configuration._load_config_file() method
"""
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
configuration = Configuration([])
validated_conf = configuration._load_config_file('somefile')
assert file_mock.call_count == 1
assert validated_conf.items() >= default_conf.items()
assert 'internals' in validated_conf
assert log_has('Validating configuration ...', caplog.record_tuples)
def test_load_config_file_exception(mocker, caplog) -> None:
"""
Test Configuration._load_config_file() method
"""
mocker.patch(
'freqtrade.configuration.open',
MagicMock(side_effect=FileNotFoundError('File not found'))
)
configuration = Configuration([])
with pytest.raises(SystemExit):
configuration._load_config_file('somefile')
assert log_has(
'Config file "somefile" not found. Please create your config file',
caplog.record_tuples
)
def test_load_config(default_conf, mocker) -> None:
"""
Test Configuration.load_config() without any cli params
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = Arguments([], '').get_parsed_arg()
configuration = Configuration(args)
validated_conf = configuration.load_config()
assert 'strategy' in validated_conf
assert validated_conf['strategy'] == 'default_strategy'
assert 'dynamic_whitelist' not in validated_conf
assert 'dry_run_db' not in validated_conf
def test_load_config_with_params(default_conf, mocker) -> None:
"""
Test Configuration.load_config() with cli params used
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'--dynamic-whitelist', '10',
'--strategy', 'test_strategy',
'--dry-run-db'
]
args = Arguments(args, '').get_parsed_arg()
configuration = Configuration(args)
validated_conf = configuration.load_config()
assert 'dynamic_whitelist' in validated_conf
assert validated_conf['dynamic_whitelist'] == 10
assert 'strategy' in validated_conf
assert validated_conf['strategy'] == 'test_strategy'
assert 'dry_run_db' in validated_conf
assert validated_conf['dry_run_db'] is True
def test_show_info(default_conf, mocker, caplog) -> None:
"""
Test Configuration.show_info()
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'--dynamic-whitelist', '10',
'--strategy', 'test_strategy',
'--dry-run-db'
]
args = Arguments(args, '').get_parsed_arg()
configuration = Configuration(args)
configuration.get_config()
assert log_has(
'Parameter --dynamic-whitelist detected. '
'Using dynamically generated whitelist. '
'(not applicable with Backtesting and Hyperopt)',
caplog.record_tuples
)
assert log_has(
'Parameter --dry-run-db detected ...',
caplog.record_tuples
)
assert log_has(
'Dry_run will use the DB file: "tradesv3.dry_run.sqlite"',
caplog.record_tuples
)
# Test the Dry run condition
configuration.config.update({'dry_run': False})
configuration._load_common_config(configuration.config)
assert log_has(
'Dry run is disabled. (--dry_run_db ignored)',
caplog.record_tuples
)
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'--config', 'config.json',
'--strategy', 'default_strategy',
'backtesting'
]
args = Arguments(args, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
assert 'max_open_trades' in config
assert 'stake_currency' in config
assert 'stake_amount' in config
assert 'exchange' in config
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
assert not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
assert 'live' not in config
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation' not in config
assert not log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
assert 'refresh_pairs' not in config
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
assert 'timerange' not in config
assert 'export' not in config
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'--config', 'config.json',
'--strategy', 'default_strategy',
'--datadir', '/foo/bar',
'backtesting',
'--ticker-interval', '1',
'--live',
'--realistic-simulation',
'--refresh-pairs-cached',
'--timerange', ':100',
'--export', '/bar/foo'
]
args = Arguments(args, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
assert 'max_open_trades' in config
assert 'stake_currency' in config
assert 'stake_amount' in config
assert 'exchange' in config
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
assert log_has(
'Using ticker_interval: 1 ...',
caplog.record_tuples
)
assert 'live' in config
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation'in config
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
assert 'refresh_pairs'in config
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
assert 'timerange' in config
assert log_has(
'Parameter --timerange detected: {} ...'.format(config['timerange']),
caplog.record_tuples
)
assert 'export' in config
assert log_has(
'Parameter --export detected: {} ...'.format(config['export']),
caplog.record_tuples
)
def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'hyperopt',
'--epochs', '10',
'--use-mongodb',
'--spaces', 'all',
]
args = Arguments(args, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
assert 'epochs' in config
assert int(config['epochs']) == 10
assert log_has('Parameter --epochs detected ...', caplog.record_tuples)
assert log_has('Will run Hyperopt with for 10 epochs ...', caplog.record_tuples)
assert 'mongodb' in config
assert config['mongodb'] is True
assert log_has('Parameter --use-mongodb detected ...', caplog.record_tuples)
assert 'spaces' in config
assert config['spaces'] == ['all']
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)

View File

@ -0,0 +1,26 @@
"""
Unit test file for constants.py
"""
from freqtrade.constants import Constants
def test_constant_object() -> None:
"""
Test the Constants object has the mandatory Constants
"""
assert hasattr(Constants, 'CONF_SCHEMA')
assert hasattr(Constants, 'DYNAMIC_WHITELIST')
assert hasattr(Constants, 'PROCESS_THROTTLE_SECS')
assert hasattr(Constants, 'TICKER_INTERVAL')
assert hasattr(Constants, 'HYPEROPT_EPOCH')
assert hasattr(Constants, 'RETRY_TIMEOUT')
assert hasattr(Constants, 'DEFAULT_STRATEGY')
def test_conf_schema() -> None:
"""
Test the CONF_SCHEMA is from the right type
"""
constant = Constants()
assert isinstance(constant.CONF_SCHEMA, dict)

View File

@ -1,30 +1,33 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
import pandas import pandas
import freqtrade.optimize
from freqtrade import analyze from freqtrade.analyze import Analyze
from freqtrade.optimize import load_data
from freqtrade.strategy.strategy import Strategy from freqtrade.strategy.strategy import Strategy
_pairs = ['BTC_ETH'] _pairs = ['BTC_ETH']
def load_dataframe_pair(pairs): def load_dataframe_pair(pairs):
ld = freqtrade.optimize.load_data(None, ticker_interval=5, pairs=pairs) ld = load_data(None, ticker_interval=5, pairs=pairs)
assert isinstance(ld, dict) assert isinstance(ld, dict)
assert isinstance(pairs[0], str) assert isinstance(pairs[0], str)
dataframe = ld[pairs[0]] dataframe = ld[pairs[0]]
analyze = Analyze({'strategy': 'default_strategy'})
dataframe = analyze.analyze_ticker(dataframe) dataframe = analyze.analyze_ticker(dataframe)
return dataframe return dataframe
def test_dataframe_load(): def test_dataframe_load():
Strategy().init({'strategy': 'default_strategy'}) Strategy({'strategy': 'default_strategy'})
dataframe = load_dataframe_pair(_pairs) dataframe = load_dataframe_pair(_pairs)
assert isinstance(dataframe, pandas.core.frame.DataFrame) assert isinstance(dataframe, pandas.core.frame.DataFrame)
def test_dataframe_columns_exists(): def test_dataframe_columns_exists():
Strategy().init({'strategy': 'default_strategy'}) Strategy({'strategy': 'default_strategy'})
dataframe = load_dataframe_pair(_pairs) dataframe = load_dataframe_pair(_pairs)
assert 'high' in dataframe.columns assert 'high' in dataframe.columns
assert 'low' in dataframe.columns assert 'low' in dataframe.columns

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
import pandas as pd import pandas as pd
from freqtrade.indicator_helpers import went_up, went_down from freqtrade.indicator_helpers import went_up, went_down

View File

@ -0,0 +1,97 @@
"""
Unit test file for logger.py
"""
import logging
from freqtrade.logger import Logger
def test_logger_object() -> None:
"""
Test the Constants object has the mandatory Constants
:return: None
"""
logger = Logger()
assert logger.name == ''
assert logger.level == 20
assert logger.level is logging.INFO
assert hasattr(logger, 'get_logger')
logger = Logger(name='Foo', level=logging.WARNING)
assert logger.name == 'Foo'
assert logger.name != ''
assert logger.level == 30
assert logger.level is logging.WARNING
def test_get_logger() -> None:
"""
Test Logger.get_logger() and Logger._init_logger()
:return: None
"""
logger = Logger(name='get_logger', level=logging.WARNING)
get_logger = logger.get_logger()
assert logger.logger is get_logger
assert get_logger is not None
assert hasattr(get_logger, 'debug')
assert hasattr(get_logger, 'info')
assert hasattr(get_logger, 'warning')
assert hasattr(get_logger, 'critical')
assert hasattr(get_logger, 'exception')
def test_set_name() -> None:
"""
Test Logger.set_name()
:return: None
"""
logger = Logger(name='set_name')
assert logger.name == 'set_name'
logger.set_name('set_name_new')
assert logger.name == 'set_name_new'
def test_set_level() -> None:
"""
Test Logger.set_name()
:return: None
"""
logger = Logger(name='Foo', level=logging.WARNING)
assert logger.level == logging.WARNING
assert logger.get_logger().level == logging.WARNING
logger.set_level(logging.INFO)
assert logger.level == logging.INFO
assert logger.get_logger().level == logging.INFO
def test_sending_msg(caplog) -> None:
"""
Test send a logging message
:return: None
"""
logger = Logger(name='sending_msg', level=logging.WARNING).get_logger()
logger.info('I am an INFO message')
assert('sending_msg', logging.INFO, 'I am an INFO message') not in caplog.record_tuples
logger.warning('I am an WARNING message')
assert ('sending_msg', logging.WARNING, 'I am an WARNING message') in caplog.record_tuples
def test_set_format(caplog) -> None:
"""
Test Logger.set_format()
:return: None
"""
log = Logger(name='set_format')
logger = log.get_logger()
logger.info('I am the first message')
assert ('set_format', logging.INFO, 'I am the first message') in caplog.record_tuples
log.set_format(log_format='%(message)s', propagate=True)
logger.info('I am the second message')
assert ('set_format', logging.INFO, 'I am the second message') in caplog.record_tuples

View File

@ -1,30 +1,23 @@
# pragma pylint: disable=missing-docstring, C0103 """
import copy Unit test file for main.py
"""
import logging import logging
from unittest.mock import MagicMock from unittest.mock import MagicMock
import arrow
import pytest import pytest
import requests
from sqlalchemy import create_engine
import freqtrade.main as main from freqtrade.main import main, set_loggers
from freqtrade import DependencyException, OperationalException
from freqtrade.exchange import Exchanges
from freqtrade.main import (_process, check_handle_timedout, create_trade,
execute_sell, get_target_bid, handle_trade, init)
from freqtrade.misc import State, get_state
from freqtrade.persistence import Trade
from freqtrade.tests.conftest import log_has from freqtrade.tests.conftest import log_has
def test_parse_args_backtesting(mocker): def test_parse_args_backtesting(mocker) -> None:
""" Test that main() can start backtesting or hyperopt. """
and also ensure we can pass some specific arguments Test that main() can start backtesting and also ensure we can pass some specific arguments
further argument parsing is done in test_misc.py """ further argument parsing is done in test_arguments.py
backtesting_mock = mocker.patch( """
'freqtrade.optimize.backtesting.start', MagicMock()) backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock())
main.main(['backtesting']) main(['backtesting'])
assert backtesting_mock.call_count == 1 assert backtesting_mock.call_count == 1
call_args = backtesting_mock.call_args[0][0] call_args = backtesting_mock.call_args[0][0]
assert call_args.config == 'config.json' assert call_args.config == 'config.json'
@ -35,10 +28,12 @@ def test_parse_args_backtesting(mocker):
assert call_args.ticker_interval is None assert call_args.ticker_interval is None
def test_main_start_hyperopt(mocker): def test_main_start_hyperopt(mocker) -> None:
hyperopt_mock = mocker.patch( """
'freqtrade.optimize.hyperopt.start', MagicMock()) Test that main() can start hyperopt
main.main(['hyperopt']) """
hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock())
main(['hyperopt'])
assert hyperopt_mock.call_count == 1 assert hyperopt_mock.call_count == 1
call_args = hyperopt_mock.call_args[0][0] call_args = hyperopt_mock.call_args[0][0]
assert call_args.config == 'config.json' assert call_args.config == 'config.json'
@ -47,793 +42,52 @@ def test_main_start_hyperopt(mocker):
assert call_args.func is not None assert call_args.func is not None
def test_process_maybe_execute_buy(default_conf, mocker): def test_set_loggers() -> None:
mocker.patch.dict('freqtrade.main._CONF', default_conf) """
mocker.patch('freqtrade.main.create_trade', return_value=True) Test set_loggers() update the logger level for third-party libraries
assert main.process_maybe_execute_buy(int(default_conf['ticker_interval'])) """
mocker.patch('freqtrade.main.create_trade', return_value=False) previous_value1 = logging.getLogger('requests.packages.urllib3').level
assert not main.process_maybe_execute_buy(int(default_conf['ticker_interval'])) previous_value2 = logging.getLogger('telegram').level
set_loggers()
value1 = logging.getLogger('requests.packages.urllib3').level
assert previous_value1 is not value1
assert value1 is logging.INFO
value2 = logging.getLogger('telegram').level
assert previous_value2 is not value2
assert value2 is logging.INFO
def test_process_maybe_execute_sell(default_conf, mocker): def test_main(mocker, caplog) -> None:
mocker.patch.dict('freqtrade.main._CONF', default_conf) """
mocker.patch('freqtrade.main.handle_trade', return_value=True) Test main() function
mocker.patch('freqtrade.exchange.get_order', return_value=1) In this test we are skipping the while True loop by throwing an exception.
trade = MagicMock() """
trade.open_order_id = '123' mocker.patch.multiple(
assert not main.process_maybe_execute_sell(trade, int(default_conf['ticker_interval'])) 'freqtrade.freqtradebot.FreqtradeBot',
trade.is_open = True _init_modules=MagicMock(),
trade.open_order_id = None worker=MagicMock(
# Assert we call handle_trade() if trade is feasible for execution side_effect=KeyboardInterrupt
assert main.process_maybe_execute_sell(trade, int(default_conf['ticker_interval'])) ),
clean=MagicMock(),
def test_process_maybe_execute_buy_exception(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.create_trade', MagicMock(side_effect=DependencyException))
main.process_maybe_execute_buy(int(default_conf['ticker_interval']))
assert log_has('Unable to create trade: ', caplog.record_tuples)
def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(return_value='mocked_limit_buy'),
get_order=MagicMock(return_value=limit_buy_order))
init(default_conf, create_engine('sqlite://'))
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert not trades
result = _process(interval=int(default_conf['ticker_interval']))
assert result is True
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 1
trade = trades[0]
assert trade is not None
assert trade.stake_amount == default_conf['stake_amount']
assert trade.is_open
assert trade.open_date is not None
assert trade.exchange == Exchanges.BITTREX.name
assert trade.open_rate == 0.00001099
assert trade.amount == 90.99181073703367
def test_process_exchange_failures(default_conf, ticker, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(side_effect=requests.exceptions.RequestException))
init(default_conf, create_engine('sqlite://'))
result = _process(interval=int(default_conf['ticker_interval']))
assert result is False
assert sleep_mock.has_calls()
def test_process_operational_exception(default_conf, ticker, health, mocker):
msg_mock = MagicMock()
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(side_effect=OperationalException))
init(default_conf, create_engine('sqlite://'))
assert get_state() == State.RUNNING
result = _process(interval=int(default_conf['ticker_interval']))
assert result is False
assert get_state() == State.STOPPED
assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]
def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(return_value='mocked_limit_buy'),
get_order=MagicMock(return_value=limit_buy_order))
init(default_conf, create_engine('sqlite://'))
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert not trades
result = _process(interval=int(default_conf['ticker_interval']))
assert result is True
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 1
result = _process(interval=int(default_conf['ticker_interval']))
assert result is False
def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
# Save state of current whitelist
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
init(default_conf, create_engine('sqlite://'))
create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first()
assert trade is not None
assert trade.stake_amount == 0.001
assert trade.is_open
assert trade.open_date is not None
assert trade.exchange == Exchanges.BITTREX.name
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
assert trade.open_rate == 0.00001099
assert trade.amount == 90.99181073
assert whitelist == default_conf['exchange']['pair_whitelist']
def test_create_trade_minimal_amount(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
buy_mock = mocker.patch(
'freqtrade.main.exchange.buy', MagicMock(return_value='mocked_limit_buy')
) )
mocker.patch.multiple('freqtrade.main.exchange', args = ['-c', 'config.json.example']
validate_pairs=MagicMock(),
get_ticker=ticker)
init(default_conf, create_engine('sqlite://'))
min_stake_amount = 0.0005
create_trade(min_stake_amount, int(default_conf['ticker_interval']))
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
assert rate * amount >= min_stake_amount
# Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit) as pytest_wrapped_e:
main(args)
log_has('Starting freqtrade', caplog.record_tuples)
log_has('Got SIGINT, aborting ...', caplog.record_tuples)
assert pytest_wrapped_e.type == SystemExit
assert pytest_wrapped_e.value.code == 42
def test_create_trade_no_stake_amount(default_conf, ticker, mocker): # Test the BaseException case
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch(
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) 'freqtrade.freqtradebot.FreqtradeBot.worker',
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) MagicMock(side_effect=BaseException)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'),
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
create_trade(default_conf['stake_amount'], int(default_conf['ticker_interval']))
def test_create_trade_no_pairs(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
with pytest.raises(DependencyException, match=r'.*No currency pairs in whitelist.*'):
conf = copy.deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = []
mocker.patch.dict('freqtrade.main._CONF', conf)
create_trade(default_conf['stake_amount'], int(default_conf['ticker_interval']))
def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
with pytest.raises(DependencyException, match=r'.*No currency pairs in whitelist.*'):
conf = copy.deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ["BTC_ETH"]
conf['exchange']['pair_blacklist'] = ["BTC_ETH"]
mocker.patch.dict('freqtrade.main._CONF', conf)
create_trade(default_conf['stake_amount'], int(default_conf['ticker_interval']))
def test_create_trade_no_signal(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', MagicMock(return_value=(False, False)))
mocker.patch.multiple('freqtrade.exchange',
get_ticker_history=MagicMock(return_value=20))
mocker.patch.multiple('freqtrade.main.exchange',
get_balance=MagicMock(return_value=20))
stake_amount = 10
Trade.query = MagicMock()
Trade.query.filter = MagicMock()
assert not create_trade(stake_amount, int(default_conf['ticker_interval']))
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00001172,
'ask': 0.00001173,
'last': 0.00001172
}),
buy=MagicMock(return_value='mocked_limit_buy'),
sell=MagicMock(return_value='mocked_limit_sell'))
mocker.patch.multiple('freqtrade.fiat_convert.Market',
ticker=MagicMock(return_value={'price_usd': 15000.0}))
init(default_conf, create_engine('sqlite://'))
create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first()
assert trade
trade.update(limit_buy_order)
assert trade.is_open is True
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
assert handle_trade(trade, int(default_conf['ticker_interval'])) is True
assert trade.open_order_id == 'mocked_limit_sell'
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
assert trade.close_rate == 0.00001173
assert trade.close_profit == 0.06201057
assert trade.calc_profit() == 0.00006217
assert trade.close_date is not None
def test_handle_overlpapping_signals(default_conf, ticker, mocker):
default_conf.update({'experimental': {'use_sell_signal': True}})
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, True))
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
init(default_conf, create_engine('sqlite://'))
create_trade(0.001, int(default_conf['ticker_interval']))
# Buy and Sell triggering, so doing nothing ...
trades = Trade.query.all()
nb_trades = len(trades)
assert nb_trades == 0
# Buy is triggering, so buying ...
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
create_trade(0.001, int(default_conf['ticker_interval']))
trades = Trade.query.all()
nb_trades = len(trades)
assert nb_trades == 1
assert trades[0].is_open is True
# Buy and Sell are not triggering, so doing nothing ...
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, False))
assert handle_trade(trades[0], int(default_conf['ticker_interval'])) is False
trades = Trade.query.all()
nb_trades = len(trades)
assert nb_trades == 1
assert trades[0].is_open is True
# Buy and Sell are triggering, so doing nothing ...
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, True))
assert handle_trade(trades[0], int(default_conf['ticker_interval'])) is False
trades = Trade.query.all()
nb_trades = len(trades)
assert nb_trades == 1
assert trades[0].is_open is True
# Sell is triggering, guess what : we are Selling!
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
trades = Trade.query.all()
assert handle_trade(trades[0], int(default_conf['ticker_interval'])) is True
def test_handle_trade_roi(default_conf, ticker, mocker, caplog):
caplog.set_level(logging.DEBUG)
default_conf.update({'experimental': {'use_sell_signal': True}})
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
mocker.patch('freqtrade.main.min_roi_reached', return_value=True)
init(default_conf, create_engine('sqlite://'))
create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first()
trade.is_open = True
# FIX: sniffing logs, suggest handle_trade should not execute_sell
# instead that responsibility should be moved out of handle_trade(),
# we might just want to check if we are in a sell condition without
# executing
# if ROI is reached we must sell
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
assert handle_trade(trade, interval=int(default_conf['ticker_interval']))
assert log_has('Required profit reached. Selling..', caplog.record_tuples)
# if ROI is reached we must sell even if sell-signal is not signalled
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
assert handle_trade(trade, interval=int(default_conf['ticker_interval']))
assert log_has('Required profit reached. Selling..', caplog.record_tuples)
def test_handle_trade_experimental(default_conf, ticker, mocker, caplog):
caplog.set_level(logging.DEBUG)
default_conf.update({'experimental': {'use_sell_signal': True}})
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
init(default_conf, create_engine('sqlite://'))
create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first()
trade.is_open = True
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, False))
value_returned = handle_trade(trade, int(default_conf['ticker_interval']))
assert value_returned is False
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
assert handle_trade(trade, int(default_conf['ticker_interval']))
assert log_has('Sell signal received. Selling..', caplog.record_tuples)
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
# Create trade and sell it
init(default_conf, create_engine('sqlite://'))
create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first()
assert trade
trade.update(limit_buy_order)
trade.update(limit_sell_order)
assert trade.is_open is False
with pytest.raises(ValueError, match=r'.*closed trade.*'):
handle_trade(trade, int(default_conf['ticker_interval']))
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock()
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,
get_order=MagicMock(return_value=limit_buy_order_old),
cancel_order=cancel_order_mock)
init(default_conf, create_engine('sqlite://'))
trade_buy = Trade(
pair='BTC_ETH',
open_rate=0.00001099,
exchange='BITTREX',
open_order_id='123456789',
amount=90.99181073,
fee=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=True
) )
with pytest.raises(SystemExit):
Trade.session.add(trade_buy) main(args)
log_has('Got fatal exception!', caplog.record_tuples)
# check it does cancel buy orders over the time limit
check_handle_timedout(600)
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
nb_trades = len(trades)
assert nb_trades == 0
def test_handle_timedout_limit_buy(mocker):
cancel_order = MagicMock()
mocker.patch('freqtrade.exchange.cancel_order', cancel_order)
Trade.session = MagicMock()
trade = MagicMock()
order = {'remaining': 1,
'amount': 1}
assert main.handle_timedout_limit_buy(trade, order)
assert cancel_order.call_count == 1
order['amount'] = 2
assert not main.handle_timedout_limit_buy(trade, order)
assert cancel_order.call_count == 2
def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock()
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,
get_order=MagicMock(return_value=limit_sell_order_old),
cancel_order=cancel_order_mock)
init(default_conf, create_engine('sqlite://'))
trade_sell = Trade(
pair='BTC_ETH',
open_rate=0.00001099,
exchange='BITTREX',
open_order_id='123456789',
amount=90.99181073,
fee=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(hours=-5).datetime,
close_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=False
)
Trade.session.add(trade_sell)
# check it does cancel sell orders over the time limit
check_handle_timedout(600)
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
assert trade_sell.is_open is True
def test_handle_timedout_limit_sell(mocker):
cancel_order = MagicMock()
mocker.patch('freqtrade.exchange.cancel_order', cancel_order)
trade = MagicMock()
order = {'remaining': 1,
'amount': 1}
assert main.handle_timedout_limit_sell(trade, order)
assert cancel_order.call_count == 1
order['amount'] = 2
assert not main.handle_timedout_limit_sell(trade, order)
# Assert cancel_order was not called (callcount remains unchanged)
assert cancel_order.call_count == 1
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock()
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,
get_order=MagicMock(return_value=limit_buy_order_old_partial),
cancel_order=cancel_order_mock)
init(default_conf, create_engine('sqlite://'))
trade_buy = Trade(
pair='BTC_ETH',
open_rate=0.00001099,
exchange='BITTREX',
open_order_id='123456789',
amount=90.99181073,
fee=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=True
)
Trade.session.add(trade_buy)
# check it does cancel buy orders over the time limit
# note this is for a partially-complete buy order
check_handle_timedout(600)
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
assert len(trades) == 1
assert trades[0].amount == 23.0
assert trades[0].stake_amount == trade_buy.open_rate * trades[0].amount
def test_balance_fully_ask_side(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}})
assert get_target_bid({'ask': 20, 'last': 10}) == 20
def test_balance_fully_last_side(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
assert get_target_bid({'ask': 20, 'last': 10}) == 10
def test_balance_bigger_last_ask(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
assert get_target_bid({'ask': 5, 'last': 10}) == 5
def test_execute_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, t: (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)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
init(default_conf, create_engine('sqlite://'))
# Create some test data
create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first()
assert trade
# Increase the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up)
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
assert rpc_mock.call_count == 2
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
assert 'Profit' in rpc_mock.call_args_list[-1][0][0]
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0]
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch('freqtrade.rpc.init', MagicMock())
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
init(default_conf, create_engine('sqlite://'))
# Create some test data
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)
execute_sell(trade=trade, limit=ticker_sell_down()['bid'])
assert rpc_mock.call_count == 2
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
assert 'Amount' 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]
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
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('freqtrade.main.get_signal', side_effect=lambda s, t: (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()
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'])
assert rpc_mock.call_count == 2
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
assert '[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, t: (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()
assert trade
# Increase the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up)
mocker.patch('freqtrade.main._CONF', {})
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
assert rpc_mock.call_count == 2
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
assert '(profit: 6.11%, 0.00006126)' in rpc_mock.call_args_list[-1][0][0]
assert 'USD' not in rpc_mock.call_args_list[-1][0][0]
def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker):
default_conf['experimental'] = {
'use_sell_signal': True,
'sell_profit_only': True,
}
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00002172,
'ask': 0.00002173,
'last': 0.00002172
}),
buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://'))
create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first()
trade.update(limit_buy_order)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, 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):
default_conf['experimental'] = {
'use_sell_signal': True,
'sell_profit_only': False,
}
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00002172,
'ask': 0.00002173,
'last': 0.00002172
}),
buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://'))
create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first()
trade.update(limit_buy_order)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, 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):
default_conf['experimental'] = {
'use_sell_signal': True,
'sell_profit_only': True,
}
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00000172,
'ask': 0.00000173,
'last': 0.00000172
}),
buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://'))
create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first()
trade.update(limit_buy_order)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
assert handle_trade(trade, int(default_conf['ticker_interval'])) is False
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker):
default_conf['experimental'] = {
'use_sell_signal': True,
'sell_profit_only': False,
}
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00000172,
'ask': 0.00000173,
'last': 0.00000172
}),
buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://'))
create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first()
trade.update(limit_buy_order)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
assert handle_trade(trade, int(default_conf['ticker_interval'])) is True

View File

@ -1,188 +1,34 @@
# pragma pylint: disable=missing-docstring,C0103 # pragma pylint: disable=missing-docstring,C0103
import argparse
import json """
import time Unit test file for misc.py
from copy import deepcopy """
from unittest.mock import MagicMock
import datetime import datetime
import pytest from unittest.mock import MagicMock
from jsonschema import ValidationError
from freqtrade.analyze import parse_ticker_dataframe from freqtrade.analyze import Analyze
from freqtrade.misc import (common_args_parser, file_dump_json, load_config, from freqtrade.misc import (shorten_date, datesarray_to_datetimearray,
parse_args, parse_timerange, throttle, datesarray_to_datetimearray) common_datearray, file_dump_json)
from freqtrade.optimize.__init__ import load_tickerdata_file
def test_throttle(): def test_shorten_date() -> None:
"""
def func(): Test shorten_date() function
return 42 :return: None
"""
start = time.time() str_data = '1 day, 2 hours, 3 minutes, 4 seconds ago'
result = throttle(func, min_secs=0.1) str_shorten_data = '1 d, 2 h, 3 min, 4 sec ago'
end = time.time() assert shorten_date(str_data) == str_shorten_data
assert result == 42
assert end - start > 0.1
result = throttle(func, min_secs=-1)
assert result == 42
def test_throttle_with_assets():
def func(nb_assets=-1):
return nb_assets
result = throttle(func, min_secs=0.1, nb_assets=666)
assert result == 666
result = throttle(func, min_secs=0.1)
assert result == -1
# Parse common command-line-arguments. Used for all tools
def test_parse_args_none():
args = common_args_parser('')
assert isinstance(args, argparse.ArgumentParser)
def test_parse_args_defaults():
args = parse_args([], '')
assert args.config == 'config.json'
assert args.dynamic_whitelist is None
assert args.loglevel == 20
def test_parse_args_config():
args = parse_args(['-c', '/dev/null'], '')
assert args.config == '/dev/null'
args = parse_args(['--config', '/dev/null'], '')
assert args.config == '/dev/null'
def test_parse_args_verbose():
args = parse_args(['-v'], '')
assert args.loglevel == 10
args = parse_args(['--verbose'], '')
assert args.loglevel == 10
def test_parse_args_version():
with pytest.raises(SystemExit, match=r'0'):
parse_args(['--version'], '')
def test_parse_args_invalid():
with pytest.raises(SystemExit, match=r'2'):
parse_args(['-c'], '')
# Parse command-line-arguments
# used for main, backtesting and hyperopt
def test_parse_args_dynamic_whitelist():
args = parse_args(['--dynamic-whitelist'], '')
assert args.dynamic_whitelist == 20
def test_parse_args_dynamic_whitelist_10():
args = parse_args(['--dynamic-whitelist', '10'], '')
assert args.dynamic_whitelist == 10
def test_parse_args_dynamic_whitelist_invalid_values():
with pytest.raises(SystemExit, match=r'2'):
parse_args(['--dynamic-whitelist', 'abc'], '')
def test_parse_args_backtesting_invalid():
with pytest.raises(SystemExit, match=r'2'):
parse_args(['backtesting --ticker-interval'], '')
with pytest.raises(SystemExit, match=r'2'):
parse_args(['backtesting --ticker-interval', 'abc'], '')
def test_parse_args_backtesting_custom():
args = [
'-c', 'test_conf.json',
'backtesting',
'--live',
'--ticker-interval', '1',
'--refresh-pairs-cached']
call_args = parse_args(args, '')
assert call_args.config == 'test_conf.json'
assert call_args.live is True
assert call_args.loglevel == 20
assert call_args.subparser == 'backtesting'
assert call_args.func is not None
assert call_args.ticker_interval == 1
assert call_args.refresh_pairs is True
def test_parse_args_hyperopt_custom():
args = ['-c', 'test_conf.json', 'hyperopt', '--epochs', '20']
call_args = parse_args(args, '')
assert call_args.config == 'test_conf.json'
assert call_args.epochs == 20
assert call_args.loglevel == 20
assert call_args.subparser == 'hyperopt'
assert call_args.func is not None
def test_file_dump_json(mocker):
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
json_dump = mocker.patch('json.dump', MagicMock())
file_dump_json('somefile', [1, 2, 3])
assert file_open.call_count == 1
assert json_dump.call_count == 1
def test_parse_timerange_incorrect():
assert ((None, 'line'), None, -200) == parse_timerange('-200')
assert (('line', None), 200, None) == parse_timerange('200-')
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
parse_timerange('-')
def test_load_config(default_conf, mocker):
file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
validated_conf = load_config('somefile')
assert file_mock.call_count == 1
assert validated_conf.items() >= default_conf.items()
def test_load_config_invalid_pair(default_conf, mocker):
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'].append('BTC-ETH')
mocker.patch(
'freqtrade.misc.open',
mocker.mock_open(
read_data=json.dumps(conf)))
with pytest.raises(ValidationError, match=r'.*does not match.*'):
load_config('somefile')
def test_load_config_missing_attributes(default_conf, mocker):
conf = deepcopy(default_conf)
conf.pop('exchange')
mocker.patch(
'freqtrade.misc.open',
mocker.mock_open(
read_data=json.dumps(conf)))
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
load_config('somefile')
def test_datesarray_to_datetimearray(ticker_history): def test_datesarray_to_datetimearray(ticker_history):
dataframes = parse_ticker_dataframe(ticker_history) """
Test datesarray_to_datetimearray() function
:return: None
"""
dataframes = Analyze.parse_ticker_dataframe(ticker_history)
dates = datesarray_to_datetimearray(dataframes['date']) dates = datesarray_to_datetimearray(dataframes['date'])
assert isinstance(dates[0], datetime.datetime) assert isinstance(dates[0], datetime.datetime)
@ -194,3 +40,34 @@ def test_datesarray_to_datetimearray(ticker_history):
date_len = len(dates) date_len = len(dates)
assert date_len == 3 assert date_len == 3
def test_common_datearray(default_conf, mocker) -> None:
"""
Test common_datearray()
:return: None
"""
mocker.patch('freqtrade.strategy.strategy.Strategy', MagicMock())
analyze = Analyze(default_conf)
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
tickerlist = {'BTC_UNITEST': tick}
dataframes = analyze.tickerdata_to_dataframe(tickerlist)
dates = common_datearray(dataframes)
assert dates.size == dataframes['BTC_UNITEST']['date'].size
assert dates[0] == dataframes['BTC_UNITEST']['date'][0]
assert dates[-1] == dataframes['BTC_UNITEST']['date'][-1]
def test_file_dump_json(mocker) -> None:
"""
Test file_dump_json()
:return: None
"""
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
json_dump = mocker.patch('json.dump', MagicMock())
file_dump_json('somefile', [1, 2, 3])
assert file_open.call_count == 1
assert json_dump.call_count == 1

View File

@ -1,7 +1,9 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
import os import os
import pytest import pytest
from sqlalchemy import create_engine from sqlalchemy import create_engine
from freqtrade.exchange import Exchanges from freqtrade.exchange import Exchanges
from freqtrade.persistence import Trade, init, clean_dry_run_db from freqtrade.persistence import Trade, init, clean_dry_run_db

View File

@ -0,0 +1,14 @@
"""
Unit test file for constants.py
"""
from freqtrade.state import State
def test_state_object() -> None:
"""
Test the State object has the mandatory states
:return: None
"""
assert hasattr(State, 'RUNNING')
assert hasattr(State, 'STOPPED')

View File

@ -1,12 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""This script generate json data from bittrex""" """This script generate json data from bittrex"""
import sys
import json import json
import sys
from freqtrade import exchange from freqtrade import exchange
from freqtrade.exchange import Bittrex
from freqtrade import misc from freqtrade import misc
from freqtrade.exchange import Bittrex
parser = misc.common_args_parser('download utility') parser = misc.common_args_parser('download utility')
parser.add_argument( parser.add_argument(

View File

@ -1,41 +1,55 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""
Script to display when the bot will buy a specific pair
Mandatory Cli parameters:
-p / --pair: pair to examine
Optional Cli parameters
-s / --strategy: strategy to use
-d / --datadir: path to pair backtest data
--timerange: specify what timerange of data to use.
-l / --live: Live, to download the latest ticker for the pair
"""
import sys import sys
import logging from argparse import Namespace
from typing import List
from plotly import tools from plotly import tools
from plotly.offline import plot from plotly.offline import plot
import plotly.graph_objs as go import plotly.graph_objs as go
from freqtrade import exchange, analyze from freqtrade.arguments import Arguments
from freqtrade.strategy.strategy import Strategy from freqtrade.analyze import Analyze
import freqtrade.misc as misc from freqtrade import exchange
from freqtrade.logger import Logger
import freqtrade.optimize as optimize import freqtrade.optimize as optimize
logger = logging.getLogger(__name__) logger = Logger(name="Graph dataframe").get_logger()
def plot_parse_args(args): def plot_analyzed_dataframe(args: Namespace) -> None:
parser = misc.common_args_parser('Graph dataframe')
misc.backtesting_options(parser)
misc.scripts_options(parser)
return parser.parse_args(args)
def plot_analyzed_dataframe(args) -> None:
""" """
Calls analyze() and plots the returned dataframe Calls analyze() and plots the returned dataframe
:param pair: pair as str
:return: None :return: None
""" """
pair = args.pair.replace('-', '_') pair = args.pair.replace('-', '_')
timerange = misc.parse_timerange(args.timerange) timerange = Arguments.parse_timerange(args.timerange)
# Init strategy # Init strategy
strategy = Strategy() try:
strategy.init({'strategy': args.strategy}) analyze = Analyze({'strategy': args.strategy})
tick_interval = strategy.ticker_interval except AttributeError:
logger.critical(
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
args.strategy
)
exit()
tick_interval = analyze.strategy.ticker_interval
tickers = {} tickers = {}
if args.live: if args.live:
@ -44,27 +58,32 @@ def plot_analyzed_dataframe(args) -> None:
exchange._API = exchange.Bittrex({'key': '', 'secret': ''}) exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
tickers[pair] = exchange.get_ticker_history(pair, tick_interval) tickers[pair] = exchange.get_ticker_history(pair, tick_interval)
else: else:
tickers = optimize.load_data(args.datadir, pairs=[pair], tickers = optimize.load_data(
ticker_interval=tick_interval, datadir=args.datadir,
refresh_pairs=False, pairs=[pair],
timerange=timerange) ticker_interval=tick_interval,
dataframes = optimize.tickerdata_to_dataframe(tickers) refresh_pairs=False,
timerange=timerange
)
dataframes = analyze.tickerdata_to_dataframe(tickers)
dataframe = dataframes[pair] dataframe = dataframes[pair]
dataframe = analyze.populate_buy_trend(dataframe) dataframe = analyze.populate_buy_trend(dataframe)
dataframe = analyze.populate_sell_trend(dataframe) dataframe = analyze.populate_sell_trend(dataframe)
if (len(dataframe.index) > 750): if len(dataframe.index) > 750:
logger.warn('Ticker contained more than 750 candles, clipping.') logger.warning('Ticker contained more than 750 candles, clipping.')
df = dataframe.tail(750) data = dataframe.tail(750)
candles = go.Candlestick(x=df.date, candles = go.Candlestick(
open=df.open, x=data.date,
high=df.high, open=data.open,
low=df.low, high=data.high,
close=df.close, low=data.low,
name='Price') close=data.close,
name='Price'
)
df_buy = df[df['buy'] == 1] df_buy = data[data['buy'] == 1]
buys = go.Scattergl( buys = go.Scattergl(
x=df_buy.date, x=df_buy.date,
y=df_buy.close, y=df_buy.close,
@ -73,13 +92,11 @@ def plot_analyzed_dataframe(args) -> None:
marker=dict( marker=dict(
symbol='triangle-up-dot', symbol='triangle-up-dot',
size=9, size=9,
line=dict( line=dict(width=1),
width=1,
),
color='green', color='green',
) )
) )
df_sell = df[df['sell'] == 1] df_sell = data[data['sell'] == 1]
sells = go.Scattergl( sells = go.Scattergl(
x=df_sell.date, x=df_sell.date,
y=df_sell.close, y=df_sell.close,
@ -88,30 +105,28 @@ def plot_analyzed_dataframe(args) -> None:
marker=dict( marker=dict(
symbol='triangle-down-dot', symbol='triangle-down-dot',
size=9, size=9,
line=dict( line=dict(width=1),
width=1,
),
color='red', color='red',
) )
) )
bb_lower = go.Scatter( bb_lower = go.Scatter(
x=df.date, x=data.date,
y=df.bb_lowerband, y=data.bb_lowerband,
name='BB lower', name='BB lower',
line={'color': "transparent"}, line={'color': "transparent"},
) )
bb_upper = go.Scatter( bb_upper = go.Scatter(
x=df.date, x=data.date,
y=df.bb_upperband, y=data.bb_upperband,
name='BB upper', name='BB upper',
fill="tonexty", fill="tonexty",
fillcolor="rgba(0,176,246,0.2)", fillcolor="rgba(0,176,246,0.2)",
line={'color': "transparent"}, line={'color': "transparent"},
) )
macd = go.Scattergl(x=df['date'], y=df['macd'], name='MACD') macd = go.Scattergl(x=data['date'], y=data['macd'], name='MACD')
macdsignal = go.Scattergl(x=df['date'], y=df['macdsignal'], name='MACD signal') macdsignal = go.Scattergl(x=data['date'], y=data['macdsignal'], name='MACD signal')
volume = go.Bar(x=df['date'], y=df['volume'], name='Volume') volume = go.Bar(x=data['date'], y=data['volume'], name='Volume')
fig = tools.make_subplots( fig = tools.make_subplots(
rows=3, rows=3,
@ -138,6 +153,31 @@ def plot_analyzed_dataframe(args) -> None:
plot(fig, filename='freqtrade-plot.html') plot(fig, filename='freqtrade-plot.html')
def plot_parse_args(args: List[str]) -> Namespace:
"""
Parse args passed to the script
:param args: Cli arguments
:return: args: Array with all arguments
"""
arguments = Arguments(args, 'Graph dataframe')
arguments.scripts_options()
arguments.common_args_parser()
arguments.optimizer_shared_options(arguments.parser)
arguments.backtesting_options(arguments.parser)
return arguments.parse_args()
def main(sysargv: List[str]) -> None:
"""
This function will initiate the bot and start the trading loop.
:return: None
"""
logger.info('Starting Plot Dataframe')
plot_analyzed_dataframe(
plot_parse_args(sysargv)
)
if __name__ == '__main__': if __name__ == '__main__':
args = plot_parse_args(sys.argv[1:]) main(sys.argv[1:])
plot_analyzed_dataframe(args)

View File

@ -1,32 +1,45 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""
Script to display profits
Mandatory Cli parameters:
-p / --pair: pair to examine
Optional Cli parameters
-c / --config: specify configuration file
-s / --strategy: strategy to use
--timerange: specify what timerange of data to use.
"""
import sys import sys
import json import json
from argparse import Namespace
from typing import List, Optional
import numpy as np import numpy as np
from plotly import tools from plotly import tools
from plotly.offline import plot from plotly.offline import plot
import plotly.graph_objs as go import plotly.graph_objs as go
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.analyze import Analyze
from freqtrade.logger import Logger
import freqtrade.optimize as optimize import freqtrade.optimize as optimize
import freqtrade.misc as misc import freqtrade.misc as misc
from freqtrade.strategy.strategy import Strategy
def plot_parse_args(args): logger = Logger(name="Graph profits").get_logger()
parser = misc.common_args_parser('Graph profits')
# FIX: perhaps delete those backtesting options that are not feasible (shows up in -h)
misc.backtesting_options(parser)
misc.scripts_options(parser)
return parser.parse_args(args)
# data:: [ pair, profit-%, enter, exit, time, duration] # data:: [ pair, profit-%, enter, exit, time, duration]
# data:: ['BTC_XMR', 0.00537847, '1511176800', '1511178000', 5057, 1] # data:: ["BTC_ETH", 0.0023975, "1515598200", "1515602100", "2018-01-10 07:30:00+00:00", 65]
# FIX: make use of the enter/exit dates to insert the def make_profit_array(
# profit more precisely into the pg array data: List, px: int, min_date: int,
def make_profit_array(data, px, filter_pairs=[]): interval: int, filter_pairs: Optional[List] = None) -> np.ndarray:
pg = np.zeros(px) pg = np.zeros(px)
filter_pairs = filter_pairs or []
# Go through the trades # Go through the trades
# and make an total profit # and make an total profit
# array # array
@ -35,10 +48,11 @@ def make_profit_array(data, px, filter_pairs=[]):
if filter_pairs and pair not in filter_pairs: if filter_pairs and pair not in filter_pairs:
continue continue
profit = trade[1] profit = trade[1]
tim = trade[4] trade_sell_time = int(trade[3])
dur = trade[5]
ix = tim + dur - 1 ix = define_index(min_date, trade_sell_time, interval)
if ix < px: if ix < px:
logger.debug('[%s]: Add profit %s on %s', pair, profit, trade[4])
pg[ix] += profit pg[ix] += profit
# rewrite the pg array to go from # rewrite the pg array to go from
@ -53,7 +67,7 @@ def make_profit_array(data, px, filter_pairs=[]):
return pg return pg
def plot_profit(args) -> None: def plot_profit(args: Namespace) -> None:
""" """
Plots the total profit for all pairs. Plots the total profit for all pairs.
Note, the profit calculation isn't realistic. Note, the profit calculation isn't realistic.
@ -64,47 +78,62 @@ def plot_profit(args) -> None:
# We need to use the same pairs, same tick_interval # We need to use the same pairs, same tick_interval
# and same timeperiod as used in backtesting # and same timeperiod as used in backtesting
# to match the tickerdata against the profits-results # to match the tickerdata against the profits-results
timerange = Arguments.parse_timerange(args.timerange)
filter_pairs = args.pair config = Configuration(args).get_config()
config = misc.load_config(args.config)
config.update({'strategy': args.strategy})
# Init strategy # Init strategy
strategy = Strategy() try:
strategy.init(config) analyze = Analyze({'strategy': config.get('strategy')})
except AttributeError:
logger.critical(
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
config.get('strategy')
)
exit()
# Take pairs from the cli otherwise switch to the pair in the config file
if args.pair:
filter_pairs = args.pair
filter_pairs = filter_pairs.split(',')
else:
filter_pairs = config['exchange']['pair_whitelist']
tick_interval = analyze.strategy.ticker_interval
pairs = config['exchange']['pair_whitelist'] pairs = config['exchange']['pair_whitelist']
if filter_pairs: if filter_pairs:
filter_pairs = filter_pairs.split(',')
pairs = list(set(pairs) & set(filter_pairs)) pairs = list(set(pairs) & set(filter_pairs))
print('Filter, keep pairs %s' % pairs) logger.info('Filter, keep pairs %s' % pairs)
timerange = misc.parse_timerange(args.timerange) tickers = optimize.load_data(
tickers = optimize.load_data(args.datadir, pairs=pairs, datadir=args.datadir,
ticker_interval=strategy.ticker_interval, pairs=pairs,
refresh_pairs=False, ticker_interval=tick_interval,
timerange=timerange) refresh_pairs=False,
dataframes = optimize.preprocess(tickers) timerange=timerange
)
dataframes = analyze.tickerdata_to_dataframe(tickers)
# NOTE: the dataframes are of unequal length, # NOTE: the dataframes are of unequal length,
# 'dates' is an merged date array of them all. # 'dates' is an merged date array of them all.
dates = misc.common_datearray(dataframes) dates = misc.common_datearray(dataframes)
max_x = dates.size min_date = int(min(dates).timestamp())
max_date = int(max(dates).timestamp())
num_iterations = define_index(min_date, max_date, tick_interval) + 1
# Make an average close price of all the pairs that was involved. # Make an average close price of all the pairs that was involved.
# this could be useful to gauge the overall market trend # this could be useful to gauge the overall market trend
# We are essentially saying: # We are essentially saying:
# array <- sum dataframes[*]['close'] / num_items dataframes # array <- sum dataframes[*]['close'] / num_items dataframes
# FIX: there should be some onliner numpy/panda for this # FIX: there should be some onliner numpy/panda for this
avgclose = np.zeros(max_x) avgclose = np.zeros(num_iterations)
num = 0 num = 0
for pair, pair_data in dataframes.items(): for pair, pair_data in dataframes.items():
close = pair_data['close'] close = pair_data['close']
maxprice = max(close) # Normalize price to [0,1] maxprice = max(close) # Normalize price to [0,1]
print('Pair %s has length %s' % (pair, len(close))) logger.info('Pair %s has length %s' % (pair, len(close)))
for x in range(0, len(close)): for x in range(0, len(close)):
avgclose[x] += close[x] / maxprice avgclose[x] += close[x] / maxprice
# avgclose += close # avgclose += close
@ -114,10 +143,16 @@ def plot_profit(args) -> None:
# Load the profits results # Load the profits results
# And make an profits-growth array # And make an profits-growth array
filename = 'backtest-result.json' try:
with open(filename) as file: filename = 'backtest-result.json'
data = json.load(file) with open(filename) as file:
pg = make_profit_array(data, max_x, filter_pairs) data = json.load(file)
except FileNotFoundError:
logger.critical('File "backtest-result.json" not found. This script require backtesting '
'results to run.\nPlease run a backtesting with the parameter --export.')
exit(0)
pg = make_profit_array(data, num_iterations, min_date, tick_interval, filter_pairs)
# #
# Plot the pairs average close prices, and total profit growth # Plot the pairs average close prices, and total profit growth
@ -128,6 +163,7 @@ def plot_profit(args) -> None:
y=avgclose, y=avgclose,
name='Avg close price', name='Avg close price',
) )
profit = go.Scattergl( profit = go.Scattergl(
x=dates, x=dates,
y=pg, y=pg,
@ -140,7 +176,7 @@ def plot_profit(args) -> None:
fig.append_trace(profit, 2, 1) fig.append_trace(profit, 2, 1)
for pair in pairs: for pair in pairs:
pg = make_profit_array(data, max_x, pair) pg = make_profit_array(data, num_iterations, min_date, tick_interval, pair)
pair_profit = go.Scattergl( pair_profit = go.Scattergl(
x=dates, x=dates,
y=pg, y=pg,
@ -151,6 +187,38 @@ def plot_profit(args) -> None:
plot(fig, filename='freqtrade-profit-plot.html') plot(fig, filename='freqtrade-profit-plot.html')
def define_index(min_date: int, max_date: int, interval: int) -> int:
"""
Return the index of a specific date
"""
return int((max_date - min_date) / (interval * 60))
def plot_parse_args(args: List[str]) -> Namespace:
"""
Parse args passed to the script
:param args: Cli arguments
:return: args: Array with all arguments
"""
arguments = Arguments(args, 'Graph profits')
arguments.scripts_options()
arguments.common_args_parser()
arguments.optimizer_shared_options(arguments.parser)
arguments.backtesting_options(arguments.parser)
return arguments.parse_args()
def main(sysargv: List[str]) -> None:
"""
This function will initiate the bot and start the trading loop.
:return: None
"""
logger.info('Starting Plot Dataframe')
plot_profit(
plot_parse_args(sysargv)
)
if __name__ == '__main__': if __name__ == '__main__':
args = plot_parse_args(sys.argv[1:]) main(sys.argv[1:])
plot_profit(args)