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.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)
|
||||
|
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']
|
||||
|
||||
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)
|
||||
|
@ -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 )
|
||||
...
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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():
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user