mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-01-30 10:50:24 +00:00
Merge pull request #12627 from freqtrade/unify/algo_stop_orders
refactor stoploss methods for exchanges with algo orders
This commit is contained in:
@@ -17,7 +17,7 @@ from freqtrade.exchange.binance_public_data import (
|
|||||||
download_archive_trades,
|
download_archive_trades,
|
||||||
)
|
)
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas, Tickers
|
from freqtrade.exchange.exchange_types import FtHas, Tickers
|
||||||
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_msecs
|
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_msecs
|
||||||
from freqtrade.misc import deep_merge_dicts, json_load
|
from freqtrade.misc import deep_merge_dicts, json_load
|
||||||
from freqtrade.util import FtTTLCache
|
from freqtrade.util import FtTTLCache
|
||||||
@@ -51,6 +51,8 @@ class Binance(Exchange):
|
|||||||
"funding_fee_candle_limit": 1000,
|
"funding_fee_candle_limit": 1000,
|
||||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||||
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
||||||
|
"stoploss_query_requires_stop_flag": True,
|
||||||
|
"stoploss_algo_order_info_id": "actualOrderId",
|
||||||
"tickers_have_price": False,
|
"tickers_have_price": False,
|
||||||
"floor_leverage": True,
|
"floor_leverage": True,
|
||||||
"fetch_orders_limit_minutes": 7 * 1440, # "fetch_orders" is limited to 7 days
|
"fetch_orders_limit_minutes": 7 * 1440, # "fetch_orders" is limited to 7 days
|
||||||
@@ -145,34 +147,6 @@ class Binance(Exchange):
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def fetch_stoploss_order(
|
|
||||||
self, order_id: str, pair: str, params: dict | None = None
|
|
||||||
) -> CcxtOrder:
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
|
||||||
params = params or {}
|
|
||||||
params.update({"stop": True})
|
|
||||||
order = self.fetch_order(order_id, pair, params)
|
|
||||||
if self.trading_mode == TradingMode.FUTURES and order.get("status", "open") == "closed":
|
|
||||||
# Places a real order - which we need to fetch explicitly.
|
|
||||||
|
|
||||||
if new_orderid := order.get("info", {}).get("actualOrderId"):
|
|
||||||
order1 = self.fetch_order(order_id=new_orderid, pair=pair, params={})
|
|
||||||
order1["id_stop"] = order1["id"]
|
|
||||||
order1["id"] = order_id
|
|
||||||
order1["type"] = "stoploss"
|
|
||||||
order1["stopPrice"] = order.get("stopPrice")
|
|
||||||
order1["status_stop"] = "triggered"
|
|
||||||
|
|
||||||
return order1
|
|
||||||
|
|
||||||
return order
|
|
||||||
|
|
||||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
|
||||||
params = params or {}
|
|
||||||
params.update({"stop": True})
|
|
||||||
return self.cancel_order(order_id=order_id, pair=pair, params=params)
|
|
||||||
|
|
||||||
def get_historic_ohlcv(
|
def get_historic_ohlcv(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class Bitget(Exchange):
|
|||||||
"stop_price_prop": "stopPrice",
|
"stop_price_prop": "stopPrice",
|
||||||
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
||||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||||
|
"stoploss_query_requires_stop_flag": True,
|
||||||
"ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones.
|
"ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones.
|
||||||
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||||
}
|
}
|
||||||
@@ -128,9 +129,6 @@ class Bitget(Exchange):
|
|||||||
|
|
||||||
return self._fetch_stop_order_fallback(order_id, pair)
|
return self._fetch_stop_order_fallback(order_id, pair)
|
||||||
|
|
||||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
|
||||||
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
|
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def additional_exchange_init(self) -> None:
|
def additional_exchange_init(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ class Exchange:
|
|||||||
"stop_price_prop": "stopLossPrice", # Used for stoploss_on_exchange response parsing
|
"stop_price_prop": "stopLossPrice", # Used for stoploss_on_exchange response parsing
|
||||||
"stoploss_order_types": {},
|
"stoploss_order_types": {},
|
||||||
"stoploss_blocks_assets": True, # By default stoploss orders block assets
|
"stoploss_blocks_assets": True, # By default stoploss orders block assets
|
||||||
|
"stoploss_query_requires_stop_flag": False, # Require "stop": True" to fetch stop orders
|
||||||
"order_time_in_force": ["GTC"],
|
"order_time_in_force": ["GTC"],
|
||||||
"ohlcv_params": {},
|
"ohlcv_params": {},
|
||||||
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
|
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
|
||||||
@@ -1687,7 +1688,24 @@ class Exchange:
|
|||||||
def fetch_stoploss_order(
|
def fetch_stoploss_order(
|
||||||
self, order_id: str, pair: str, params: dict | None = None
|
self, order_id: str, pair: str, params: dict | None = None
|
||||||
) -> CcxtOrder:
|
) -> CcxtOrder:
|
||||||
return self.fetch_order(order_id, pair, params)
|
if self.get_option("stoploss_query_requires_stop_flag"):
|
||||||
|
params = params or {}
|
||||||
|
params["stop"] = True
|
||||||
|
order = self.fetch_order(order_id, pair, params)
|
||||||
|
val = self.get_option("stoploss_algo_order_info_id")
|
||||||
|
if val and order.get("status", "open") == "closed":
|
||||||
|
if new_orderid := order.get("info", {}).get(val):
|
||||||
|
# Fetch real order, which was placed by the algo order.
|
||||||
|
actual_order = self.fetch_order(order_id=new_orderid, pair=pair, params=None)
|
||||||
|
actual_order["id_stop"] = actual_order["id"]
|
||||||
|
actual_order["id"] = order_id
|
||||||
|
actual_order["type"] = "stoploss"
|
||||||
|
actual_order["stopPrice"] = order.get("stopPrice")
|
||||||
|
actual_order["status_stop"] = "triggered"
|
||||||
|
|
||||||
|
return actual_order
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
def fetch_order_or_stoploss_order(
|
def fetch_order_or_stoploss_order(
|
||||||
self, order_id: str, pair: str, stoploss_order: bool = False
|
self, order_id: str, pair: str, stoploss_order: bool = False
|
||||||
@@ -1741,6 +1759,9 @@ class Exchange:
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
||||||
|
if self.get_option("stoploss_query_requires_stop_flag"):
|
||||||
|
params = params or {}
|
||||||
|
params["stop"] = True
|
||||||
return self.cancel_order(order_id, pair, params)
|
return self.cancel_order(order_id, pair, params)
|
||||||
|
|
||||||
def is_cancel_order_result_suitable(self, corder) -> TypeGuard[CcxtOrder]:
|
def is_cancel_order_result_suitable(self, corder) -> TypeGuard[CcxtOrder]:
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ class FtHas(TypedDict, total=False):
|
|||||||
stop_price_type_value_mapping: dict
|
stop_price_type_value_mapping: dict
|
||||||
stoploss_order_types: dict[str, str]
|
stoploss_order_types: dict[str, str]
|
||||||
stoploss_blocks_assets: bool
|
stoploss_blocks_assets: bool
|
||||||
|
stoploss_query_requires_stop_flag: bool
|
||||||
|
stoploss_algo_order_info_id: str
|
||||||
# ohlcv
|
# ohlcv
|
||||||
ohlcv_params: dict
|
ohlcv_params: dict
|
||||||
ohlcv_candle_limit: int
|
ohlcv_candle_limit: int
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class Gate(Exchange):
|
|||||||
"stoploss_order_types": {"limit": "limit"},
|
"stoploss_order_types": {"limit": "limit"},
|
||||||
"stop_price_param": "stopPrice",
|
"stop_price_param": "stopPrice",
|
||||||
"stop_price_prop": "stopPrice",
|
"stop_price_prop": "stopPrice",
|
||||||
|
"stoploss_query_requires_stop_flag": True,
|
||||||
|
"stoploss_algo_order_info_id": "fired_order_id",
|
||||||
"l2_limit_upper": 1000,
|
"l2_limit_upper": 1000,
|
||||||
"marketOrderRequiresPrice": True,
|
"marketOrderRequiresPrice": True,
|
||||||
"trades_has_history": False, # Endpoint would support this - but ccxt doesn't.
|
"trades_has_history": False, # Endpoint would support this - but ccxt doesn't.
|
||||||
@@ -42,6 +44,7 @@ class Gate(Exchange):
|
|||||||
"stop_price_type_field": "price_type",
|
"stop_price_type_field": "price_type",
|
||||||
"l2_limit_upper": 300,
|
"l2_limit_upper": 300,
|
||||||
"stoploss_blocks_assets": False,
|
"stoploss_blocks_assets": False,
|
||||||
|
"stoploss_algo_order_info_id": "trade_id",
|
||||||
"stop_price_type_value_mapping": {
|
"stop_price_type_value_mapping": {
|
||||||
PriceType.LAST: 0,
|
PriceType.LAST: 0,
|
||||||
PriceType.MARK: 1,
|
PriceType.MARK: 1,
|
||||||
@@ -132,25 +135,3 @@ class Gate(Exchange):
|
|||||||
|
|
||||||
def get_order_id_conditional(self, order: CcxtOrder) -> str:
|
def get_order_id_conditional(self, order: CcxtOrder) -> str:
|
||||||
return safe_value_fallback2(order, order, "id_stop", "id")
|
return safe_value_fallback2(order, order, "id_stop", "id")
|
||||||
|
|
||||||
def fetch_stoploss_order(
|
|
||||||
self, order_id: str, pair: str, params: dict | None = None
|
|
||||||
) -> CcxtOrder:
|
|
||||||
order = self.fetch_order(order_id=order_id, pair=pair, params={"stop": True})
|
|
||||||
if order.get("status", "open") == "closed":
|
|
||||||
# Places a real order - which we need to fetch explicitly.
|
|
||||||
val = "trade_id" if self.trading_mode == TradingMode.FUTURES else "fired_order_id"
|
|
||||||
|
|
||||||
if new_orderid := order.get("info", {}).get(val):
|
|
||||||
order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params)
|
|
||||||
order1["id_stop"] = order1["id"]
|
|
||||||
order1["id"] = order_id
|
|
||||||
order1["type"] = "stoploss"
|
|
||||||
order1["stopPrice"] = order.get("stopPrice")
|
|
||||||
order1["status_stop"] = "triggered"
|
|
||||||
|
|
||||||
return order1
|
|
||||||
return order
|
|
||||||
|
|
||||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
|
||||||
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class Okx(Exchange):
|
|||||||
"ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
|
"ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
|
||||||
"stoploss_order_types": {"limit": "limit"},
|
"stoploss_order_types": {"limit": "limit"},
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_query_requires_stop_flag": True,
|
||||||
"trades_has_history": False, # Endpoint doesn't have a "since" parameter
|
"trades_has_history": False, # Endpoint doesn't have a "since" parameter
|
||||||
"ws_enabled": True,
|
"ws_enabled": True,
|
||||||
}
|
}
|
||||||
@@ -263,9 +264,6 @@ class Okx(Exchange):
|
|||||||
return safe_value_fallback2(order, order, "id_stop", "id")
|
return safe_value_fallback2(order, order, "id_stop", "id")
|
||||||
return order["id"]
|
return order["id"]
|
||||||
|
|
||||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
|
||||||
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
|
|
||||||
|
|
||||||
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[CcxtOrder]:
|
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[CcxtOrder]:
|
||||||
orders = []
|
orders = []
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ def test_fetch_stoploss_order_gate(default_conf, mocker):
|
|||||||
|
|
||||||
exchange.fetch_stoploss_order("1234", "ETH/BTC")
|
exchange.fetch_stoploss_order("1234", "ETH/BTC")
|
||||||
assert fetch_order_mock.call_count == 1
|
assert fetch_order_mock.call_count == 1
|
||||||
assert fetch_order_mock.call_args_list[0][1]["order_id"] == "1234"
|
assert fetch_order_mock.call_args_list[0][0][0] == "1234"
|
||||||
assert fetch_order_mock.call_args_list[0][1]["pair"] == "ETH/BTC"
|
assert fetch_order_mock.call_args_list[0][0][1] == "ETH/BTC"
|
||||||
assert fetch_order_mock.call_args_list[0][1]["params"] == {"stop": True}
|
assert fetch_order_mock.call_args_list[0][0][2] == {"stop": True}
|
||||||
|
|
||||||
default_conf["trading_mode"] = "futures"
|
default_conf["trading_mode"] = "futures"
|
||||||
default_conf["margin_mode"] = "isolated"
|
default_conf["margin_mode"] = "isolated"
|
||||||
@@ -36,21 +36,19 @@ def test_fetch_stoploss_order_gate(default_conf, mocker):
|
|||||||
|
|
||||||
exchange.fetch_stoploss_order("1234", "ETH/BTC")
|
exchange.fetch_stoploss_order("1234", "ETH/BTC")
|
||||||
assert exchange.fetch_order.call_count == 2
|
assert exchange.fetch_order.call_count == 2
|
||||||
assert exchange.fetch_order.call_args_list[0][1]["order_id"] == "1234"
|
assert exchange.fetch_order.call_args_list[0][0][0] == "1234"
|
||||||
assert exchange.fetch_order.call_args_list[1][1]["order_id"] == "222555"
|
assert exchange.fetch_order.call_args_list[1][1]["order_id"] == "222555"
|
||||||
|
|
||||||
|
|
||||||
def test_cancel_stoploss_order_gate(default_conf, mocker):
|
def test_cancel_stoploss_order_gate(default_conf, mocker):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, exchange="gate")
|
exchange = get_patched_exchange(mocker, default_conf, exchange="gate")
|
||||||
|
cancel_order_mock = mocker.patch.object(exchange, "cancel_order", autospec=True)
|
||||||
cancel_order_mock = MagicMock()
|
|
||||||
exchange.cancel_order = cancel_order_mock
|
|
||||||
|
|
||||||
exchange.cancel_stoploss_order("1234", "ETH/BTC")
|
exchange.cancel_stoploss_order("1234", "ETH/BTC")
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
assert cancel_order_mock.call_args_list[0][1]["order_id"] == "1234"
|
assert cancel_order_mock.call_args_list[0][0][0] == "1234"
|
||||||
assert cancel_order_mock.call_args_list[0][1]["pair"] == "ETH/BTC"
|
assert cancel_order_mock.call_args_list[0][0][1] == "ETH/BTC"
|
||||||
assert cancel_order_mock.call_args_list[0][1]["params"] == {"stop": True}
|
assert cancel_order_mock.call_args_list[0][0][2] == {"stop": True}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|||||||
@@ -661,14 +661,14 @@ def test_stoploss_adjust_okx(mocker, default_conf, sl1, sl2, sl3, side):
|
|||||||
|
|
||||||
def test_stoploss_cancel_okx(mocker, default_conf):
|
def test_stoploss_cancel_okx(mocker, default_conf):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, exchange="okx")
|
exchange = get_patched_exchange(mocker, default_conf, exchange="okx")
|
||||||
|
co_mock = mocker.patch.object(exchange, "cancel_order", autospec=True)
|
||||||
exchange.cancel_order = MagicMock()
|
|
||||||
|
|
||||||
exchange.cancel_stoploss_order("1234", "ETH/USDT")
|
exchange.cancel_stoploss_order("1234", "ETH/USDT")
|
||||||
assert exchange.cancel_order.call_count == 1
|
assert co_mock.call_count == 1
|
||||||
assert exchange.cancel_order.call_args_list[0][1]["order_id"] == "1234"
|
args, _ = co_mock.call_args
|
||||||
assert exchange.cancel_order.call_args_list[0][1]["pair"] == "ETH/USDT"
|
assert args[0] == "1234"
|
||||||
assert exchange.cancel_order.call_args_list[0][1]["params"] == {"stop": True}
|
assert args[1] == "ETH/USDT"
|
||||||
|
assert args[2] == {"stop": True}
|
||||||
|
|
||||||
|
|
||||||
def test__get_stop_params_okx(mocker, default_conf):
|
def test__get_stop_params_okx(mocker, default_conf):
|
||||||
|
|||||||
Reference in New Issue
Block a user