Merge branch 'develop' into feat/short
This commit is contained in:
@@ -18,6 +18,7 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
|
||||
from freqtrade.exchange.ftx import Ftx
|
||||
from freqtrade.exchange.gateio import Gateio
|
||||
from freqtrade.exchange.hitbtc import Hitbtc
|
||||
from freqtrade.exchange.huobi import Huobi
|
||||
from freqtrade.exchange.kraken import Kraken
|
||||
from freqtrade.exchange.kucoin import Kucoin
|
||||
from freqtrade.exchange.okx import Okx
|
||||
|
@@ -6,13 +6,10 @@ from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
import ccxt
|
||||
|
||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exceptions import (DDosProtection, OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -22,6 +19,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'],
|
||||
"time_in_force_parameter": "timeInForce",
|
||||
"ohlcv_candle_limit": 1000,
|
||||
@@ -52,82 +50,6 @@ class Binance(Exchange):
|
||||
(side == "buy" and stop_loss < float(order['info']['stopPrice']))
|
||||
)
|
||||
|
||||
@retrier(retries=0)
|
||||
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.
|
||||
:param side: "buy" or "sell"
|
||||
"""
|
||||
# Limit price threshold: As limit price should always be below stop-price
|
||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
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' if self.trading_mode == TradingMode.FUTURES else 'stop_loss_limit'
|
||||
|
||||
stop_price = self.price_to_precision(pair, stop_price)
|
||||
|
||||
bad_stop_price = (stop_price <= rate) if side == "sell" else (stop_price >= rate)
|
||||
|
||||
# Ensure rate is less than stop price
|
||||
if bad_stop_price:
|
||||
raise OperationalException(
|
||||
'In stoploss limit order, stop price should be better than limit price')
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, side, amount, stop_price, leverage)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
params = self._params.copy()
|
||||
params.update({'stopPrice': stop_price})
|
||||
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)
|
||||
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)
|
||||
self._log_exchange_response('create_stoploss_order', order)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise InsufficientFundsError(
|
||||
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(
|
||||
f'Could not create {ordertype} {side} order on market {pair}. '
|
||||
f'Tried to {side} amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
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 _set_leverage(
|
||||
self,
|
||||
|
@@ -769,7 +769,8 @@ class Exchange:
|
||||
# Dry-run methods
|
||||
|
||||
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||
rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]:
|
||||
rate: float, leverage: float, params: Dict = {},
|
||||
stop_loss: bool = False) -> Dict[str, Any]:
|
||||
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
||||
_amount = self.amount_to_precision(pair, amount)
|
||||
dry_order: Dict[str, Any] = {
|
||||
@@ -785,15 +786,18 @@ class Exchange:
|
||||
'remaining': _amount,
|
||||
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||
'status': "closed" if ordertype == "market" else "open",
|
||||
'status': "closed" if ordertype == "market" and not stop_loss else "open",
|
||||
'fee': None,
|
||||
'info': {},
|
||||
'leverage': leverage
|
||||
}
|
||||
if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
|
||||
if stop_loss:
|
||||
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
||||
dry_order["stopPrice"] = dry_order["price"]
|
||||
# Workaround to avoid filling stoploss orders immediately
|
||||
dry_order["ft_order_type"] = "stoploss"
|
||||
|
||||
if dry_order["type"] == "market":
|
||||
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
|
||||
# Update market order pricing
|
||||
average = self.get_dry_market_fill_price(pair, side, amount, rate)
|
||||
dry_order.update({
|
||||
@@ -884,7 +888,9 @@ class Exchange:
|
||||
"""
|
||||
Check dry-run limit order fill and update fee (if it filled).
|
||||
"""
|
||||
if order['status'] != "closed" and order['type'] in ["limit"]:
|
||||
if (order['status'] != "closed"
|
||||
and order['type'] in ["limit"]
|
||||
and not order.get('ft_order_type')):
|
||||
pair = order['symbol']
|
||||
if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
|
||||
order.update({
|
||||
@@ -1002,19 +1008,99 @@ class Exchange:
|
||||
"""
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float,
|
||||
order_types: Dict, side: str, leverage: float) -> Dict:
|
||||
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||
params = self._params.copy()
|
||||
# Verify if stopPrice works for your exchange!
|
||||
params.update({'stopPrice': stop_price})
|
||||
return params
|
||||
|
||||
@retrier(retries=0)
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, side: str, leverage: float) -> Dict:
|
||||
"""
|
||||
creates a stoploss order.
|
||||
requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market
|
||||
to the corresponding exchange type.
|
||||
|
||||
The precise ordertype is determined by the order_types dict or exchange default.
|
||||
Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
|
||||
exchange's subclass.
|
||||
|
||||
The exception below should never raise, since we disallow
|
||||
starting the bot in validate_ordertypes()
|
||||
Note: Changes to this interface need to be applied to all sub-classes too.
|
||||
"""
|
||||
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
This may work with a limited number of other exchanges, but correct working
|
||||
needs to be tested individually.
|
||||
WARNING: setting `stoploss_on_exchange` to True will NOT auto-enable stoploss on exchange.
|
||||
`stoploss_adjust` must still be implemented for this to work.
|
||||
"""
|
||||
if not self._ft_has['stoploss_on_exchange']:
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
user_order_type = order_types.get('stoploss', 'market')
|
||||
if user_order_type in self._ft_has["stoploss_order_types"].keys():
|
||||
ordertype = self._ft_has["stoploss_order_types"][user_order_type]
|
||||
else:
|
||||
# Otherwise pick only one available
|
||||
ordertype = list(self._ft_has["stoploss_order_types"].values())[0]
|
||||
user_order_type = list(self._ft_has["stoploss_order_types"].keys())[0]
|
||||
|
||||
stop_price_norm = self.price_to_precision(pair, stop_price)
|
||||
rate = None
|
||||
if user_order_type == 'limit':
|
||||
# Limit price threshold: As limit price should always be below stop-price
|
||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
rate = stop_price * limit_price_pct
|
||||
|
||||
# Ensure rate is less than stop price
|
||||
if stop_price_norm <= rate:
|
||||
raise OperationalException(
|
||||
'In stoploss limit order, stop price should be more than limit price')
|
||||
rate = self.price_to_precision(pair, rate)
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair,
|
||||
ordertype,
|
||||
side,
|
||||
amount,
|
||||
stop_price_norm,
|
||||
stop_loss=True,
|
||||
leverage=leverage,
|
||||
)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
params = self._get_stop_params(ordertype=ordertype, stop_price=stop_price_norm)
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
params['reduceOnly'] = True
|
||||
|
||||
amount = self.amount_to_precision(pair, amount)
|
||||
|
||||
self._lev_prep(pair, leverage, side)
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
|
||||
amount=amount, price=rate, params=params)
|
||||
logger.info(f"stoploss {user_order_type} order added for {pair}. "
|
||||
f"stop price: {stop_price}. limit: {rate}")
|
||||
self._log_exchange_response('create_stoploss_order', order)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise InsufficientFundsError(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
# Errors:
|
||||
# `Order would trigger immediately.`
|
||||
raise InvalidOrderException(
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f"Could not place stoploss order due to {e.__class__.__name__}. "
|
||||
f"Message: {e}") from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||
def fetch_order(self, order_id: str, pair: str) -> Dict:
|
||||
@@ -2384,7 +2470,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non
|
||||
|
||||
|
||||
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
||||
return exchange_name in ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okx']
|
||||
return exchange_name in ['binance', 'bittrex', 'ftx', 'gateio', 'huobi', 'kraken', 'okx']
|
||||
|
||||
|
||||
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||
|
@@ -62,7 +62,7 @@ class Ftx(Exchange):
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, side, amount, stop_price, leverage)
|
||||
pair, ordertype, side, amount, stop_price, leverage, stop_loss=True)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
|
39
freqtrade/exchange/huobi.py
Normal file
39
freqtrade/exchange/huobi.py
Normal file
@@ -0,0 +1,39 @@
|
||||
""" Huobi exchange subclass """
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Huobi(Exchange):
|
||||
"""
|
||||
Huobi exchange class. Contains adjustments needed for Freqtrade to work
|
||||
with this exchange.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "stop-limit"},
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"l2_limit_range": [5, 10, 20],
|
||||
"l2_limit_range_required": False,
|
||||
}
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
return order['type'] == 'stop' and stop_loss > float(order['stopPrice'])
|
||||
|
||||
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||
|
||||
params = self._params.copy()
|
||||
params.update({
|
||||
"stopPrice": stop_price,
|
||||
"operator": "lte",
|
||||
})
|
||||
return params
|
@@ -99,6 +99,8 @@ class Kraken(Exchange):
|
||||
"""
|
||||
Creates a stoploss market order.
|
||||
Stoploss market orders is the only stoploss type supported by kraken.
|
||||
TODO: investigate if this can be combined with generic implementation
|
||||
(careful, prices are reversed)
|
||||
"""
|
||||
params = self._params.copy()
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
@@ -119,7 +121,7 @@ class Kraken(Exchange):
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, side, amount, stop_price, leverage)
|
||||
pair, ordertype, side, amount, stop_price, leverage, stop_loss=True)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
|
@@ -19,8 +19,26 @@ class Kucoin(Exchange):
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||
"l2_limit_range": [20, 100],
|
||||
"l2_limit_range_required": False,
|
||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||
"time_in_force_parameter": "timeInForce",
|
||||
}
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
return order['info'].get('stop') is not None and stop_loss > float(order['stopPrice'])
|
||||
|
||||
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||
|
||||
params = self._params.copy()
|
||||
params.update({
|
||||
'stopPrice': stop_price,
|
||||
'stop': 'loss'
|
||||
})
|
||||
return params
|
||||
|
Reference in New Issue
Block a user