Add Binance exchange support
This commit is contained in:
@@ -227,7 +227,7 @@ def test_cancel_order_dry_run(default_conf, mocker):
|
||||
default_conf['dry_run'] = True
|
||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||
|
||||
assert cancel_order(order_id='123') is None
|
||||
assert cancel_order(order_id='123',pair='ABC_XYZ') is None
|
||||
|
||||
|
||||
# Ensure that if not dry_run, we should call API
|
||||
@@ -237,7 +237,7 @@ def test_cancel_order(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
api_mock.cancel_order = MagicMock(return_value=123)
|
||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||
assert cancel_order(order_id='_') == 123
|
||||
assert cancel_order(order_id='_',pair='ABC_XYZ') == 123
|
||||
|
||||
|
||||
def test_get_order(default_conf, mocker):
|
||||
@@ -245,16 +245,18 @@ def test_get_order(default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||
order = MagicMock()
|
||||
order.myid = 123
|
||||
order.pair = 'ABC_XYZ'
|
||||
exchange._DRY_RUN_OPEN_ORDERS['X'] = order
|
||||
print(exchange.get_order('X'))
|
||||
assert exchange.get_order('X').myid == 123
|
||||
print(exchange.get_order('X','ABC_XYZ'))
|
||||
assert exchange.get_order('X','ABC_XYZ').myid == 123
|
||||
assert exchange.get_order('X','ABC_XYZ').pair == 'ABC_XYZ'
|
||||
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||
api_mock = MagicMock()
|
||||
api_mock.get_order = MagicMock(return_value=456)
|
||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||
assert exchange.get_order('X') == 456
|
||||
assert exchange.get_order('X', 'ABC_XYZ') == 456
|
||||
|
||||
|
||||
def test_get_name(default_conf, mocker):
|
||||
|
349
freqtrade/tests/exchange/test_exchange_binance.py
Normal file
349
freqtrade/tests/exchange/test_exchange_binance.py
Normal file
@@ -0,0 +1,349 @@
|
||||
# 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'
|
@@ -264,27 +264,27 @@ def test_exchange_bittrex_get_ticker_history():
|
||||
def test_exchange_bittrex_get_order():
|
||||
wb = make_wrap_bittrex()
|
||||
fb = FakeBittrex()
|
||||
order = wb.get_order('someUUID')
|
||||
order = wb.get_order('someUUID','somePAIR')
|
||||
assert order['id'] == 'ABC123'
|
||||
fb.success = False
|
||||
with pytest.raises(btx.OperationalException, match=r'lost'):
|
||||
wb.get_order('someUUID')
|
||||
wb.get_order('someUUID','somePAIR')
|
||||
|
||||
|
||||
def test_exchange_bittrex_cancel_order():
|
||||
wb = make_wrap_bittrex()
|
||||
fb = FakeBittrex()
|
||||
wb.cancel_order('someUUID')
|
||||
wb.cancel_order('someUUID','somePAIR')
|
||||
with pytest.raises(btx.OperationalException, match=r'no such order'):
|
||||
fb.success = False
|
||||
wb.cancel_order('someUUID')
|
||||
wb.cancel_order('someUUID','somePAIR')
|
||||
# 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')
|
||||
wb.cancel_order('someUUID','somePAIR')
|
||||
with pytest.raises(btx.OperationalException, match=r'foo'):
|
||||
fb.result = {'success': False, 'message': 'foo'}
|
||||
wb.cancel_order('someUUID')
|
||||
wb.cancel_order('someUUID','somePAIR')
|
||||
|
||||
|
||||
def test_exchange_get_pair_detail_url():
|
||||
|
Reference in New Issue
Block a user