stable/freqtrade/exchange/binance.py

370 lines
15 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 Dict, List, Optional, Tuple
2019-02-24 18:30:05 +00:00
2021-09-16 04:28:10 +00:00
import arrow
import ccxt
from freqtrade.enums import CandleType, MarginMode, 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
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
2021-09-19 23:02:09 +00:00
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.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
2022-02-10 15:41:57 +00:00
ordertype = 'stop' if self.trading_mode == TradingMode.FUTURES else 'stop_loss_limit'
return order['type'] == ordertype and (
2021-09-19 23:02:09 +00:00
(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)
2022-02-10 15:41:57 +00:00
ordertype = 'stop' if self.trading_mode == TradingMode.FUTURES else '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})
2022-02-02 04:23:05 +00:00
if self.trading_mode == TradingMode.FUTURES:
params.update({'reduceOnly': True})
amount = self.amount_to_precision(pair, amount)
rate = self.price_to_precision(pair, rate)
self._lev_prep(pair, leverage, side)
2022-02-02 04:23:05 +00:00
order = self._api.create_order(
symbol=pair,
type=ordertype,
side=side,
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
@retrier
def fill_leverage_brackets(self) -> None:
2021-09-19 23:02:09 +00:00
"""
2021-11-09 18:22:29 +00:00
Assigns property _leverage_brackets to a dictionary of information about the leverage
allowed on each pair
After exectution, self._leverage_brackets = {
"pair_name": [
[notional_floor, maintenenace_margin_ratio, maintenance_amt],
...
],
...
}
e.g. {
"ETH/USDT:USDT": [
[0.0, 0.01, 0.0],
[10000, 0.02, 0.01],
...
],
...
}
2021-09-19 23:02:09 +00:00
"""
if self.trading_mode == TradingMode.FUTURES:
try:
if self._config['dry_run']:
leverage_brackets_path = (
2022-02-06 04:36:28 +00:00
Path(__file__).parent / 'binance_leverage_tiers.json'
2021-09-19 23:02:09 +00:00
)
with open(leverage_brackets_path) as json_file:
leverage_brackets = json.load(json_file)
else:
2022-02-06 04:36:28 +00:00
leverage_brackets = self._api.fetch_leverage_tiers()
2021-09-19 23:02:09 +00:00
2022-02-06 04:36:28 +00:00
for pair, tiers in leverage_brackets.items():
2022-01-10 18:28:22 +00:00
brackets = []
2022-02-06 04:36:28 +00:00
for tier in tiers:
info = tier['info']
brackets.append({
'min': tier['notionalFloor'],
'max': tier['notionalCap'],
'mmr': tier['maintenanceMarginRatio'],
'lev': tier['maxLeverage'],
'maintAmt': float(info['cum']) if 'cum' in info else None,
})
2022-01-10 18:28:22 +00:00
self._leverage_brackets[pair] = brackets
2021-09-19 23:02:09 +00:00
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: str, stake_amount: Optional[float]) -> float:
2021-09-19 23:02:09 +00:00
"""
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
:stake_amount: The total value of the traders margin_mode in quote currency
2021-09-19 23:02:09 +00:00
"""
2022-02-06 04:36:28 +00:00
if self.trading_mode == TradingMode.SPOT:
2021-11-01 08:46:35 +00:00
return 1.0
2022-02-06 04:36:28 +00:00
if self._api.has['fetchLeverageTiers']:
# Checks and edge cases
if stake_amount is None:
raise OperationalException(
'binance.get_max_leverage requires argument stake_amount')
if pair not in self._leverage_brackets: # Not a leveraged market
return 1.0
if stake_amount == 0:
return self._leverage_brackets[pair][0]['lev'] # Max lev for lowest amount
pair_brackets = self._leverage_brackets[pair]
num_brackets = len(pair_brackets)
for bracket_index in range(num_brackets):
bracket = pair_brackets[bracket_index]
lev = bracket['lev']
if bracket_index < num_brackets - 1:
next_bracket = pair_brackets[bracket_index+1]
next_floor = next_bracket['min'] / next_bracket['lev']
if next_floor > stake_amount: # Next bracket min too high for stake amount
return min((bracket['max'] / stake_amount), lev)
#
# With the two leverage brackets below,
# - a stake amount of 150 would mean a max leverage of (10000 / 150) = 66.66
# - stakes below 133.33 = max_lev of 75
# - stakes between 133.33-200 = max_lev of 10000/stake = 50.01-74.99
# - stakes from 200 + 1000 = max_lev of 50
#
# {
# "min": 0, # stake = 0.0
# "max": 10000, # max_stake@75 = 10000/75 = 133.33333333333334
# "lev": 75,
# },
# {
# "min": 10000, # stake = 200.0
# "max": 50000, # max_stake@50 = 50000/50 = 1000.0
# "lev": 50,
# }
#
else: # if on the last bracket
if stake_amount > bracket['max']: # If stake is > than max tradeable amount
raise InvalidOrderException(f'Amount {stake_amount} too high for {pair}')
else:
return bracket['lev']
raise OperationalException(
'Looped through all tiers without finding a max leverage. Should never be reached'
)
else: # Search markets.limits for max lev
market = self.markets[pair]
if market['limits']['leverage']['max'] is not None:
return market['limits']['leverage']['max']
else:
2022-02-06 04:36:28 +00:00
return 1.0 # Default if max leverage cannot be found
2021-09-19 23:02:09 +00:00
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:
2022-02-02 04:29:04 +00:00
self._api.set_leverage(symbol=pair, leverage=round(leverage))
2021-09-19 23:02:09 +00:00
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
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
2021-12-03 13:11:24 +00:00
since_ms: int, candle_type: CandleType,
is_new_pair: bool = False, raise_: bool = False,
) -> Tuple[str, 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"
2021-12-03 13:11:24 +00:00
:param candle_type: Any of the enum CandleType (must match trading mode!)
2021-09-16 04:28:10 +00:00
"""
if is_new_pair:
2021-12-03 15:44:05 +00:00
x = await self._async_get_candle_history(pair, timeframe, candle_type, 0)
2021-11-23 16:43:37 +00:00
if x and x[3] and x[3][0] and x[3][0][0] > since_ms:
2021-09-16 04:28:10 +00:00
# Set starting date to first available candle.
2021-11-23 16:43:37 +00:00
since_ms = x[3][0][0]
2021-09-16 04:28:10 +00:00
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: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)
def dry_run_liquidation_price(
self,
pair: str,
open_rate: float, # Entry price of position
is_short: bool,
position: float, # Absolute value of position size
2022-01-29 08:06:56 +00:00
wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed
PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
:param exchange_name:
:param open_rate: (EP1) Entry price of position
:param is_short: True if the trade is a short, false otherwise
:param position: Absolute value of position size (in base currency)
:param wallet_balance: (WB)
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
2022-01-29 08:06:56 +00:00
:param maintenance_amt:
# * Only required for Cross
:param mm_ex_1: (TMM)
Cross-Margin Mode: Maintenance Margin of all other contracts, excluding Contract 1
Isolated-Margin Mode: 0
:param upnl_ex_1: (UPNL)
Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1.
Isolated-Margin Mode: 0
"""
side_1 = -1 if is_short else 1
position = abs(position)
cross_vars = upnl_ex_1 - mm_ex_1 if self.margin_mode == MarginMode.CROSS else 0.0
2022-01-29 08:06:56 +00:00
# mm_ratio: Binance's formula specifies maintenance margin rate which is mm_ratio * 100%
# maintenance_amt: (CUM) Maintenance Amount of position
mm_ratio, maintenance_amt = self.get_maintenance_ratio_and_amt(pair, position)
if (maintenance_amt is None):
raise OperationalException(
"Parameter maintenance_amt is required by Binance.liquidation_price"
f"for {self.trading_mode.value}"
)
2022-01-29 08:06:56 +00:00
if self.trading_mode == TradingMode.FUTURES:
return (
(
(wallet_balance + cross_vars + maintenance_amt) -
(side_1 * position * open_rate)
) / (
(position * mm_ratio) - (side_1 * position)
)
)
else:
raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading")