350 lines
11 KiB
Python
350 lines
11 KiB
Python
# pragma pylint: disable=missing-docstring, C0103, protected-access, unused-argument
|
|
|
|
from unittest.mock import MagicMock
|
|
import pytest
|
|
from freqtrade.exchange.binance import Binance
|
|
import freqtrade.exchange.binance as bin
|
|
|
|
|
|
# Eat this flake8
|
|
# +------------------+
|
|
# | binance.Binance |
|
|
# +------------------+
|
|
# |
|
|
# (mock Fake_binance)
|
|
# |
|
|
# +-----------------------------+
|
|
# | freqtrade.exchange.Binance |
|
|
# +-----------------------------+
|
|
# Call into Binance will flow up to the
|
|
# external package binance.Binance.
|
|
# By inserting a mock, we redirect those
|
|
# calls.
|
|
# The faked binance API is called just 'fb'
|
|
# The freqtrade.exchange.Binance is a
|
|
# wrapper, and is called 'wb'
|
|
|
|
|
|
def _stub_config():
|
|
return {'key': '',
|
|
'secret': ''}
|
|
|
|
|
|
class FakeBinance():
|
|
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
|
|
bin._API = MagicMock()
|
|
bin._API.order_limit_buy = self.fake_order_limit_buy
|
|
bin._API.order_limit_sell = self.fake_order_limit_sell
|
|
bin._API.get_asset_balance = self.fake_get_asset_balance
|
|
bin._API.get_account = self.fake_get_account
|
|
bin._API.get_ticker = self.fake_get_ticker
|
|
bin._API.get_klines = self.fake_get_klines
|
|
bin._API.get_all_orders = self.fake_get_all_orders
|
|
bin._API.cancel_order = self.fake_cancel_order
|
|
bin._API.get_all_tickers = self.fake_get_all_tickers
|
|
bin._API.get_exchange_info = self.fake_get_exchange_info
|
|
bin._EXCHANGE_CONF = {'stake_currency': 'BTC'}
|
|
|
|
def fake_order_limit_buy(self, symbol, quantity, price):
|
|
return {"symbol": "BTCETH",
|
|
"orderId": 42,
|
|
"clientOrderId": "6gCrw2kRUAF9CvJDGP16IP",
|
|
"transactTime": 1507725176595,
|
|
"price": "0.00000000",
|
|
"origQty": "10.00000000",
|
|
"executedQty": "10.00000000",
|
|
"status": "FILLED",
|
|
"timeInForce": "GTC",
|
|
"type": "LIMIT",
|
|
"side": "BUY"}
|
|
|
|
def fake_order_limit_sell(self, symbol, quantity, price):
|
|
return {"symbol": "BTCETH",
|
|
"orderId": 42,
|
|
"clientOrderId": "6gCrw2kRUAF9CvJDGP16IP",
|
|
"transactTime": 1507725176595,
|
|
"price": "0.00000000",
|
|
"origQty": "10.00000000",
|
|
"executedQty": "10.00000000",
|
|
"status": "FILLED",
|
|
"timeInForce": "GTC",
|
|
"type": "LIMIT",
|
|
"side": "SELL"}
|
|
|
|
def fake_get_asset_balance(self, asset):
|
|
return {
|
|
"asset": "BTC",
|
|
"free": "4723846.89208129",
|
|
"locked": "0.00000000"
|
|
}
|
|
|
|
def fake_get_account(self):
|
|
return {
|
|
"makerCommission": 15,
|
|
"takerCommission": 15,
|
|
"buyerCommission": 0,
|
|
"sellerCommission": 0,
|
|
"canTrade": True,
|
|
"canWithdraw": True,
|
|
"canDeposit": True,
|
|
"balances": [
|
|
{
|
|
"asset": "BTC",
|
|
"free": "4723846.89208129",
|
|
"locked": "0.00000000"
|
|
},
|
|
{
|
|
"asset": "LTC",
|
|
"free": "4763368.68006011",
|
|
"locked": "0.00000000"
|
|
}
|
|
]
|
|
}
|
|
|
|
def fake_get_ticker(self, symbol=None):
|
|
self.get_ticker_call_count += 1
|
|
t = {"symbol": "ETHBTC",
|
|
"priceChange": "-94.99999800",
|
|
"priceChangePercent": "-95.960",
|
|
"weightedAvgPrice": "0.29628482",
|
|
"prevClosePrice": "0.10002000",
|
|
"lastPrice": "4.00000200",
|
|
"bidPrice": "4.00000000",
|
|
"askPrice": "4.00000200",
|
|
"openPrice": "99.00000000",
|
|
"highPrice": "100.00000000",
|
|
"lowPrice": "0.10000000",
|
|
"volume": "8913.30000000",
|
|
"openTime": 1499783499040,
|
|
"closeTime": 1499869899040,
|
|
"fristId": 28385,
|
|
"lastId": 28460,
|
|
"count": 76}
|
|
return t if symbol else [t]
|
|
|
|
def fake_get_klines(self, symbol, interval):
|
|
return [[0,
|
|
"0",
|
|
"0",
|
|
"0",
|
|
"0",
|
|
"0",
|
|
0,
|
|
"0",
|
|
0,
|
|
"0",
|
|
"0",
|
|
"0"]]
|
|
|
|
def fake_get_all_orders(self, symbol, orderId):
|
|
return [{"symbol": "LTCBTC",
|
|
"orderId": 42,
|
|
"clientOrderId": "myOrder1",
|
|
"price": "0.1",
|
|
"origQty": "1.0",
|
|
"executedQty": "0.0",
|
|
"status": "NEW",
|
|
"timeInForce": "GTC",
|
|
"type": "LIMIT",
|
|
"side": "BUY",
|
|
"stopPrice": "0.0",
|
|
"icebergQty": "0.0",
|
|
"status_code": "200",
|
|
"time": 1499827319559}]
|
|
|
|
def fake_cancel_order(self, symbol, orderId):
|
|
return {"symbol": "LTCBTC",
|
|
"origClientOrderId": "myOrder1",
|
|
"orderId": 42,
|
|
"clientOrderId": "cancelMyOrder1"}
|
|
|
|
def fake_get_all_tickers(self):
|
|
return [{"symbol": "LTCBTC",
|
|
"price": "4.00000200"},
|
|
{"symbol": "ETHBTC",
|
|
"price": "0.07946600"}]
|
|
|
|
def fake_get_exchange_info(self):
|
|
return {
|
|
"timezone": "UTC",
|
|
"serverTime": 1508631584636,
|
|
"rateLimits": [
|
|
{
|
|
"rateLimitType": "REQUESTS",
|
|
"interval": "MINUTE",
|
|
"limit": 1200
|
|
},
|
|
{
|
|
"rateLimitType": "ORDERS",
|
|
"interval": "SECOND",
|
|
"limit": 10
|
|
},
|
|
{
|
|
"rateLimitType": "ORDERS",
|
|
"interval": "DAY",
|
|
"limit": 100000
|
|
}
|
|
],
|
|
"exchangeFilters": [],
|
|
"symbols": [
|
|
{
|
|
"symbol": "ETHBTC",
|
|
"status": "TRADING",
|
|
"baseAsset": "ETH",
|
|
"baseAssetPrecision": 8,
|
|
"quoteAsset": "BTC",
|
|
"quotePrecision": 8,
|
|
"orderTypes": ["LIMIT", "MARKET"],
|
|
"icebergAllowed": False,
|
|
"filters": [
|
|
{
|
|
"filterType": "PRICE_FILTER",
|
|
"minPrice": "0.00000100",
|
|
"maxPrice": "100000.00000000",
|
|
"tickSize": "0.00000100"
|
|
}, {
|
|
"filterType": "LOT_SIZE",
|
|
"minQty": "0.00100000",
|
|
"maxQty": "100000.00000000",
|
|
"stepSize": "0.00100000"
|
|
}, {
|
|
"filterType": "MIN_NOTIONAL",
|
|
"minNotional": "0.00100000"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
|
|
|
|
# The freqtrade.exchange.binance is called wrap_binance
|
|
# to not confuse naming with binance.binance
|
|
def make_wrap_binance():
|
|
conf = _stub_config()
|
|
wb = bin.Binance(conf)
|
|
return wb
|
|
|
|
|
|
def test_exchange_binance_class():
|
|
conf = _stub_config()
|
|
b = Binance(conf)
|
|
assert isinstance(b, Binance)
|
|
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 Binance.buy
|
|
# type(getattr(b, name)) => class 'method'
|
|
|
|
|
|
def test_exchange_binance_fee():
|
|
fee = Binance.fee.__get__(Binance)
|
|
assert fee >= 0 and fee < 0.1 # Fee is 0-10 %
|
|
|
|
|
|
def test_exchange_binance_buy_good():
|
|
wb = make_wrap_binance()
|
|
fb = FakeBinance()
|
|
uuid = wb.buy('BTC_ETH', 1, 1)
|
|
assert uuid == fb.fake_order_limit_buy(1, 2, 3)['orderId']
|
|
|
|
with pytest.raises(IndexError, match=r'.*'):
|
|
wb.buy('BAD', 1, 1)
|
|
|
|
|
|
def test_exchange_binance_sell_good():
|
|
wb = make_wrap_binance()
|
|
fb = FakeBinance()
|
|
uuid = wb.sell('BTC_ETH', 1, 1)
|
|
assert uuid == fb.fake_order_limit_sell(1, 2, 3)['orderId']
|
|
|
|
with pytest.raises(IndexError, match=r'.*'):
|
|
uuid = wb.sell('BAD', 1, 1)
|
|
|
|
|
|
def test_exchange_binance_get_balance():
|
|
wb = make_wrap_binance()
|
|
fb = FakeBinance()
|
|
bal = wb.get_balance('BTC')
|
|
assert str(bal) == fb.fake_get_asset_balance(1)['free']
|
|
|
|
|
|
def test_exchange_binance_get_balances():
|
|
wb = make_wrap_binance()
|
|
fb = FakeBinance()
|
|
bals = wb.get_balances()
|
|
assert len(bals) <= len(fb.fake_get_account()['balances'])
|
|
|
|
|
|
def test_exchange_binance_get_ticker():
|
|
wb = make_wrap_binance()
|
|
FakeBinance()
|
|
|
|
# Poll ticker, which updates the cache
|
|
tick = wb.get_ticker('BTC_ETH')
|
|
for x in ['bid', 'ask', 'last']:
|
|
assert x in tick
|
|
|
|
|
|
def test_exchange_binance_get_ticker_history_intervals():
|
|
wb = make_wrap_binance()
|
|
FakeBinance()
|
|
for tick_interval in [1, 5]:
|
|
assert ([{'O': 0.0, 'H': 0.0,
|
|
'L': 0.0, 'C': 0.0,
|
|
'V': 0.0, 'T': '1970-01-01T01:00:00', 'BV': 0.0}] ==
|
|
wb.get_ticker_history('BTC_ETH', tick_interval))
|
|
|
|
|
|
def test_exchange_binance_get_ticker_history():
|
|
wb = make_wrap_binance()
|
|
FakeBinance()
|
|
assert wb.get_ticker_history('BTC_ETH', 5)
|
|
|
|
|
|
def test_exchange_binance_get_order():
|
|
wb = make_wrap_binance()
|
|
FakeBinance()
|
|
order = wb.get_order('42', 'BTC_LTC')
|
|
assert order['id'] == 42
|
|
|
|
|
|
def test_exchange_binance_cancel_order():
|
|
wb = make_wrap_binance()
|
|
FakeBinance()
|
|
assert wb.cancel_order('42', 'BTC_LTC')['orderId'] == 42
|
|
|
|
|
|
def test_exchange_get_pair_detail_url():
|
|
wb = make_wrap_binance()
|
|
FakeBinance()
|
|
assert wb.get_pair_detail_url('BTC_ETH')
|
|
|
|
|
|
def test_exchange_get_markets():
|
|
wb = make_wrap_binance()
|
|
FakeBinance()
|
|
x = wb.get_markets()
|
|
assert len(x) > 0
|
|
|
|
|
|
def test_exchange_get_market_summaries():
|
|
wb = make_wrap_binance()
|
|
FakeBinance()
|
|
assert wb.get_market_summaries()
|
|
|
|
|
|
def test_exchange_get_wallet_health():
|
|
wb = make_wrap_binance()
|
|
FakeBinance()
|
|
x = wb.get_wallet_health()
|
|
assert x[0]['Currency'] == 'ETH'
|