merged order book
This commit is contained in:
@@ -12,7 +12,8 @@ class DependencyException(BaseException):
|
||||
class OperationalException(BaseException):
|
||||
"""
|
||||
Requires manual intervention.
|
||||
This happens when an exchange returns an unexpected error during runtime.
|
||||
This happens when an exchange returns an unexpected error during runtime
|
||||
or given configuration is invalid.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
15
freqtrade/__main__.py
Normal file
15
freqtrade/__main__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
__main__.py for Freqtrade
|
||||
To launch Freqtrade as a module
|
||||
|
||||
> python -m freqtrade (with Python >= 3.6)
|
||||
"""
|
||||
|
||||
import sys
|
||||
from freqtrade import main
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main.set_loggers()
|
||||
main.main(sys.argv[1:])
|
||||
@@ -12,7 +12,7 @@ from pandas import DataFrame, to_datetime
|
||||
from freqtrade import constants
|
||||
from freqtrade.exchange import get_ticker_history
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy.resolver import StrategyResolver
|
||||
from freqtrade.strategy.resolver import StrategyResolver, IStrategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,7 +37,7 @@ class Analyze(object):
|
||||
:param config: Bot configuration (use the one from Configuration())
|
||||
"""
|
||||
self.config = config
|
||||
self.strategy = StrategyResolver(self.config).strategy
|
||||
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
||||
|
||||
@staticmethod
|
||||
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||
@@ -62,6 +62,7 @@ class Analyze(object):
|
||||
'close': 'last',
|
||||
'volume': 'max',
|
||||
})
|
||||
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
|
||||
return frame
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||
|
||||
@@ -2,24 +2,36 @@
|
||||
This module contains the argument manager class
|
||||
"""
|
||||
|
||||
import os
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import arrow
|
||||
from typing import List, Tuple, Optional
|
||||
from typing import List, Optional, NamedTuple
|
||||
|
||||
from freqtrade import __version__, constants
|
||||
|
||||
|
||||
class TimeRange(NamedTuple):
|
||||
"""
|
||||
NamedTuple Defining timerange inputs.
|
||||
[start/stop]type defines if [start/stop]ts shall be used.
|
||||
if *type is none, don't use corresponding startvalue.
|
||||
"""
|
||||
starttype: Optional[str] = None
|
||||
stoptype: Optional[str] = None
|
||||
startts: int = 0
|
||||
stopts: int = 0
|
||||
|
||||
|
||||
class Arguments(object):
|
||||
"""
|
||||
Arguments Class. Manage the arguments received by the cli
|
||||
"""
|
||||
|
||||
def __init__(self, args: List[str], description: str):
|
||||
def __init__(self, args: List[str], description: str) -> None:
|
||||
self.args = args
|
||||
self.parsed_arg = None
|
||||
self.parsed_arg: Optional[argparse.Namespace] = None
|
||||
self.parser = argparse.ArgumentParser(description=description)
|
||||
|
||||
def _load_args(self) -> None:
|
||||
@@ -60,7 +72,7 @@ class Arguments(object):
|
||||
self.parser.add_argument(
|
||||
'--version',
|
||||
action='version',
|
||||
version='%(prog)s {}'.format(__version__),
|
||||
version=f'%(prog)s {__version__}'
|
||||
)
|
||||
self.parser.add_argument(
|
||||
'-c', '--config',
|
||||
@@ -72,9 +84,9 @@ class Arguments(object):
|
||||
)
|
||||
self.parser.add_argument(
|
||||
'-d', '--datadir',
|
||||
help='path to backtest data (default: %(default)s',
|
||||
help='path to backtest data',
|
||||
dest='datadir',
|
||||
default=os.path.join('freqtrade', 'tests', 'testdata'),
|
||||
default=None,
|
||||
type=str,
|
||||
metavar='PATH',
|
||||
)
|
||||
@@ -95,8 +107,8 @@ class Arguments(object):
|
||||
)
|
||||
self.parser.add_argument(
|
||||
'--dynamic-whitelist',
|
||||
help='dynamically generate and update whitelist \
|
||||
based on 24h BaseVolume (Default 20 currencies)', # noqa
|
||||
help='dynamically generate and update whitelist'
|
||||
' based on 24h BaseVolume (default: %(const)s)',
|
||||
dest='dynamic_whitelist',
|
||||
const=constants.DYNAMIC_WHITELIST,
|
||||
type=int,
|
||||
@@ -104,11 +116,13 @@ class Arguments(object):
|
||||
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',
|
||||
'--db-url',
|
||||
help='Override trades database URL, this is useful if dry_run is enabled'
|
||||
' or in custom deployments (default: %(default)s)',
|
||||
dest='db_url',
|
||||
default=constants.DEFAULT_DB_PROD_URL,
|
||||
type=str,
|
||||
metavar='PATH',
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -124,8 +138,8 @@ class Arguments(object):
|
||||
)
|
||||
parser.add_argument(
|
||||
'-r', '--refresh-pairs-cached',
|
||||
help='refresh the pairs files in tests/testdata with the latest data from the exchange. \
|
||||
Use it if you want to run your backtesting with up-to-date data.',
|
||||
help='refresh the pairs files in tests/testdata with the latest data from the '
|
||||
'exchange. Use it if you want to run your backtesting with up-to-date data.',
|
||||
action='store_true',
|
||||
dest='refresh_pairs',
|
||||
)
|
||||
@@ -137,9 +151,25 @@ class Arguments(object):
|
||||
default=None,
|
||||
dest='export',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--export-filename',
|
||||
help='Save backtest results to this filename \
|
||||
requires --export to be set as well\
|
||||
Example --export-filename=user_data/backtest_data/backtest_today.json\
|
||||
(default: %(default)s)',
|
||||
type=str,
|
||||
default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'),
|
||||
dest='exportfilename',
|
||||
metavar='PATH',
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
Parses given common arguments for Backtesting and Hyperopt scripts.
|
||||
:param parser:
|
||||
:return:
|
||||
"""
|
||||
parser.add_argument(
|
||||
'-i', '--ticker-interval',
|
||||
help='specify ticker interval (1m, 5m, 30m, 1h, 1d)',
|
||||
@@ -211,17 +241,20 @@ class Arguments(object):
|
||||
self.hyperopt_options(hyperopt_cmd)
|
||||
|
||||
@staticmethod
|
||||
def parse_timerange(text: str) -> Optional[Tuple[List, int, int]]:
|
||||
def parse_timerange(text: Optional[str]) -> TimeRange:
|
||||
"""
|
||||
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
|
||||
return TimeRange(None, None, 0, 0)
|
||||
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
||||
(r'^(\d{8})-$', ('date', None)),
|
||||
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
|
||||
(r'^-(\d{10})$', (None, 'date')),
|
||||
(r'^(\d{10})-$', ('date', None)),
|
||||
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
|
||||
(r'^(-\d+)$', (None, 'line')),
|
||||
(r'^(\d+)-$', ('line', None)),
|
||||
(r'^(\d+)-(\d+)$', ('index', 'index'))]
|
||||
@@ -231,22 +264,24 @@ class Arguments(object):
|
||||
if match: # Regex has matched
|
||||
rvals = match.groups()
|
||||
index = 0
|
||||
start = None
|
||||
stop = None
|
||||
start: int = 0
|
||||
stop: int = 0
|
||||
if stype[0]:
|
||||
start = rvals[index]
|
||||
starts = rvals[index]
|
||||
if stype[0] == 'date':
|
||||
start = arrow.get(start, 'YYYYMMDD').timestamp
|
||||
start = int(starts) if len(starts) == 10 \
|
||||
else arrow.get(starts, 'YYYYMMDD').timestamp
|
||||
else:
|
||||
start = int(start)
|
||||
start = int(starts)
|
||||
index += 1
|
||||
if stype[1]:
|
||||
stop = rvals[index]
|
||||
stops = rvals[index]
|
||||
if stype[1] == 'date':
|
||||
stop = arrow.get(stop, 'YYYYMMDD').timestamp
|
||||
stop = int(stops) if len(stops) == 10 \
|
||||
else arrow.get(stops, 'YYYYMMDD').timestamp
|
||||
else:
|
||||
stop = int(stop)
|
||||
return stype, start, stop
|
||||
stop = int(stops)
|
||||
return TimeRange(stype[0], stype[1], start, stop)
|
||||
raise Exception('Incorrect syntax for timerange "%s"' % text)
|
||||
|
||||
def scripts_options(self) -> None:
|
||||
@@ -260,7 +295,6 @@ class Arguments(object):
|
||||
default=None
|
||||
)
|
||||
|
||||
|
||||
self.parser.add_argument(
|
||||
'--stop-loss',
|
||||
help='Renders stop/loss information in the main chart',
|
||||
@@ -365,25 +399,42 @@ class Arguments(object):
|
||||
'--pairs-file',
|
||||
help='File containing a list of pairs to download',
|
||||
dest='pairs_file',
|
||||
default=None
|
||||
default=None,
|
||||
metavar='PATH',
|
||||
)
|
||||
|
||||
self.parser.add_argument(
|
||||
'--export',
|
||||
help='Export files to given dir',
|
||||
dest='export',
|
||||
default=None)
|
||||
default=None,
|
||||
metavar='PATH',
|
||||
)
|
||||
|
||||
self.parser.add_argument(
|
||||
'--days',
|
||||
help='Download data for number of days',
|
||||
dest='days',
|
||||
type=int,
|
||||
default=None)
|
||||
metavar='INT',
|
||||
default=None
|
||||
)
|
||||
|
||||
self.parser.add_argument(
|
||||
'--exchange',
|
||||
help='Exchange name',
|
||||
help='Exchange name (default: %(default)s)',
|
||||
dest='exchange',
|
||||
type=str,
|
||||
default='bittrex')
|
||||
default='bittrex'
|
||||
)
|
||||
|
||||
self.parser.add_argument(
|
||||
'-t', '--timeframes',
|
||||
help='Specify which tickers to download. Space separated list. \
|
||||
Default: %(default)s',
|
||||
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
|
||||
'6h', '8h', '12h', '1d', '3d', '1w'],
|
||||
default=['1m', '5m'],
|
||||
nargs='+',
|
||||
dest='timeframes',
|
||||
)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""
|
||||
This module contains the configuration class
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from argparse import Namespace
|
||||
from typing import Dict, Any
|
||||
from typing import Optional, Dict, Any
|
||||
from jsonschema import Draft4Validator, validate
|
||||
from jsonschema.exceptions import ValidationError, best_match
|
||||
import ccxt
|
||||
@@ -23,7 +23,7 @@ class Configuration(object):
|
||||
"""
|
||||
def __init__(self, args: Namespace) -> None:
|
||||
self.args = args
|
||||
self.config = None
|
||||
self.config: Optional[Dict[str, Any]] = None
|
||||
|
||||
def load_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -61,11 +61,9 @@ class Configuration(object):
|
||||
with open(path) as file:
|
||||
conf = json.load(file)
|
||||
except FileNotFoundError:
|
||||
logger.critical(
|
||||
'Config file "%s" not found. Please create your config file',
|
||||
path
|
||||
)
|
||||
exit(0)
|
||||
raise OperationalException(
|
||||
'Config file "{}" not found!'
|
||||
' Please create a config file or check whether it exists.'.format(path))
|
||||
|
||||
if 'internals' not in conf:
|
||||
conf['internals'] = {}
|
||||
@@ -97,22 +95,35 @@ class Configuration(object):
|
||||
'(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})
|
||||
logger.info('Parameter --dry-run-db detected ...')
|
||||
if self.args.db_url != constants.DEFAULT_DB_PROD_URL:
|
||||
config.update({'db_url': self.args.db_url})
|
||||
logger.info('Parameter --db-url detected ...')
|
||||
|
||||
if config.get('dry_run_db', False):
|
||||
if config.get('dry_run', False):
|
||||
logger.info('Dry_run will use the DB file: "tradesv3.dry_run.sqlite"')
|
||||
else:
|
||||
logger.info('Dry run is disabled. (--dry_run_db ignored)')
|
||||
if config.get('dry_run', False):
|
||||
logger.info('Dry run is enabled')
|
||||
if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]:
|
||||
# Default to in-memory db for dry_run if not specified
|
||||
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
|
||||
else:
|
||||
if not config.get('db_url', None):
|
||||
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
||||
logger.info('Dry run is disabled')
|
||||
|
||||
logger.info('Using DB: "{}"'.format(config['db_url']))
|
||||
|
||||
# Check if the exchange set by the user is supported
|
||||
self.check_exchange(config)
|
||||
|
||||
return config
|
||||
|
||||
def _create_default_datadir(self, config: Dict[str, Any]) -> str:
|
||||
exchange_name = config.get('exchange', {}).get('name').lower()
|
||||
default_path = os.path.join('user_data', 'data', exchange_name)
|
||||
if not os.path.isdir(default_path):
|
||||
os.makedirs(default_path)
|
||||
logger.info(f'Created data directory: {default_path}')
|
||||
return default_path
|
||||
|
||||
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract information for sys.argv and load Backtesting configuration
|
||||
@@ -145,7 +156,9 @@ class Configuration(object):
|
||||
# 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})
|
||||
logger.info('Parameter --datadir detected: %s ...', self.args.datadir)
|
||||
else:
|
||||
config.update({'datadir': self._create_default_datadir(config)})
|
||||
logger.info('Using data folder: %s ...', config.get('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:
|
||||
@@ -157,6 +170,11 @@ class Configuration(object):
|
||||
config.update({'export': self.args.export})
|
||||
logger.info('Parameter --export detected: %s ...', self.args.export)
|
||||
|
||||
# If --export-filename is used we add it to the configuration
|
||||
if 'export' in config and 'exportfilename' in self.args and self.args.exportfilename:
|
||||
config.update({'exportfilename': self.args.exportfilename})
|
||||
logger.info('Storing backtest results to %s ...', self.args.exportfilename)
|
||||
|
||||
return config
|
||||
|
||||
def _load_hyperopt_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
@@ -192,7 +210,7 @@ class Configuration(object):
|
||||
validate(conf, constants.CONF_SCHEMA)
|
||||
return conf
|
||||
except ValidationError as exception:
|
||||
logger.fatal(
|
||||
logger.critical(
|
||||
'Invalid configuration. See config.json.example. Reason: %s',
|
||||
exception
|
||||
)
|
||||
@@ -218,9 +236,8 @@ class Configuration(object):
|
||||
exchange = config.get('exchange', {}).get('name').lower()
|
||||
if exchange not in ccxt.exchanges:
|
||||
|
||||
exception_msg = 'Exchange "{}" not supported.\n' \
|
||||
'The following exchanges are supported: {}'\
|
||||
.format(exchange, ', '.join(ccxt.exchanges))
|
||||
exception_msg = f'Exchange "{exchange}" not supported.\n' \
|
||||
f'The following exchanges are supported: {", ".join(ccxt.exchanges)}'
|
||||
|
||||
logger.critical(exception_msg)
|
||||
raise OperationalException(
|
||||
|
||||
@@ -9,9 +9,12 @@ TICKER_INTERVAL = 5 # min
|
||||
HYPEROPT_EPOCH = 100 # epochs
|
||||
RETRY_TIMEOUT = 30 # sec
|
||||
DEFAULT_STRATEGY = 'DefaultStrategy'
|
||||
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
||||
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
|
||||
|
||||
TICKER_INTERVAL_MINUTES = {
|
||||
'1m': 1,
|
||||
'3m': 3,
|
||||
'5m': 5,
|
||||
'15m': 15,
|
||||
'30m': 30,
|
||||
@@ -19,11 +22,20 @@ TICKER_INTERVAL_MINUTES = {
|
||||
'2h': 120,
|
||||
'4h': 240,
|
||||
'6h': 360,
|
||||
'8h': 480,
|
||||
'12h': 720,
|
||||
'1d': 1440,
|
||||
'3d': 4320,
|
||||
'1w': 10080,
|
||||
}
|
||||
|
||||
SUPPORTED_FIAT = [
|
||||
"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",
|
||||
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
|
||||
]
|
||||
|
||||
# Required json-schema for user specified config
|
||||
CONF_SCHEMA = {
|
||||
@@ -31,16 +43,9 @@ CONF_SCHEMA = {
|
||||
'properties': {
|
||||
'max_open_trades': {'type': 'integer', 'minimum': 0},
|
||||
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
|
||||
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
|
||||
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT', 'EUR', 'USD']},
|
||||
'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']},
|
||||
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
|
||||
'dry_run': {'type': 'boolean'},
|
||||
'minimal_roi': {
|
||||
'type': 'object',
|
||||
@@ -60,9 +65,19 @@ CONF_SCHEMA = {
|
||||
'maximum': 1,
|
||||
'exclusiveMaximum': False
|
||||
},
|
||||
'use_book_order': {'type': 'boolean'},
|
||||
'book_order_top': {'type': 'number', 'maximum':20,'minimum':1}
|
||||
},
|
||||
'required': ['ask_last_balance']
|
||||
},
|
||||
'ask_strategy': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'use_book_order': {'type': 'boolean'},
|
||||
'book_order_min': {'type': 'number', 'minimum':1},
|
||||
'book_order_max': {'type': 'number', 'minimum':1}
|
||||
},
|
||||
},
|
||||
'exchange': {'$ref': '#/definitions/exchange'},
|
||||
'experimental': {
|
||||
'type': 'object',
|
||||
@@ -80,6 +95,7 @@ CONF_SCHEMA = {
|
||||
},
|
||||
'required': ['enabled', 'token', 'chat_id']
|
||||
},
|
||||
'db_url': {'type': 'string'},
|
||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||
'internals': {
|
||||
'type': 'object',
|
||||
|
||||
@@ -18,6 +18,8 @@ _API: ccxt.Exchange = None
|
||||
_CONF: Dict = {}
|
||||
API_RETRY_COUNT = 4
|
||||
|
||||
_CACHED_TICKER: Dict[str, Any] = {}
|
||||
|
||||
# Holds all open sell orders for dry_run
|
||||
_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {}
|
||||
|
||||
@@ -57,7 +59,7 @@ def init_ccxt(exchange_config: dict) -> ccxt.Exchange:
|
||||
name = exchange_config['name']
|
||||
|
||||
if name not in ccxt.exchanges:
|
||||
raise OperationalException('Exchange {} is not supported'.format(name))
|
||||
raise OperationalException(f'Exchange {name} is not supported')
|
||||
try:
|
||||
api = getattr(ccxt, name.lower())({
|
||||
'apiKey': exchange_config.get('key'),
|
||||
@@ -67,7 +69,7 @@ def init_ccxt(exchange_config: dict) -> ccxt.Exchange:
|
||||
'enableRateLimit': True,
|
||||
})
|
||||
except (KeyError, AttributeError):
|
||||
raise OperationalException('Exchange {} is not supported'.format(name))
|
||||
raise OperationalException(f'Exchange {name} is not supported')
|
||||
|
||||
return api
|
||||
|
||||
@@ -116,11 +118,10 @@ def validate_pairs(pairs: List[str]) -> None:
|
||||
# TODO: add a support for having coins in BTC/USDT format
|
||||
if not pair.endswith(stake_cur):
|
||||
raise OperationalException(
|
||||
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
|
||||
)
|
||||
f'Pair {pair} not compatible with stake_currency: {stake_cur}')
|
||||
if pair not in markets:
|
||||
raise OperationalException(
|
||||
'Pair {} is not available at {}'.format(pair, get_name()))
|
||||
f'Pair {pair} is not available at {get_name()}')
|
||||
|
||||
|
||||
def exchange_has(endpoint: str) -> bool:
|
||||
@@ -136,7 +137,7 @@ def exchange_has(endpoint: str) -> bool:
|
||||
def buy(pair: str, rate: float, amount: float) -> Dict:
|
||||
if _CONF['dry_run']:
|
||||
global _DRY_RUN_OPEN_ORDERS
|
||||
order_id = 'dry_run_buy_{}'.format(randint(0, 10**6))
|
||||
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
||||
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
||||
'pair': pair,
|
||||
'price': rate,
|
||||
@@ -154,20 +155,17 @@ def buy(pair: str, rate: float, amount: float) -> Dict:
|
||||
return _API.create_limit_buy_order(pair, amount, rate)
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
'Insufficient funds to create limit buy order on market {}.'
|
||||
'Tried to buy amount {} at rate {} (total {}).'
|
||||
'Message: {}'.format(pair, amount, rate, rate*amount, e)
|
||||
)
|
||||
f'Insufficient funds to create limit buy order on market {pair}.'
|
||||
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
|
||||
f'Message: {e}')
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise DependencyException(
|
||||
'Could not create limit buy order on market {}.'
|
||||
'Tried to buy amount {} at rate {} (total {}).'
|
||||
'Message: {}'.format(pair, amount, rate, rate*amount, e)
|
||||
)
|
||||
f'Could not create limit buy order on market {pair}.'
|
||||
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
|
||||
f'Message: {e}')
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
'Could not place buy order due to {}. Message: {}'.format(
|
||||
e.__class__.__name__, e))
|
||||
f'Could not place buy order due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
|
||||
@@ -175,7 +173,7 @@ def buy(pair: str, rate: float, amount: float) -> Dict:
|
||||
def sell(pair: str, rate: float, amount: float) -> Dict:
|
||||
if _CONF['dry_run']:
|
||||
global _DRY_RUN_OPEN_ORDERS
|
||||
order_id = 'dry_run_sell_{}'.format(randint(0, 10**6))
|
||||
order_id = f'dry_run_sell_{randint(0, 10**6)}'
|
||||
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
||||
'pair': pair,
|
||||
'price': rate,
|
||||
@@ -192,20 +190,17 @@ def sell(pair: str, rate: float, amount: float) -> Dict:
|
||||
return _API.create_limit_sell_order(pair, amount, rate)
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
'Insufficient funds to create limit sell order on market {}.'
|
||||
'Tried to sell amount {} at rate {} (total {}).'
|
||||
'Message: {}'.format(pair, amount, rate, rate*amount, e)
|
||||
)
|
||||
f'Insufficient funds to create limit sell order on market {pair}.'
|
||||
f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).'
|
||||
f'Message: {e}')
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise DependencyException(
|
||||
'Could not create limit sell order on market {}.'
|
||||
'Tried to sell amount {} at rate {} (total {}).'
|
||||
'Message: {}'.format(pair, amount, rate, rate*amount, e)
|
||||
)
|
||||
f'Could not create limit sell order on market {pair}.'
|
||||
f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).'
|
||||
f'Message: {e}')
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
'Could not place sell order due to {}. Message: {}'.format(
|
||||
e.__class__.__name__, e))
|
||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
|
||||
@@ -220,8 +215,7 @@ def get_balance(currency: str) -> float:
|
||||
balance = balances.get(currency)
|
||||
if balance is None:
|
||||
raise TemporaryError(
|
||||
'Could not get {} balance due to malformed exchange response: {}'.format(
|
||||
currency, balances))
|
||||
f'Could not get {currency} balance due to malformed exchange response: {balances}')
|
||||
return balance['free']
|
||||
|
||||
|
||||
@@ -241,11 +235,23 @@ def get_balances() -> dict:
|
||||
return balances
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
'Could not get balance due to {}. Message: {}'.format(
|
||||
e.__class__.__name__, e))
|
||||
f'Could not get balance due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
|
||||
@retrier
|
||||
def get_order_book(pair: str, refresh: Optional[bool] = True) -> dict:
|
||||
try:
|
||||
return _API.fetch_order_book(pair)
|
||||
except ccxt.NotSupported as e:
|
||||
raise OperationalException(
|
||||
f'Exchange {_API.name} does not support fetching order book.'
|
||||
f'Message: {e}')
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not load order book due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
|
||||
@retrier
|
||||
def get_tickers() -> Dict:
|
||||
@@ -253,28 +259,37 @@ def get_tickers() -> Dict:
|
||||
return _API.fetch_tickers()
|
||||
except ccxt.NotSupported as e:
|
||||
raise OperationalException(
|
||||
'Exchange {} does not support fetching tickers in batch.'
|
||||
'Message: {}'.format(_API.name, e)
|
||||
)
|
||||
f'Exchange {_API.name} does not support fetching tickers in batch.'
|
||||
f'Message: {e}')
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
'Could not load tickers due to {}. Message: {}'.format(
|
||||
e.__class__.__name__, e))
|
||||
f'Could not load tickers due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
|
||||
|
||||
# TODO: remove refresh argument, keeping it to keep track of where it was intended to be used
|
||||
@retrier
|
||||
def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
|
||||
try:
|
||||
return _API.fetch_ticker(pair)
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
'Could not load ticker history due to {}. Message: {}'.format(
|
||||
e.__class__.__name__, e))
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
global _CACHED_TICKER
|
||||
if refresh or pair not in _CACHED_TICKER.keys():
|
||||
try:
|
||||
data = _API.fetch_ticker(pair)
|
||||
try:
|
||||
_CACHED_TICKER[pair] = {
|
||||
'bid': float(data['bid']),
|
||||
'ask': float(data['ask']),
|
||||
}
|
||||
except KeyError:
|
||||
logger.debug("Could not cache ticker data for %s", pair)
|
||||
return data
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
else:
|
||||
logger.info("returning cached ticker-data for %s", pair)
|
||||
return _CACHED_TICKER[pair]
|
||||
|
||||
|
||||
@retrier
|
||||
@@ -290,10 +305,15 @@ def get_ticker_history(pair: str, tick_interval: str, since_ms: Optional[int] =
|
||||
# chached data was already downloaded
|
||||
till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000)
|
||||
|
||||
data = []
|
||||
data: List[Dict[Any, Any]] = []
|
||||
while not since_ms or since_ms < till_time_ms:
|
||||
data_part = _API.fetch_ohlcv(pair, timeframe=tick_interval, since=since_ms)
|
||||
|
||||
# Because some exchange sort Tickers ASC and other DESC.
|
||||
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last)
|
||||
# when GDAX returns a list of tickers DESC (newest first, oldest last)
|
||||
data_part = sorted(data_part, key=lambda x: x[0])
|
||||
|
||||
if not data_part:
|
||||
break
|
||||
|
||||
@@ -308,15 +328,13 @@ def get_ticker_history(pair: str, tick_interval: str, since_ms: Optional[int] =
|
||||
return data
|
||||
except ccxt.NotSupported as e:
|
||||
raise OperationalException(
|
||||
'Exchange {} does not support fetching historical candlestick data.'
|
||||
'Message: {}'.format(_API.name, e)
|
||||
)
|
||||
f'Exchange {_API.name} does not support fetching historical candlestick data.'
|
||||
f'Message: {e}')
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
'Could not load ticker history due to {}. Message: {}'.format(
|
||||
e.__class__.__name__, e))
|
||||
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException('Could not fetch ticker data. Msg: {}'.format(e))
|
||||
raise OperationalException(f'Could not fetch ticker data. Msg: {e}')
|
||||
|
||||
|
||||
@retrier
|
||||
@@ -328,12 +346,10 @@ def cancel_order(order_id: str, pair: str) -> None:
|
||||
return _API.cancel_order(order_id, pair)
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise DependencyException(
|
||||
'Could not cancel order. Message: {}'.format(e)
|
||||
)
|
||||
f'Could not cancel order. Message: {e}')
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
'Could not cancel order due to {}. Message: {}'.format(
|
||||
e.__class__.__name__, e))
|
||||
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
|
||||
@@ -350,12 +366,10 @@ def get_order(order_id: str, pair: str) -> Dict:
|
||||
return _API.fetch_order(order_id, pair)
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise DependencyException(
|
||||
'Could not get order. Message: {}'.format(e)
|
||||
)
|
||||
f'Could not get order. Message: {e}')
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
'Could not get order due to {}. Message: {}'.format(
|
||||
e.__class__.__name__, e))
|
||||
f'Could not get order due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
|
||||
@@ -374,8 +388,7 @@ def get_trades_for_order(order_id: str, pair: str, since: datetime) -> List:
|
||||
|
||||
except ccxt.NetworkError as e:
|
||||
raise TemporaryError(
|
||||
'Could not get trades due to networking error. Message: {}'.format(e)
|
||||
)
|
||||
f'Could not get trades due to networking error. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
|
||||
@@ -397,8 +410,7 @@ def get_markets() -> List[dict]:
|
||||
return _API.fetch_markets()
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
'Could not load markets due to {}. Message: {}'.format(
|
||||
e.__class__.__name__, e))
|
||||
f'Could not load markets due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
|
||||
@@ -423,8 +435,7 @@ def get_fee(symbol='ETH/BTC', type='', side='', amount=1,
|
||||
price=price, takerOrMaker=taker_or_maker)['rate']
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
'Could not get fee info due to {}. Message: {}'.format(
|
||||
e.__class__.__name__, e))
|
||||
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ e.g BTC to USD
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict
|
||||
from typing import Dict, List
|
||||
|
||||
from coinmarketcap import Market
|
||||
from requests.exceptions import RequestException
|
||||
from freqtrade.constants import SUPPORTED_FIAT
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -34,7 +35,7 @@ class CryptoFiat(object):
|
||||
self.price = 0.0
|
||||
|
||||
# Private attributes
|
||||
self._expiration = 0
|
||||
self._expiration = 0.0
|
||||
|
||||
self.crypto_symbol = crypto_symbol.upper()
|
||||
self.fiat_symbol = fiat_symbol.upper()
|
||||
@@ -65,15 +66,7 @@ class CryptoToFiatConverter(object):
|
||||
This object is also a Singleton
|
||||
"""
|
||||
__instance = None
|
||||
_coinmarketcap = None
|
||||
|
||||
# Constants
|
||||
SUPPORTED_FIAT = [
|
||||
"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"
|
||||
]
|
||||
_coinmarketcap: Market = None
|
||||
|
||||
_cryptomap: Dict = {}
|
||||
|
||||
@@ -87,7 +80,7 @@ class CryptoToFiatConverter(object):
|
||||
return CryptoToFiatConverter.__instance
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._pairs = []
|
||||
self._pairs: List[CryptoFiat] = []
|
||||
self._load_cryptomap()
|
||||
|
||||
def _load_cryptomap(self) -> None:
|
||||
@@ -95,8 +88,11 @@ class CryptoToFiatConverter(object):
|
||||
coinlistings = self._coinmarketcap.listings()
|
||||
self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])),
|
||||
coinlistings["data"]))
|
||||
except (ValueError, RequestException) as e:
|
||||
logger.error("Could not load FIAT Cryptocurrency map for the following problem: %s", e)
|
||||
except (ValueError, RequestException) as exception:
|
||||
logger.error(
|
||||
"Could not load FIAT Cryptocurrency map for the following problem: %s",
|
||||
exception
|
||||
)
|
||||
|
||||
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
|
||||
"""
|
||||
@@ -123,7 +119,7 @@ class CryptoToFiatConverter(object):
|
||||
|
||||
# Check if the fiat convertion you want is supported
|
||||
if not self._is_supported_fiat(fiat=fiat_symbol):
|
||||
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
|
||||
raise ValueError(f'The fiat {fiat_symbol} is not supported.')
|
||||
|
||||
# Get the pair that interest us and return the price in fiat
|
||||
for pair in self._pairs:
|
||||
@@ -175,7 +171,7 @@ class CryptoToFiatConverter(object):
|
||||
|
||||
fiat = fiat.upper()
|
||||
|
||||
return fiat in self.SUPPORTED_FIAT
|
||||
return fiat in SUPPORTED_FIAT
|
||||
|
||||
def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float:
|
||||
"""
|
||||
@@ -186,12 +182,17 @@ class CryptoToFiatConverter(object):
|
||||
"""
|
||||
# Check if the fiat convertion you want is supported
|
||||
if not self._is_supported_fiat(fiat=fiat_symbol):
|
||||
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
|
||||
raise ValueError(f'The fiat {fiat_symbol} is not supported.')
|
||||
|
||||
# No need to convert if both crypto and fiat are the same
|
||||
if crypto_symbol == fiat_symbol:
|
||||
return 1.0
|
||||
|
||||
if crypto_symbol not in self._cryptomap:
|
||||
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
|
||||
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
|
||||
return 0.0
|
||||
|
||||
try:
|
||||
return float(
|
||||
self._coinmarketcap.ticker(
|
||||
@@ -199,6 +200,6 @@ class CryptoToFiatConverter(object):
|
||||
convert=fiat_symbol
|
||||
)['data']['quotes'][fiat_symbol.upper()]['price']
|
||||
)
|
||||
except BaseException as ex:
|
||||
logger.error("Error in _find_price: %s", ex)
|
||||
except BaseException as exception:
|
||||
logger.error("Error in _find_price: %s", exception)
|
||||
return 0.0
|
||||
|
||||
@@ -33,12 +33,11 @@ class FreqtradeBot(object):
|
||||
This is from here the bot start its logic.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any], db_url: Optional[str] = None):
|
||||
def __init__(self, config: Dict[str, Any])-> 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)
|
||||
"""
|
||||
|
||||
logger.info(
|
||||
@@ -51,26 +50,22 @@ class FreqtradeBot(object):
|
||||
|
||||
# Init objects
|
||||
self.config = config
|
||||
self.analyze = None
|
||||
self.fiat_converter = None
|
||||
self.rpc = None
|
||||
self.analyze = Analyze(self.config)
|
||||
self.fiat_converter = CryptoToFiatConverter()
|
||||
self.rpc: RPCManager = RPCManager(self)
|
||||
self.persistence = None
|
||||
self.exchange = None
|
||||
|
||||
self._init_modules(db_url=db_url)
|
||||
self._init_modules()
|
||||
|
||||
def _init_modules(self, db_url: Optional[str] = None) -> None:
|
||||
def _init_modules(self) -> 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)
|
||||
persistence.init(self.config)
|
||||
exchange.init(self.config)
|
||||
|
||||
# Set initial application state
|
||||
@@ -93,7 +88,7 @@ class FreqtradeBot(object):
|
||||
persistence.cleanup()
|
||||
return True
|
||||
|
||||
def worker(self, old_state: None) -> State:
|
||||
def worker(self, old_state: State = None) -> State:
|
||||
"""
|
||||
Trading routine that must be run at each loop
|
||||
:param old_state: the previous service state from the previous call
|
||||
@@ -102,7 +97,7 @@ class FreqtradeBot(object):
|
||||
# Log state transition
|
||||
state = self.state
|
||||
if state != old_state:
|
||||
self.rpc.send_msg('*Status:* `{}`'.format(state.name.lower()))
|
||||
self.rpc.send_msg(f'*Status:* `{state.name.lower()}`')
|
||||
logger.info('Changing state to: %s', state.name)
|
||||
|
||||
if state == State.STOPPED:
|
||||
@@ -176,12 +171,10 @@ class FreqtradeBot(object):
|
||||
logger.warning('%s, retrying in 30 seconds...', error)
|
||||
time.sleep(constants.RETRY_TIMEOUT)
|
||||
except OperationalException:
|
||||
tb = traceback.format_exc()
|
||||
hint = 'Issue `/start` if you think it is safe to restart.'
|
||||
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.'
|
||||
)
|
||||
f'*Status:* OperationalException:\n```\n{tb}```{hint}'
|
||||
)
|
||||
logger.exception('OperationalException. Stopping trader ...')
|
||||
self.state = State.STOPPED
|
||||
@@ -244,27 +237,37 @@ class FreqtradeBot(object):
|
||||
|
||||
return final_list
|
||||
|
||||
def get_target_bid(self, ticker: Dict[str, float]) -> float:
|
||||
def get_target_bid(self, pair: str) -> 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'])
|
||||
|
||||
if self.config['bid_strategy']['use_book_order']:
|
||||
logger.info('Getting price from Order Book')
|
||||
orderBook = exchange.get_order_book(pair)
|
||||
return orderBook['bids'][self.config['bid_strategy']['book_order_top']][0]
|
||||
else:
|
||||
logger.info('Using Ask / Last Price')
|
||||
ticker = exchange.get_ticker(pair);
|
||||
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()
|
||||
stake_currency = self.config['stake_currency']
|
||||
fiat_currency = self.config['fiat_display_currency']
|
||||
exc_name = exchange.get_name()
|
||||
|
||||
logger.info(
|
||||
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
||||
@@ -272,10 +275,9 @@ class FreqtradeBot(object):
|
||||
)
|
||||
whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
|
||||
# Check if stake_amount is fulfilled
|
||||
if exchange.get_balance(self.config['stake_currency']) < stake_amount:
|
||||
if exchange.get_balance(stake_currency) < stake_amount:
|
||||
raise DependencyException(
|
||||
'stake amount is not fulfilled (currency={})'.format(self.config['stake_currency'])
|
||||
)
|
||||
f'stake amount is not fulfilled (currency={stake_currency})')
|
||||
|
||||
# Remove currently opened and latest pairs from whitelist
|
||||
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
||||
@@ -294,32 +296,25 @@ class FreqtradeBot(object):
|
||||
break
|
||||
else:
|
||||
return False
|
||||
|
||||
pair_s = pair.replace('_', '/')
|
||||
pair_url = exchange.get_pair_detail_url(pair)
|
||||
# Calculate amount
|
||||
buy_limit = self.get_target_bid(exchange.get_ticker(pair))
|
||||
buy_limit = self.get_target_bid(pair)
|
||||
amount = stake_amount / buy_limit
|
||||
|
||||
order_id = exchange.buy(pair, buy_limit, amount)['id']
|
||||
|
||||
stake_amount_fiat = self.fiat_converter.convert_amount(
|
||||
stake_amount,
|
||||
self.config['stake_currency'],
|
||||
self.config['fiat_display_currency']
|
||||
stake_currency,
|
||||
fiat_currency
|
||||
)
|
||||
|
||||
# Create trade entity and return
|
||||
self.rpc.send_msg(
|
||||
'*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '
|
||||
.format(
|
||||
exchange.get_name(),
|
||||
pair.replace('_', '/'),
|
||||
exchange.get_pair_detail_url(pair),
|
||||
buy_limit,
|
||||
stake_amount,
|
||||
self.config['stake_currency'],
|
||||
stake_amount_fiat,
|
||||
self.config['fiat_display_currency']
|
||||
)
|
||||
f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \
|
||||
with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
||||
{stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`"""
|
||||
)
|
||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||
fee = exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||
@@ -420,12 +415,12 @@ class FreqtradeBot(object):
|
||||
fee_abs += exectrade['fee']['cost']
|
||||
|
||||
if amount != order_amount:
|
||||
logger.warning("amount {} does not match amount {}".format(amount, trade.amount))
|
||||
logger.warning(f"amount {amount} does not match amount {trade.amount}")
|
||||
raise OperationalException("Half bought? Amounts don't match")
|
||||
real_amount = amount - fee_abs
|
||||
if fee_abs != 0:
|
||||
logger.info("Applying fee on amount for {} (from {} to {}) from Trades".format(
|
||||
trade, order['amount'], real_amount))
|
||||
logger.info(f"""Applying fee on amount for {trade} \
|
||||
(from {order_amount} to {real_amount}) from Trades""")
|
||||
return real_amount
|
||||
|
||||
def handle_trade(self, trade: Trade) -> bool:
|
||||
@@ -434,22 +429,39 @@ class FreqtradeBot(object):
|
||||
:return: True if trade has been sold, False otherwise
|
||||
"""
|
||||
if not trade.is_open:
|
||||
raise ValueError('attempt to handle closed trade: {}'.format(trade))
|
||||
raise ValueError(f'attempt to handle closed trade: {trade}')
|
||||
|
||||
logger.debug('Handling %s ...', trade)
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
sell_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
|
||||
if self.config['ask_strategy']['use_book_order']:
|
||||
logger.info('Using order book for selling...')
|
||||
orderBook = exchange.get_order_book(trade.pair)
|
||||
# logger.debug('Order book %s',orderBook)
|
||||
for i in range(self.config['ask_strategy']['book_order_min'],self.config['ask_strategy']['book_order_max']):
|
||||
sell_rate = orderBook['asks'][i-1][0]
|
||||
logger.debug('checking sell rate %s) %.8f > %.8f (%f %%)',i,trade.open_rate,sell_rate,((sell_rate/trade.open_rate)*100)-100)
|
||||
if self.check_sell(trade, sell_rate, buy, sell):
|
||||
return True
|
||||
break
|
||||
else:
|
||||
if self.check_sell(trade, sell_rate, buy, sell):
|
||||
return True
|
||||
|
||||
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
|
||||
return False
|
||||
|
||||
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool ) -> bool:
|
||||
if self.analyze.should_sell(trade, sell_rate, datetime.utcnow(), buy, sell):
|
||||
self.execute_sell(trade, sell_rate)
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_handle_timedout(self, timeoutvalue: int) -> None:
|
||||
"""
|
||||
Check if any orders are timed out and cancel if neccessary
|
||||
@@ -460,6 +472,12 @@ class FreqtradeBot(object):
|
||||
|
||||
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
||||
try:
|
||||
# FIXME: Somehow the query above returns results
|
||||
# where the open_order_id is in fact None.
|
||||
# This is probably because the record got
|
||||
# updated via /forcesell in a different thread.
|
||||
if not trade.open_order_id:
|
||||
continue
|
||||
order = exchange.get_order(trade.open_order_id, trade.pair)
|
||||
except requests.exceptions.RequestException:
|
||||
logger.info(
|
||||
@@ -485,16 +503,14 @@ class FreqtradeBot(object):
|
||||
"""Buy timeout - cancel order
|
||||
:return: True if order was fully cancelled
|
||||
"""
|
||||
pair_s = trade.pair.replace('_', '/')
|
||||
exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||
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)
|
||||
self.rpc.send_msg('*Timeout:* Unfilled buy order for {} cancelled'.format(
|
||||
trade.pair.replace('_', '/')))
|
||||
self.rpc.send_msg(f'*Timeout:* Unfilled buy order for {pair_s} cancelled')
|
||||
return True
|
||||
|
||||
# if trade is partially complete, edit the stake details for the trade
|
||||
@@ -503,8 +519,7 @@ class FreqtradeBot(object):
|
||||
trade.stake_amount = trade.amount * trade.open_rate
|
||||
trade.open_order_id = None
|
||||
logger.info('Partial buy order timeout for %s.', trade)
|
||||
self.rpc.send_msg('*Timeout:* Remaining buy order for {} cancelled'.format(
|
||||
trade.pair.replace('_', '/')))
|
||||
self.rpc.send_msg(f'*Timeout:* Remaining buy order for {pair_s} cancelled')
|
||||
return False
|
||||
|
||||
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
|
||||
@@ -513,6 +528,7 @@ class FreqtradeBot(object):
|
||||
Sell timeout - cancel order and update trade
|
||||
:return: True if order was fully cancelled
|
||||
"""
|
||||
pair_s = trade.pair.replace('_', '/')
|
||||
if order['remaining'] == order['amount']:
|
||||
# if trade is not partially completed, just cancel the trade
|
||||
exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||
@@ -521,8 +537,7 @@ class FreqtradeBot(object):
|
||||
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.rpc.send_msg(f'*Timeout:* Unfilled sell order for {pair_s} cancelled')
|
||||
logger.info('Sell order timeout for %s.', trade)
|
||||
return True
|
||||
|
||||
@@ -536,6 +551,8 @@ class FreqtradeBot(object):
|
||||
:param limit: limit rate for the sell order
|
||||
:return: None
|
||||
"""
|
||||
exc = trade.exchange
|
||||
pair = trade.pair
|
||||
# Execute sell and update trade record
|
||||
order_id = exchange.sell(str(trade.pair), limit, trade.amount)['id']
|
||||
trade.open_order_id = order_id
|
||||
@@ -545,43 +562,31 @@ class FreqtradeBot(object):
|
||||
profit_trade = trade.calc_profit(rate=limit)
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
profit = trade.calc_profit_percent(limit)
|
||||
pair_url = exchange.get_pair_detail_url(trade.pair)
|
||||
gain = "profit" if fmt_exp_profit > 0 else "loss"
|
||||
|
||||
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),
|
||||
)
|
||||
message = f"*{exc}:* Selling\n" \
|
||||
f"*Current Pair:* [{pair}]({pair_url})\n" \
|
||||
f"*Limit:* `{limit}`\n" \
|
||||
f"*Amount:* `{round(trade.amount, 8)}`\n" \
|
||||
f"*Open Rate:* `{trade.open_rate:.8f}`\n" \
|
||||
f"*Current Rate:* `{current_rate:.8f}`\n" \
|
||||
f"*Profit:* `{round(profit * 100, 2):.2f}%`" \
|
||||
""
|
||||
|
||||
# For regular case, when the configuration exists
|
||||
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
|
||||
stake = self.config['stake_currency']
|
||||
fiat = self.config['fiat_display_currency']
|
||||
fiat_converter = CryptoToFiatConverter()
|
||||
profit_fiat = fiat_converter.convert_amount(
|
||||
profit_trade,
|
||||
self.config['stake_currency'],
|
||||
self.config['fiat_display_currency']
|
||||
stake,
|
||||
fiat
|
||||
)
|
||||
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'],
|
||||
)
|
||||
message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \
|
||||
f'` / {profit_fiat:.3f} {fiat})`'\
|
||||
''
|
||||
# Because telegram._forcesell does not have the configuration
|
||||
# Ignore the FIAT value and does not show the stake_currency as well
|
||||
else:
|
||||
|
||||
@@ -13,7 +13,7 @@ def went_down(series: Series) -> bool:
|
||||
return series < series.shift(1)
|
||||
|
||||
|
||||
def ehlers_super_smoother(series: Series, smoothing: float = 6) -> type(Series):
|
||||
def ehlers_super_smoother(series: Series, smoothing: float = 6) -> Series:
|
||||
magic = pi * sqrt(2) / smoothing
|
||||
a1 = exp(-magic)
|
||||
coeff2 = 2 * a1 * cos(magic)
|
||||
|
||||
@@ -7,6 +7,7 @@ import logging
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.configuration import Configuration
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
@@ -47,6 +48,9 @@ def main(sysargv: List[str]) -> None:
|
||||
except KeyboardInterrupt:
|
||||
logger.info('SIGINT received, aborting ...')
|
||||
return_code = 0
|
||||
except OperationalException as e:
|
||||
logger.error(str(e))
|
||||
return_code = 2
|
||||
except BaseException:
|
||||
logger.exception('Fatal exception!')
|
||||
finally:
|
||||
|
||||
@@ -83,7 +83,7 @@ def file_dump_json(filename, data, is_zip=False) -> None:
|
||||
json.dump(data, fp, default=str)
|
||||
|
||||
|
||||
def format_ms_time(date: str) -> str:
|
||||
def format_ms_time(date: int) -> str:
|
||||
"""
|
||||
convert MS date to readable format.
|
||||
: epoch-string in ms
|
||||
|
||||
@@ -4,44 +4,45 @@ import gzip
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional, List, Dict, Tuple, Any
|
||||
import arrow
|
||||
from typing import Optional, List, Dict, Tuple
|
||||
|
||||
from freqtrade import misc, constants
|
||||
from freqtrade.exchange import get_ticker_history
|
||||
from freqtrade.arguments import TimeRange
|
||||
|
||||
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def trim_tickerlist(tickerlist: List[Dict], timerange: Tuple[Tuple, int, int]) -> List[Dict]:
|
||||
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
|
||||
if not tickerlist:
|
||||
return tickerlist
|
||||
|
||||
stype, start, stop = timerange
|
||||
|
||||
start_index = 0
|
||||
stop_index = len(tickerlist)
|
||||
|
||||
if stype[0] == 'line':
|
||||
stop_index = start
|
||||
if stype[0] == 'index':
|
||||
start_index = start
|
||||
elif stype[0] == 'date':
|
||||
while start_index < len(tickerlist) and tickerlist[start_index][0] < start * 1000:
|
||||
if timerange.starttype == 'line':
|
||||
stop_index = timerange.startts
|
||||
if timerange.starttype == 'index':
|
||||
start_index = timerange.startts
|
||||
elif timerange.starttype == 'date':
|
||||
while (start_index < len(tickerlist) and
|
||||
tickerlist[start_index][0] < timerange.startts * 1000):
|
||||
start_index += 1
|
||||
|
||||
if stype[1] == 'line':
|
||||
start_index = len(tickerlist) + stop
|
||||
if stype[1] == 'index':
|
||||
stop_index = stop
|
||||
elif stype[1] == 'date':
|
||||
while stop_index > 0 and tickerlist[stop_index-1][0] > stop * 1000:
|
||||
if timerange.stoptype == 'line':
|
||||
start_index = len(tickerlist) + timerange.stopts
|
||||
if timerange.stoptype == 'index':
|
||||
stop_index = timerange.stopts
|
||||
elif timerange.stoptype == 'date':
|
||||
while (stop_index > 0 and
|
||||
tickerlist[stop_index-1][0] > timerange.stopts * 1000):
|
||||
stop_index -= 1
|
||||
|
||||
if start_index > stop_index:
|
||||
raise ValueError(f'The timerange [{start},{stop}] is incorrect')
|
||||
raise ValueError(f'The timerange [{timerange.startts},{timerange.stopts}] is incorrect')
|
||||
|
||||
return tickerlist[start_index:stop_index]
|
||||
|
||||
@@ -49,7 +50,7 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: Tuple[Tuple, int, int]) -
|
||||
def load_tickerdata_file(
|
||||
datadir: str, pair: str,
|
||||
ticker_interval: str,
|
||||
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Optional[List[Dict]]:
|
||||
timerange: Optional[TimeRange] = None) -> Optional[List[Dict]]:
|
||||
"""
|
||||
Load a pair from file,
|
||||
:return dict OR empty if unsuccesful
|
||||
@@ -84,7 +85,7 @@ def load_data(datadir: str,
|
||||
ticker_interval: str,
|
||||
pairs: Optional[List[str]] = None,
|
||||
refresh_pairs: Optional[bool] = False,
|
||||
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Dict[str, List]:
|
||||
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> Dict[str, List]:
|
||||
"""
|
||||
Loads ticker history data for the given parameters
|
||||
:return: dict
|
||||
@@ -104,8 +105,10 @@ def load_data(datadir: str,
|
||||
result[pair] = pairdata
|
||||
else:
|
||||
logger.warning(
|
||||
'No data for pair %s, use --refresh-pairs-cached to download the data',
|
||||
pair
|
||||
'No data for pair: "%s", Interval: %s. '
|
||||
'Use --refresh-pairs-cached to download the data',
|
||||
pair,
|
||||
ticker_interval
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -122,7 +125,7 @@ def make_testdata_path(datadir: str) -> str:
|
||||
|
||||
def download_pairs(datadir, pairs: List[str],
|
||||
ticker_interval: str,
|
||||
timerange: Optional[Tuple[Tuple, int, int]] = None) -> bool:
|
||||
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> bool:
|
||||
"""For each pairs passed in parameters, download the ticker intervals"""
|
||||
for pair in pairs:
|
||||
try:
|
||||
@@ -142,7 +145,9 @@ def download_pairs(datadir, pairs: List[str],
|
||||
|
||||
def load_cached_data_for_updating(filename: str,
|
||||
tick_interval: str,
|
||||
timerange: Optional[Tuple[Tuple, int, int]]) -> Tuple[list, int]:
|
||||
timerange: Optional[TimeRange]) -> Tuple[
|
||||
List[Any],
|
||||
Optional[int]]:
|
||||
"""
|
||||
Load cached data and choose what part of the data should be updated
|
||||
"""
|
||||
@@ -151,10 +156,10 @@ def load_cached_data_for_updating(filename: str,
|
||||
|
||||
# user sets timerange, so find the start time
|
||||
if timerange:
|
||||
if timerange[0][0] == 'date':
|
||||
since_ms = timerange[1] * 1000
|
||||
elif timerange[0][1] == 'line':
|
||||
num_minutes = timerange[2] * constants.TICKER_INTERVAL_MINUTES[tick_interval]
|
||||
if timerange.starttype == 'date':
|
||||
since_ms = timerange.startts * 1000
|
||||
elif timerange.stoptype == 'line':
|
||||
num_minutes = timerange.stopts * constants.TICKER_INTERVAL_MINUTES[tick_interval]
|
||||
since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000
|
||||
|
||||
# read the cached file
|
||||
@@ -184,7 +189,7 @@ def load_cached_data_for_updating(filename: str,
|
||||
def download_backtesting_testdata(datadir: str,
|
||||
pair: str,
|
||||
tick_interval: str = '5m',
|
||||
timerange: Optional[Tuple[Tuple, int, int]] = None) -> None:
|
||||
timerange: Optional[TimeRange] = None) -> None:
|
||||
|
||||
"""
|
||||
Download the latest ticker intervals from the exchange for the pairs passed in parameters
|
||||
|
||||
@@ -33,18 +33,6 @@ class Backtesting(object):
|
||||
"""
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
self.config = config
|
||||
self.analyze = None
|
||||
self.ticker_interval = None
|
||||
self.tickerdata_to_dataframe = None
|
||||
self.populate_buy_trend = None
|
||||
self.populate_sell_trend = None
|
||||
self._init()
|
||||
|
||||
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
|
||||
@@ -78,7 +66,7 @@ class Backtesting(object):
|
||||
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')
|
||||
stake_currency = str(self.config.get('stake_currency'))
|
||||
|
||||
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
|
||||
tabular_data = []
|
||||
@@ -106,7 +94,7 @@ class Backtesting(object):
|
||||
len(results[results.profit_BTC > 0]),
|
||||
len(results[results.profit_BTC < 0])
|
||||
])
|
||||
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
|
||||
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
|
||||
|
||||
def _get_sell_trade_entry(
|
||||
self, pair: str, buy_row: DataFrame,
|
||||
@@ -166,13 +154,22 @@ class Backtesting(object):
|
||||
max_open_trades = args.get('max_open_trades', 0)
|
||||
realistic = args.get('realistic', False)
|
||||
record = args.get('record', None)
|
||||
recordfilename = args.get('recordfn', 'backtest-result.json')
|
||||
records = []
|
||||
trades = []
|
||||
trade_count_lock = {}
|
||||
trade_count_lock: Dict = {}
|
||||
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_data = self.populate_sell_trend(
|
||||
self.populate_buy_trend(pair_data))[headers].copy()
|
||||
|
||||
# to avoid using data from future, we buy/sell with signal from previous candle
|
||||
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1)
|
||||
ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1)
|
||||
|
||||
ticker_data.drop(ticker_data.head(1).index, inplace=True)
|
||||
|
||||
ticker = [x for x in ticker_data.itertuples()]
|
||||
|
||||
lock_pair_until = None
|
||||
@@ -208,8 +205,8 @@ class Backtesting(object):
|
||||
# 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:
|
||||
logger.info('Dumping backtest results')
|
||||
file_dump_json('backtest-result.json', records)
|
||||
logger.info('Dumping backtest results to %s', recordfilename)
|
||||
file_dump_json(recordfilename, records)
|
||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
||||
return DataFrame.from_records(trades, columns=labels)
|
||||
|
||||
@@ -230,7 +227,8 @@ class Backtesting(object):
|
||||
else:
|
||||
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
||||
|
||||
timerange = Arguments.parse_timerange(self.config.get('timerange'))
|
||||
timerange = Arguments.parse_timerange(None if self.config.get(
|
||||
'timerange') is None else str(self.config.get('timerange')))
|
||||
data = optimize.load_data(
|
||||
self.config['datadir'],
|
||||
pairs=pairs,
|
||||
@@ -268,7 +266,8 @@ class Backtesting(object):
|
||||
'realistic': self.config.get('realistic_simulation', False),
|
||||
'sell_profit_only': sell_profit_only,
|
||||
'use_sell_signal': use_sell_signal,
|
||||
'record': self.config.get('export')
|
||||
'record': self.config.get('export'),
|
||||
'recordfn': self.config.get('exportfilename'),
|
||||
}
|
||||
)
|
||||
logger.info(
|
||||
|
||||
@@ -14,7 +14,7 @@ from argparse import Namespace
|
||||
from functools import reduce
|
||||
from math import exp
|
||||
from operator import itemgetter
|
||||
from typing import Dict, Any, Callable
|
||||
from typing import Dict, Any, Callable, Optional
|
||||
|
||||
import numpy
|
||||
import talib.abstract as ta
|
||||
@@ -60,7 +60,7 @@ class Hyperopt(Backtesting):
|
||||
self.expected_max_profit = 3.0
|
||||
|
||||
# Configuration and data used by hyperopt
|
||||
self.processed = None
|
||||
self.processed: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Hyperopt Trials
|
||||
self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle')
|
||||
@@ -344,7 +344,7 @@ class Hyperopt(Backtesting):
|
||||
"""
|
||||
Return the space to use during Hyperopt
|
||||
"""
|
||||
spaces = {}
|
||||
spaces: Dict = {}
|
||||
if self.has_space('buy'):
|
||||
spaces = {**spaces, **Hyperopt.indicator_space()}
|
||||
if self.has_space('roi'):
|
||||
@@ -455,6 +455,7 @@ class Hyperopt(Backtesting):
|
||||
|
||||
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
|
||||
print('.', end='')
|
||||
sys.stdout.flush()
|
||||
return {
|
||||
'status': STATUS_FAIL,
|
||||
'loss': float('inf')
|
||||
@@ -479,31 +480,32 @@ class Hyperopt(Backtesting):
|
||||
'result': result_explanation,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def format_results(results: DataFrame) -> str:
|
||||
def format_results(self, results: DataFrame) -> str:
|
||||
"""
|
||||
Return the format result in a string
|
||||
"""
|
||||
return ('{:6d} trades. Avg profit {: 5.2f}%. '
|
||||
'Total profit {: 11.8f} BTC ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
|
||||
'Total profit {: 11.8f} {} ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
|
||||
len(results.index),
|
||||
results.profit_percent.mean() * 100.0,
|
||||
results.profit_BTC.sum(),
|
||||
self.config['stake_currency'],
|
||||
results.profit_percent.sum(),
|
||||
results.duration.mean(),
|
||||
)
|
||||
|
||||
def start(self) -> None:
|
||||
timerange = Arguments.parse_timerange(self.config.get('timerange'))
|
||||
timerange = Arguments.parse_timerange(None if self.config.get(
|
||||
'timerange') is None else str(self.config.get('timerange')))
|
||||
data = load_data(
|
||||
datadir=self.config.get('datadir'),
|
||||
datadir=str(self.config.get('datadir')),
|
||||
pairs=self.config['exchange']['pair_whitelist'],
|
||||
ticker_interval=self.ticker_interval,
|
||||
timerange=timerange
|
||||
)
|
||||
|
||||
if self.has_space('buy'):
|
||||
self.analyze.populate_indicators = Hyperopt.populate_indicators
|
||||
self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore
|
||||
self.processed = self.tickerdata_to_dataframe(data)
|
||||
|
||||
if self.config.get('mongodb'):
|
||||
|
||||
@@ -5,48 +5,54 @@ This module contains the class to persist trades into SQLite
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, getcontext
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, Any
|
||||
|
||||
import arrow
|
||||
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
|
||||
create_engine)
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy.exc import NoSuchModuleError
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm.scoping import scoped_session
|
||||
from sqlalchemy.orm.session import sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from freqtrade import OperationalException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_CONF = {}
|
||||
_DECL_BASE = declarative_base()
|
||||
_DECL_BASE: Any = declarative_base()
|
||||
|
||||
|
||||
def init(config: dict, engine: Optional[Engine] = None) -> None:
|
||||
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
|
||||
:param engine: database engine for sqlalchemy (Optional)
|
||||
:return: None
|
||||
"""
|
||||
_CONF.update(config)
|
||||
if not engine:
|
||||
if _CONF.get('dry_run', False):
|
||||
# the user wants dry run to use a DB
|
||||
if _CONF.get('dry_run_db', False):
|
||||
engine = create_engine('sqlite:///tradesv3.dry_run.sqlite')
|
||||
# Otherwise dry run will store in memory
|
||||
else:
|
||||
engine = create_engine('sqlite://',
|
||||
connect_args={'check_same_thread': False},
|
||||
poolclass=StaticPool,
|
||||
echo=False)
|
||||
else:
|
||||
engine = create_engine('sqlite:///tradesv3.sqlite')
|
||||
|
||||
db_url = _CONF.get('db_url', None)
|
||||
kwargs = {}
|
||||
|
||||
# Take care of thread ownership if in-memory db
|
||||
if db_url == 'sqlite://':
|
||||
kwargs.update({
|
||||
'connect_args': {'check_same_thread': False},
|
||||
'poolclass': StaticPool,
|
||||
'echo': False,
|
||||
})
|
||||
|
||||
try:
|
||||
engine = create_engine(db_url, **kwargs)
|
||||
except NoSuchModuleError:
|
||||
error = 'Given value for db_url: \'{}\' is no valid database URL! (See {}).'.format(
|
||||
db_url, 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
|
||||
)
|
||||
raise OperationalException(error)
|
||||
|
||||
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
||||
Trade.session = session()
|
||||
@@ -54,8 +60,8 @@ def init(config: dict, engine: Optional[Engine] = None) -> None:
|
||||
_DECL_BASE.metadata.create_all(engine)
|
||||
check_migrate(engine)
|
||||
|
||||
# Clean dry_run DB
|
||||
if _CONF.get('dry_run', False) and _CONF.get('dry_run_db', False):
|
||||
# Clean dry_run DB if the db is not in-memory
|
||||
if _CONF.get('dry_run', False) and db_url != 'sqlite://':
|
||||
clean_dry_run_db()
|
||||
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
This module contains class to define a RPC communications
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
from typing import Tuple, Any
|
||||
from typing import Dict, Tuple, Any
|
||||
|
||||
import arrow
|
||||
import sqlalchemy as sql
|
||||
from pandas import DataFrame
|
||||
from numpy import mean, nan_to_num
|
||||
|
||||
from freqtrade import exchange
|
||||
from freqtrade.misc import shorten_date
|
||||
@@ -117,7 +118,7 @@ class RPC(object):
|
||||
self, timescale: int,
|
||||
stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
|
||||
today = datetime.utcnow().date()
|
||||
profit_days = {}
|
||||
profit_days: Dict[date, Dict] = {}
|
||||
|
||||
if not (isinstance(timescale, int) and timescale > 0):
|
||||
return True, '*Daily [n]:* `must be an integer greater than 0`'
|
||||
@@ -175,7 +176,7 @@ class RPC(object):
|
||||
durations = []
|
||||
|
||||
for trade in trades:
|
||||
current_rate = None
|
||||
current_rate: float = 0.0
|
||||
|
||||
if not trade.open_rate:
|
||||
continue
|
||||
@@ -212,14 +213,14 @@ class RPC(object):
|
||||
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_percent = round(nan_to_num(mean(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_percent = round(nan_to_num(mean(profit_all_percent)) * 100, 2)
|
||||
profit_all_fiat = fiat.convert_amount(
|
||||
profit_all_coin,
|
||||
stake_currency,
|
||||
@@ -281,7 +282,7 @@ class RPC(object):
|
||||
value = fiat.convert_amount(total, 'BTC', symbol)
|
||||
return False, (output, total, symbol, value)
|
||||
|
||||
def rpc_start(self) -> (bool, str):
|
||||
def rpc_start(self) -> Tuple[bool, str]:
|
||||
"""
|
||||
Handler for start.
|
||||
"""
|
||||
@@ -291,7 +292,7 @@ class RPC(object):
|
||||
self.freqtrade.state = State.RUNNING
|
||||
return False, '`Starting trader ...`'
|
||||
|
||||
def rpc_stop(self) -> (bool, str):
|
||||
def rpc_stop(self) -> Tuple[bool, str]:
|
||||
"""
|
||||
Handler for stop.
|
||||
"""
|
||||
@@ -356,6 +357,7 @@ class RPC(object):
|
||||
return True, 'Invalid argument.'
|
||||
|
||||
_exec_forcesell(trade)
|
||||
Trade.session.flush()
|
||||
return False, ''
|
||||
|
||||
def rpc_performance(self) -> Tuple[bool, Any]:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
This module contains class to manage RPC communications (Telegram, Slack, ...)
|
||||
"""
|
||||
from typing import Any, List
|
||||
import logging
|
||||
|
||||
from freqtrade.rpc.telegram import Telegram
|
||||
@@ -21,8 +22,8 @@ class RPCManager(object):
|
||||
"""
|
||||
self.freqtrade = freqtrade
|
||||
|
||||
self.registered_modules = []
|
||||
self.telegram = None
|
||||
self.registered_modules: List[str] = []
|
||||
self.telegram: Any = None
|
||||
self._init()
|
||||
|
||||
def _init(self) -> None:
|
||||
|
||||
@@ -18,7 +18,7 @@ from freqtrade.rpc.rpc import RPC
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
|
||||
def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]:
|
||||
"""
|
||||
Decorator to check if the message comes from the correct chat_id
|
||||
:param command_handler: Telegram CommandHandler
|
||||
@@ -65,7 +65,7 @@ class Telegram(RPC):
|
||||
"""
|
||||
super().__init__(freqtrade)
|
||||
|
||||
self._updater = None
|
||||
self._updater: Updater = None
|
||||
self._config = freqtrade.config
|
||||
self._init()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
IStrategy interface
|
||||
This module defines the interface to apply for strategies
|
||||
"""
|
||||
|
||||
from typing import Dict
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from pandas import DataFrame
|
||||
@@ -16,9 +16,13 @@ class IStrategy(ABC):
|
||||
Attributes you can use:
|
||||
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
||||
stoploss -> float: optimal stoploss designed for the strategy
|
||||
ticker_interval -> int: value of the ticker interval to use for the strategy
|
||||
ticker_interval -> str: value of the ticker interval to use for the strategy
|
||||
"""
|
||||
|
||||
minimal_roi: Dict
|
||||
stoploss: float
|
||||
ticker_interval: str
|
||||
|
||||
@abstractmethod
|
||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
|
||||
@@ -36,7 +36,8 @@ class StrategyResolver(object):
|
||||
|
||||
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
||||
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
|
||||
self.strategy = self._load_strategy(strategy_name, extra_dir=config.get('strategy_path'))
|
||||
self.strategy: IStrategy = self._load_strategy(strategy_name,
|
||||
extra_dir=config.get('strategy_path'))
|
||||
|
||||
# Set attributes
|
||||
# Check if we need to override configuration
|
||||
@@ -64,7 +65,7 @@ class StrategyResolver(object):
|
||||
self.strategy.stoploss = float(self.strategy.stoploss)
|
||||
|
||||
def _load_strategy(
|
||||
self, strategy_name: str, extra_dir: Optional[str] = None) -> Optional[IStrategy]:
|
||||
self, strategy_name: str, extra_dir: Optional[str] = None) -> IStrategy:
|
||||
"""
|
||||
Search and loads the specified strategy.
|
||||
:param strategy_name: name of the module to import
|
||||
@@ -123,7 +124,7 @@ class StrategyResolver(object):
|
||||
# Generate spec based on absolute path
|
||||
spec = importlib.util.spec_from_file_location('user_data.strategies', module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
|
||||
|
||||
valid_strategies_gen = (
|
||||
obj for name, obj in inspect.getmembers(module, inspect.isclass)
|
||||
|
||||
@@ -9,7 +9,6 @@ from unittest.mock import MagicMock
|
||||
import arrow
|
||||
import pytest
|
||||
from jsonschema import validate
|
||||
from sqlalchemy import create_engine
|
||||
from telegram import Chat, Message, Update
|
||||
|
||||
from freqtrade.analyze import Analyze
|
||||
@@ -45,7 +44,7 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.Analyze.get_signal', MagicMock())
|
||||
|
||||
return FreqtradeBot(config, create_engine('sqlite://'))
|
||||
return FreqtradeBot(config)
|
||||
|
||||
|
||||
def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> None:
|
||||
@@ -88,7 +87,14 @@ def default_conf():
|
||||
"stoploss": -0.10,
|
||||
"unfilledtimeout": 600,
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
"use_book_order": False,
|
||||
"book_order_top": 6,
|
||||
"ask_last_balance": 0.0,
|
||||
},
|
||||
"ask_strategy": {
|
||||
"use_book_order": False,
|
||||
"book_order_min": 1,
|
||||
"book_order_max": 10
|
||||
},
|
||||
"exchange": {
|
||||
"name": "bittrex",
|
||||
@@ -108,7 +114,8 @@ def default_conf():
|
||||
"chat_id": "0"
|
||||
},
|
||||
"initial_state": "running",
|
||||
"loglevel": logging.DEBUG
|
||||
"db_url": "sqlite://",
|
||||
"loglevel": logging.DEBUG,
|
||||
}
|
||||
validate(configuration, constants.CONF_SCHEMA)
|
||||
return configuration
|
||||
|
||||
@@ -310,9 +310,19 @@ def test_get_ticker(default_conf, mocker):
|
||||
# if not fetching a new result we should get the cached ticker
|
||||
ticker = get_ticker(pair='ETH/BTC')
|
||||
|
||||
assert api_mock.fetch_ticker.call_count == 1
|
||||
assert ticker['bid'] == 0.5
|
||||
assert ticker['ask'] == 1
|
||||
|
||||
assert 'ETH/BTC' in exchange._CACHED_TICKER
|
||||
assert exchange._CACHED_TICKER['ETH/BTC']['bid'] == 0.5
|
||||
assert exchange._CACHED_TICKER['ETH/BTC']['ask'] == 1
|
||||
|
||||
# Test caching
|
||||
api_mock.fetch_ticker = MagicMock()
|
||||
get_ticker(pair='ETH/BTC', refresh=False)
|
||||
assert api_mock.fetch_ticker.call_count == 0
|
||||
|
||||
with pytest.raises(TemporaryError): # test retrier
|
||||
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError)
|
||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||
@@ -323,6 +333,10 @@ def test_get_ticker(default_conf, mocker):
|
||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||
get_ticker(pair='ETH/BTC', refresh=True)
|
||||
|
||||
api_mock.fetch_ticker = MagicMock(return_value={})
|
||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||
get_ticker(pair='ETH/BTC', refresh=True)
|
||||
|
||||
|
||||
def make_fetch_ohlcv_mock(data):
|
||||
def fetch_ohlcv_mock(pair, timeframe, since):
|
||||
@@ -393,6 +407,78 @@ def test_get_ticker_history(default_conf, mocker):
|
||||
get_ticker_history('EFGH/BTC', default_conf['ticker_interval'])
|
||||
|
||||
|
||||
def test_get_ticker_history_sort(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
|
||||
# GDAX use-case (real data from GDAX)
|
||||
# This ticker history is ordered DESC (newest first, oldest last)
|
||||
tick = [
|
||||
[1527833100000, 0.07666, 0.07671, 0.07666, 0.07668, 16.65244264],
|
||||
[1527832800000, 0.07662, 0.07666, 0.07662, 0.07666, 1.30051526],
|
||||
[1527832500000, 0.07656, 0.07661, 0.07656, 0.07661, 12.034778840000001],
|
||||
[1527832200000, 0.07658, 0.07658, 0.07655, 0.07656, 0.59780186],
|
||||
[1527831900000, 0.07658, 0.07658, 0.07658, 0.07658, 1.76278136],
|
||||
[1527831600000, 0.07658, 0.07658, 0.07658, 0.07658, 2.22646521],
|
||||
[1527831300000, 0.07655, 0.07657, 0.07655, 0.07657, 1.1753],
|
||||
[1527831000000, 0.07654, 0.07654, 0.07651, 0.07651, 0.8073060299999999],
|
||||
[1527830700000, 0.07652, 0.07652, 0.07651, 0.07652, 10.04822687],
|
||||
[1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867]
|
||||
]
|
||||
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
|
||||
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
|
||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||
|
||||
# Test the ticker history sort
|
||||
ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
|
||||
assert ticks[0][0] == 1527830400000
|
||||
assert ticks[0][1] == 0.07649
|
||||
assert ticks[0][2] == 0.07651
|
||||
assert ticks[0][3] == 0.07649
|
||||
assert ticks[0][4] == 0.07651
|
||||
assert ticks[0][5] == 2.5734867
|
||||
|
||||
assert ticks[9][0] == 1527833100000
|
||||
assert ticks[9][1] == 0.07666
|
||||
assert ticks[9][2] == 0.07671
|
||||
assert ticks[9][3] == 0.07666
|
||||
assert ticks[9][4] == 0.07668
|
||||
assert ticks[9][5] == 16.65244264
|
||||
|
||||
# Bittrex use-case (real data from Bittrex)
|
||||
# This ticker history is ordered ASC (oldest first, newest last)
|
||||
tick = [
|
||||
[1527827700000, 0.07659999, 0.0766, 0.07627, 0.07657998, 1.85216924],
|
||||
[1527828000000, 0.07657995, 0.07657995, 0.0763, 0.0763, 26.04051037],
|
||||
[1527828300000, 0.0763, 0.07659998, 0.0763, 0.0764, 10.36434124],
|
||||
[1527828600000, 0.0764, 0.0766, 0.0764, 0.0766, 5.71044773],
|
||||
[1527828900000, 0.0764, 0.07666998, 0.0764, 0.07666998, 47.48888565],
|
||||
[1527829200000, 0.0765, 0.07672999, 0.0765, 0.07672999, 3.37640326],
|
||||
[1527829500000, 0.0766, 0.07675, 0.0765, 0.07675, 8.36203831],
|
||||
[1527829800000, 0.07675, 0.07677999, 0.07620002, 0.076695, 119.22963884],
|
||||
[1527830100000, 0.076695, 0.07671, 0.07624171, 0.07671, 1.80689244],
|
||||
[1527830400000, 0.07671, 0.07674399, 0.07629216, 0.07655213, 2.31452783]
|
||||
]
|
||||
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
|
||||
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
|
||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||
|
||||
# Test the ticker history sort
|
||||
ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
|
||||
assert ticks[0][0] == 1527827700000
|
||||
assert ticks[0][1] == 0.07659999
|
||||
assert ticks[0][2] == 0.0766
|
||||
assert ticks[0][3] == 0.07627
|
||||
assert ticks[0][4] == 0.07657998
|
||||
assert ticks[0][5] == 1.85216924
|
||||
|
||||
assert ticks[9][0] == 1527830400000
|
||||
assert ticks[9][1] == 0.07671
|
||||
assert ticks[9][2] == 0.07674399
|
||||
assert ticks[9][3] == 0.07629216
|
||||
assert ticks[9][4] == 0.07655213
|
||||
assert ticks[9][5] == 2.31452783
|
||||
|
||||
|
||||
def test_cancel_order_dry_run(default_conf, mocker):
|
||||
default_conf['dry_run'] = True
|
||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||
|
||||
@@ -13,7 +13,7 @@ from arrow import Arrow
|
||||
|
||||
from freqtrade import optimize
|
||||
from freqtrade.analyze import Analyze
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.arguments import Arguments, TimeRange
|
||||
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
|
||||
from freqtrade.tests.conftest import log_has
|
||||
|
||||
@@ -30,7 +30,7 @@ def trim_dictlist(dict_list, num):
|
||||
|
||||
|
||||
def load_data_test(what):
|
||||
timerange = ((None, 'line'), None, -100)
|
||||
timerange = TimeRange(None, 'line', 0, -101)
|
||||
data = optimize.load_data(None, ticker_interval='1m',
|
||||
pairs=['UNITTEST/BTC'], timerange=timerange)
|
||||
pair = data['UNITTEST/BTC']
|
||||
@@ -112,14 +112,14 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals
|
||||
# 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)
|
||||
ticks = trim_dictlist(ticks, -201)
|
||||
return ticks[pair]
|
||||
|
||||
|
||||
# FIX: fixturize this?
|
||||
def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None):
|
||||
data = optimize.load_data(None, ticker_interval='8m', pairs=[pair])
|
||||
data = trim_dictlist(data, -200)
|
||||
data = trim_dictlist(data, -201)
|
||||
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
|
||||
backtesting = Backtesting(conf)
|
||||
return {
|
||||
@@ -183,7 +183,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert log_has(
|
||||
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
||||
'Using data folder: {} ...'.format(config['datadir']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert 'ticker_interval' in config
|
||||
@@ -220,7 +220,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||
'--realistic-simulation',
|
||||
'--refresh-pairs-cached',
|
||||
'--timerange', ':100',
|
||||
'--export', '/bar/foo'
|
||||
'--export', '/bar/foo',
|
||||
'--export-filename', 'foo_bar.json'
|
||||
]
|
||||
|
||||
config = setup_configuration(get_args(args))
|
||||
@@ -231,7 +232,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert log_has(
|
||||
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
||||
'Using data folder: {} ...'.format(config['datadir']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert 'ticker_interval' in config
|
||||
@@ -261,6 +262,11 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||
'Parameter --export detected: {} ...'.format(config['export']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert 'exportfilename' in config
|
||||
assert log_has(
|
||||
'Storing backtest results to {} ...'.format(config['exportfilename']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
|
||||
|
||||
def test_start(mocker, fee, default_conf, caplog) -> None:
|
||||
@@ -288,23 +294,6 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
|
||||
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(mocker, default_conf) -> None:
|
||||
"""
|
||||
Test Backtesting._init() method
|
||||
@@ -324,13 +313,13 @@ def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
|
||||
Test Backtesting.tickerdata_to_dataframe() method
|
||||
"""
|
||||
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
|
||||
timerange = ((None, 'line'), None, -100)
|
||||
timerange = TimeRange(None, 'line', 0, -100)
|
||||
tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||
tickerlist = {'UNITTEST/BTC': tick}
|
||||
|
||||
backtesting = Backtesting(default_conf)
|
||||
data = backtesting.tickerdata_to_dataframe(tickerlist)
|
||||
assert len(data['UNITTEST/BTC']) == 100
|
||||
assert len(data['UNITTEST/BTC']) == 99
|
||||
|
||||
# Load Analyze to compare the result between Backtesting function and Analyze are the same
|
||||
analyze = Analyze(default_conf)
|
||||
@@ -354,7 +343,7 @@ def test_get_timeframe(default_conf, mocker) -> None:
|
||||
)
|
||||
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'
|
||||
assert max_date.isoformat() == '2017-11-14T22:58:00+00:00'
|
||||
|
||||
|
||||
def test_generate_text_table(default_conf, mocker):
|
||||
@@ -376,16 +365,15 @@ def test_generate_text_table(default_conf, mocker):
|
||||
)
|
||||
|
||||
result_str = (
|
||||
'pair buy count avg profit % '
|
||||
'total profit BTC avg duration profit loss\n'
|
||||
'------- ----------- -------------- '
|
||||
'------------------ -------------- -------- ------\n'
|
||||
'ETH/BTC 2 15.00 '
|
||||
'0.60000000 20.0 2 0\n'
|
||||
'TOTAL 2 15.00 '
|
||||
'0.60000000 20.0 2 0'
|
||||
'| pair | buy count | avg profit % | '
|
||||
'total profit BTC | avg duration | profit | loss |\n'
|
||||
'|:--------|------------:|---------------:|'
|
||||
'-------------------:|---------------:|---------:|-------:|\n'
|
||||
'| ETH/BTC | 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={'ETH/BTC': {}}, results=results) == result_str
|
||||
|
||||
|
||||
@@ -492,7 +480,7 @@ def test_processed(default_conf, mocker) -> None:
|
||||
|
||||
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
||||
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
|
||||
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
|
||||
tests = [['raise', 17], ['lower', 0], ['sine', 16]]
|
||||
for [contour, numres] in tests:
|
||||
simple_backtest(default_conf, contour, numres, mocker)
|
||||
|
||||
@@ -620,10 +608,12 @@ def test_backtest_start_live(default_conf, mocker, caplog):
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'--datadir', 'freqtrade/tests/testdata',
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--live',
|
||||
'--timerange', '-100'
|
||||
'--timerange', '-100',
|
||||
'--realistic-simulation'
|
||||
]
|
||||
args = get_args(args)
|
||||
start(args)
|
||||
@@ -633,13 +623,14 @@ def test_backtest_start_live(default_conf, mocker, caplog):
|
||||
'Using ticker_interval: 1m ...',
|
||||
'Parameter -l/--live detected ...',
|
||||
'Using max_open_trades: 1 ...',
|
||||
'Parameter --timerange detected: -100 ..',
|
||||
'Parameter --datadir detected: freqtrade/tests/testdata ...',
|
||||
'Parameter --timerange detected: -100 ...',
|
||||
'Using data folder: 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)..'
|
||||
'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
||||
'Parameter --realistic-simulation detected ...'
|
||||
]
|
||||
|
||||
for line in exists:
|
||||
log_has(line, caplog.record_tuples)
|
||||
assert log_has(line, caplog.record_tuples)
|
||||
|
||||
@@ -389,10 +389,12 @@ def test_start_uses_mongotrials(mocker, init_hyperopt, default_conf) -> None:
|
||||
# test buy_strategy_generator def populate_buy_trend
|
||||
# test optimizer if 'ro_t1' in params
|
||||
|
||||
def test_format_results():
|
||||
def test_format_results(init_hyperopt):
|
||||
"""
|
||||
Test Hyperopt.format_results()
|
||||
"""
|
||||
|
||||
# Test with BTC as stake_currency
|
||||
trades = [
|
||||
('ETH/BTC', 2, 2, 123),
|
||||
('LTC/BTC', 1, 1, 123),
|
||||
@@ -400,8 +402,21 @@ def test_format_results():
|
||||
]
|
||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
||||
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||
x = Hyperopt.format_results(df)
|
||||
assert x.find(' 66.67%')
|
||||
|
||||
result = _HYPEROPT.format_results(df)
|
||||
assert result.find(' 66.67%')
|
||||
assert result.find('Total profit 1.00000000 BTC')
|
||||
assert result.find('2.0000Σ %')
|
||||
|
||||
# Test with EUR as stake_currency
|
||||
trades = [
|
||||
('ETH/EUR', 2, 2, 123),
|
||||
('LTC/EUR', 1, 1, 123),
|
||||
('XPR/EUR', -1, -2, -246)
|
||||
]
|
||||
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||
result = _HYPEROPT.format_results(df)
|
||||
assert result.find('Total profit 1.00000000 EUR')
|
||||
|
||||
|
||||
def test_signal_handler(mocker, init_hyperopt):
|
||||
|
||||
@@ -11,6 +11,7 @@ from freqtrade.misc import file_dump_json
|
||||
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs, \
|
||||
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist, \
|
||||
load_cached_data_for_updating
|
||||
from freqtrade.arguments import TimeRange
|
||||
from freqtrade.tests.conftest import log_has
|
||||
|
||||
# Change this if modifying UNITTEST/BTC testdatafile
|
||||
@@ -105,7 +106,8 @@ def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog) -> None:
|
||||
refresh_pairs=False,
|
||||
pairs=['MEME/BTC'])
|
||||
assert os.path.isfile(file) is False
|
||||
assert log_has('No data for pair MEME/BTC, use --refresh-pairs-cached to download the data',
|
||||
assert log_has('No data for pair: "MEME/BTC", Interval: 1m. '
|
||||
'Use --refresh-pairs-cached to download the data',
|
||||
caplog.record_tuples)
|
||||
|
||||
# download a new pair if refresh_pairs is set
|
||||
@@ -175,7 +177,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
||||
|
||||
# timeframe starts earlier than the cached data
|
||||
# should fully update data
|
||||
timerange = (('date', None), test_data[0][0] / 1000 - 1, None)
|
||||
timerange = TimeRange('date', None, test_data[0][0] / 1000 - 1, 0)
|
||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||
'1m',
|
||||
timerange)
|
||||
@@ -186,13 +188,13 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
||||
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120
|
||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||
'1m',
|
||||
((None, 'line'), None, -num_lines))
|
||||
TimeRange(None, 'line', 0, -num_lines))
|
||||
assert data == []
|
||||
assert start_ts < test_data[0][0] - 1
|
||||
|
||||
# timeframe starts in the center of the cached data
|
||||
# should return the chached data w/o the last item
|
||||
timerange = (('date', None), test_data[0][0] / 1000 + 1, None)
|
||||
timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0)
|
||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||
'1m',
|
||||
timerange)
|
||||
@@ -201,7 +203,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
||||
|
||||
# same with 'line' timeframe
|
||||
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 30
|
||||
timerange = ((None, 'line'), None, -num_lines)
|
||||
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||
'1m',
|
||||
timerange)
|
||||
@@ -210,7 +212,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
||||
|
||||
# timeframe starts after the chached data
|
||||
# should return the chached data w/o the last item
|
||||
timerange = (('date', None), test_data[-1][0] / 1000 + 1, None)
|
||||
timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 1, 0)
|
||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||
'1m',
|
||||
timerange)
|
||||
@@ -219,7 +221,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
||||
|
||||
# same with 'line' timeframe
|
||||
num_lines = 30
|
||||
timerange = ((None, 'line'), None, -num_lines)
|
||||
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||
'1m',
|
||||
timerange)
|
||||
@@ -229,7 +231,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
||||
# no timeframe is set
|
||||
# should return the chached data w/o the last item
|
||||
num_lines = 30
|
||||
timerange = ((None, 'line'), None, -num_lines)
|
||||
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||
'1m',
|
||||
timerange)
|
||||
@@ -238,7 +240,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
||||
|
||||
# no datafile exist
|
||||
# should return timestamp start time
|
||||
timerange = (('date', None), now_ts - 10000, None)
|
||||
timerange = TimeRange('date', None, now_ts - 10000, 0)
|
||||
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
|
||||
'1m',
|
||||
timerange)
|
||||
@@ -247,7 +249,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
||||
|
||||
# same with 'line' timeframe
|
||||
num_lines = 30
|
||||
timerange = ((None, 'line'), None, -num_lines)
|
||||
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
|
||||
'1m',
|
||||
timerange)
|
||||
@@ -342,7 +344,7 @@ def test_trim_tickerlist() -> None:
|
||||
|
||||
# Test the pattern ^(-\d+)$
|
||||
# This pattern uses the latest N elements
|
||||
timerange = ((None, 'line'), None, -5)
|
||||
timerange = TimeRange(None, 'line', 0, -5)
|
||||
ticker = trim_tickerlist(ticker_list, timerange)
|
||||
ticker_len = len(ticker)
|
||||
|
||||
@@ -352,7 +354,7 @@ def test_trim_tickerlist() -> None:
|
||||
|
||||
# Test the pattern ^(\d+)-$
|
||||
# This pattern keep X element from the end
|
||||
timerange = (('line', None), 5, None)
|
||||
timerange = TimeRange('line', None, 5, 0)
|
||||
ticker = trim_tickerlist(ticker_list, timerange)
|
||||
ticker_len = len(ticker)
|
||||
|
||||
@@ -362,7 +364,7 @@ def test_trim_tickerlist() -> None:
|
||||
|
||||
# Test the pattern ^(\d+)-(\d+)$
|
||||
# This pattern extract a window
|
||||
timerange = (('index', 'index'), 5, 10)
|
||||
timerange = TimeRange('index', 'index', 5, 10)
|
||||
ticker = trim_tickerlist(ticker_list, timerange)
|
||||
ticker_len = len(ticker)
|
||||
|
||||
@@ -373,7 +375,7 @@ def test_trim_tickerlist() -> None:
|
||||
|
||||
# Test the pattern ^(\d{8})-(\d{8})$
|
||||
# This pattern extract a window between the dates
|
||||
timerange = (('date', 'date'), ticker_list[5][0] / 1000, ticker_list[10][0] / 1000 - 1)
|
||||
timerange = TimeRange('date', 'date', ticker_list[5][0] / 1000, ticker_list[10][0] / 1000 - 1)
|
||||
ticker = trim_tickerlist(ticker_list, timerange)
|
||||
ticker_len = len(ticker)
|
||||
|
||||
@@ -384,7 +386,7 @@ def test_trim_tickerlist() -> None:
|
||||
|
||||
# Test the pattern ^-(\d{8})$
|
||||
# This pattern extracts elements from the start to the date
|
||||
timerange = ((None, 'date'), None, ticker_list[10][0] / 1000 - 1)
|
||||
timerange = TimeRange(None, 'date', 0, ticker_list[10][0] / 1000 - 1)
|
||||
ticker = trim_tickerlist(ticker_list, timerange)
|
||||
ticker_len = len(ticker)
|
||||
|
||||
@@ -394,7 +396,7 @@ def test_trim_tickerlist() -> None:
|
||||
|
||||
# Test the pattern ^(\d{8})-$
|
||||
# This pattern extracts elements from the date to now
|
||||
timerange = (('date', None), ticker_list[10][0] / 1000 - 1, None)
|
||||
timerange = TimeRange('date', None, ticker_list[10][0] / 1000 - 1, None)
|
||||
ticker = trim_tickerlist(ticker_list, timerange)
|
||||
ticker_len = len(ticker)
|
||||
|
||||
@@ -404,7 +406,7 @@ def test_trim_tickerlist() -> None:
|
||||
|
||||
# Test a wrong pattern
|
||||
# This pattern must return the list unchanged
|
||||
timerange = ((None, None), None, 5)
|
||||
timerange = TimeRange(None, None, None, 5)
|
||||
ticker = trim_tickerlist(ticker_list, timerange)
|
||||
ticker_len = len(ticker)
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ Unit test file for rpc/rpc.py
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc.rpc import RPC
|
||||
@@ -39,7 +37,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
get_fee=fee
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
@@ -89,7 +87,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||
get_fee=fee
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
@@ -125,7 +123,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||
get_fee=fee
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
stake_currency = default_conf['stake_currency']
|
||||
fiat_display_currency = default_conf['fiat_display_currency']
|
||||
|
||||
@@ -182,7 +180,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||
get_fee=fee
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
stake_currency = default_conf['stake_currency']
|
||||
fiat_display_currency = default_conf['fiat_display_currency']
|
||||
|
||||
@@ -208,15 +206,30 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
|
||||
freqtradebot.create_trade()
|
||||
trade = Trade.query.first()
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
trade.update(limit_buy_order)
|
||||
|
||||
# Update the ticker with a market going up
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.freqtradebot.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker_sell_up
|
||||
)
|
||||
trade.update(limit_sell_order)
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
|
||||
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||
assert not error
|
||||
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_fiat'], 0.93255)
|
||||
assert prec_satoshi(stats['profit_all_coin'], 6.217e-05)
|
||||
assert prec_satoshi(stats['profit_all_percent'], 6.2)
|
||||
assert prec_satoshi(stats['profit_all_fiat'], 0.93255)
|
||||
assert stats['trade_count'] == 1
|
||||
assert prec_satoshi(stats['profit_all_coin'], 5.632e-05)
|
||||
assert prec_satoshi(stats['profit_all_percent'], 2.81)
|
||||
assert prec_satoshi(stats['profit_all_fiat'], 0.8448)
|
||||
assert stats['trade_count'] == 2
|
||||
assert stats['first_trade_date'] == 'just now'
|
||||
assert stats['latest_trade_date'] == 'just now'
|
||||
assert stats['avg_duration'] == '0:00:00'
|
||||
@@ -245,7 +258,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
|
||||
get_fee=fee
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
stake_currency = default_conf['stake_currency']
|
||||
fiat_display_currency = default_conf['fiat_display_currency']
|
||||
|
||||
@@ -316,7 +329,7 @@ def test_rpc_balance_handle(default_conf, mocker):
|
||||
get_balances=MagicMock(return_value=mock_balance)
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency'])
|
||||
@@ -346,7 +359,7 @@ def test_rpc_start(mocker, default_conf) -> None:
|
||||
get_ticker=MagicMock()
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
rpc = RPC(freqtradebot)
|
||||
freqtradebot.state = State.STOPPED
|
||||
|
||||
@@ -374,7 +387,7 @@ def test_rpc_stop(mocker, default_conf) -> None:
|
||||
get_ticker=MagicMock()
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
rpc = RPC(freqtradebot)
|
||||
freqtradebot.state = State.RUNNING
|
||||
|
||||
@@ -413,7 +426,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
|
||||
get_fee=fee,
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
@@ -523,7 +536,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||
get_fee=fee
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
@@ -562,7 +575,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
|
||||
get_fee=fee,
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
(error, trades) = rpc.rpc_count()
|
||||
|
||||
@@ -11,7 +11,6 @@ from datetime import datetime
|
||||
from random import randint
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from telegram import Update, Message, Chat
|
||||
from telegram.error import NetworkError
|
||||
|
||||
@@ -156,7 +155,7 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://')))
|
||||
dummy = DummyCls(FreqtradeBot(conf))
|
||||
dummy.dummy_handler(bot=MagicMock(), update=update)
|
||||
assert dummy.state['called'] is True
|
||||
assert log_has(
|
||||
@@ -187,7 +186,7 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://')))
|
||||
dummy = DummyCls(FreqtradeBot(conf))
|
||||
dummy.dummy_handler(bot=MagicMock(), update=update)
|
||||
assert dummy.state['called'] is False
|
||||
assert not log_has(
|
||||
@@ -217,7 +216,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://')))
|
||||
dummy = DummyCls(FreqtradeBot(conf))
|
||||
dummy.dummy_exception(bot=MagicMock(), update=update)
|
||||
assert dummy.state['called'] is False
|
||||
assert not log_has(
|
||||
@@ -263,7 +262,7 @@ def test_status(default_conf, update, mocker, fee, ticker) -> None:
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
@@ -301,7 +300,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
@@ -348,7 +347,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['stake_amount'] = 15.0
|
||||
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
@@ -402,7 +401,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
@@ -470,7 +469,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Try invalid data
|
||||
@@ -511,7 +510,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._profit(bot=MagicMock(), update=update)
|
||||
@@ -608,7 +607,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
||||
send_msg=msg_mock
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._balance(bot=MagicMock(), update=update)
|
||||
@@ -638,7 +637,7 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None:
|
||||
send_msg=msg_mock
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._balance(bot=MagicMock(), update=update)
|
||||
@@ -661,7 +660,7 @@ def test_start_handle(default_conf, update, mocker) -> None:
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
@@ -685,7 +684,7 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None:
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
@@ -710,7 +709,7 @@ def test_stop_handle(default_conf, update, mocker) -> None:
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
@@ -735,7 +734,7 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
@@ -762,7 +761,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, moc
|
||||
get_fee=fee
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
@@ -802,7 +801,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, ticker_sell_do
|
||||
get_fee=fee
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
@@ -847,7 +846,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
|
||||
get_fee=fee
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
@@ -880,7 +879,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Trader is not running
|
||||
@@ -927,7 +926,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
|
||||
get_fee=fee
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
@@ -962,7 +961,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
|
||||
send_msg=msg_mock
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Trader is not running
|
||||
@@ -991,7 +990,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||
buy=MagicMock(return_value={'id': 'mocked_order_id'})
|
||||
)
|
||||
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
@@ -1027,7 +1026,7 @@ def test_help_handle(default_conf, update, mocker) -> None:
|
||||
_init=MagicMock(),
|
||||
send_msg=msg_mock
|
||||
)
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._help(bot=MagicMock(), update=update)
|
||||
@@ -1047,7 +1046,7 @@ def test_version_handle(default_conf, update, mocker) -> None:
|
||||
_init=MagicMock(),
|
||||
send_msg=msg_mock
|
||||
)
|
||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._version(bot=MagicMock(), update=update)
|
||||
@@ -1064,7 +1063,7 @@ def test_send_msg(default_conf, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
conf = deepcopy(default_conf)
|
||||
bot = MagicMock()
|
||||
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._config['telegram']['enabled'] = False
|
||||
@@ -1087,7 +1086,7 @@ def test_send_msg_network_error(default_conf, mocker, caplog) -> None:
|
||||
conf = deepcopy(default_conf)
|
||||
bot = MagicMock()
|
||||
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
|
||||
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
|
||||
freqtradebot = FreqtradeBot(conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._config['telegram']['enabled'] = True
|
||||
|
||||
@@ -13,6 +13,7 @@ from pandas import DataFrame
|
||||
|
||||
from freqtrade.analyze import Analyze, SignalType
|
||||
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||
from freqtrade.arguments import TimeRange
|
||||
from freqtrade.tests.conftest import log_has
|
||||
|
||||
# Avoid to reinit the same object again and again
|
||||
@@ -45,7 +46,7 @@ def test_analyze_object() -> None:
|
||||
|
||||
def test_dataframe_correct_length(result):
|
||||
dataframe = Analyze.parse_ticker_dataframe(result)
|
||||
assert len(result.index) == len(dataframe.index)
|
||||
assert len(result.index) - 1 == len(dataframe.index) # last partial candle removed
|
||||
|
||||
|
||||
def test_dataframe_correct_columns(result):
|
||||
@@ -183,8 +184,8 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
|
||||
"""
|
||||
analyze = Analyze(default_conf)
|
||||
|
||||
timerange = ((None, 'line'), None, -100)
|
||||
timerange = TimeRange(None, 'line', 0, -100)
|
||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||
tickerlist = {'UNITTEST/BTC': tick}
|
||||
data = analyze.tickerdata_to_dataframe(tickerlist)
|
||||
assert len(data['UNITTEST/BTC']) == 100
|
||||
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
|
||||
|
||||
@@ -9,7 +9,7 @@ import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.arguments import Arguments, TimeRange
|
||||
|
||||
|
||||
def test_arguments_object() -> None:
|
||||
@@ -46,6 +46,11 @@ def test_parse_args_config() -> None:
|
||||
assert args.config == '/dev/null'
|
||||
|
||||
|
||||
def test_parse_args_db_url() -> None:
|
||||
args = Arguments(['--db-url', 'sqlite:///test.sqlite'], '').get_parsed_arg()
|
||||
assert args.db_url == 'sqlite:///test.sqlite'
|
||||
|
||||
|
||||
def test_parse_args_verbose() -> None:
|
||||
args = Arguments(['-v'], '').get_parsed_arg()
|
||||
assert args.loglevel == logging.DEBUG
|
||||
@@ -107,14 +112,24 @@ def test_parse_args_dynamic_whitelist_invalid_values() -> None:
|
||||
|
||||
|
||||
def test_parse_timerange_incorrect() -> None:
|
||||
assert ((None, 'line'), None, -200) == Arguments.parse_timerange('-200')
|
||||
assert (('line', None), 200, None) == Arguments.parse_timerange('200-')
|
||||
assert (('index', 'index'), 200, 500) == Arguments.parse_timerange('200-500')
|
||||
assert TimeRange(None, 'line', 0, -200) == Arguments.parse_timerange('-200')
|
||||
assert TimeRange('line', None, 200, 0) == Arguments.parse_timerange('200-')
|
||||
assert TimeRange('index', 'index', 200, 500) == Arguments.parse_timerange('200-500')
|
||||
|
||||
assert (('date', None), 1274486400, None) == Arguments.parse_timerange('20100522-')
|
||||
assert ((None, 'date'), None, 1274486400) == Arguments.parse_timerange('-20100522')
|
||||
assert TimeRange('date', None, 1274486400, 0) == Arguments.parse_timerange('20100522-')
|
||||
assert TimeRange(None, 'date', 0, 1274486400) == Arguments.parse_timerange('-20100522')
|
||||
timerange = Arguments.parse_timerange('20100522-20150730')
|
||||
assert timerange == (('date', 'date'), 1274486400, 1438214400)
|
||||
assert timerange == TimeRange('date', 'date', 1274486400, 1438214400)
|
||||
|
||||
# Added test for unix timestamp - BTC genesis date
|
||||
assert TimeRange('date', None, 1231006505, 0) == Arguments.parse_timerange('1231006505-')
|
||||
assert TimeRange(None, 'date', 0, 1233360000) == Arguments.parse_timerange('-1233360000')
|
||||
timerange = Arguments.parse_timerange('1231006505-1233360000')
|
||||
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
|
||||
|
||||
# TODO: Find solution for the following case (passing timestamp in ms)
|
||||
timerange = Arguments.parse_timerange('1231006505000-1233360000000')
|
||||
assert TimeRange('date', 'date', 1231006505, 1233360000) != timerange
|
||||
|
||||
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
|
||||
Arguments.parse_timerange('-')
|
||||
@@ -159,3 +174,19 @@ def test_parse_args_hyperopt_custom() -> None:
|
||||
assert call_args.subparser == 'hyperopt'
|
||||
assert call_args.spaces == ['buy']
|
||||
assert call_args.func is not None
|
||||
|
||||
|
||||
def test_testdata_dl_options() -> None:
|
||||
args = [
|
||||
'--pairs-file', 'file_with_pairs',
|
||||
'--export', 'export/folder',
|
||||
'--days', '30',
|
||||
'--exchange', 'binance'
|
||||
]
|
||||
arguments = Arguments(args, '')
|
||||
arguments.testdata_dl_options()
|
||||
args = arguments.parse_args()
|
||||
assert args.pairs_file == 'file_with_pairs'
|
||||
assert args.export == 'export/folder'
|
||||
assert args.days == 30
|
||||
assert args.exchange == 'binance'
|
||||
|
||||
@@ -6,6 +6,7 @@ Unit test file for configuration.py
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock
|
||||
from argparse import Namespace
|
||||
|
||||
import pytest
|
||||
from jsonschema import ValidationError
|
||||
@@ -37,7 +38,7 @@ def test_load_config_invalid_pair(default_conf) -> None:
|
||||
conf['exchange']['pair_whitelist'].append('ETH-BTC')
|
||||
|
||||
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
||||
configuration = Configuration([])
|
||||
configuration = Configuration(Namespace())
|
||||
configuration._validate_config(conf)
|
||||
|
||||
|
||||
@@ -49,7 +50,7 @@ def test_load_config_missing_attributes(default_conf) -> None:
|
||||
conf.pop('exchange')
|
||||
|
||||
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
||||
configuration = Configuration([])
|
||||
configuration = Configuration(Namespace())
|
||||
configuration._validate_config(conf)
|
||||
|
||||
|
||||
@@ -61,7 +62,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
configuration = Configuration([])
|
||||
configuration = Configuration(Namespace())
|
||||
validated_conf = configuration._load_config_file('somefile')
|
||||
assert file_mock.call_count == 1
|
||||
assert validated_conf.items() >= default_conf.items()
|
||||
@@ -79,12 +80,12 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
|
||||
read_data=json.dumps(conf)
|
||||
))
|
||||
|
||||
Configuration([])._load_config_file('somefile')
|
||||
Configuration(Namespace())._load_config_file('somefile')
|
||||
assert file_mock.call_count == 1
|
||||
assert log_has('Validating configuration ...', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_load_config_file_exception(mocker, caplog) -> None:
|
||||
def test_load_config_file_exception(mocker) -> None:
|
||||
"""
|
||||
Test Configuration._load_config_file() method
|
||||
"""
|
||||
@@ -92,14 +93,10 @@ def test_load_config_file_exception(mocker, caplog) -> None:
|
||||
'freqtrade.configuration.open',
|
||||
MagicMock(side_effect=FileNotFoundError('File not found'))
|
||||
)
|
||||
configuration = Configuration([])
|
||||
configuration = Configuration(Namespace())
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
with pytest.raises(OperationalException, match=r'.*Config file "somefile" not found!*'):
|
||||
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:
|
||||
@@ -117,7 +114,6 @@ def test_load_config(default_conf, mocker) -> None:
|
||||
assert validated_conf.get('strategy') == 'DefaultStrategy'
|
||||
assert validated_conf.get('strategy_path') is None
|
||||
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:
|
||||
@@ -128,13 +124,13 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = [
|
||||
arglist = [
|
||||
'--dynamic-whitelist', '10',
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path',
|
||||
'--dry-run-db',
|
||||
'--db-url', 'sqlite:///someurl',
|
||||
]
|
||||
args = Arguments(args, '').get_parsed_arg()
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
@@ -142,7 +138,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
||||
assert validated_conf.get('dynamic_whitelist') == 10
|
||||
assert validated_conf.get('strategy') == 'TestStrategy'
|
||||
assert validated_conf.get('strategy_path') == '/some/path'
|
||||
assert validated_conf.get('dry_run_db') is True
|
||||
assert validated_conf.get('db_url') == 'sqlite:///someurl'
|
||||
|
||||
|
||||
def test_load_custom_strategy(default_conf, mocker) -> None:
|
||||
@@ -174,12 +170,12 @@ def test_show_info(default_conf, mocker, caplog) -> None:
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = [
|
||||
arglist = [
|
||||
'--dynamic-whitelist', '10',
|
||||
'--strategy', 'TestStrategy',
|
||||
'--dry-run-db'
|
||||
'--db-url', 'sqlite:///tmp/testdb',
|
||||
]
|
||||
args = Arguments(args, '').get_parsed_arg()
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
configuration.get_config()
|
||||
@@ -191,23 +187,8 @@ def test_show_info(default_conf, mocker, caplog) -> None:
|
||||
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
|
||||
)
|
||||
assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples)
|
||||
assert log_has('Dry run is enabled', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||
@@ -218,13 +199,13 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = [
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'backtesting'
|
||||
]
|
||||
|
||||
args = Arguments(args, '').get_parsed_arg()
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
@@ -235,7 +216,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert log_has(
|
||||
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
||||
'Using data folder: {} ...'.format(config['datadir']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert 'ticker_interval' in config
|
||||
@@ -262,7 +243,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = [
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'--datadir', '/foo/bar',
|
||||
@@ -275,7 +256,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||
'--export', '/bar/foo'
|
||||
]
|
||||
|
||||
args = Arguments(args, '').get_parsed_arg()
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
@@ -286,7 +267,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert log_has(
|
||||
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
||||
'Using data folder: {} ...'.format(config['datadir']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert 'ticker_interval' in config
|
||||
@@ -326,14 +307,14 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = [
|
||||
arglist = [
|
||||
'hyperopt',
|
||||
'--epochs', '10',
|
||||
'--use-mongodb',
|
||||
'--spaces', 'all',
|
||||
]
|
||||
|
||||
args = Arguments(args, '').get_parsed_arg()
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
@@ -357,7 +338,7 @@ def test_check_exchange(default_conf) -> None:
|
||||
Test the configuration validator with a missing attribute
|
||||
"""
|
||||
conf = deepcopy(default_conf)
|
||||
configuration = Configuration([])
|
||||
configuration = Configuration(Namespace())
|
||||
|
||||
# Test a valid exchange
|
||||
conf.get('exchange').update({'name': 'BITTREX'})
|
||||
|
||||
@@ -9,7 +9,7 @@ import pytest
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
|
||||
from freqtrade.tests.conftest import patch_coinmarketcap
|
||||
from freqtrade.tests.conftest import log_has, patch_coinmarketcap
|
||||
|
||||
|
||||
def test_pair_convertion_object():
|
||||
@@ -90,6 +90,13 @@ def test_fiat_convert_find_price(mocker):
|
||||
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 13000.2
|
||||
|
||||
|
||||
def test_fiat_convert_unsupported_crypto(mocker, caplog):
|
||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[])
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0
|
||||
assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_fiat_convert_get_price(mocker):
|
||||
api_mock = MagicMock(return_value={
|
||||
'price_usd': 28000.0,
|
||||
@@ -126,6 +133,20 @@ def test_fiat_convert_get_price(mocker):
|
||||
assert fiat_convert._pairs[0]._expiration is not expiration
|
||||
|
||||
|
||||
def test_fiat_convert_same_currencies(mocker):
|
||||
patch_coinmarketcap(mocker)
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
|
||||
assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='USD') == 1.0
|
||||
|
||||
|
||||
def test_fiat_convert_two_FIAT(mocker):
|
||||
patch_coinmarketcap(mocker)
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
|
||||
assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='EUR') == 0.0
|
||||
|
||||
|
||||
def test_loadcryptomap(mocker):
|
||||
patch_coinmarketcap(mocker)
|
||||
|
||||
@@ -147,7 +168,8 @@ def test_fiat_init_network_exception(mocker):
|
||||
fiat_convert._cryptomap = {}
|
||||
fiat_convert._load_cryptomap()
|
||||
|
||||
assert len(fiat_convert._cryptomap) == 0
|
||||
length_cryptomap = len(fiat_convert._cryptomap)
|
||||
assert length_cryptomap == 0
|
||||
|
||||
|
||||
def test_fiat_convert_without_network():
|
||||
@@ -161,3 +183,22 @@ def test_fiat_convert_without_network():
|
||||
assert fiat_convert._coinmarketcap is None
|
||||
assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0
|
||||
CryptoToFiatConverter._coinmarketcap = cmc_temp
|
||||
|
||||
|
||||
def test_convert_amount(mocker):
|
||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0)
|
||||
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
result = fiat_convert.convert_amount(
|
||||
crypto_amount=1.23,
|
||||
crypto_symbol="BTC",
|
||||
fiat_symbol="USD"
|
||||
)
|
||||
assert result == 15184.35
|
||||
|
||||
result = fiat_convert.convert_amount(
|
||||
crypto_amount=1.23,
|
||||
crypto_symbol="BTC",
|
||||
fiat_symbol="BTC"
|
||||
)
|
||||
assert result == 1.23
|
||||
|
||||
@@ -13,7 +13,6 @@ from unittest.mock import MagicMock
|
||||
import arrow
|
||||
import pytest
|
||||
import requests
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
from freqtrade import DependencyException, OperationalException, TemporaryError
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
@@ -36,7 +35,7 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
||||
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
||||
patch_coinmarketcap(mocker)
|
||||
|
||||
return FreqtradeBot(config, create_engine('sqlite://'))
|
||||
return FreqtradeBot(config)
|
||||
|
||||
|
||||
def patch_get_signal(mocker, value=(True, False)) -> None:
|
||||
@@ -237,7 +236,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non
|
||||
|
||||
# Save state of current whitelist
|
||||
whitelist = deepcopy(default_conf['exchange']['pair_whitelist'])
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
freqtrade.create_trade()
|
||||
|
||||
trade = Trade.query.first()
|
||||
@@ -274,7 +273,7 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, fee,
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['stake_amount'] = 0.0005
|
||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(conf)
|
||||
|
||||
freqtrade.create_trade()
|
||||
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
|
||||
@@ -296,7 +295,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee
|
||||
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5),
|
||||
get_fee=fee,
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
|
||||
freqtrade.create_trade()
|
||||
@@ -320,7 +319,7 @@ def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, mocke
|
||||
conf = deepcopy(default_conf)
|
||||
conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
|
||||
conf['exchange']['pair_blacklist'] = ["ETH/BTC"]
|
||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(conf)
|
||||
|
||||
freqtrade.create_trade()
|
||||
|
||||
@@ -347,7 +346,7 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker,
|
||||
conf = deepcopy(default_conf)
|
||||
conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
|
||||
conf['exchange']['pair_blacklist'] = ["ETH/BTC"]
|
||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(conf)
|
||||
|
||||
freqtrade.create_trade()
|
||||
|
||||
@@ -375,7 +374,7 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None:
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['stake_amount'] = 10
|
||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(conf)
|
||||
|
||||
Trade.query = MagicMock()
|
||||
Trade.query.filter = MagicMock()
|
||||
@@ -399,7 +398,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order,
|
||||
get_order=MagicMock(return_value=limit_buy_order),
|
||||
get_fee=fee,
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
assert not trades
|
||||
@@ -440,7 +439,7 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non
|
||||
)
|
||||
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
result = freqtrade._process()
|
||||
assert result is False
|
||||
assert sleep_mock.has_calls()
|
||||
@@ -460,7 +459,7 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) ->
|
||||
get_markets=markets,
|
||||
buy=MagicMock(side_effect=OperationalException)
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
assert freqtrade.state == State.RUNNING
|
||||
|
||||
result = freqtrade._process()
|
||||
@@ -486,7 +485,7 @@ def test_process_trade_handling(
|
||||
get_order=MagicMock(return_value=limit_buy_order),
|
||||
get_fee=fee,
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
assert not trades
|
||||
@@ -503,27 +502,27 @@ def test_balance_fully_ask_side(mocker) -> None:
|
||||
"""
|
||||
Test get_target_bid() method
|
||||
"""
|
||||
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'ask_last_balance': 0.0}})
|
||||
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'use_book_order':False,'book_order_top':6,'ask_last_balance': 0.0}})
|
||||
|
||||
assert freqtrade.get_target_bid({'ask': 20, 'last': 10}) == 20
|
||||
assert freqtrade.get_target_bid('ETH/BTC') >= 0.07
|
||||
|
||||
|
||||
def test_balance_fully_last_side(mocker) -> None:
|
||||
"""
|
||||
Test get_target_bid() method
|
||||
"""
|
||||
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'ask_last_balance': 1.0}})
|
||||
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'use_book_order':False,'book_order_top':6,'ask_last_balance': 1.0}})
|
||||
|
||||
assert freqtrade.get_target_bid({'ask': 20, 'last': 10}) == 10
|
||||
assert freqtrade.get_target_bid('ETH/BTC') >= 0.07
|
||||
|
||||
|
||||
def test_balance_bigger_last_ask(mocker) -> None:
|
||||
"""
|
||||
Test get_target_bid() method
|
||||
"""
|
||||
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'ask_last_balance': 1.0}})
|
||||
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'use_book_order':False,'book_order_top':6,'ask_last_balance': 1.0}})
|
||||
|
||||
assert freqtrade.get_target_bid({'ask': 5, 'last': 10}) == 5
|
||||
assert freqtrade.get_target_bid('ETH/BTC') >= 0.07
|
||||
|
||||
|
||||
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
|
||||
@@ -570,8 +569,10 @@ def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplo
|
||||
trade.open_fee = 0.001
|
||||
assert not freqtrade.process_maybe_execute_sell(trade)
|
||||
# Test amount not modified by fee-logic
|
||||
assert not log_has('Applying fee to amount for Trade {} from 90.99181073 to 90.81'.format(
|
||||
trade), caplog.record_tuples)
|
||||
assert not log_has(
|
||||
'Applying fee to amount for Trade {} from 90.99181073 to 90.81'.format(trade),
|
||||
caplog.record_tuples
|
||||
)
|
||||
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81)
|
||||
# test amount modified by fee-logic
|
||||
@@ -582,6 +583,38 @@ def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplo
|
||||
# Assert we call handle_trade() if trade is feasible for execution
|
||||
assert freqtrade.process_maybe_execute_sell(trade)
|
||||
|
||||
regexp = re.compile('Found open order for.*')
|
||||
assert filter(regexp.match, caplog.record_tuples)
|
||||
|
||||
|
||||
def test_process_maybe_execute_sell_exception(mocker, default_conf,
|
||||
limit_buy_order, caplog) -> None:
|
||||
"""
|
||||
Test the exceptions in process_maybe_execute_sell()
|
||||
"""
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
mocker.patch('freqtrade.freqtradebot.exchange.get_order', return_value=limit_buy_order)
|
||||
|
||||
trade = MagicMock()
|
||||
trade.open_order_id = '123'
|
||||
trade.open_fee = 0.001
|
||||
|
||||
# Test raise of OperationalException exception
|
||||
mocker.patch(
|
||||
'freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
|
||||
side_effect=OperationalException()
|
||||
)
|
||||
freqtrade.process_maybe_execute_sell(trade)
|
||||
assert log_has('could not update trade amount: ', caplog.record_tuples)
|
||||
|
||||
# Test raise of DependencyException exception
|
||||
mocker.patch(
|
||||
'freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
|
||||
side_effect=DependencyException()
|
||||
)
|
||||
freqtrade.process_maybe_execute_sell(trade)
|
||||
assert log_has('Unable to sell trade: ', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mocker) -> None:
|
||||
"""
|
||||
@@ -603,7 +636,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock
|
||||
)
|
||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
freqtrade.create_trade()
|
||||
|
||||
@@ -646,7 +679,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee,
|
||||
get_fee=fee,
|
||||
)
|
||||
|
||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(conf)
|
||||
|
||||
freqtrade.create_trade()
|
||||
|
||||
@@ -705,7 +738,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, fee, mocker, ca
|
||||
)
|
||||
|
||||
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True)
|
||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(conf)
|
||||
freqtrade.create_trade()
|
||||
|
||||
trade = Trade.query.first()
|
||||
@@ -742,7 +775,7 @@ def test_handle_trade_experimental(
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False)
|
||||
|
||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(conf)
|
||||
freqtrade.create_trade()
|
||||
|
||||
trade = Trade.query.first()
|
||||
@@ -770,7 +803,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, fe
|
||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||
get_fee=fee,
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
# Create trade and sell it
|
||||
freqtrade.create_trade()
|
||||
@@ -801,7 +834,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fe
|
||||
cancel_order=cancel_order_mock,
|
||||
get_fee=fee
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
trade_buy = Trade(
|
||||
pair='ETH/BTC',
|
||||
@@ -841,7 +874,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
|
||||
get_order=MagicMock(return_value=limit_sell_order_old),
|
||||
cancel_order=cancel_order_mock
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
trade_sell = Trade(
|
||||
pair='ETH/BTC',
|
||||
@@ -881,7 +914,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
|
||||
get_order=MagicMock(return_value=limit_buy_order_old_partial),
|
||||
cancel_order=cancel_order_mock
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
trade_buy = Trade(
|
||||
pair='ETH/BTC',
|
||||
@@ -929,7 +962,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) -
|
||||
get_order=MagicMock(side_effect=requests.exceptions.RequestException('Oh snap')),
|
||||
cancel_order=cancel_order_mock
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
trade_buy = Trade(
|
||||
pair='ETH/BTC',
|
||||
@@ -968,7 +1001,7 @@ def test_handle_timedout_limit_buy(mocker, default_conf) -> None:
|
||||
cancel_order=cancel_order_mock
|
||||
)
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
Trade.session = MagicMock()
|
||||
trade = MagicMock()
|
||||
@@ -994,7 +1027,7 @@ def test_handle_timedout_limit_sell(mocker, default_conf) -> None:
|
||||
cancel_order=cancel_order_mock
|
||||
)
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
trade = MagicMock()
|
||||
order = {'remaining': 1,
|
||||
@@ -1021,7 +1054,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
|
||||
get_fee=fee
|
||||
)
|
||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
# Create some test data
|
||||
freqtrade.create_trade()
|
||||
@@ -1062,7 +1095,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
|
||||
get_ticker=ticker,
|
||||
get_fee=fee
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
# Create some test data
|
||||
freqtrade.create_trade()
|
||||
@@ -1102,7 +1135,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
|
||||
get_ticker=ticker,
|
||||
get_fee=fee
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
# Create some test data
|
||||
freqtrade.create_trade()
|
||||
@@ -1143,7 +1176,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee,
|
||||
get_ticker=ticker,
|
||||
get_fee=fee
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
# Create some test data
|
||||
freqtrade.create_trade()
|
||||
@@ -1192,7 +1225,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, mock
|
||||
'use_sell_signal': True,
|
||||
'sell_profit_only': True,
|
||||
}
|
||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(conf)
|
||||
freqtrade.create_trade()
|
||||
|
||||
trade = Trade.query.first()
|
||||
@@ -1225,7 +1258,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, fee, moc
|
||||
'use_sell_signal': True,
|
||||
'sell_profit_only': False,
|
||||
}
|
||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(conf)
|
||||
freqtrade.create_trade()
|
||||
|
||||
trade = Trade.query.first()
|
||||
@@ -1258,7 +1291,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker
|
||||
'use_sell_signal': True,
|
||||
'sell_profit_only': True,
|
||||
}
|
||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(conf)
|
||||
freqtrade.create_trade()
|
||||
|
||||
trade = Trade.query.first()
|
||||
@@ -1293,7 +1326,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke
|
||||
'sell_profit_only': False,
|
||||
}
|
||||
|
||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(conf)
|
||||
freqtrade.create_trade()
|
||||
|
||||
trade = Trade.query.first()
|
||||
@@ -1321,7 +1354,7 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, ca
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456"
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
# Amount is reduced by "fee"
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
|
||||
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||
@@ -1348,7 +1381,7 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker):
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456"
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
# Amount is reduced by "fee"
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
||||
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||
@@ -1356,7 +1389,7 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker):
|
||||
caplog.record_tuples)
|
||||
|
||||
|
||||
def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, caplog, mocker):
|
||||
def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mocker):
|
||||
"""
|
||||
Test get_real_amount - fees in Stake currency
|
||||
"""
|
||||
@@ -1375,7 +1408,7 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, ca
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456"
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
# Amount does not change
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
||||
|
||||
@@ -1401,7 +1434,7 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mock
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456"
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
# Amount does not change
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
||||
|
||||
@@ -1424,7 +1457,7 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456"
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
# Amount is reduced by "fee"
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
|
||||
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||
@@ -1452,7 +1485,7 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456"
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
# Amount is reduced by "fee"
|
||||
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount - 0.004
|
||||
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||
@@ -1480,7 +1513,7 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456"
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
# Amount does not change
|
||||
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount
|
||||
|
||||
@@ -1505,6 +1538,31 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee,
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456"
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
# Amount does not change
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
||||
|
||||
|
||||
def test_get_real_amount_open_trade(default_conf, mocker):
|
||||
"""
|
||||
Test get_real_amount condition trade.fee_open == 0 or order['status'] == 'open'
|
||||
"""
|
||||
patch_get_signal(mocker)
|
||||
patch_RPCManager(mocker)
|
||||
patch_coinmarketcap(mocker)
|
||||
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
|
||||
amount = 12345
|
||||
trade = Trade(
|
||||
pair='LTC/ETH',
|
||||
amount=amount,
|
||||
exchange='binance',
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456"
|
||||
)
|
||||
order = {
|
||||
'id': 'mocked_order',
|
||||
'amount': amount,
|
||||
'status': 'open',
|
||||
}
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
assert freqtrade.get_real_amount(trade, order) == amount
|
||||
|
||||
@@ -7,6 +7,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.main import main, set_loggers
|
||||
from freqtrade.tests.conftest import log_has
|
||||
|
||||
@@ -60,7 +61,7 @@ def test_set_loggers() -> None:
|
||||
assert value2 is logging.INFO
|
||||
|
||||
|
||||
def test_main(mocker, caplog) -> None:
|
||||
def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test main() function
|
||||
In this test we are skipping the while True loop by throwing an exception.
|
||||
@@ -68,26 +69,74 @@ def test_main(mocker, caplog) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.freqtradebot.FreqtradeBot',
|
||||
_init_modules=MagicMock(),
|
||||
worker=MagicMock(
|
||||
side_effect=KeyboardInterrupt
|
||||
),
|
||||
worker=MagicMock(side_effect=Exception),
|
||||
clean=MagicMock(),
|
||||
)
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.Configuration._load_config_file',
|
||||
lambda *args, **kwargs: default_conf
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
args = ['-c', 'config.json.example']
|
||||
|
||||
# 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
|
||||
|
||||
# Test the BaseException case
|
||||
mocker.patch(
|
||||
'freqtrade.freqtradebot.FreqtradeBot.worker',
|
||||
MagicMock(side_effect=BaseException)
|
||||
)
|
||||
with pytest.raises(SystemExit):
|
||||
main(args)
|
||||
log_has('Got fatal exception!', caplog.record_tuples)
|
||||
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
||||
assert log_has('Fatal exception!', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test main() function
|
||||
In this test we are skipping the while True loop by throwing an exception.
|
||||
"""
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.freqtradebot.FreqtradeBot',
|
||||
_init_modules=MagicMock(),
|
||||
worker=MagicMock(side_effect=KeyboardInterrupt),
|
||||
clean=MagicMock(),
|
||||
)
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.Configuration._load_config_file',
|
||||
lambda *args, **kwargs: default_conf
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
args = ['-c', 'config.json.example']
|
||||
|
||||
# Test Main + the KeyboardInterrupt exception
|
||||
with pytest.raises(SystemExit):
|
||||
main(args)
|
||||
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
||||
assert log_has('SIGINT received, aborting ...', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test main() function
|
||||
In this test we are skipping the while True loop by throwing an exception.
|
||||
"""
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.freqtradebot.FreqtradeBot',
|
||||
_init_modules=MagicMock(),
|
||||
worker=MagicMock(side_effect=OperationalException('Oh snap!')),
|
||||
clean=MagicMock(),
|
||||
)
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.Configuration._load_config_file',
|
||||
lambda *args, **kwargs: default_conf
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
args = ['-c', 'config.json.example']
|
||||
|
||||
# Test Main + the KeyboardInterrupt exception
|
||||
with pytest.raises(SystemExit):
|
||||
main(args)
|
||||
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
||||
assert log_has('Oh snap!', caplog.record_tuples)
|
||||
|
||||
@@ -39,7 +39,7 @@ def test_datesarray_to_datetimearray(ticker_history):
|
||||
assert dates[0].minute == 50
|
||||
|
||||
date_len = len(dates)
|
||||
assert date_len == 3
|
||||
assert date_len == 2
|
||||
|
||||
|
||||
def test_common_datearray(default_conf) -> None:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
import os
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
from freqtrade import constants, OperationalException
|
||||
from freqtrade.persistence import Trade, init, clean_dry_run_db
|
||||
|
||||
|
||||
@@ -21,77 +23,54 @@ def test_init_create_session(default_conf, mocker):
|
||||
assert 'Session' in type(Trade.session).__name__
|
||||
|
||||
|
||||
def test_init_dry_run_db(default_conf, mocker):
|
||||
default_conf.update({'dry_run_db': True})
|
||||
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
|
||||
def test_init_custom_db_url(default_conf, mocker):
|
||||
conf = deepcopy(default_conf)
|
||||
|
||||
# First, protect the existing 'tradesv3.dry_run.sqlite' (Do not delete user data)
|
||||
dry_run_db = 'tradesv3.dry_run.sqlite'
|
||||
dry_run_db_swp = dry_run_db + '.swp'
|
||||
# Update path to a value other than default, but still in-memory
|
||||
conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
|
||||
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||
mocker.patch.dict('freqtrade.persistence._CONF', conf)
|
||||
|
||||
if os.path.isfile(dry_run_db):
|
||||
os.rename(dry_run_db, dry_run_db_swp)
|
||||
|
||||
# Check if the new tradesv3.dry_run.sqlite was created
|
||||
init(default_conf)
|
||||
assert os.path.isfile(dry_run_db) is True
|
||||
|
||||
# Delete the file made for this unitest and rollback to the previous
|
||||
# tradesv3.dry_run.sqlite file
|
||||
|
||||
# 1. Delete file from the test
|
||||
if os.path.isfile(dry_run_db):
|
||||
os.remove(dry_run_db)
|
||||
|
||||
# 2. Rollback to the initial file
|
||||
if os.path.isfile(dry_run_db_swp):
|
||||
os.rename(dry_run_db_swp, dry_run_db)
|
||||
init(conf)
|
||||
assert create_engine_mock.call_count == 1
|
||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
|
||||
|
||||
|
||||
def test_init_dry_run_without_db(default_conf, mocker):
|
||||
default_conf.update({'dry_run_db': False})
|
||||
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
|
||||
def test_init_invalid_db_url(default_conf, mocker):
|
||||
conf = deepcopy(default_conf)
|
||||
|
||||
# First, protect the existing 'tradesv3.dry_run.sqlite' (Do not delete user data)
|
||||
dry_run_db = 'tradesv3.dry_run.sqlite'
|
||||
dry_run_db_swp = dry_run_db + '.swp'
|
||||
# Update path to a value other than default, but still in-memory
|
||||
conf.update({'db_url': 'unknown:///some.url'})
|
||||
mocker.patch.dict('freqtrade.persistence._CONF', conf)
|
||||
|
||||
if os.path.isfile(dry_run_db):
|
||||
os.rename(dry_run_db, dry_run_db_swp)
|
||||
|
||||
# Check if the new tradesv3.dry_run.sqlite was created
|
||||
init(default_conf)
|
||||
assert os.path.isfile(dry_run_db) is False
|
||||
|
||||
# Rollback to the initial 'tradesv3.dry_run.sqlite' file
|
||||
if os.path.isfile(dry_run_db_swp):
|
||||
os.rename(dry_run_db_swp, dry_run_db)
|
||||
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
|
||||
init(conf)
|
||||
|
||||
|
||||
def test_init_prod_db(default_conf, mocker):
|
||||
default_conf.update({'dry_run': False})
|
||||
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'dry_run': False})
|
||||
conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
|
||||
|
||||
# First, protect the existing 'tradesv3.sqlite' (Do not delete user data)
|
||||
prod_db = 'tradesv3.sqlite'
|
||||
prod_db_swp = prod_db + '.swp'
|
||||
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||
mocker.patch.dict('freqtrade.persistence._CONF', conf)
|
||||
|
||||
if os.path.isfile(prod_db):
|
||||
os.rename(prod_db, prod_db_swp)
|
||||
init(conf)
|
||||
assert create_engine_mock.call_count == 1
|
||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
|
||||
|
||||
# Check if the new tradesv3.sqlite was created
|
||||
init(default_conf)
|
||||
assert os.path.isfile(prod_db) is True
|
||||
|
||||
# Delete the file made for this unitest and rollback to the previous tradesv3.sqlite file
|
||||
def test_init_dryrun_db(default_conf, mocker):
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'dry_run': True})
|
||||
conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
|
||||
|
||||
# 1. Delete file from the test
|
||||
if os.path.isfile(prod_db):
|
||||
os.remove(prod_db)
|
||||
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||
mocker.patch.dict('freqtrade.persistence._CONF', conf)
|
||||
|
||||
# Rollback to the initial 'tradesv3.sqlite' file
|
||||
if os.path.isfile(prod_db_swp):
|
||||
os.rename(prod_db_swp, prod_db)
|
||||
init(conf)
|
||||
assert create_engine_mock.call_count == 1
|
||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://'
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
@@ -328,7 +307,7 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee):
|
||||
|
||||
|
||||
def test_clean_dry_run_db(default_conf, fee):
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
init(default_conf)
|
||||
|
||||
# Simulate dry_run entries
|
||||
trade = Trade(
|
||||
@@ -377,7 +356,7 @@ def test_clean_dry_run_db(default_conf, fee):
|
||||
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1
|
||||
|
||||
|
||||
def test_migrate_old(default_conf, fee):
|
||||
def test_migrate_old(mocker, default_conf, fee):
|
||||
"""
|
||||
Test Database migration(starting with old pairformat)
|
||||
"""
|
||||
@@ -409,11 +388,13 @@ def test_migrate_old(default_conf, fee):
|
||||
amount=amount
|
||||
)
|
||||
engine = create_engine('sqlite://')
|
||||
mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
|
||||
|
||||
# Create table using the old format
|
||||
engine.execute(create_table_old)
|
||||
engine.execute(insert_table_old)
|
||||
# Run init to test migration
|
||||
init(default_conf, engine)
|
||||
init(default_conf)
|
||||
|
||||
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
||||
trade = Trade.query.filter(Trade.id == 1).first()
|
||||
@@ -428,7 +409,7 @@ def test_migrate_old(default_conf, fee):
|
||||
assert trade.exchange == "bittrex"
|
||||
|
||||
|
||||
def test_migrate_new(default_conf, fee):
|
||||
def test_migrate_new(mocker, default_conf, fee):
|
||||
"""
|
||||
Test Database migration (starting with new pairformat)
|
||||
"""
|
||||
@@ -462,11 +443,13 @@ def test_migrate_new(default_conf, fee):
|
||||
amount=amount
|
||||
)
|
||||
engine = create_engine('sqlite://')
|
||||
mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
|
||||
|
||||
# Create table using the old format
|
||||
engine.execute(create_table_old)
|
||||
engine.execute(insert_table_old)
|
||||
# Run init to test migration
|
||||
init(default_conf, engine)
|
||||
init(default_conf)
|
||||
|
||||
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
||||
trade = Trade.query.filter(Trade.id == 1).first()
|
||||
|
||||
Reference in New Issue
Block a user