From 14d16d573cb8369df54bc80171b1431cae818cf3 Mon Sep 17 00:00:00 2001 From: Samuel Husso Date: Wed, 21 Mar 2018 19:31:15 +0200 Subject: [PATCH] Remove bittrex related interface code and tests --- freqtrade/exchange/bittrex.py | 211 ----------- freqtrade/exchange/interface.py | 172 --------- .../tests/exchange/test_exchange_bittrex.py | 349 ------------------ 3 files changed, 732 deletions(-) delete mode 100644 freqtrade/exchange/bittrex.py delete mode 100644 freqtrade/exchange/interface.py delete mode 100644 freqtrade/tests/exchange/test_exchange_bittrex.py diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py deleted file mode 100644 index 0cba621af..000000000 --- a/freqtrade/exchange/bittrex.py +++ /dev/null @@ -1,211 +0,0 @@ -import logging -from typing import Dict, List, Optional - -from bittrex.bittrex import API_V1_1, API_V2_0 -from bittrex.bittrex import Bittrex as _Bittrex -from requests.exceptions import ContentDecodingError - -from freqtrade import OperationalException -from freqtrade.exchange.interface import Exchange - -logger = logging.getLogger(__name__) - -_API: _Bittrex = None -_API_V2: _Bittrex = None -_EXCHANGE_CONF: dict = {} - - -class Bittrex(Exchange): - """ - Bittrex API wrapper. - """ - # Base URL and API endpoints - BASE_URL: str = 'https://www.bittrex.com' - PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index' - - def __init__(self, config: dict) -> None: - global _API, _API_V2, _EXCHANGE_CONF - - _EXCHANGE_CONF.update(config) - _API = _Bittrex( - api_key=_EXCHANGE_CONF['key'], - api_secret=_EXCHANGE_CONF['secret'], - calls_per_second=1, - api_version=API_V1_1, - ) - _API_V2 = _Bittrex( - api_key=_EXCHANGE_CONF['key'], - api_secret=_EXCHANGE_CONF['secret'], - calls_per_second=1, - api_version=API_V2_0, - ) - self.cached_ticker = {} - - @staticmethod - def _validate_response(response) -> None: - """ - Validates the given bittrex response - and raises a ContentDecodingError if a non-fatal issue happened. - """ - temp_error_messages = [ - 'NO_API_RESPONSE', - 'MIN_TRADE_REQUIREMENT_NOT_MET', - ] - if response['message'] in temp_error_messages: - raise ContentDecodingError(response['message']) - - @property - def fee(self) -> float: - # 0.25 %: See https://bittrex.com/fees - return 0.0025 - - def buy(self, pair: str, rate: float, amount: float) -> str: - data = _API.buy_limit(pair.replace('_', '-'), amount, rate) - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format( - message=data['message'], - pair=pair, - rate=rate, - amount=amount)) - return data['result']['uuid'] - - def sell(self, pair: str, rate: float, amount: float) -> str: - data = _API.sell_limit(pair.replace('_', '-'), amount, rate) - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format( - message=data['message'], - pair=pair, - rate=rate, - amount=amount)) - return data['result']['uuid'] - - def get_balance(self, currency: str) -> float: - data = _API.get_balance(currency) - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message} params=({currency})'.format( - message=data['message'], - currency=currency)) - return float(data['result']['Balance'] or 0.0) - - def get_balances(self): - data = _API.get_balances() - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message}'.format(message=data['message'])) - return data['result'] - - def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: - if refresh or pair not in self.cached_ticker.keys(): - data = _API.get_ticker(pair.replace('_', '-')) - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message} params=({pair})'.format( - message=data['message'], - pair=pair)) - keys = ['Bid', 'Ask', 'Last'] - if not data.get('result') or\ - not all(key in data.get('result', {}) for key in keys) or\ - not all(data.get('result', {})[key] is not None for key in keys): - raise ContentDecodingError('Invalid response from Bittrex params=({pair})'.format( - pair=pair)) - # Update the pair - self.cached_ticker[pair] = { - 'bid': float(data['result']['Bid']), - 'ask': float(data['result']['Ask']), - 'last': float(data['result']['Last']), - } - return self.cached_ticker[pair] - - def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]: - if tick_interval == 1: - interval = 'oneMin' - elif tick_interval == 5: - interval = 'fiveMin' - elif tick_interval == 30: - interval = 'thirtyMin' - elif tick_interval == 60: - interval = 'hour' - elif tick_interval == 1440: - interval = 'Day' - else: - raise ValueError('Unknown tick_interval: {}'.format(tick_interval)) - - data = _API_V2.get_candles(pair.replace('_', '-'), interval) - - # These sanity check are necessary because bittrex cannot keep their API stable. - if not data.get('result'): - raise ContentDecodingError('Invalid response from Bittrex params=({pair})'.format( - pair=pair)) - - for prop in ['C', 'V', 'O', 'H', 'L', 'T']: - for tick in data['result']: - if prop not in tick.keys(): - raise ContentDecodingError('Required property {} not present ' - 'in response params=({})'.format(prop, pair)) - - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message} params=({pair})'.format( - message=data['message'], - pair=pair)) - - return data['result'] - - def get_order(self, order_id: str) -> Dict: - data = _API.get_order(order_id) - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message} params=({order_id})'.format( - message=data['message'], - order_id=order_id)) - data = data['result'] - return { - 'id': data['OrderUuid'], - 'type': data['Type'], - 'pair': data['Exchange'].replace('-', '_'), - 'opened': data['Opened'], - 'rate': data['PricePerUnit'], - 'amount': data['Quantity'], - 'remaining': data['QuantityRemaining'], - 'closed': data['Closed'], - } - - def cancel_order(self, order_id: str) -> None: - data = _API.cancel(order_id) - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message} params=({order_id})'.format( - message=data['message'], - order_id=order_id)) - - def get_pair_detail_url(self, pair: str) -> str: - return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-')) - - def get_markets(self) -> List[str]: - data = _API.get_markets() - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException(data['message']) - return [m['MarketName'].replace('-', '_') for m in data['result']] - - def get_market_summaries(self) -> List[Dict]: - data = _API.get_market_summaries() - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException(data['message']) - return data['result'] - - def get_wallet_health(self) -> List[Dict]: - data = _API_V2.get_wallet_health() - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException(data['message']) - return [{ - 'Currency': entry['Health']['Currency'], - 'IsActive': entry['Health']['IsActive'], - 'LastChecked': entry['Health']['LastChecked'], - 'Notice': entry['Currency'].get('Notice'), - } for entry in data['result']] diff --git a/freqtrade/exchange/interface.py b/freqtrade/exchange/interface.py deleted file mode 100644 index 6121a98b3..000000000 --- a/freqtrade/exchange/interface.py +++ /dev/null @@ -1,172 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Dict, List, Optional - - -class Exchange(ABC): - @property - def name(self) -> str: - """ - Name of the exchange. - :return: str representation of the class name - """ - return self.__class__.__name__ - - @property - def fee(self) -> float: - """ - Fee for placing an order - :return: percentage in float - """ - - @abstractmethod - def buy(self, pair: str, rate: float, amount: float) -> str: - """ - Places a limit buy order. - :param pair: Pair as str, format: BTC_ETH - :param rate: Rate limit for order - :param amount: The amount to purchase - :return: order_id of the placed buy order - """ - - @abstractmethod - def sell(self, pair: str, rate: float, amount: float) -> str: - """ - Places a limit sell order. - :param pair: Pair as str, format: BTC_ETH - :param rate: Rate limit for order - :param amount: The amount to sell - :return: order_id of the placed sell order - """ - - @abstractmethod - def get_balance(self, currency: str) -> float: - """ - Gets account balance. - :param currency: Currency as str, format: BTC - :return: float - """ - - @abstractmethod - def get_balances(self) -> List[dict]: - """ - Gets account balances across currencies - :return: List of dicts, format: [ - { - 'Currency': str, - 'Balance': float, - 'Available': float, - 'Pending': float, - } - ... - ] - """ - - @abstractmethod - def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: - """ - Gets ticker for given pair. - :param pair: Pair as str, format: BTC_ETC - :param refresh: Shall we query a new value or a cached value is enough - :return: dict, format: { - 'bid': float, - 'ask': float, - 'last': float - } - """ - - @abstractmethod - def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]: - """ - Gets ticker history for given pair. - :param pair: Pair as str, format: BTC_ETC - :param tick_interval: ticker interval in minutes - :return: list, format: [ - { - 'O': float, (Open) - 'H': float, (High) - 'L': float, (Low) - 'C': float, (Close) - 'V': float, (Volume) - 'T': datetime, (Time) - 'BV': float, (Base Volume) - }, - ... - ] - """ - - def get_order(self, order_id: str) -> Dict: - """ - Get order details for the given order_id. - :param order_id: ID as str - :return: dict, format: { - 'id': str, - 'type': str, - 'pair': str, - 'opened': str ISO 8601 datetime, - 'closed': str ISO 8601 datetime, - 'rate': float, - 'amount': float, - 'remaining': int - } - """ - - @abstractmethod - def cancel_order(self, order_id: str) -> None: - """ - Cancels order for given order_id. - :param order_id: ID as str - :return: None - """ - - @abstractmethod - def get_pair_detail_url(self, pair: str) -> str: - """ - Returns the market detail url for the given pair. - :param pair: Pair as str, format: BTC_ETC - :return: URL as str - """ - - @abstractmethod - def get_markets(self) -> List[str]: - """ - Returns all available markets. - :return: List of all available pairs - """ - - @abstractmethod - def get_market_summaries(self) -> List[Dict]: - """ - Returns a 24h market summary for all available markets - :return: list, format: [ - { - 'MarketName': str, - 'High': float, - 'Low': float, - 'Volume': float, - 'Last': float, - 'TimeStamp': datetime, - 'BaseVolume': float, - 'Bid': float, - 'Ask': float, - 'OpenBuyOrders': int, - 'OpenSellOrders': int, - 'PrevDay': float, - 'Created': datetime - }, - ... - ] - """ - - @abstractmethod - def get_wallet_health(self) -> List[Dict]: - """ - Returns a list of all wallet health information - :return: list, format: [ - { - 'Currency': str, - 'IsActive': bool, - 'LastChecked': str, - 'Notice': str - }, - ... - """ diff --git a/freqtrade/tests/exchange/test_exchange_bittrex.py b/freqtrade/tests/exchange/test_exchange_bittrex.py deleted file mode 100644 index 2c66215c2..000000000 --- a/freqtrade/tests/exchange/test_exchange_bittrex.py +++ /dev/null @@ -1,349 +0,0 @@ -# pragma pylint: disable=missing-docstring, C0103, protected-access, unused-argument - -from unittest.mock import MagicMock - -import pytest -from requests.exceptions import ContentDecodingError - -import freqtrade.exchange.bittrex as btx -from freqtrade.exchange.bittrex import Bittrex - - -# Eat this flake8 -# +------------------+ -# | bittrex.Bittrex | -# +------------------+ -# | -# (mock Fake_bittrex) -# | -# +-----------------------------+ -# | freqtrade.exchange.Bittrex | -# +-----------------------------+ -# Call into Bittrex will flow up to the -# external package bittrex.Bittrex. -# By inserting a mock, we redirect those -# calls. -# The faked bittrex API is called just 'fb' -# The freqtrade.exchange.Bittrex is a -# wrapper, and is called 'wb' - - -def _stub_config(): - return {'key': '', - 'secret': ''} - - -class FakeBittrex(): - def __init__(self, success=True): - self.success = True # Believe in yourself - self.result = None - self.get_ticker_call_count = 0 - # This is really ugly, doing side-effect during instance creation - # But we're allowed to in testing-code - btx._API = MagicMock() - btx._API.buy_limit = self.fake_buysell_limit - btx._API.sell_limit = self.fake_buysell_limit - btx._API.get_balance = self.fake_get_balance - btx._API.get_balances = self.fake_get_balances - btx._API.get_ticker = self.fake_get_ticker - btx._API.get_order = self.fake_get_order - btx._API.cancel = self.fake_cancel_order - btx._API.get_markets = self.fake_get_markets - btx._API.get_market_summaries = self.fake_get_market_summaries - btx._API_V2 = MagicMock() - btx._API_V2.get_candles = self.fake_get_candles - btx._API_V2.get_wallet_health = self.fake_get_wallet_health - - def fake_buysell_limit(self, pair, amount, limit): - return {'success': self.success, - 'result': {'uuid': '1234'}, - 'message': 'barter'} - - def fake_get_balance(self, cur): - return {'success': self.success, - 'result': {'Balance': 1234}, - 'message': 'unbalanced'} - - def fake_get_balances(self): - return {'success': self.success, - 'result': [{'BTC_ETH': 1234}], - 'message': 'no balances'} - - def fake_get_ticker(self, pair): - self.get_ticker_call_count += 1 - return self.result or {'success': self.success, - 'result': {'Bid': 1, 'Ask': 1, 'Last': 1}, - 'message': 'NO_API_RESPONSE'} - - def fake_get_candles(self, pair, interval): - return self.result or {'success': self.success, - 'result': [{'C': 0, 'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}], - 'message': 'candles lit'} - - def fake_get_order(self, uuid): - return {'success': self.success, - 'result': {'OrderUuid': 'ABC123', - 'Type': 'Type', - 'Exchange': 'BTC_ETH', - 'Opened': True, - 'PricePerUnit': 1, - 'Quantity': 1, - 'QuantityRemaining': 1, - 'Closed': True}, - 'message': 'lost'} - - def fake_cancel_order(self, uuid): - return self.result or {'success': self.success, - 'message': 'no such order'} - - def fake_get_markets(self): - return self.result or {'success': self.success, - 'message': 'market gone', - 'result': [{'MarketName': '-_'}]} - - def fake_get_market_summaries(self): - return self.result or {'success': self.success, - 'message': 'no summary', - 'result': ['sum']} - - def fake_get_wallet_health(self): - return self.result or {'success': self.success, - 'message': 'bad health', - 'result': [{'Health': {'Currency': 'BTC_ETH', - 'IsActive': True, - 'LastChecked': 0}, - 'Currency': {'Notice': True}}]} - - -# The freqtrade.exchange.bittrex is called wrap_bittrex -# to not confuse naming with bittrex.bittrex -def make_wrap_bittrex(): - conf = _stub_config() - wb = btx.Bittrex(conf) - return wb - - -def test_exchange_bittrex_class(): - conf = _stub_config() - b = Bittrex(conf) - assert isinstance(b, Bittrex) - slots = dir(b) - for name in ['fee', 'buy', 'sell', 'get_balance', 'get_balances', - 'get_ticker', 'get_ticker_history', 'get_order', - 'cancel_order', 'get_pair_detail_url', 'get_markets', - 'get_market_summaries', 'get_wallet_health']: - assert name in slots - # FIX: ensure that the slot is also a method in the class - # getattr(b, name) => bound method Bittrex.buy - # type(getattr(b, name)) => class 'method' - - -def test_exchange_bittrex_fee(): - fee = Bittrex.fee.__get__(Bittrex) - assert fee >= 0 and fee < 0.1 # Fee is 0-10 % - - -def test_exchange_bittrex_buy_good(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - uuid = wb.buy('BTC_ETH', 1, 1) - assert uuid == fb.fake_buysell_limit(1, 2, 3)['result']['uuid'] - - fb.success = False - with pytest.raises(btx.OperationalException, match=r'barter.*'): - wb.buy('BAD', 1, 1) - - -def test_exchange_bittrex_sell_good(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - uuid = wb.sell('BTC_ETH', 1, 1) - assert uuid == fb.fake_buysell_limit(1, 2, 3)['result']['uuid'] - - fb.success = False - with pytest.raises(btx.OperationalException, match=r'barter.*'): - uuid = wb.sell('BAD', 1, 1) - - -def test_exchange_bittrex_get_balance(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - bal = wb.get_balance('BTC_ETH') - assert bal == fb.fake_get_balance(1)['result']['Balance'] - - fb.success = False - with pytest.raises(btx.OperationalException, match=r'unbalanced'): - wb.get_balance('BTC_ETH') - - -def test_exchange_bittrex_get_balances(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - bals = wb.get_balances() - assert bals == fb.fake_get_balances()['result'] - - fb.success = False - with pytest.raises(btx.OperationalException, match=r'no balances'): - wb.get_balances() - - -def test_exchange_bittrex_get_ticker(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - - # Poll ticker, which updates the cache - tick = wb.get_ticker('BTC_ETH') - for x in ['bid', 'ask', 'last']: - assert x in tick - # Ensure the side-effect was made (update the ticker cache) - assert 'BTC_ETH' in wb.cached_ticker.keys() - - # taint the cache, so we can recognize the cache wall utilized - wb.cached_ticker['BTC_ETH']['bid'] = 1234 - # Poll again, getting the cached result - fb.get_ticker_call_count = 0 - tick = wb.get_ticker('BTC_ETH', False) - # Ensure the result was from the cache, and that we didn't call exchange - assert wb.cached_ticker['BTC_ETH']['bid'] == 1234 - assert fb.get_ticker_call_count == 0 - - -def test_exchange_bittrex_get_ticker_bad(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - fb.result = {'success': True, 'result': {'Bid': 1, 'Ask': 0}} # incomplete result - - with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'): - wb.get_ticker('BTC_ETH') - fb.result = {'success': False, 'message': 'gone bad'} - with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'): - wb.get_ticker('BTC_ETH') - - fb.result = {'success': True, 'result': {}} # incomplete result - with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'): - wb.get_ticker('BTC_ETH') - fb.result = {'success': False, 'message': 'gone bad'} - with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'): - wb.get_ticker('BTC_ETH') - - fb.result = {'success': True, - 'result': {'Bid': 1, 'Ask': 0, 'Last': None}} # incomplete result - with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'): - wb.get_ticker('BTC_ETH') - - -def test_exchange_bittrex_get_ticker_history_intervals(): - wb = make_wrap_bittrex() - FakeBittrex() - for tick_interval in [1, 5, 30, 60, 1440]: - assert ([{'C': 0, 'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}] == - wb.get_ticker_history('BTC_ETH', tick_interval)) - - -def test_exchange_bittrex_get_ticker_history(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - assert wb.get_ticker_history('BTC_ETH', 5) - with pytest.raises(ValueError, match=r'.*Unknown tick_interval.*'): - wb.get_ticker_history('BTC_ETH', 2) - - fb.success = False - with pytest.raises(btx.OperationalException, match=r'candles lit.*'): - wb.get_ticker_history('BTC_ETH', 5) - - fb.success = True - with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex.*'): - fb.result = {'bad': 0} - wb.get_ticker_history('BTC_ETH', 5) - - with pytest.raises(ContentDecodingError, match=r'.*Required property C not present.*'): - fb.result = {'success': True, - 'result': [{'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}], # close is missing - 'message': 'candles lit'} - wb.get_ticker_history('BTC_ETH', 5) - - -def test_exchange_bittrex_get_order(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - order = wb.get_order('someUUID') - assert order['id'] == 'ABC123' - fb.success = False - with pytest.raises(btx.OperationalException, match=r'lost'): - wb.get_order('someUUID') - - -def test_exchange_bittrex_cancel_order(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - wb.cancel_order('someUUID') - with pytest.raises(btx.OperationalException, match=r'no such order'): - fb.success = False - wb.cancel_order('someUUID') - # Note: this can be a bug in exchange.bittrex._validate_response - with pytest.raises(KeyError): - fb.result = {'success': False} # message is missing! - wb.cancel_order('someUUID') - with pytest.raises(btx.OperationalException, match=r'foo'): - fb.result = {'success': False, 'message': 'foo'} - wb.cancel_order('someUUID') - - -def test_exchange_get_pair_detail_url(): - wb = make_wrap_bittrex() - assert wb.get_pair_detail_url('BTC_ETH') - - -def test_exchange_get_markets(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - x = wb.get_markets() - assert x == ['__'] - with pytest.raises(btx.OperationalException, match=r'market gone'): - fb.success = False - wb.get_markets() - - -def test_exchange_get_market_summaries(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - assert ['sum'] == wb.get_market_summaries() - with pytest.raises(btx.OperationalException, match=r'no summary'): - fb.success = False - wb.get_market_summaries() - - -def test_exchange_get_wallet_health(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - x = wb.get_wallet_health() - assert x[0]['Currency'] == 'BTC_ETH' - with pytest.raises(btx.OperationalException, match=r'bad health'): - fb.success = False - wb.get_wallet_health() - - -def test_validate_response_success(): - response = { - 'message': '', - 'result': [], - } - Bittrex._validate_response(response) - - -def test_validate_response_no_api_response(): - response = { - 'message': 'NO_API_RESPONSE', - 'result': None, - } - with pytest.raises(ContentDecodingError, match=r'.*NO_API_RESPONSE.*'): - Bittrex._validate_response(response) - - -def test_validate_response_min_trade_requirement_not_met(): - response = { - 'message': 'MIN_TRADE_REQUIREMENT_NOT_MET', - 'result': None, - } - with pytest.raises(ContentDecodingError, match=r'.*MIN_TRADE_REQUIREMENT_NOT_MET.*'): - Bittrex._validate_response(response)