commit
a79ff1c6c9
1
.gitignore
vendored
1
.gitignore
vendored
@ -81,6 +81,7 @@ target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
*.ipynb
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
71
config_kraken.json.example
Normal file
71
config_kraken.json.example
Normal 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
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
""" FreqTrade bot """
|
||||
__version__ = '0.18.1-dev'
|
||||
__version__ = '0.18.2-dev'
|
||||
|
||||
|
||||
class DependencyException(BaseException):
|
||||
|
@ -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)
|
||||
|
12
freqtrade/exchange/kraken.py
Normal file
12
freqtrade/exchange/kraken.py
Normal 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"}
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
55
freqtrade/resolvers/exchange_resolver.py
Normal file
55
freqtrade/resolvers/exchange_resolver.py
Normal 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)
|
||||
)
|
@ -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()))
|
||||
|
@ -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'):
|
||||
|
@ -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()))
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user