stable/freqtrade/exchange/binance.py

241 lines
9.5 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-10-24 03:10:36 +00:00
async def _async_get_historic_ohlcv(
self,
pair: str,
timeframe: str,
since_ms: int,
is_new_pair: bool,
raise_: bool = False,
candle_type: Optional[str] = ""
2021-10-24 03:10:36 +00:00
) -> 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"
:param candle_type: "mark" if retrieving the mark price cnadles
2021-09-16 04:28:10 +00:00
"""
if is_new_pair:
x = await self._async_get_candle_history(pair, timeframe, 0, candle_type)
2021-09-16 04:28:10 +00:00
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()}.")
2021-10-24 03:10:36 +00:00
2021-09-16 04:28:10 +00:00
return await super()._async_get_historic_ohlcv(
2021-10-24 03:10:36 +00:00
pair=pair,
timeframe=timeframe,
since_ms=since_ms,
is_new_pair=is_new_pair,
raise_=raise_,
candle_type=candle_type
2021-10-24 03:10:36 +00:00
)
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)