Merge branch 'feat/objectify-ccxt' into ccxt-objectify-pr1

This commit is contained in:
Samuel Husso
2018-04-06 10:56:06 +03:00
committed by GitHub
7 changed files with 277 additions and 14 deletions

View File

@@ -5,10 +5,11 @@ This module contains the configuration class
import json
from argparse import Namespace
from typing import Dict, Any
from jsonschema import Draft4Validator, validate
from jsonschema.exceptions import ValidationError, best_match
import ccxt
from freqtrade import OperationalException
from freqtrade.constants import Constants
from freqtrade.logger import Logger
@@ -100,6 +101,9 @@ class Configuration(object):
else:
self.logger.info('Dry run is disabled. (--dry_run_db ignored)')
# Check if the exchange set by the user is supported
self.check_exchange(config)
return config
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
@@ -198,3 +202,23 @@ class Configuration(object):
self.config = self.load_config()
return self.config
def check_exchange(self, config: Dict[str, Any]) -> bool:
"""
Check if the exchange name in the config file is supported by Freqtrade
:return: True or raised an exception if the exchange if not supported
"""
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))
self.logger.critical(exception_msg)
raise OperationalException(
exception_msg
)
self.logger.debug('Exchange "%s" supported', exchange)
return True

View File

@@ -67,7 +67,6 @@ def init(config: dict) -> None:
if name not in ccxt.exchanges:
raise OperationalException('Exchange {} is not supported'.format(name))
try:
_API = getattr(ccxt, name.lower())({
'apiKey': exchange_config.get('key'),
@@ -75,7 +74,7 @@ def init(config: dict) -> None:
'password': exchange_config.get('password'),
'uid': exchange_config.get('uid'),
})
except KeyError:
except (KeyError, AttributeError):
raise OperationalException('Exchange {} is not supported'.format(name))
logger.info('Using Exchange "%s"', get_name())
@@ -92,9 +91,6 @@ def validate_pairs(pairs: List[str]) -> None:
:return: None
"""
if not _API.markets:
_API.load_markets()
try:
markets = _API.load_markets()
except ccxt.BaseError as e:

View File

@@ -5,6 +5,7 @@ Various tool function for Freqtrade and scripts
import json
import logging
import re
import gzip
from datetime import datetime
from typing import Dict
@@ -63,15 +64,21 @@ def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray:
return np.sort(arr, axis=0)
def file_dump_json(filename, data) -> None:
def file_dump_json(filename, data, is_zip=False) -> None:
"""
Dump JSON data into a file
:param filename: file to create
:param data: JSON Data to save
:return:
"""
with open(filename, 'w') as fp:
json.dump(data, fp, default=str)
if is_zip:
if not filename.endswith('.gz'):
filename = filename + '.gz'
with gzip.open(filename, 'w') as fp:
json.dump(data, fp, default=str)
else:
with open(filename, 'w') as fp:
json.dump(data, fp, default=str)
def format_ms_time(date: str) -> str:

View File

@@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
# pragma pylint: disable=protected-access
import logging
from copy import deepcopy
from random import randint
from unittest.mock import MagicMock, PropertyMock
import ccxt
@@ -69,21 +70,40 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
})
default_conf['stake_currency'] = 'ETH'
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
mocker.patch.dict('freqtrade.exchange._CONF', conf)
with pytest.raises(OperationalException, match=r'not compatible'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
validate_pairs(conf['exchange']['pair_whitelist'])
def test_validate_pairs_exception(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
api_mock = MagicMock()
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError())
api_mock.name = 'binance'
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at binance'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
validate_pairs(default_conf['exchange']['pair_whitelist'])
assert log_has('Unable to validate pairs (assuming they are correct). Reason: ',
caplog.record_tuples)
def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
conf = deepcopy(default_conf)
conf['stake_currency'] = 'ETH'
api_mock = MagicMock()
api_mock.name = 'binance'
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', conf)
with pytest.raises(
OperationalException,
match=r'Pair ETH/BTC not compatible with stake_currency: ETH'
):
validate_pairs(default_conf['exchange']['pair_whitelist'])
def test_buy_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
@@ -93,7 +113,6 @@ def test_buy_dry_run(default_conf, mocker):
assert 'id' in order
assert 'dry_run_buy_' in order['id']
def test_buy_prod(default_conf, mocker):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
@@ -265,6 +284,7 @@ def test_get_ticker(default_conf, mocker):
# retrieve original ticker
ticker = get_ticker(pair='ETH/BTC')
assert ticker['bid'] == 0.00001098
assert ticker['ask'] == 0.00001099
@@ -281,6 +301,7 @@ def test_get_ticker(default_conf, mocker):
# if not caching the result we should get the same ticker
# if not fetching a new result we should get the cached ticker
ticker = get_ticker(pair='ETH/BTC')
assert ticker['bid'] == 0.5
assert ticker['ask'] == 1

View File

@@ -13,6 +13,7 @@ from jsonschema import ValidationError
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.tests.conftest import log_has
from freqtrade import OperationalException
def test_configuration_object() -> None:
@@ -28,7 +29,7 @@ def test_configuration_object() -> None:
assert hasattr(Configuration, 'get_config')
def test_load_config_invalid_pair(default_conf, mocker) -> None:
def test_load_config_invalid_pair(default_conf) -> None:
"""
Test the configuration validator with an invalid PAIR format
"""
@@ -40,7 +41,7 @@ def test_load_config_invalid_pair(default_conf, mocker) -> None:
configuration._validate_config(conf)
def test_load_config_missing_attributes(default_conf, mocker) -> None:
def test_load_config_missing_attributes(default_conf) -> None:
"""
Test the configuration validator with a missing attribute
"""
@@ -314,3 +315,29 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
assert 'spaces' in config
assert config['spaces'] == ['all']
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)
def test_check_exchange(default_conf) -> None:
"""
Test the configuration validator with a missing attribute
"""
conf = deepcopy(default_conf)
configuration = Configuration([])
# Test a valid exchange
conf.get('exchange').update({'name': 'BITTREX'})
assert configuration.check_exchange(conf)
# Test a valid exchange
conf.get('exchange').update({'name': 'binance'})
assert configuration.check_exchange(conf)
# Test a invalid exchange
conf.get('exchange').update({'name': 'unknown_exchange'})
configuration.config = conf
with pytest.raises(
OperationalException,
match=r'.*Exchange "unknown_exchange" not supported.*'
):
configuration.check_exchange(conf)

View File

@@ -71,3 +71,8 @@ def test_file_dump_json(mocker) -> None:
file_dump_json('somefile', [1, 2, 3])
assert file_open.call_count == 1
assert json_dump.call_count == 1
file_open = mocker.patch('freqtrade.misc.gzip.open', MagicMock())
json_dump = mocker.patch('json.dump', MagicMock())
file_dump_json('somefile', [1, 2, 3], True)
assert file_open.call_count == 1
assert json_dump.call_count == 1