Add Binance exchange support
This commit is contained in:
parent
0f041b424d
commit
15692421d9
@ -11,6 +11,7 @@ from cachetools import cached, TTLCache
|
|||||||
|
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
from freqtrade.exchange.bittrex import Bittrex
|
from freqtrade.exchange.bittrex import Bittrex
|
||||||
|
from freqtrade.exchange.binance import Binance
|
||||||
from freqtrade.exchange.interface import Exchange
|
from freqtrade.exchange.interface import Exchange
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -28,6 +29,7 @@ class Exchanges(enum.Enum):
|
|||||||
Maps supported exchange names to correspondent classes.
|
Maps supported exchange names to correspondent classes.
|
||||||
"""
|
"""
|
||||||
BITTREX = Bittrex
|
BITTREX = Bittrex
|
||||||
|
BINANCE = Binance
|
||||||
|
|
||||||
|
|
||||||
def init(config: dict) -> None:
|
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)
|
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']:
|
if _CONF['dry_run']:
|
||||||
return
|
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']:
|
if _CONF['dry_run']:
|
||||||
order = _DRY_RUN_OPEN_ORDERS[order_id]
|
order = _DRY_RUN_OPEN_ORDERS[order_id]
|
||||||
order.update({
|
order.update({
|
||||||
@ -158,7 +160,7 @@ def get_order(order_id: str) -> Dict:
|
|||||||
})
|
})
|
||||||
return order
|
return order
|
||||||
|
|
||||||
return _API.get_order(order_id)
|
return _API.get_order(order_id, pair)
|
||||||
|
|
||||||
|
|
||||||
def get_pair_detail_url(pair: str) -> str:
|
def get_pair_detail_url(pair: str) -> str:
|
||||||
@ -183,3 +185,7 @@ def get_fee() -> float:
|
|||||||
|
|
||||||
def get_wallet_health() -> List[Dict]:
|
def get_wallet_health() -> List[Dict]:
|
||||||
return _API.get_wallet_health()
|
return _API.get_wallet_health()
|
||||||
|
|
||||||
|
|
||||||
|
def get_trade_qty(pair: str) -> tuple:
|
||||||
|
return _API.get_trade_qty(pair)
|
||||||
|
374
freqtrade/exchange/binance.py
Normal file
374
freqtrade/exchange/binance.py
Normal 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
|
@ -157,7 +157,8 @@ class Bittrex(Exchange):
|
|||||||
|
|
||||||
return data['result']
|
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)
|
data = _API.get_order(order_id)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
Bittrex._validate_response(data)
|
Bittrex._validate_response(data)
|
||||||
@ -176,7 +177,8 @@ class Bittrex(Exchange):
|
|||||||
'closed': data['Closed'],
|
'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)
|
data = _API.cancel(order_id)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
Bittrex._validate_response(data)
|
Bittrex._validate_response(data)
|
||||||
@ -212,3 +214,6 @@ class Bittrex(Exchange):
|
|||||||
'LastChecked': entry['Health']['LastChecked'],
|
'LastChecked': entry['Health']['LastChecked'],
|
||||||
'Notice': entry['Currency'].get('Notice'),
|
'Notice': entry['Currency'].get('Notice'),
|
||||||
} for entry in data['result']]
|
} for entry in data['result']]
|
||||||
|
|
||||||
|
def get_trade_qty(self, pair: str) -> tuple:
|
||||||
|
return (None, None, None)
|
||||||
|
@ -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.
|
Get order details for the given order_id.
|
||||||
:param order_id: ID as str
|
:param order_id: ID as str
|
||||||
@ -111,7 +111,7 @@ class Exchange(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@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.
|
Cancels order for given order_id.
|
||||||
:param order_id: ID as str
|
: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 )
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
@ -84,7 +84,7 @@ def process_maybe_execute_sell(trade: Trade, interval: int) -> bool:
|
|||||||
if trade.open_order_id:
|
if trade.open_order_id:
|
||||||
# Update trade with order values
|
# Update trade with order values
|
||||||
logger.info('Got open order for %s', trade)
|
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:
|
if trade.is_open and trade.open_order_id is None:
|
||||||
# Check if we can sell our current pair
|
# 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
|
"""Buy timeout - cancel order
|
||||||
:return: True if order was fully cancelled
|
: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 order['remaining'] == order['amount']:
|
||||||
# if trade is not partially completed, just delete the trade
|
# if trade is not partially completed, just delete the trade
|
||||||
Trade.session.delete(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 order['remaining'] == order['amount']:
|
||||||
# if trade is not partially completed, just cancel the trade
|
# 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_rate = None
|
||||||
trade.close_profit = None
|
trade.close_profit = None
|
||||||
trade.close_date = 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():
|
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
||||||
try:
|
try:
|
||||||
order = exchange.get_order(trade.open_order_id)
|
order = exchange.get_order(trade.open_order_id, trade.pair)
|
||||||
except requests.exceptions.RequestException:
|
except requests.exceptions.RequestException:
|
||||||
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
|
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
|
||||||
continue
|
continue
|
||||||
@ -400,15 +400,42 @@ def create_trade(stake_amount: float, interval: int) -> bool:
|
|||||||
else:
|
else:
|
||||||
return False
|
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))
|
buy_limit = get_target_bid(exchange.get_ticker(pair))
|
||||||
|
|
||||||
|
# Calculate base amount
|
||||||
amount = stake_amount / buy_limit
|
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()
|
fiat_converter = CryptoToFiatConverter()
|
||||||
stake_amount_fiat = fiat_converter.convert_amount(
|
stake_amount_fiat = fiat_converter.convert_amount(
|
||||||
stake_amount,
|
real_stake_amount,
|
||||||
_CONF['stake_currency'],
|
_CONF['stake_currency'],
|
||||||
_CONF['fiat_display_currency']
|
_CONF['fiat_display_currency']
|
||||||
)
|
)
|
||||||
@ -418,7 +445,7 @@ def create_trade(stake_amount: float, interval: int) -> bool:
|
|||||||
exchange.get_name().upper(),
|
exchange.get_name().upper(),
|
||||||
pair.replace('_', '/'),
|
pair.replace('_', '/'),
|
||||||
exchange.get_pair_detail_url(pair),
|
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']
|
stake_amount_fiat, _CONF['fiat_display_currency']
|
||||||
))
|
))
|
||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
|
@ -6,12 +6,12 @@ from typing import Dict, Tuple
|
|||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame, Series
|
from pandas import DataFrame, Series
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
from freqtrade import OperationalException
|
||||||
|
|
||||||
import freqtrade.misc as misc
|
import freqtrade.misc as misc
|
||||||
import freqtrade.optimize as optimize
|
import freqtrade.optimize as optimize
|
||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
from freqtrade.analyze import populate_buy_trend, populate_sell_trend
|
from freqtrade.analyze import populate_buy_trend, populate_sell_trend
|
||||||
from freqtrade.exchange import Bittrex
|
|
||||||
from freqtrade.main import should_sell
|
from freqtrade.main import should_sell
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.strategy.strategy import Strategy
|
from freqtrade.strategy.strategy import Strategy
|
||||||
@ -116,7 +116,19 @@ def backtest(args) -> DataFrame:
|
|||||||
records = []
|
records = []
|
||||||
trades = []
|
trades = []
|
||||||
trade_count_lock: dict = {}
|
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():
|
for pair, pair_data in processed.items():
|
||||||
pair_data['buy'], pair_data['sell'] = 0, 0
|
pair_data['buy'], pair_data['sell'] = 0, 0
|
||||||
ticker = populate_sell_trend(populate_buy_trend(pair_data))
|
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',
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
)
|
)
|
||||||
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
logger.info('Using config: %s ...', args.config)
|
logger.info('Using config: %s ...', args.config)
|
||||||
config = misc.load_config(args.config)
|
config = misc.load_config(args.config)
|
||||||
|
|
||||||
@ -184,6 +194,15 @@ def start(args):
|
|||||||
|
|
||||||
logger.info('Using ticker_interval: %d ...', strategy.ticker_interval)
|
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 = {}
|
data = {}
|
||||||
pairs = config['exchange']['pair_whitelist']
|
pairs = config['exchange']['pair_whitelist']
|
||||||
if args.live:
|
if args.live:
|
||||||
|
@ -396,6 +396,43 @@ def _version(bot: Bot, update: Update) -> None:
|
|||||||
send_msg('*Version:* `{}`'.format(__version__), bot=bot)
|
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:
|
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
||||||
"""
|
"""
|
||||||
Send given markdown message
|
Send given markdown message
|
||||||
|
@ -227,7 +227,7 @@ def test_cancel_order_dry_run(default_conf, mocker):
|
|||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
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
|
# 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 = MagicMock()
|
||||||
api_mock.cancel_order = MagicMock(return_value=123)
|
api_mock.cancel_order = MagicMock(return_value=123)
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
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):
|
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)
|
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||||
order = MagicMock()
|
order = MagicMock()
|
||||||
order.myid = 123
|
order.myid = 123
|
||||||
|
order.pair = 'ABC_XYZ'
|
||||||
exchange._DRY_RUN_OPEN_ORDERS['X'] = order
|
exchange._DRY_RUN_OPEN_ORDERS['X'] = order
|
||||||
print(exchange.get_order('X'))
|
print(exchange.get_order('X','ABC_XYZ'))
|
||||||
assert exchange.get_order('X').myid == 123
|
assert exchange.get_order('X','ABC_XYZ').myid == 123
|
||||||
|
assert exchange.get_order('X','ABC_XYZ').pair == 'ABC_XYZ'
|
||||||
|
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.get_order = MagicMock(return_value=456)
|
api_mock.get_order = MagicMock(return_value=456)
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
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):
|
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():
|
def test_exchange_bittrex_get_order():
|
||||||
wb = make_wrap_bittrex()
|
wb = make_wrap_bittrex()
|
||||||
fb = FakeBittrex()
|
fb = FakeBittrex()
|
||||||
order = wb.get_order('someUUID')
|
order = wb.get_order('someUUID','somePAIR')
|
||||||
assert order['id'] == 'ABC123'
|
assert order['id'] == 'ABC123'
|
||||||
fb.success = False
|
fb.success = False
|
||||||
with pytest.raises(btx.OperationalException, match=r'lost'):
|
with pytest.raises(btx.OperationalException, match=r'lost'):
|
||||||
wb.get_order('someUUID')
|
wb.get_order('someUUID','somePAIR')
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_cancel_order():
|
def test_exchange_bittrex_cancel_order():
|
||||||
wb = make_wrap_bittrex()
|
wb = make_wrap_bittrex()
|
||||||
fb = FakeBittrex()
|
fb = FakeBittrex()
|
||||||
wb.cancel_order('someUUID')
|
wb.cancel_order('someUUID','somePAIR')
|
||||||
with pytest.raises(btx.OperationalException, match=r'no such order'):
|
with pytest.raises(btx.OperationalException, match=r'no such order'):
|
||||||
fb.success = False
|
fb.success = False
|
||||||
wb.cancel_order('someUUID')
|
wb.cancel_order('someUUID','somePAIR')
|
||||||
# Note: this can be a bug in exchange.bittrex._validate_response
|
# Note: this can be a bug in exchange.bittrex._validate_response
|
||||||
with pytest.raises(KeyError):
|
with pytest.raises(KeyError):
|
||||||
fb.result = {'success': False} # message is missing!
|
fb.result = {'success': False} # message is missing!
|
||||||
wb.cancel_order('someUUID')
|
wb.cancel_order('someUUID','somePAIR')
|
||||||
with pytest.raises(btx.OperationalException, match=r'foo'):
|
with pytest.raises(btx.OperationalException, match=r'foo'):
|
||||||
fb.result = {'success': False, 'message': 'foo'}
|
fb.result = {'success': False, 'message': 'foo'}
|
||||||
wb.cancel_order('someUUID')
|
wb.cancel_order('someUUID','somePAIR')
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_get_pair_detail_url():
|
def test_exchange_get_pair_detail_url():
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
python-bittrex==0.3.0
|
python-bittrex==0.3.0
|
||||||
|
python-binance==0.6.1
|
||||||
SQLAlchemy==1.2.2
|
SQLAlchemy==1.2.2
|
||||||
python-telegram-bot==9.0.0
|
python-telegram-bot==9.0.0
|
||||||
arrow==0.12.1
|
arrow==0.12.1
|
||||||
|
Loading…
Reference in New Issue
Block a user