Refactor exchange to class

This commit is contained in:
xmatthias 2018-06-17 12:41:33 +02:00
parent e3c91df081
commit 21edcbdc27
5 changed files with 356 additions and 371 deletions

View File

@ -10,7 +10,7 @@ import arrow
from pandas import DataFrame, to_datetime from pandas import DataFrame, to_datetime
from freqtrade import constants from freqtrade import constants
from freqtrade.exchange import get_ticker_history from freqtrade.exchange import Exchange
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.strategy.resolver import StrategyResolver, IStrategy from freqtrade.strategy.resolver import StrategyResolver, IStrategy
@ -110,14 +110,14 @@ class Analyze(object):
dataframe = self.populate_sell_trend(dataframe) dataframe = self.populate_sell_trend(dataframe)
return dataframe return dataframe
def get_signal(self, pair: str, interval: str) -> Tuple[bool, bool]: def get_signal(self, exchange: Exchange, pair: str, interval: str) -> Tuple[bool, bool]:
""" """
Calculates current signal based several technical analysis indicators Calculates current signal based several technical analysis indicators
:param pair: pair in format ANT/BTC :param pair: pair in format ANT/BTC
:param interval: Interval to use (in min) :param interval: Interval to use (in min)
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal :return: (Buy, Sell) A bool-tuple indicating buy/sell signal
""" """
ticker_hist = get_ticker_history(pair, interval) ticker_hist = exchange.get_ticker_history(pair, interval)
if not ticker_hist: if not ticker_hist:
logger.warning('Empty ticker history for pair %s', pair) logger.warning('Empty ticker history for pair %s', pair)
return False, False return False, False

View File

