Add Binance exchange support

This commit is contained in:
Ramon Bastiaans 2018-01-31 23:25:01 +01:00
parent 0f041b424d
commit 15692421d9
11 changed files with 859 additions and 31 deletions

View File

@ -11,6 +11,7 @@ from cachetools import cached, TTLCache
from freqtrade import OperationalException
from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.binance import Binance
from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__)
@ -28,6 +29,7 @@ class Exchanges(enum.Enum):
Maps supported exchange names to correspondent classes.
"""
BITTREX = Bittrex
BINANCE = Binance
def init(config: dict) -> None:
@ -143,14 +145,14 @@ def get_ticker_history(pair: str, tick_interval) -> List[Dict]:
return _API.get_ticker_history(pair, tick_interval)
def cancel_order(order_id: str) -> None:
def cancel_order(order_id: str, pair: str) -> None:
if _CONF['dry_run']:
return
return _API.cancel_order(order_id)
return _API.cancel_order(order_id, pair)
def get_order(order_id: str) -> Dict:
def get_order(order_id: str, pair: str) -> Dict:
if _CONF['dry_run']:
order = _DRY_RUN_OPEN_ORDERS[order_id]
order.update({
@ -158,7 +160,7 @@ def get_order(order_id: str) -> Dict:
})
return order
return _API.get_order(order_id)
return _API.get_order(order_id, pair)
def get_pair_detail_url(pair: str) -> str:
@ -183,3 +185,7 @@ def get_fee() -> float:
def get_wallet_health() -> List[Dict]:
return _API.get_wallet_health()
def get_trade_qty(pair: str) -> tuple:
return _API.get_trade_qty(pair)

View File

@ -0,0 +1,374 @@
import logging
import datetime
import json
import http
from typing import List, Dict, Optional
from binance.client import Client as _Binance
from binance.exceptions import BinanceAPIException
from binance.enums import *
from decimal import Decimal
from freqtrade import OperationalException
from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__)
_API: _Binance = None
_EXCHANGE_CONF: dict = {}
_CONF: dict = {}
class Binance(Exchange):
"""
Binance API wrapper.
"""
# Base URL and API endpoints
BASE_URL: str = 'https://www.binance.com'
def __init__(self, config: dict) -> None:
global _API, _EXCHANGE_CONF, _CONF
_EXCHANGE_CONF.update(config)
_API = _Binance(_EXCHANGE_CONF['key'], _EXCHANGE_CONF['secret'])
def _pair_to_symbol(self, pair, seperator='') -> str:
"""
Turns freqtrade pair into Binance symbol
- Freqtrade pair = <stake_currency>_<currency>
i.e.: BTC_XALT
- Binance symbol = <currency><stake_currency>
i.e.: XALTBTC
"""
pair_currencies = pair.split('_')
return '{0}{1}{2}'.format(pair_currencies[1], seperator, pair_currencies[0])
def _symbol_to_pair(self, symbol) -> str:
"""
Turns Binance symbol into freqtrade pair
- Freqtrade pair = <stake_currency>_<currency>
i.e.: BTC_XALT
- Binance symbol = <currency><stake_currency>
i.e.: XALTBTC
"""
stake = _EXCHANGE_CONF['stake_currency']
symbol_stake_currency = symbol[-len(stake):]
symbol_currency = symbol[:-len(stake)]
return '{0}_{1}'.format(symbol_stake_currency, symbol_currency)
@staticmethod
def _handle_exception(excepter) -> None:
"""
Validates the given Binance response/exception
and raises a ContentDecodingError if a non-fatal issue happened.
"""
# Could to alternate exception handling for specific exceptions/errors
# See: http://python-binance.readthedocs.io/en/latest/binance.html#module-binance.exceptions
if type(excepter) == http.client.RemoteDisconnected:
logger.info(
'Got HTTP error from Binance: %s' % excepter
)
return True
if type(excepter) == json.decoder.JSONDecodeError:
logger.info(
'Got JSON error from Binance: %s' % excepter
)
return True
if type(excepter) == BinanceAPIException:
logger.info(
'Got API error from Binance: %s' % excepter
)
return True
raise type(excepter)(excepter.args)
@property
def fee(self) -> float:
# 0.1 %: See https://support.binance.com/hc/en-us
# /articles/115000429332-Fee-Structure-on-Binance
return 0.001
def buy(self, pair: str, rate: float, amount: float) -> str:
symbol = self._pair_to_symbol(pair)
try:
data = _API.order_limit_buy(
symbol=symbol,
quantity="{0:.8f}".format(amount),
price="{0:.8f}".format(rate))
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=str(e),
pair=pair,
rate=Decimal(rate),
amount=Decimal(amount)))
return data['orderId']
def sell(self, pair: str, rate: float, amount: float) -> str:
symbol = self._pair_to_symbol(pair)
try:
data = _API.order_limit_sell(
symbol=symbol,
quantity="{0:.8f}".format(amount),
price="{0:.8f}".format(rate))
except Exception as e:
Binance._handle_exception(e)
raise OperationalException(
'{message} params=({pair}, {rate}, {amount})'.format(
message=str(e),
pair=pair,
rate=rate,
amount=amount))
return data['orderId']
def get_balance(self, currency: str) -> float:
try:
data = _API.get_asset_balance(asset=currency)
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message} params=({currency})'.format(
message=str(e),
currency=currency))
return float(data['free'] or 0.0)
def get_balances(self) -> List[Dict]:
try:
data = _API.get_account()
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message}'.format(message=str(e)))
balances = data['balances']
currency_balances = []
for currency in balances:
balance = {}
if float(currency['free']) == 0 and float(currency['locked']) == 0:
continue
balance['Currency'] = currency.pop('asset')
balance['Available'] = currency.pop('free')
balance['Pending'] = currency.pop('locked')
balance['Balance'] = float(balance['Available']) + float(balance['Pending'])
currency_balances.append(balance)
return currency_balances
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
symbol = self._pair_to_symbol(pair)
try:
data = _API.get_ticker(symbol=symbol)
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message} params=({pair})'.format(
message=str(e),
pair=pair))
return {
'bid': float(data['bidPrice']),
'ask': float(data['askPrice']),
'last': float(data['lastPrice']),
}
def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
INTERVAL_ENUM = eval('KLINE_INTERVAL_' + str(tick_interval) + 'MINUTE')
if INTERVAL_ENUM in ['', None]:
raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))
symbol = self._pair_to_symbol(pair)
try:
data = _API.get_klines(symbol=symbol, interval=INTERVAL_ENUM)
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message} params=({pair})'.format(
message=str(e),
pair=pair))
tick_data = []
for tick in data:
t = {}
t['O'] = float(tick[1])
t['H'] = float(tick[2])
t['L'] = float(tick[3])
t['C'] = float(tick[4])
t['V'] = float(tick[5])
t['T'] = datetime.datetime.fromtimestamp(int(tick[6])/1000).isoformat()
t['BV'] = float(tick[7])
tick_data.append(t)
return tick_data
def get_order(self, order_id: str, pair: str) -> Dict:
symbol = self._pair_to_symbol(pair)
try:
data = _API.get_all_orders(symbol=symbol, orderId=order_id)
except Exception as e:
Binance._handle_exception(e)
raise OperationalException(
'{message} params=({symbol},{order_id})'.format(
message=str(e),
symbol=symbol,
order_id=order_id))
order = {}
for o in data:
if o['orderId'] == int(order_id):
order['id'] = o['orderId']
order['type'] = "{}_{}".format(o['type'], o['side'])
order['pair'] = self._symbol_to_pair(o['symbol'])
order['opened'] = datetime.datetime.fromtimestamp(
int(o['time'])/1000).isoformat()
order['closed'] = datetime.datetime.fromtimestamp(
int(o['time'])/1000).isoformat()\
if o['status'] == 'FILLED' else None
order['rate'] = float(o['price'])
order['amount'] = float(o['origQty'])
order['remaining'] = int(float(o['origQty']) - float(o['executedQty']))
return order
def cancel_order(self, order_id: str, pair: str) -> None:
symbol = self._pair_to_symbol(pair)
try:
data = _API.cancel_order(symbol=symbol, orderId=order_id)
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message} params=({order_id})'.format(
message=str(e),
order_id=order_id))
return data
def get_pair_detail_url(self, pair: str) -> str:
symbol = self._pair_to_symbol(pair, '_')
return 'https://www.binance.com/indexSpa.html#/trade/index?symbol={}'.format(symbol)
def get_markets(self) -> List[str]:
try:
data = _API.get_all_tickers()
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message}'.format(message=str(e)))
markets = []
stake = _EXCHANGE_CONF['stake_currency']
for t in data:
symbol = t['symbol']
symbol_stake_currency = symbol[-len(stake):]
if symbol_stake_currency == stake:
pair = self._symbol_to_pair(symbol)
markets.append(pair)
return markets
def get_market_summaries(self) -> List[Dict]:
try:
data = _API.get_ticker()
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message}'.format(message=str(e)))
market_summaries = []
for t in data:
market = {}
# Looks like this one is only one actually used
market['MarketName'] = self._symbol_to_pair(t['symbol'])
market['High'] = t['highPrice']
market['Low'] = t['lowPrice']
market['Volume'] = t['volume']
market['Last'] = t['lastPrice']
market['TimeStamp'] = t['closeTime']
market['BaseVolume'] = t['volume']
market['Bid'] = t['bidPrice']
market['Ask'] = t['askPrice']
market['OpenBuyOrders'] = None # TODO: Implement me (or dont care)
market['OpenSellOrders'] = None # TODO: Implement me (or dont care)
market['PrevDay'] = t['prevClosePrice']
market['Created'] = None # TODO: Implement me (or don't care)
market_summaries.append(market)
return market_summaries
def get_trade_qty(self, pair: str) -> tuple:
try:
data = _API.get_exchange_info()
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message}'.format(message=str(e)))
symbol = self._pair_to_symbol(pair)
for s in data['symbols']:
if symbol == s['symbol']:
for f in s['filters']:
if f['filterType'] == 'LOT_SIZE':
return (float(f['minQty']), float(f['maxQty']), float(f['stepSize']))
return (None, None, None)
def get_wallet_health(self) -> List[Dict]:
try:
data = _API.get_exchange_info()
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message}'.format(message=str(e)))
wallet_health = []
for s in data['symbols']:
wallet = {}
wallet['Currency'] = s['baseAsset']
wallet['IsActive'] = True if s['status'] == 'TRADING' else False
wallet['LastChecked'] = None # TODO
wallet['Notice'] = s['status'] if s['status'] != 'TRADING' else ''
wallet_health.append(wallet)
return wallet_health

View File

@ -157,7 +157,8 @@ class Bittrex(Exchange):
return data['result']
def get_order(self, order_id: str) -> Dict:
def get_order(self, order_id: str, pair: str) -> Dict:
data = _API.get_order(order_id)
if not data['success']:
Bittrex._validate_response(data)
@ -176,7 +177,8 @@ class Bittrex(Exchange):
'closed': data['Closed'],
}
def cancel_order(self, order_id: str) -> None:
def cancel_order(self, order_id: str, pair: str) -> None:
data = _API.cancel(order_id)
if not data['success']:
Bittrex._validate_response(data)
@ -212,3 +214,6 @@ class Bittrex(Exchange):
'LastChecked': entry['Health']['LastChecked'],
'Notice': entry['Currency'].get('Notice'),
} for entry in data['result']]
def get_trade_qty(self, pair: str) -> tuple:
return (None, None, None)

View File

@ -94,7 +94,7 @@ class Exchange(ABC):
]
"""
def get_order(self, order_id: str) -> Dict:
def get_order(self, order_id: str, pair: str) -> Dict:
"""
Get order details for the given order_id.
:param order_id: ID as str
@ -111,7 +111,7 @@ class Exchange(ABC):
"""
@abstractmethod
def cancel_order(self, order_id: str) -> None:
def cancel_order(self, order_id: str, pair: str) -> None:
"""
Cancels order for given order_id.
:param order_id: ID as str
@ -170,3 +170,11 @@ class Exchange(ABC):
},
...
"""
@abstractmethod
def get_trade_qty(self, pair: str) -> tuple:
"""
Returns a tuple of trade quantity limits
:return: tuple, format: ( min_qty: str, max_qty: str, step_qty: str )
...
"""

