Remove bittrex related interface code and tests
This commit is contained in:
parent
556533f160
commit
14d16d573c
@ -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']]
|
|
@ -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
|
|
||||||
},
|
|
||||||
...
|
|
||||||
"""
|
|
@ -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)
|
|
Loading…
Reference in New Issue
Block a user