Merge branch 'develop' into pr/asuiu/8296

This commit is contained in:
Matthias
2023-03-26 10:28:02 +02:00
107 changed files with 5060 additions and 2328 deletions

View File

@@ -7,6 +7,7 @@ from typing import Dict, List, Optional, Tuple
import arrow
import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange
@@ -23,7 +24,7 @@ class Binance(Exchange):
_ft_has: Dict = {
"stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "stop_loss_limit"},
"order_time_in_force": ['GTC', 'FOK', 'IOC'],
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
"ohlcv_candle_limit": 1000,
"trades_pagination": "id",
"trades_pagination_arg": "fromId",
@@ -31,6 +32,7 @@ class Binance(Exchange):
}
_ft_has_futures: Dict = {
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
"order_time_in_force": ["GTC", "FOK", "IOC"],
"tickers_have_price": False,
"floor_leverage": True,
"stop_price_type_field": "workingType",
@@ -47,6 +49,26 @@ class Binance(Exchange):
(TradingMode.FUTURES, MarginMode.ISOLATED)
]
def _get_params(
self,
side: BuySell,
ordertype: str,
leverage: float,
reduceOnly: bool,
time_in_force: str = 'GTC',
) -> Dict:
params = super()._get_params(side, ordertype, leverage, reduceOnly, time_in_force)
if (
time_in_force == 'PO'
and ordertype != 'market'
and self.trading_mode == TradingMode.SPOT
# Only spot can do post only orders
):
params.pop('timeInForce')
params['postOnly'] = True
return params
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
tickers = super().get_tickers(symbols=symbols, cached=cached)
if self.trading_mode == TradingMode.FUTURES:

File diff suppressed because it is too large Load Diff

View File

