stable/freqtrade/exchange/binance.py

229 lines
9.3 KiB
Python
Raw Normal View History

2019-02-24 18:30:05 +00:00
""" Binance exchange subclass """
2021-09-19 23:02:09 +00:00
import json
2019-02-24 18:30:05 +00:00
import logging
from datetime import datetime
2021-09-19 23:02:09 +00:00
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
2019-02-24 18:30:05 +00:00
2021-09-16 04:28:10 +00:00
import arrow
import ccxt
2021-09-19 23:02:09 +00:00
from freqtrade.enums import Collateral, TradingMode
2020-09-28 17:39:41 +00:00
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
2020-09-28 17:39:41 +00:00
2019-02-24 18:30:05 +00:00
logger = logging.getLogger(__name__)
class Binance(Exchange):
2019-02-24 19:01:20 +00:00
_ft_has: Dict = {
2019-02-24 18:30:05 +00:00
"stoploss_on_exchange": True,
"order_time_in_force": ['gtc', 'fok', 'ioc'],
2021-09-16 04:28:10 +00:00
"time_in_force_parameter": "timeInForce",
2020-12-20 10:44:50 +00:00
"ohlcv_candle_limit": 1000,
2019-08-14 17:22:52 +00:00
"trades_pagination": "id",
"trades_pagination_arg": "fromId",
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
"ccxt_futures_name": "future"
2019-02-24 18:30:05 +00:00
}
2021-09-19 23:02:09 +00:00
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list
2021-11-14 01:45:41 +00:00
# TODO-lev: Uncomment once supported
# (TradingMode.MARGIN, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.ISOLATED)
2021-09-19 23:02:09 +00:00
]
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
2021-09-19 23:02:09 +00:00
:param side: "buy" or "sell"
"""
2021-09-19 23:02:09 +00:00
return order['type'] == 'stop_loss_limit' and (
(side == "sell" and stop_loss > float(order['info']['stopPrice'])) or
(side == "buy" and stop_loss < float(order['info']['stopPrice']))
)
@retrier(retries=0)
2021-09-19 23:02:09 +00:00
def stoploss(self, pair: str, amount: float, stop_price: float,
order_types: Dict, side: str, leverage: float) -> Dict:
"""
creates a stoploss limit order.
this stoploss-limit is binance-specific.
It may work with a limited number of other exchanges, but this has not been tested yet.
2021-09-19 23:02:09 +00:00
:param side: "buy" or "sell"
"""
# Limit price threshold: As limit price should always be below stop-price
2020-02-02 09:47:44 +00:00
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
2021-09-19 23:02:09 +00:00
if side == "sell":
# TODO: Name limit_rate in other exchange subclasses
rate = stop_price * limit_price_pct
else:
rate = stop_price * (2 - limit_price_pct)
ordertype = "stop_loss_limit"
stop_price = self.price_to_precision(pair, stop_price)
2021-09-19 23:02:09 +00:00
bad_stop_price = (stop_price <= rate) if side == "sell" else (stop_price >= rate)
# Ensure rate is less than stop price
2021-09-19 23:02:09 +00:00
if bad_stop_price:
raise OperationalException(
2021-09-19 23:02:09 +00:00
'In stoploss limit order, stop price should be better than limit price')
if self._config['dry_run']:
dry_order = self.create_dry_run_order(
2021-09-19 23:02:09 +00:00
pair, ordertype, side, amount, stop_price, leverage)
return dry_order
try:
params = self._params.copy()
params.update({'stopPrice': stop_price})
amount = self.amount_to_precision(pair, amount)
rate = self.price_to_precision(pair, rate)
2021-09-19 23:02:09 +00:00
self._lev_prep(pair, leverage)
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
2020-05-13 05:15:18 +00:00
amount=amount, price=rate, params=params)
logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s', pair, stop_price, rate)
2021-06-10 18:09:25 +00:00
self._log_exchange_response('create_stoploss_order', order)
return order
except ccxt.InsufficientFunds as e:
raise InsufficientFundsError(
2021-09-19 23:02:09 +00:00
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
f'Tried to {side} amount {amount} at rate {rate}. '
f'Message: {e}') from e
except ccxt.InvalidOrder as e:
# Errors:
# `binance Order would trigger immediately.`
raise InvalidOrderException(
2021-09-19 23:02:09 +00:00
f'Could not create {ordertype} {side} order on market {pair}. '
f'Tried to {side} amount {amount} at rate {rate}. '
f'Message: {e}') from e
2020-06-28 09:17:06 +00:00
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
2021-09-19 23:02:09 +00:00
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
def market_is_future(self, market: Dict[str, Any]) -> bool:
# TODO-lev: This should be unified in ccxt to "swap"...
return market.get('future', False) is True
2021-09-19 23:02:09 +00:00
@retrier
def fill_leverage_brackets(self):
"""
2021-11-09 18:22:29 +00:00
Assigns property _leverage_brackets to a dictionary of information about the leverage
allowed on each pair
2021-09-19 23:02:09 +00:00
"""
if self.trading_mode == TradingMode.FUTURES:
try:
if self._config['dry_run']:
leverage_brackets_path = (
Path(__file__).parent / 'binance_leverage_brackets.json'
)
with open(leverage_brackets_path) as json_file:
leverage_brackets = json.load(json_file)
else:
leverage_brackets = self._api.load_leverage_brackets()
for pair, brackets in leverage_brackets.items():
self._leverage_brackets[pair] = [
[
min_amount,
float(margin_req)
] for [
min_amount,
margin_req
] in brackets
]
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(f'Could not fetch leverage amounts due to'
f'{e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float:
"""
2021-11-09 18:22:29 +00:00
Returns the maximum leverage that a pair can be traded at
:param pair: The base/quote currency pair being traded
:nominal_value: The total value of the trade in quote currency (collateral + debt)
2021-09-19 23:02:09 +00:00
"""
2021-11-01 08:46:35 +00:00
if pair not in self._leverage_brackets:
return 1.0
2021-09-19 23:02:09 +00:00
pair_brackets = self._leverage_brackets[pair]
max_lev = 1.0
for [min_amount, margin_req] in pair_brackets:
if nominal_value >= min_amount:
max_lev = 1/margin_req
return max_lev
2021-10-02 03:21:59 +00:00
@retrier
2021-09-19 23:02:09 +00:00
def _set_leverage(
self,
leverage: float,
pair: Optional[str] = None,
trading_mode: Optional[TradingMode] = None
):
"""
2021-11-09 18:22:29 +00:00
Set's the leverage before making a trade, in order to not
have the same leverage on every trade
2021-09-19 23:02:09 +00:00
"""
trading_mode = trading_mode or self.trading_mode
if self._config['dry_run'] or trading_mode != TradingMode.FUTURES:
return
try:
self._api.set_leverage(symbol=pair, leverage=leverage)
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
2021-09-16 04:28:10 +00:00
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
since_ms: int, is_new_pair: bool = False,
raise_: bool = False
) -> Tuple[str, str, List]:
2021-09-16 04:28:10 +00:00
"""
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
Does not work for other exchanges, which don't return the earliest data when called with "0"
"""
if is_new_pair:
x = await self._async_get_candle_history(pair, timeframe, 0)
if x and x[2] and x[2][0] and x[2][0][0] > since_ms:
# Set starting date to first available candle.
since_ms = x[2][0][0]
logger.info(f"Candle-data for {pair} available starting with "
f"{arrow.get(since_ms // 1000).isoformat()}.")
return await super()._async_get_historic_ohlcv(
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair,
raise_=raise_)
2021-11-09 07:17:29 +00:00
def funding_fee_cutoff(self, open_date: datetime):
2021-11-09 18:40:42 +00:00
"""
2021-11-09 07:00:57 +00:00
# TODO-lev: Double check that gateio, ftx, and kraken don't also have this
2021-11-09 07:17:29 +00:00
:param open_date: The open date for a trade
2021-11-09 07:00:57 +00:00
:return: The cutoff open time for when a funding fee is charged
2021-11-09 18:40:42 +00:00
"""
2021-11-09 07:17:29 +00:00
return open_date.minute > 0 or (open_date.minute == 0 and open_date.second > 15)