diff --git a/.gitignore b/.gitignore index b52a31d8e..9ed046c40 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,7 @@ target/ # Jupyter Notebook .ipynb_checkpoints +*.ipynb # pyenv .python-version diff --git a/config_kraken.json.example b/config_kraken.json.example new file mode 100644 index 000000000..7a47b701f --- /dev/null +++ b/config_kraken.json.example @@ -0,0 +1,71 @@ +{ + "max_open_trades": 5, + "stake_currency": "EUR", + "stake_amount": 10, + "fiat_display_currency": "EUR", + "ticker_interval" : "5m", + "dry_run": true, + "db_url": "sqlite:///tradesv3.dryrun.sqlite", + "trailing_stop": false, + "unfilledtimeout": { + "buy": 10, + "sell": 30 + }, + "bid_strategy": { + "ask_last_balance": 0.0, + "use_order_book": false, + "order_book_top": 1, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "ask_strategy":{ + "use_order_book": false, + "order_book_min": 1, + "order_book_max": 9 + }, + "exchange": { + "name": "kraken", + "key": "", + "secret": "", + "ccxt_config": {"enableRateLimit": true}, + "ccxt_async_config": { + "enableRateLimit": true, + "rateLimit": 1000 + }, + "pair_whitelist": [ + "ETH/EUR", + "BTC/EUR", + "BCH/EUR" + ], + "pair_blacklist": [ + + ] + }, + "edge": { + "enabled": false, + "process_throttle_secs": 3600, + "calculate_since_number_of_days": 7, + "capital_available_percentage": 0.5, + "allowed_risk": 0.01, + "stoploss_range_min": -0.01, + "stoploss_range_max": -0.1, + "stoploss_range_step": -0.01, + "minimum_winrate": 0.60, + "minimum_expectancy": 0.20, + "min_trade_number": 10, + "max_trade_duration_minute": 1440, + "remove_pumps": false + }, + "telegram": { + "enabled": false, + "token": "", + "chat_id": "" + }, + "initial_state": "running", + "forcebuy_enable": false, + "internals": { + "process_throttle_secs": 5 + } +} diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index ca148f518..0d1ae9c26 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '0.18.1-dev' +__version__ = '0.18.2-dev' class DependencyException(BaseException): diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 145e802fa..25d3e62c0 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -67,6 +67,7 @@ def retrier(f): class Exchange(object): _conf: Dict = {} + _params: Dict = {} def __init__(self, config: dict) -> None: """ @@ -303,11 +304,12 @@ class Exchange(object): amount = self.symbol_amount_prec(pair, amount) rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None - if time_in_force == 'gtc': - return self._api.create_order(pair, ordertype, 'buy', amount, rate) - else: - return self._api.create_order(pair, ordertype, 'buy', - amount, rate, {'timeInForce': time_in_force}) + params = self._params.copy() + if time_in_force != 'gtc': + params.update({'timeInForce': time_in_force}) + + return self._api.create_order(pair, ordertype, 'buy', + amount, rate, params) except ccxt.InsufficientFunds as e: raise DependencyException( @@ -346,11 +348,12 @@ class Exchange(object): amount = self.symbol_amount_prec(pair, amount) rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None - if time_in_force == 'gtc': - return self._api.create_order(pair, ordertype, 'sell', amount, rate) - else: - return self._api.create_order(pair, ordertype, 'sell', - amount, rate, {'timeInForce': time_in_force}) + params = self._params.copy() + if time_in_force != 'gtc': + params.update({'timeInForce': time_in_force}) + + return self._api.create_order(pair, ordertype, 'sell', + amount, rate, params) except ccxt.InsufficientFunds as e: raise DependencyException( @@ -402,8 +405,12 @@ class Exchange(object): return self._dry_run_open_orders[order_id] try: + + params = self._params.copy() + params.update({'stopPrice': stop_price}) + order = self._api.create_order(pair, 'stop_loss_limit', 'sell', - amount, rate, {'stopPrice': stop_price}) + amount, rate, params) logger.info('stoploss limit order added for %s. ' 'stop price: %s. limit: %s' % (pair, stop_price, rate)) return order @@ -541,8 +548,8 @@ class Exchange(object): # Gather coroutines to run for pair, ticker_interval in set(pair_list): - if not ((pair, ticker_interval) in self._klines) \ - or self._now_is_time_to_refresh(pair, ticker_interval): + if (not ((pair, ticker_interval) in self._klines) + or self._now_is_time_to_refresh(pair, ticker_interval)): input_coroutines.append(self._async_get_candle_history(pair, ticker_interval)) else: logger.debug("Using cached ohlcv data for %s, %s ...", pair, ticker_interval) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py new file mode 100644 index 000000000..91b41a159 --- /dev/null +++ b/freqtrade/exchange/kraken.py @@ -0,0 +1,12 @@ +""" Kraken exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + +logger = logging.getLogger(__name__) + + +class Kraken(Exchange): + + _params: Dict = {"trading_agreement": "agree"} diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f67be724c..92bdbc042 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -17,10 +17,9 @@ from freqtrade import (DependencyException, OperationalException, from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.exchange import Exchange from freqtrade.persistence import Trade from freqtrade.rpc import RPCManager, RPCMessageType -from freqtrade.resolvers import StrategyResolver, PairListResolver +from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver from freqtrade.state import State from freqtrade.strategy.interface import SellType, IStrategy from freqtrade.wallets import Wallets @@ -55,7 +54,10 @@ class FreqtradeBot(object): self.strategy: IStrategy = StrategyResolver(self.config).strategy self.rpc: RPCManager = RPCManager(self) - self.exchange = Exchange(self.config) + + exchange_name = self.config.get('exchange', {}).get('name', 'bittrex').title() + self.exchange = ExchangeResolver(exchange_name, self.config).exchange + self.wallets = Wallets(self.exchange) self.dataprovider = DataProvider(self.config, self.exchange) diff --git a/freqtrade/resolvers/__init__.py b/freqtrade/resolvers/__init__.py index da2987b27..5cf6c616a 100644 --- a/freqtrade/resolvers/__init__.py +++ b/freqtrade/resolvers/__init__.py @@ -1,4 +1,5 @@ from freqtrade.resolvers.iresolver import IResolver # noqa: F401 +from freqtrade.resolvers.exchange_resolver import ExchangeResolver # noqa: F401 from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # noqa: F401 from freqtrade.resolvers.pairlist_resolver import PairListResolver # noqa: F401 from freqtrade.resolvers.strategy_resolver import StrategyResolver # noqa: F401 diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py new file mode 100644 index 000000000..a68219527 --- /dev/null +++ b/freqtrade/resolvers/exchange_resolver.py @@ -0,0 +1,55 @@ +""" +This module loads custom exchanges +""" +import logging +from pathlib import Path + +from freqtrade.exchange import Exchange +from freqtrade.resolvers import IResolver + +logger = logging.getLogger(__name__) + + +class ExchangeResolver(IResolver): + """ + This class contains all the logic to load a custom exchange class + """ + + __slots__ = ['exchange'] + + def __init__(self, exchange_name: str, config: dict) -> None: + """ + Load the custom class from config parameter + :param config: configuration dictionary or None + """ + try: + self.exchange = self._load_exchange(exchange_name, kwargs={'config': config}) + except ImportError: + logger.info( + f"No {exchange_name} specific subclass found. Using the generic class instead.") + self.exchange = Exchange(config) + + def _load_exchange( + self, exchange_name: str, kwargs: dict) -> Exchange: + """ + Search and loads the specified exchange. + :param exchange_name: name of the module to import + :param extra_dir: additional directory to search for the given exchange + :return: Exchange instance or None + """ + abs_path = Path(__file__).parent.parent.joinpath('exchange').resolve() + + try: + exchange = self._search_object(directory=abs_path, object_type=Exchange, + object_name=exchange_name, + kwargs=kwargs) + if exchange: + logger.info("Using resolved exchange %s from '%s'", exchange_name, abs_path) + return exchange + except FileNotFoundError: + logger.warning('Path "%s" does not exist', abs_path.relative_to(Path.cwd())) + + raise ImportError( + "Impossible to load Exchange '{}'. This class does not exist" + " or contains Python code errors".format(exchange_name) + ) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 6bf7fa17d..e7683bc78 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -63,7 +63,7 @@ class HyperOptResolver(IResolver): hyperopt = self._search_object(directory=_path, object_type=IHyperOpt, object_name=hyperopt_name) if hyperopt: - logger.info('Using resolved hyperopt %s from \'%s\'', hyperopt_name, _path) + logger.info("Using resolved hyperopt %s from '%s'", hyperopt_name, _path) return hyperopt except FileNotFoundError: logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index aee292926..852d1dc0c 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -47,7 +47,7 @@ class IResolver(object): :param directory: relative or absolute directory path :return: object instance """ - logger.debug('Searching for %s %s in \'%s\'', object_type.__name__, object_name, directory) + logger.debug("Searching for %s %s in '%s'", object_type.__name__, object_name, directory) for entry in directory.iterdir(): # Only consider python files if not str(entry).endswith('.py'): diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 286cea5bf..4306a9669 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -48,7 +48,7 @@ class PairListResolver(IResolver): object_name=pairlist_name, kwargs=kwargs) if pairlist: - logger.info('Using resolved pairlist %s from \'%s\'', pairlist_name, _path) + logger.info("Using resolved pairlist %s from '%s'", pairlist_name, _path) return pairlist except FileNotFoundError: logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 01467b0a1..c49da9205 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -149,7 +149,7 @@ class StrategyResolver(IResolver): strategy = self._search_object(directory=_path, object_type=IStrategy, object_name=strategy_name, kwargs={'config': config}) if strategy: - logger.info('Using resolved strategy %s from \'%s\'', strategy_name, _path) + logger.info("Using resolved strategy %s from '%s'", strategy_name, _path) strategy._populate_fun_len = len( getfullargspec(strategy.populate_indicators).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 809dc12e0..d6628d925 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -16,6 +16,7 @@ from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.exchange import Exchange from freqtrade.edge import Edge, PairInfo from freqtrade.freqtradebot import FreqtradeBot +from freqtrade.resolvers import ExchangeResolver logging.getLogger('').setLevel(logging.INFO) @@ -49,7 +50,11 @@ def patch_exchange(mocker, api_mock=None, id='bittrex') -> None: def get_patched_exchange(mocker, config, api_mock=None, id='bittrex') -> Exchange: patch_exchange(mocker, api_mock, id) - exchange = Exchange(config) + config["exchange"]["name"] = id + try: + exchange = ExchangeResolver(id.title(), config).exchange + except ImportError: + exchange = Exchange(config) return exchange diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index b384035b0..72919103c 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -13,7 +13,8 @@ from pandas import DataFrame from freqtrade import DependencyException, OperationalException, TemporaryError from freqtrade.exchange import API_RETRY_COUNT, Exchange -from freqtrade.tests.conftest import get_patched_exchange, log_has +from freqtrade.tests.conftest import get_patched_exchange, log_has, log_has_re +from freqtrade.resolvers.exchange_resolver import ExchangeResolver # Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines @@ -106,6 +107,23 @@ def test_init_exception(default_conf, mocker): Exchange(default_conf) +def test_exchange_resolver(default_conf, mocker, caplog): + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock())) + mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + exchange = ExchangeResolver('Binance', default_conf).exchange + assert isinstance(exchange, Exchange) + assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", + caplog.record_tuples) + caplog.clear() + + exchange = ExchangeResolver('Kraken', default_conf).exchange + assert isinstance(exchange, Exchange) + assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", + caplog.record_tuples) + + def test_symbol_amount_prec(default_conf, mocker): ''' Test rounds down to 4 Decimal places @@ -531,6 +549,67 @@ def test_buy_considers_time_in_force(default_conf, mocker): assert api_mock.create_order.call_args[0][5] == {'timeInForce': 'ioc'} +def test_buy_kraken_trading_agreement(default_conf, mocker): + api_mock = MagicMock() + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_type = 'market' + time_in_force = 'ioc' + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + default_conf['dry_run'] = False + + mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") + + order = exchange.buy(pair='ETH/BTC', ordertype=order_type, + amount=1, rate=200, time_in_force=time_in_force) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == order_type + assert api_mock.create_order.call_args[0][2] == 'buy' + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] is None + assert api_mock.create_order.call_args[0][5] == {'timeInForce': 'ioc', + 'trading_agreement': 'agree'} + + +def test_sell_kraken_trading_agreement(default_conf, mocker): + api_mock = MagicMock() + order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) + order_type = 'market' + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + default_conf['dry_run'] = False + + mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") + + order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == order_type + assert api_mock.create_order.call_args[0][2] == 'sell' + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] is None + assert api_mock.create_order.call_args[0][5] == {'trading_agreement': 'agree'} + + def test_sell_dry_run(default_conf, mocker): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf) @@ -552,11 +631,12 @@ def test_sell_prod(default_conf, mocker): }) default_conf['dry_run'] = False - exchange = get_patched_exchange(mocker, default_conf, api_mock) mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock) order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) + assert 'id' in order assert 'info' in order assert order['id'] == order_id