@@ -27,11 +27,10 @@ class Bybit(Exchange):
"""
_ft_has: Dict = {
"ohlcv_candle_limit": 1000,
"ohlcv_candle_limit": 200,
"ohlcv_has_history": False,
}
_ft_has_futures: Dict = {
"ohlcv_candle_limit": 200,
"ohlcv_has_history": True,
"mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h",
@@ -115,7 +114,7 @@ class Bybit(Exchange):
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
return data
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
if self.trading_mode != TradingMode.SPOT:
params = {'leverage': leverage}
self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params)

View File

@@ -60,7 +60,6 @@ class Exchange:
_ft_has_default: Dict = {
"stoploss_on_exchange": False,
"order_time_in_force": ["GTC"],
"time_in_force_parameter": "timeInForce",
"ohlcv_params": {},
"ohlcv_candle_limit": 500,
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
@@ -69,6 +68,7 @@ class Exchange:
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
"ohlcv_volume_currency": "base", # "base" or "quote"
"tickers_have_quoteVolume": True,
"tickers_have_bid_ask": True, # bid / ask empty for fetch_tickers
"tickers_have_price": True,
"trades_pagination": "time", # Possible are "time" or "id"
"trades_pagination_arg": "since",
@@ -1020,10 +1020,10 @@ class Exchange:
# Order handling
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
if self.trading_mode != TradingMode.SPOT:
self.set_margin_mode(pair, self.margin_mode)
self._set_leverage(leverage, pair)
self.set_margin_mode(pair, self.margin_mode, accept_fail)
self._set_leverage(leverage, pair, accept_fail)
def _get_params(
self,
@@ -1035,8 +1035,7 @@ class Exchange:
) -> Dict:
params = self._params.copy()
if time_in_force != 'GTC' and ordertype != 'market':
param = self._ft_has.get('time_in_force_parameter', '')
params.update({param: time_in_force.upper()})
params.update({'timeInForce': time_in_force.upper()})
if reduceOnly:
params.update({'reduceOnly': True})
return params
@@ -1088,7 +1087,7 @@ class Exchange:
f'Tried to {side} amount {amount} at rate {rate}.'
f'Message: {e}') from e
except ccxt.InvalidOrder as e:
raise ExchangeError(
raise InvalidOrderException(
f'Could not create {ordertype} {side} order on market {pair}. '
f'Tried to {side} amount {amount} at rate {rate}. '
f'Message: {e}') from e
@@ -1138,8 +1137,15 @@ class Exchange:
"sell" else (stop_price >= limit_rate))
# Ensure rate is less than stop price
if bad_stop_price:
raise OperationalException(
'In stoploss limit order, stop price should be more than limit price')
# This can for example happen if the stop / liquidation price is set to 0
# Which is possible if a market-order closes right away.
# The InvalidOrderException will bubble up to exit_positions, where it will be
# handled gracefully.
raise InvalidOrderException(
"In stoploss limit order, stop price should be more than limit price. "
f"Stop price: {stop_price}, Limit price: {limit_rate}, "
f"Limit Price pct: {limit_price_pct}"
)
return limit_rate
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
@@ -1202,7 +1208,7 @@ class Exchange:
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
self._lev_prep(pair, leverage, side)
self._lev_prep(pair, leverage, side, accept_fail=True)
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
amount=amount, price=limit_rate, params=params)
self._log_exchange_response('create_stoploss_order', order)
@@ -2527,7 +2533,6 @@ class Exchange:
self,
leverage: float,
pair: Optional[str] = None,
trading_mode: Optional[TradingMode] = None,
accept_fail: bool = False,
):
"""
@@ -2545,7 +2550,7 @@ class Exchange:
self._log_exchange_response('set_leverage', res)
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except ccxt.BadRequest as e:
except (ccxt.BadRequest, ccxt.InsufficientFunds) as e:
if not accept_fail:
raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
@@ -2756,10 +2761,10 @@ class Exchange:
raise OperationalException(
f"{self.name} does not support {self.margin_mode} {self.trading_mode}")
isolated_liq = None
liquidation_price = None
if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
isolated_liq = self.dry_run_liquidation_price(
liquidation_price = self.dry_run_liquidation_price(
pair=pair,
open_rate=open_rate,
is_short=is_short,
@@ -2774,16 +2779,16 @@ class Exchange:
positions = self.fetch_positions(pair)
if len(positions) > 0:
pos = positions[0]
isolated_liq = pos['liquidationPrice']
liquidation_price = pos['liquidationPrice']
if isolated_liq is not None:
buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer
isolated_liq = (
isolated_liq - buffer_amount
if liquidation_price is not None:
buffer_amount = abs(open_rate - liquidation_price) * self.liquidation_buffer
liquidation_price_buffer = (
liquidation_price - buffer_amount
if is_short else
isolated_liq + buffer_amount
liquidation_price + buffer_amount
)
return isolated_liq
return max(liquidation_price_buffer, 0.0)
else:
return None

View File

@@ -32,6 +32,7 @@ class Gate(Exchange):
_ft_has_futures: Dict = {
"needs_trading_fees": True,
"tickers_have_bid_ask": False,
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
"stop_price_type_field": "price_type",
@@ -74,8 +75,7 @@ class Gate(Exchange):
)
if ordertype == 'market' and self.trading_mode == TradingMode.FUTURES:
params['type'] = 'market'
param = self._ft_has.get('time_in_force_parameter', '')
params.update({param: 'IOC'})
params.update({'timeInForce': 'IOC'})
return params
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,

View File

@@ -158,7 +158,6 @@ class Kraken(Exchange):
self,
leverage: float,
pair: Optional[str] = None,
trading_mode: Optional[TradingMode] = None,
accept_fail: bool = False,
):
"""

View File

@@ -1,14 +1,16 @@
import logging
from typing import Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple
import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.enums.pricetype import PriceType
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exceptions import (DDosProtection, OperationalException, RetryableOrderError,
TemporaryError)
from freqtrade.exchange import Exchange, date_minus_candles
from freqtrade.exchange.common import retrier
from freqtrade.misc import safe_value_fallback2
logger = logging.getLogger(__name__)
@@ -24,11 +26,13 @@ class Okx(Exchange):
"ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
"mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h",
"stoploss_order_types": {"limit": "limit"},
"stoploss_on_exchange": True,
}
_ft_has_futures: Dict = {
"tickers_have_quoteVolume": False,
"fee_cost_in_contracts": True,
"stop_price_type_field": "tpTriggerPxType",
"stop_price_type_field": "slTriggerPxType",
"stop_price_type_value_mapping": {
PriceType.LAST: "last",
PriceType.MARK: "index",
@@ -121,10 +125,9 @@ class Okx(Exchange):
return params
@retrier
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
try:
# TODO-lev: Test me properly (check mgnMode passed)
res = self._api.set_leverage(
leverage=leverage,
symbol=pair,
@@ -157,3 +160,78 @@ class Okx(Exchange):
pair_tiers = self._leverage_tiers[pair]
return pair_tiers[-1]['maxNotional'] / leverage
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
params = self._params.copy()
# Verify if stopPrice works for your exchange!
params.update({'stopLossPrice': stop_price})
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
params['tdMode'] = self.margin_mode.value
params['posSide'] = self._get_posSide(side, True)
return params
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
OKX uses non-default stoploss price naming.
"""
if not self._ft_has.get('stoploss_on_exchange'):
raise OperationalException(f"stoploss is not implemented for {self.name}.")
return (
order.get('stopLossPrice', None) is None
or ((side == "sell" and stop_loss > float(order['stopLossPrice'])) or
(side == "buy" and stop_loss < float(order['stopLossPrice'])))
)
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
if self._config['dry_run']:
return self.fetch_dry_run_order(order_id)
try:
params1 = {'stop': True}
order_reg = self._api.fetch_order(order_id, pair, params=params1)
self._log_exchange_response('fetch_stoploss_order', order_reg)
return order_reg
except ccxt.OrderNotFound:
pass
params2 = {'stop': True, 'ordType': 'conditional'}
for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders,
self._api.fetch_canceled_orders):
try:
orders = method(pair, params=params2)
orders_f = [order for order in orders if order['id'] == order_id]
if orders_f:
order = orders_f[0]
if (order['status'] == 'closed'
and (real_order_id := order.get('info', {}).get('ordId')) is not None):
# Once a order triggered, we fetch the regular followup order.
order_reg = self.fetch_order(real_order_id, pair)
self._log_exchange_response('fetch_stoploss_order1', order_reg)
order_reg['id_stop'] = order_reg['id']
order_reg['id'] = order_id
order_reg['type'] = 'stoploss'
order_reg['status_stop'] = 'triggered'
return order_reg
order['type'] = 'stoploss'
return order
except ccxt.BaseError:
pass
raise RetryableOrderError(
f'StoplossOrder not found (pair: {pair} id: {order_id}).')
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
if order['type'] == 'stop':
return safe_value_fallback2(order, order, 'id_stop', 'id')
return order['id']
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
params1 = {'stop': True}
# 'ordType': 'conditional'
#
return self.cancel_order(
order_id=order_id,
pair=pair,
params=params1,
)