View File

@ -84,7 +84,7 @@ def process_maybe_execute_sell(trade: Trade, interval: int) -> bool:
if trade.open_order_id:
# Update trade with order values
logger.info('Got open order for %s', trade)
trade.update(exchange.get_order(trade.open_order_id))
trade.update(exchange.get_order(trade.open_order_id, trade.pair))
if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair
@ -151,7 +151,7 @@ def handle_timedout_limit_buy(trade: Trade, order: Dict) -> bool:
"""Buy timeout - cancel order
:return: True if order was fully cancelled
"""
exchange.cancel_order(trade.open_order_id)
exchange.cancel_order(trade.open_order_id, trade.pair)
if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade
Trade.session.delete(trade)
@ -182,7 +182,7 @@ def handle_timedout_limit_sell(trade: Trade, order: Dict) -> bool:
"""
if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade
exchange.cancel_order(trade.open_order_id)
exchange.cancel_order(trade.open_order_id, trade.pair)
trade.close_rate = None
trade.close_profit = None
trade.close_date = None
@ -207,7 +207,7 @@ def check_handle_timedout(timeoutvalue: int) -> None:
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
try:
order = exchange.get_order(trade.open_order_id)
order = exchange.get_order(trade.open_order_id, trade.pair)
except requests.exceptions.RequestException:
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
continue
@ -400,15 +400,42 @@ def create_trade(stake_amount: float, interval: int) -> bool:
else:
return False
# Calculate amount
min_qty = None
max_qty = None
step_qty = None
(min_qty, max_qty, step_qty) = exchange.get_trade_qty(pair)
# Calculate bid price
buy_limit = get_target_bid(exchange.get_ticker(pair))
# Calculate base amount
amount = stake_amount / buy_limit
order_id = exchange.buy(pair, buy_limit, amount)
# if amount above max qty: just buy max qty
if max_qty:
if amount > max_qty:
amount = max_qty
if min_qty:
if amount < min_qty:
raise DependencyException(
'stake amount is too low (min_qty={})'.format(min_qty)
)
# make trade exact amount of step qty
if step_qty:
real_amount = (amount // step_qty) * step_qty
else:
real_amount = amount
order_id = exchange.buy(pair, buy_limit, real_amount)
real_stake_amount = buy_limit * real_amount
fiat_converter = CryptoToFiatConverter()
stake_amount_fiat = fiat_converter.convert_amount(
stake_amount,
real_stake_amount,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
@ -418,7 +445,7 @@ def create_trade(stake_amount: float, interval: int) -> bool:
exchange.get_name().upper(),
pair.replace('_', '/'),
exchange.get_pair_detail_url(pair),
buy_limit, stake_amount, _CONF['stake_currency'],
buy_limit, real_stake_amount, _CONF['stake_currency'],
stake_amount_fiat, _CONF['fiat_display_currency']
))
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL

View File

@ -6,12 +6,12 @@ from typing import Dict, Tuple
import arrow
from pandas import DataFrame, Series
from tabulate import tabulate
from freqtrade import OperationalException
import freqtrade.misc as misc
import freqtrade.optimize as optimize
from freqtrade import exchange
from freqtrade.analyze import populate_buy_trend, populate_sell_trend
from freqtrade.exchange import Bittrex
from freqtrade.main import should_sell
from freqtrade.persistence import Trade
from freqtrade.strategy.strategy import Strategy
@ -116,7 +116,19 @@ def backtest(args) -> DataFrame:
records = []
trades = []
trade_count_lock: dict = {}
exchange._API = Bittrex({'key': '', 'secret': ''})
# Monkey patch config
from freqtrade import main
exchange_config = main._CONF['exchange']
exchange_name = exchange_config['name']
try:
exchange_class = exchange.Exchanges[exchange_name.upper()].value
except KeyError:
raise OperationalException('Exchange {} is not supported'.format(
exchange_name))
exchange._API = exchange_class({'key': '', 'secret': ''})
for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0
ticker = populate_sell_trend(populate_buy_trend(pair_data))
@ -167,8 +179,6 @@ def start(args):
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
exchange._API = Bittrex({'key': '', 'secret': ''})
logger.info('Using config: %s ...', args.config)
config = misc.load_config(args.config)
@ -184,6 +194,15 @@ def start(args):
logger.info('Using ticker_interval: %d ...', strategy.ticker_interval)
exchange_name = config['exchange']['name']
try:
exchange_class = exchange.Exchanges[exchange_name.upper()].value
except KeyError:
raise OperationalException('Exchange {} is not supported'.format(
exchange_name))
exchange._API = exchange_class({'key': '', 'secret': ''})
data = {}
pairs = config['exchange']['pair_whitelist']
if args.live:

View File

@ -396,6 +396,43 @@ def _version(bot: Bot, update: Update) -> None:
send_msg('*Version:* `{}`'.format(__version__), bot=bot)
<<<<<<< HEAD
=======
def shorten_date(_date):
"""
Trim the date so it fits on small screens
"""
new_date = re.sub('seconds?', 'sec', _date)
new_date = re.sub('minutes?', 'min', new_date)
new_date = re.sub('hours?', 'h', new_date)
new_date = re.sub('days?', 'd', new_date)
new_date = re.sub('^an?', '1', new_date)
return new_date
def _exec_forcesell(trade: Trade) -> None:
# Check if there is there is an open order
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id, trade.pair)
# Cancel open LIMIT_BUY orders and close trade
if order and not order['closed'] and order['type'] == 'LIMIT_BUY':
exchange.cancel_order(trade.open_order_id, trade.pair)
trade.close(order.get('rate') or trade.open_rate)
# TODO: sell amount which has been bought already
return
# Ignore trades with an attached LIMIT_SELL order
if order and not order['closed'] and order['type'] == 'LIMIT_SELL':
return
# Get current rate and execute sell
current_rate = exchange.get_ticker(trade.pair, False)['bid']
from freqtrade.main import execute_sell
execute_sell(trade, current_rate)
>>>>>>> Add Binance exchange support
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
"""
Send given markdown message

View File

@ -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):

View 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'

View File

@ -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():

View File

@ -1,4 +1,5 @@
python-bittrex==0.3.0
python-binance==0.6.1
SQLAlchemy==1.2.2
python-telegram-bot==9.0.0
arrow==0.12.1