@ -12,16 +12,8 @@ from freqtrade import constants, OperationalException, DependencyException, Temp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Current selected exchange
_API: ccxt.Exchange = None
_CONF: Dict = {}
API_RETRY_COUNT = 4 API_RETRY_COUNT = 4
_CACHED_TICKER: Dict[str, Any] = {}
# Holds all open sell orders for dry_run
_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {}
# Urls to exchange markets, insert quote and base with .format() # Urls to exchange markets, insert quote and base with .format()
_EXCHANGE_URLS = { _EXCHANGE_URLS = {
@ -74,364 +66,354 @@ def init_ccxt(exchange_config: dict) -> ccxt.Exchange:
return api return api
def init(config: dict) -> None: class Exchange(object):
"""
Initializes this module with the given config,
it does basic validation whether the specified
exchange and pairs are valid.
:param config: config to use
:return: None
"""
global _CONF, _API
_CONF.update(config) # Current selected exchange
_API: ccxt.Exchange = None
_CONF: Dict = {}
_CACHED_TICKER: Dict[str, Any] = {}
if config['dry_run']: # Holds all open sell orders for dry_run
logger.info('Instance is running with dry_run enabled') _DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {}
exchange_config = config['exchange'] def __init__(self, config: dict) -> None:
_API = init_ccxt(exchange_config) """
Initializes this module with the given config,
it does basic validation whether the specified
exchange and pairs are valid.
:param config: config to use
:return: None
"""
self._API
logger.info('Using Exchange "%s"', get_name()) self._CONF.update(config)
# Check if all pairs are available if config['dry_run']:
validate_pairs(config['exchange']['pair_whitelist']) logger.info('Instance is running with dry_run enabled')
exchange_config = config['exchange']
self._API = init_ccxt(exchange_config)
def validate_pairs(pairs: List[str]) -> None: logger.info('Using Exchange "%s"', self.get_name())
"""
Checks if all given pairs are tradable on the current exchange.
Raises OperationalException if one pair is not available.
:param pairs: list of pairs
:return: None
"""
try: # Check if all pairs are available
markets = _API.load_markets() self.validate_pairs(config['exchange']['pair_whitelist'])
except ccxt.BaseError as e:
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
return
stake_cur = _CONF['stake_currency'] def get_name(self) -> str:
for pair in pairs: return self._API.name
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
# TODO: add a support for having coins in BTC/USDT format
if not pair.endswith(stake_cur):
raise OperationalException(
f'Pair {pair} not compatible with stake_currency: {stake_cur}')
if pair not in markets:
raise OperationalException(
f'Pair {pair} is not available at {get_name()}')
def get_id(self) -> str:
return self._API.id
def exchange_has(endpoint: str) -> bool: def validate_pairs(self, pairs: List[str]) -> None:
""" """
Checks if exchange implements a specific API endpoint. Checks if all given pairs are tradable on the current exchange.
Wrapper around ccxt 'has' attribute Raises OperationalException if one pair is not available.
:param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers') :param pairs: list of pairs
:return: bool :return: None
""" """
return endpoint in _API.has and _API.has[endpoint]
def buy(pair: str, rate: float, amount: float) -> Dict:
if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS
order_id = f'dry_run_buy_{randint(0, 10**6)}'
_DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair,
'price': rate,
'amount': amount,
'type': 'limit',
'side': 'buy',
'remaining': 0.0,
'datetime': arrow.utcnow().isoformat(),
'status': 'closed',
'fee': None
}
return {'id': order_id}
try:
return _API.create_limit_buy_order(pair, amount, rate)
except ccxt.InsufficientFunds as e:
raise DependencyException(
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(
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(
f'Could not place buy order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
def sell(pair: str, rate: float, amount: float) -> Dict:
if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS
order_id = f'dry_run_sell_{randint(0, 10**6)}'
_DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair,
'price': rate,
'amount': amount,
'type': 'limit',
'side': 'sell',
'remaining': 0.0,
'datetime': arrow.utcnow().isoformat(),
'status': 'closed'
}
return {'id': order_id}
try:
return _API.create_limit_sell_order(pair, amount, rate)
except ccxt.InsufficientFunds as e:
raise DependencyException(
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(
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(
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_balance(currency: str) -> float:
if _CONF['dry_run']:
return 999.9
# ccxt exception is already handled by get_balances
balances = get_balances()
balance = balances.get(currency)
if balance is None:
raise TemporaryError(
f'Could not get {currency} balance due to malformed exchange response: {balances}')
return balance['free']
@retrier
def get_balances() -> dict:
if _CONF['dry_run']:
return {}
try:
balances = _API.fetch_balance()
# Remove additional info from ccxt results
balances.pop("info", None)
balances.pop("free", None)
balances.pop("total", None)
balances.pop("used", None)
return balances
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get balance due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_tickers() -> Dict:
try:
return _API.fetch_tickers()
except ccxt.NotSupported as e:
raise OperationalException(
f'Exchange {_API.name} does not support fetching tickers in batch.'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not load tickers due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
global _CACHED_TICKER
if refresh or pair not in _CACHED_TICKER.keys():
try: try:
data = _API.fetch_ticker(pair) markets = self._API.load_markets()
except ccxt.BaseError as e:
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
return
stake_cur = self._CONF['stake_currency']
for pair in pairs:
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
# TODO: add a support for having coins in BTC/USDT format
if not pair.endswith(stake_cur):
raise OperationalException(
f'Pair {pair} not compatible with stake_currency: {stake_cur}')
if pair not in markets:
raise OperationalException(
f'Pair {pair} is not available at {self.get_name()}')
def exchange_has(self, endpoint: str) -> bool:
"""
Checks if exchange implements a specific API endpoint.
Wrapper around ccxt 'has' attribute
:param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers')
:return: bool
"""
return endpoint in self._API.has and self._API.has[endpoint]
def buy(self, pair: str, rate: float, amount: float) -> Dict:
if self._CONF['dry_run']:
order_id = f'dry_run_buy_{randint(0, 10**6)}'
self._DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair,
'price': rate,
'amount': amount,
'type': 'limit',
'side': 'buy',
'remaining': 0.0,
'datetime': arrow.utcnow().isoformat(),
'status': 'closed',
'fee': None
}
return {'id': order_id}
try:
return self._API.create_limit_buy_order(pair, amount, rate)
except ccxt.InsufficientFunds as e:
raise DependencyException(
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(
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(
f'Could not place buy order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
def sell(self, pair: str, rate: float, amount: float) -> Dict:
if self._CONF['dry_run']:
order_id = f'dry_run_sell_{randint(0, 10**6)}'
self._DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair,
'price': rate,
'amount': amount,
'type': 'limit',
'side': 'sell',
'remaining': 0.0,
'datetime': arrow.utcnow().isoformat(),
'status': 'closed'
}
return {'id': order_id}
try:
return self._API.create_limit_sell_order(pair, amount, rate)
except ccxt.InsufficientFunds as e:
raise DependencyException(
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(
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(
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_balance(self, currency: str) -> float:
if self._CONF['dry_run']:
return 999.9
# ccxt exception is already handled by get_balances
balances = self.get_balances()
balance = balances.get(currency)
if balance is None:
raise TemporaryError(
f'Could not get {currency} balance due to malformed exchange response: {balances}')
return balance['free']
@retrier
def get_balances(self) -> dict:
if self._CONF['dry_run']:
return {}
try:
balances = self._API.fetch_balance()
# Remove additional info from ccxt results
balances.pop("info", None)
balances.pop("free", None)
balances.pop("total", None)
balances.pop("used", None)
return balances
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get balance due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_tickers(self) -> Dict:
try:
return self._API.fetch_tickers()
except ccxt.NotSupported as e:
raise OperationalException(
f'Exchange {self._API.name} does not support fetching tickers in batch.'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not load tickers due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
if refresh or pair not in self._CACHED_TICKER.keys():
try: try:
_CACHED_TICKER[pair] = { data = self._API.fetch_ticker(pair)
'bid': float(data['bid']), try:
'ask': float(data['ask']), self._CACHED_TICKER[pair] = {
} 'bid': float(data['bid']),
except KeyError: 'ask': float(data['ask']),
logger.debug("Could not cache ticker data for %s", pair) }
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 self._CACHED_TICKER[pair]
@retrier
def get_ticker_history(self, pair: str, tick_interval: str,
since_ms: Optional[int] = None) -> List[Dict]:
try:
# last item should be in the time interval [now - tick_interval, now]
till_time_ms = arrow.utcnow().shift(
minutes=-constants.TICKER_INTERVAL_MINUTES[tick_interval]
).timestamp * 1000
# it looks as if some exchanges return cached data
# and they update it one in several minute, so 10 mins interval
# is necessary to skeep downloading of an empty array when all
# chached data was already downloaded
till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000)
data: List[Dict[Any, Any]] = []
while not since_ms or since_ms < till_time_ms:
data_part = self._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
logger.debug('Downloaded data for %s time range [%s, %s]',
pair,
arrow.get(data_part[0][0] / 1000).format(),
arrow.get(data_part[-1][0] / 1000).format())
data.extend(data_part)
since_ms = data[-1][0] + 1
return data return data
except ccxt.NotSupported as e:
raise OperationalException(
f'Exchange {self._API.name} does not support fetching historical candlestick data.'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}') f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(f'Could not fetch ticker data. Msg: {e}')
@retrier
def cancel_order(self, order_id: str, pair: str) -> None:
if self._CONF['dry_run']:
return
try:
return self._API.cancel_order(order_id, pair)
except ccxt.InvalidOrder as e:
raise DependencyException(
f'Could not cancel order. Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e)
else:
logger.info("returning cached ticker-data for %s", pair)
return _CACHED_TICKER[pair]
@retrier
def get_order(self, order_id: str, pair: str) -> Dict:
if self._CONF['dry_run']:
order = self._DRY_RUN_OPEN_ORDERS[order_id]
order.update({
'id': order_id
})
return order
try:
return self._API.fetch_order(order_id, pair)
except ccxt.InvalidOrder as e:
raise DependencyException(
f'Could not get order. Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier @retrier
def get_ticker_history(pair: str, tick_interval: str, since_ms: Optional[int] = None) -> List[Dict]: def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
try: if self._CONF['dry_run']:
# last item should be in the time interval [now - tick_interval, now] return []
till_time_ms = arrow.utcnow().shift( if not self.exchange_has('fetchMyTrades'):
minutes=-constants.TICKER_INTERVAL_MINUTES[tick_interval] return []
).timestamp * 1000 try:
# it looks as if some exchanges return cached data my_trades = self._API.fetch_my_trades(pair, since.timestamp())
# and they update it one in several minute, so 10 mins interval matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
# is necessary to skeep downloading of an empty array when all
# chached data was already downloaded
till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000)
data: List[Dict[Any, Any]] = [] return matched_trades
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. except ccxt.NetworkError as e:
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last) raise TemporaryError(
# when GDAX returns a list of tickers DESC (newest first, oldest last) f'Could not get trades due to networking error. Message: {e}')
data_part = sorted(data_part, key=lambda x: x[0]) except ccxt.BaseError as e:
raise OperationalException(e)
if not data_part: def get_pair_detail_url(self, pair: str) -> str:
break try:
url_base = self._API.urls.get('www')
base, quote = pair.split('/')
logger.debug('Downloaded data for %s time range [%s, %s]', return url_base + _EXCHANGE_URLS[self._API.id].format(base=base, quote=quote)
pair, except KeyError:
arrow.get(data_part[0][0] / 1000).format(), logger.warning('Could not get exchange url for %s', self.get_name())
arrow.get(data_part[-1][0] / 1000).format()) return ""
data.extend(data_part) @retrier
since_ms = data[-1][0] + 1 def get_markets(self) -> List[dict]:
try:
return self._API.fetch_markets()
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not load markets due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
return data @retrier
except ccxt.NotSupported as e: def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1,
raise OperationalException( price=1, taker_or_maker='maker') -> float:
f'Exchange {_API.name} does not support fetching historical candlestick data.' try:
f'Message: {e}') # validate that markets are loaded before trying to get fee
except (ccxt.NetworkError, ccxt.ExchangeError) as e: if self._API.markets is None or len(self._API.markets) == 0:
raise TemporaryError( self._API.load_markets()
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(f'Could not fetch ticker data. Msg: {e}')
return self._API.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
price=price, takerOrMaker=taker_or_maker)['rate']
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier def get_amount_lots(self, pair: str, amount: float) -> float:
def cancel_order(order_id: str, pair: str) -> None: """
if _CONF['dry_run']: get buyable amount rounding, ..
return """
try:
return _API.cancel_order(order_id, pair)
except ccxt.InvalidOrder as e:
raise DependencyException(
f'Could not cancel order. Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_order(order_id: str, pair: str) -> Dict:
if _CONF['dry_run']:
order = _DRY_RUN_OPEN_ORDERS[order_id]
order.update({
'id': order_id
})
return order
try:
return _API.fetch_order(order_id, pair)
except ccxt.InvalidOrder as e:
raise DependencyException(
f'Could not get order. Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_trades_for_order(order_id: str, pair: str, since: datetime) -> List:
if _CONF['dry_run']:
return []
if not exchange_has('fetchMyTrades'):
return []
try:
my_trades = _API.fetch_my_trades(pair, since.timestamp())
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
return matched_trades
except ccxt.NetworkError as e:
raise TemporaryError(
f'Could not get trades due to networking error. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
def get_pair_detail_url(pair: str) -> str:
try:
url_base = _API.urls.get('www')
base, quote = pair.split('/')
return url_base + _EXCHANGE_URLS[_API.id].format(base=base, quote=quote)
except KeyError:
logger.warning('Could not get exchange url for %s', get_name())
return ""
@retrier
def get_markets() -> List[dict]:
try:
return _API.fetch_markets()
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not load markets due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
def get_name() -> str:
return _API.name
def get_id() -> str:
return _API.id
@retrier
def get_fee(symbol='ETH/BTC', type='', side='', amount=1,
price=1, taker_or_maker='maker') -> float:
try:
# validate that markets are loaded before trying to get fee # validate that markets are loaded before trying to get fee
if _API.markets is None or len(_API.markets) == 0: if not self._API.markets:
_API.load_markets() self._API.load_markets()
return self._API.amount_to_lots(pair, amount)
return _API.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
price=price, takerOrMaker=taker_or_maker)['rate']
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
def get_amount_lots(pair: str, amount: float) -> float:
"""
get buyable amount rounding, ..
"""
# validate that markets are loaded before trying to get fee
if not _API.markets:
_API.load_markets()
return _API.amount_to_lots(pair, amount)

View File

@ -14,11 +14,11 @@ import requests
from cachetools import TTLCache, cached from cachetools import TTLCache, cached
from freqtrade import ( from freqtrade import (
DependencyException, OperationalException, TemporaryError, DependencyException, OperationalException, TemporaryError, persistence, __version__,
exchange, persistence, __version__,
) )
from freqtrade import constants from freqtrade import constants
from freqtrade.analyze import Analyze from freqtrade.analyze import Analyze
from freqtrade.exchange import Exchange
from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.rpc_manager import RPCManager from freqtrade.rpc.rpc_manager import RPCManager
@ -66,7 +66,7 @@ class FreqtradeBot(object):
# Initialize all modules # Initialize all modules
persistence.init(self.config) persistence.init(self.config)
exchange.init(self.config) self.exchange = Exchange(self.config)
# Set initial application state # Set initial application state
initial_state = self.config.get('initial_state') initial_state = self.config.get('initial_state')
@ -186,13 +186,13 @@ class FreqtradeBot(object):
:return: List of pairs :return: List of pairs
""" """
if not exchange.exchange_has('fetchTickers'): if not self.exchange.exchange_has('fetchTickers'):
raise OperationalException( raise OperationalException(
'Exchange does not support dynamic whitelist.' 'Exchange does not support dynamic whitelist.'
'Please edit your config and restart the bot' 'Please edit your config and restart the bot'
) )
tickers = exchange.get_tickers() tickers = self.exchange.get_tickers()
# check length so that we make sure that '/' is actually in the string # check length so that we make sure that '/' is actually in the string
tickers = [v for k, v in tickers.items() tickers = [v for k, v in tickers.items()
if len(k.split('/')) == 2 and k.split('/')[1] == base_currency] if len(k.split('/')) == 2 and k.split('/')[1] == base_currency]
@ -210,7 +210,7 @@ class FreqtradeBot(object):
black_listed black_listed
""" """
sanitized_whitelist = whitelist sanitized_whitelist = whitelist
markets = exchange.get_markets() markets = self.exchange.get_markets()
markets = [m for m in markets if m['quote'] == self.config['stake_currency']] markets = [m for m in markets if m['quote'] == self.config['stake_currency']]
known_pairs = set() known_pairs = set()
@ -255,7 +255,7 @@ class FreqtradeBot(object):
interval = self.analyze.get_ticker_interval() interval = self.analyze.get_ticker_interval()
stake_currency = self.config['stake_currency'] stake_currency = self.config['stake_currency']
fiat_currency = self.config['fiat_display_currency'] fiat_currency = self.config['fiat_display_currency']
exc_name = exchange.get_name() exc_name = self.exchange.get_name()
logger.info( logger.info(
'Checking buy signals to create a new trade with stake_amount: %f ...', 'Checking buy signals to create a new trade with stake_amount: %f ...',
@ -263,7 +263,7 @@ class FreqtradeBot(object):
) )
whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist']) whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
# Check if stake_amount is fulfilled # Check if stake_amount is fulfilled
if exchange.get_balance(stake_currency) < stake_amount: if self.exchange.get_balance(stake_currency) < stake_amount:
raise DependencyException( raise DependencyException(
f'stake amount is not fulfilled (currency={stake_currency})') f'stake amount is not fulfilled (currency={stake_currency})')
@ -278,19 +278,19 @@ class FreqtradeBot(object):
# Pick pair based on buy signals # Pick pair based on buy signals
for _pair in whitelist: for _pair in whitelist:
(buy, sell) = self.analyze.get_signal(_pair, interval) (buy, sell) = self.analyze.get_signal(self.exchange, _pair, interval)
if buy and not sell: if buy and not sell:
pair = _pair pair = _pair
break break
else: else:
return False return False
pair_s = pair.replace('_', '/') pair_s = pair.replace('_', '/')
pair_url = exchange.get_pair_detail_url(pair) pair_url = self.exchange.get_pair_detail_url(pair)
# Calculate amount # Calculate amount
buy_limit = self.get_target_bid(exchange.get_ticker(pair)) buy_limit = self.get_target_bid(self.exchange.get_ticker(pair))
amount = stake_amount / buy_limit amount = stake_amount / buy_limit
order_id = exchange.buy(pair, buy_limit, amount)['id'] order_id = self.exchange.buy(pair, buy_limit, amount)['id']
stake_amount_fiat = self.fiat_converter.convert_amount( stake_amount_fiat = self.fiat_converter.convert_amount(
stake_amount, stake_amount,
@ -305,7 +305,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
{stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`""" {stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`"""
) )
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = exchange.get_fee(symbol=pair, taker_or_maker='maker') fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
trade = Trade( trade = Trade(
pair=pair, pair=pair,
stake_amount=stake_amount, stake_amount=stake_amount,
@ -315,7 +315,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
open_rate=buy_limit, open_rate=buy_limit,
open_rate_requested=buy_limit, open_rate_requested=buy_limit,
open_date=datetime.utcnow(), open_date=datetime.utcnow(),
exchange=exchange.get_id(), exchange=self.exchange.get_id(),
open_order_id=order_id open_order_id=order_id
) )
Trade.session.add(trade) Trade.session.add(trade)
@ -348,7 +348,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
if trade.open_order_id: if trade.open_order_id:
# Update trade with order values # Update trade with order values
logger.info('Found open order for %s', trade) logger.info('Found open order for %s', trade)
order = exchange.get_order(trade.open_order_id, trade.pair) order = self.exchange.get_order(trade.open_order_id, trade.pair)
# Try update amount (binance-fix) # Try update amount (binance-fix)
try: try:
new_amount = self.get_real_amount(trade, order) new_amount = self.get_real_amount(trade, order)
@ -372,7 +372,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
def get_real_amount(self, trade: Trade, order: Dict) -> float: def get_real_amount(self, trade: Trade, order: Dict) -> float:
""" """
Get real amount for the trade Get real amount for the trade
Necessary for exchanges which charge fees in base currency (e.g. binance) Necessary for self.exchanges which charge fees in base currency (e.g. binance)
""" """
order_amount = order['amount'] order_amount = order['amount']
# Only run for closed orders # Only run for closed orders
@ -388,7 +388,8 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
return new_amount return new_amount
# Fallback to Trades # Fallback to Trades
trades = exchange.get_trades_for_order(trade.open_order_id, trade.pair, trade.open_date) trades = self.exchange.get_trades_for_order(trade.open_order_id, trade.pair,
trade.open_date)
if len(trades) == 0: if len(trades) == 0:
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade) logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
@ -420,7 +421,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
raise ValueError(f'attempt to handle closed trade: {trade}') raise ValueError(f'attempt to handle closed trade: {trade}')
logger.debug('Handling %s ...', trade) logger.debug('Handling %s ...', trade)
current_rate = exchange.get_ticker(trade.pair)['bid'] current_rate = self.exchange.get_ticker(trade.pair)['bid']
(buy, sell) = (False, False) (buy, sell) = (False, False)
@ -449,7 +450,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
# updated via /forcesell in a different thread. # updated via /forcesell in a different thread.
if not trade.open_order_id: if not trade.open_order_id:
continue continue
order = exchange.get_order(trade.open_order_id, trade.pair) order = self.exchange.get_order(trade.open_order_id, trade.pair)
except requests.exceptions.RequestException: except requests.exceptions.RequestException:
logger.info( logger.info(
'Cannot query order for %s due to %s', 'Cannot query order for %s due to %s',
@ -475,7 +476,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
:return: True if order was fully cancelled :return: True if order was fully cancelled
""" """
pair_s = trade.pair.replace('_', '/') pair_s = trade.pair.replace('_', '/')
exchange.cancel_order(trade.open_order_id, trade.pair) self.exchange.cancel_order(trade.open_order_id, trade.pair)
if order['remaining'] == order['amount']: if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade # if trade is not partially completed, just delete the trade
Trade.session.delete(trade) Trade.session.delete(trade)
@ -502,7 +503,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
pair_s = trade.pair.replace('_', '/') pair_s = trade.pair.replace('_', '/')
if order['remaining'] == order['amount']: if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade # if trade is not partially completed, just cancel the trade
exchange.cancel_order(trade.open_order_id, trade.pair) self.exchange.cancel_order(trade.open_order_id, trade.pair)
trade.close_rate = None trade.close_rate = None
trade.close_profit = None trade.close_profit = None
trade.close_date = None trade.close_date = None
@ -525,15 +526,15 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
exc = trade.exchange exc = trade.exchange
pair = trade.pair pair = trade.pair
# Execute sell and update trade record # Execute sell and update trade record
order_id = exchange.sell(str(trade.pair), limit, trade.amount)['id'] order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id']
trade.open_order_id = order_id trade.open_order_id = order_id
trade.close_rate_requested = limit trade.close_rate_requested = limit
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2) fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
profit_trade = trade.calc_profit(rate=limit) profit_trade = trade.calc_profit(rate=limit)
current_rate = exchange.get_ticker(trade.pair)['bid'] current_rate = self.exchange.get_ticker(trade.pair)['bid']
profit = trade.calc_profit_percent(limit) profit = trade.calc_profit_percent(limit)
pair_url = exchange.get_pair_detail_url(trade.pair) pair_url = self.exchange.get_pair_detail_url(trade.pair)
gain = "profit" if fmt_exp_profit > 0 else "loss" gain = "profit" if fmt_exp_profit > 0 else "loss"
message = f"*{exc}:* Selling\n" \ message = f"*{exc}:* Selling\n" \

View File

@ -8,7 +8,7 @@ from typing import Optional, List, Dict, Tuple, Any
import arrow import arrow
from freqtrade import misc, constants from freqtrade import misc, constants
from freqtrade.exchange import get_ticker_history from freqtrade.exchange import Exchange
from freqtrade.arguments import TimeRange from freqtrade.arguments import TimeRange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -183,6 +183,7 @@ def load_cached_data_for_updating(filename: str,
def download_backtesting_testdata(datadir: str, def download_backtesting_testdata(datadir: str,
exchange: Exchange,
pair: str, pair: str,
tick_interval: str = '5m', tick_interval: str = '5m',
timerange: Optional[TimeRange] = None) -> None: timerange: Optional[TimeRange] = None) -> None:
@ -216,7 +217,8 @@ def download_backtesting_testdata(datadir: str,
logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None') logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None')
logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
new_data = get_ticker_history(pair=pair, tick_interval=tick_interval, since_ms=since_ms) new_data = exchange.get_ticker_history(pair=pair, tick_interval=tick_interval,
since_ms=since_ms)
data.extend(new_data) data.extend(new_data)
logger.debug("New Start: %s", misc.format_ms_time(data[0][0])) logger.debug("New Start: %s", misc.format_ms_time(data[0][0]))

View File

@ -14,7 +14,7 @@ from pandas import DataFrame
from tabulate import tabulate from tabulate import tabulate
import freqtrade.optimize as optimize import freqtrade.optimize as optimize
from freqtrade import exchange from freqtrade.exchange import Exchange
from freqtrade.analyze import Analyze from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
@ -61,7 +61,7 @@ class Backtesting(object):
self.config['exchange']['password'] = '' self.config['exchange']['password'] = ''
self.config['exchange']['uid'] = '' self.config['exchange']['uid'] = ''
self.config['dry_run'] = True self.config['dry_run'] = True
exchange.init(self.config) self.exchange = Exchange(self.config)
@staticmethod @staticmethod
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
@ -130,7 +130,7 @@ class Backtesting(object):
stake_amount = args['stake_amount'] stake_amount = args['stake_amount']
max_open_trades = args.get('max_open_trades', 0) max_open_trades = args.get('max_open_trades', 0)
fee = exchange.get_fee() fee = self.exchange.get_fee()
trade = Trade( trade = Trade(
open_rate=buy_row.close, open_rate=buy_row.close,
open_date=buy_row.date, open_date=buy_row.date,
@ -256,7 +256,7 @@ class Backtesting(object):
if self.config.get('live'): if self.config.get('live'):
logger.info('Downloading data for all pairs in whitelist ...') logger.info('Downloading data for all pairs in whitelist ...')
for pair in pairs: for pair in pairs:
data[pair] = exchange.get_ticker_history(pair, self.ticker_interval) data[pair] = self.exchange.get_ticker_history(pair, self.ticker_interval)
else: else:
logger.info('Using local backtesting data (using whitelist in given config) ...') logger.info('Using local backtesting data (using whitelist in given config) ...')