Merge pull request #1563 from iuvbio/kraken_support

Kraken support
This commit is contained in:
Matthias 2019-02-21 06:41:35 +01:00 committed by GitHub
commit a79ff1c6c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 258 additions and 24 deletions

1
.gitignore vendored
View File

@ -81,6 +81,7 @@ target/
# Jupyter Notebook # Jupyter Notebook
.ipynb_checkpoints .ipynb_checkpoints
*.ipynb
# pyenv # pyenv
.python-version .python-version

View File

@ -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
}
}

View File

@ -1,5 +1,5 @@
""" FreqTrade bot """ """ FreqTrade bot """
__version__ = '0.18.1-dev' __version__ = '0.18.2-dev'
class DependencyException(BaseException): class DependencyException(BaseException):

View File

@ -67,6 +67,7 @@ def retrier(f):
class Exchange(object): class Exchange(object):
_conf: Dict = {} _conf: Dict = {}
_params: Dict = {}
def __init__(self, config: dict) -> None: def __init__(self, config: dict) -> None:
""" """
@ -303,11 +304,12 @@ class Exchange(object):
amount = self.symbol_amount_prec(pair, amount) amount = self.symbol_amount_prec(pair, amount)
rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None
if time_in_force == 'gtc': params = self._params.copy()
return self._api.create_order(pair, ordertype, 'buy', amount, rate) if time_in_force != 'gtc':
else: params.update({'timeInForce': time_in_force})
return self._api.create_order(pair, ordertype, 'buy',
amount, rate, {'timeInForce': time_in_force}) return self._api.create_order(pair, ordertype, 'buy',
amount, rate, params)
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise DependencyException( raise DependencyException(
@ -346,11 +348,12 @@ class Exchange(object):
amount = self.symbol_amount_prec(pair, amount) amount = self.symbol_amount_prec(pair, amount)
rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None
if time_in_force == 'gtc': params = self._params.copy()
return self._api.create_order(pair, ordertype, 'sell', amount, rate) if time_in_force != 'gtc':
else: params.update({'timeInForce': time_in_force})
return self._api.create_order(pair, ordertype, 'sell',
amount, rate, {'timeInForce': time_in_force}) return self._api.create_order(pair, ordertype, 'sell',
amount, rate, params)
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise DependencyException( raise DependencyException(
@ -402,8 +405,12 @@ class Exchange(object):
return self._dry_run_open_orders[order_id] return self._dry_run_open_orders[order_id]
try: try:
params = self._params.copy()
params.update({'stopPrice': stop_price})
order = self._api.create_order(pair, 'stop_loss_limit', 'sell', 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. ' logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s' % (pair, stop_price, rate)) 'stop price: %s. limit: %s' % (pair, stop_price, rate))
return order return order
@ -541,8 +548,8 @@ class Exchange(object):
# Gather coroutines to run # Gather coroutines to run
for pair, ticker_interval in set(pair_list): for pair, ticker_interval in set(pair_list):
if not ((pair, ticker_interval) in self._klines) \ if (not ((pair, ticker_interval) in self._klines)
or self._now_is_time_to_refresh(pair, ticker_interval): or self._now_is_time_to_refresh(pair, ticker_interval)):
input_coroutines.append(self._async_get_candle_history(pair, ticker_interval)) input_coroutines.append(self._async_get_candle_history(pair, ticker_interval))
else: else:
logger.debug("Using cached ohlcv data for %s, %s ...", pair, ticker_interval) logger.debug("Using cached ohlcv data for %s, %s ...", pair, ticker_interval)

View File

@ -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"}

View File

@ -17,10 +17,9 @@ from freqtrade import (DependencyException, OperationalException,
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.exchange import Exchange
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc import RPCManager, RPCMessageType 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.state import State
from freqtrade.strategy.interface import SellType, IStrategy from freqtrade.strategy.interface import SellType, IStrategy
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
@ -55,7 +54,10 @@ class FreqtradeBot(object):
self.strategy: IStrategy = StrategyResolver(self.config).strategy self.strategy: IStrategy = StrategyResolver(self.config).strategy
self.rpc: RPCManager = RPCManager(self) 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.wallets = Wallets(self.exchange)
self.dataprovider = DataProvider(self.config, self.exchange) self.dataprovider = DataProvider(self.config, self.exchange)

View File

@ -1,4 +1,5 @@
from freqtrade.resolvers.iresolver import IResolver # noqa: F401 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.hyperopt_resolver import HyperOptResolver # noqa: F401
from freqtrade.resolvers.pairlist_resolver import PairListResolver # noqa: F401 from freqtrade.resolvers.pairlist_resolver import PairListResolver # noqa: F401
from freqtrade.resolvers.strategy_resolver import StrategyResolver # noqa: F401 from freqtrade.resolvers.strategy_resolver import StrategyResolver # noqa: F401

View File

@ -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)
)

View File

@ -63,7 +63,7 @@ class HyperOptResolver(IResolver):
hyperopt = self._search_object(directory=_path, object_type=IHyperOpt, hyperopt = self._search_object(directory=_path, object_type=IHyperOpt,
object_name=hyperopt_name) object_name=hyperopt_name)
if hyperopt: 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 return hyperopt
except FileNotFoundError: except FileNotFoundError:
logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd()))

View File

@ -47,7 +47,7 @@ class IResolver(object):
:param directory: relative or absolute directory path :param directory: relative or absolute directory path
:return: object instance :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(): for entry in directory.iterdir():
# Only consider python files # Only consider python files
if not str(entry).endswith('.py'): if not str(entry).endswith('.py'):

View File

@ -48,7 +48,7 @@ class PairListResolver(IResolver):
object_name=pairlist_name, object_name=pairlist_name,
kwargs=kwargs) kwargs=kwargs)
if pairlist: 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 return pairlist
except FileNotFoundError: except FileNotFoundError:
logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd()))

View File

@ -149,7 +149,7 @@ class StrategyResolver(IResolver):
strategy = self._search_object(directory=_path, object_type=IStrategy, strategy = self._search_object(directory=_path, object_type=IStrategy,
object_name=strategy_name, kwargs={'config': config}) object_name=strategy_name, kwargs={'config': config})
if strategy: 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( strategy._populate_fun_len = len(
getfullargspec(strategy.populate_indicators).args) getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)

View File

@ -16,6 +16,7 @@ from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.edge import Edge, PairInfo from freqtrade.edge import Edge, PairInfo
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.resolvers import ExchangeResolver
logging.getLogger('').setLevel(logging.INFO) 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: def get_patched_exchange(mocker, config, api_mock=None, id='bittrex') -> Exchange:
patch_exchange(mocker, api_mock, id) 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 return exchange

View File

@ -13,7 +13,8 @@ from pandas import DataFrame
from freqtrade import DependencyException, OperationalException, TemporaryError from freqtrade import DependencyException, OperationalException, TemporaryError
from freqtrade.exchange import API_RETRY_COUNT, Exchange 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 # 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) 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): def test_symbol_amount_prec(default_conf, mocker):
''' '''
Test rounds down to 4 Decimal places 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'} 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): def test_sell_dry_run(default_conf, mocker):
default_conf['dry_run'] = True default_conf['dry_run'] = True
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
@ -552,11 +631,12 @@ def test_sell_prod(default_conf, mocker):
}) })
default_conf['dry_run'] = False 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_amount_prec', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_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) order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
assert 'id' in order assert 'id' in order
assert 'info' in order assert 'info' in order
assert order['id'] == order_id assert order['id'] == order_id