mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-01-20 05:50:36 +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,
|
||||
)
|
||||
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.misc import deep_merge_dicts, json_load
|
||||
from freqtrade.util import FtTTLCache
|
||||
@@ -51,6 +51,8 @@ class Binance(Exchange):
|
||||
"funding_fee_candle_limit": 1000,
|
||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||
"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,
|
||||
"floor_leverage": True,
|
||||
"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:
|
||||
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(
|
||||
self,
|
||||
pair: str,
|
||||
|
||||
@@ -31,6 +31,7 @@ class Bitget(Exchange):
|
||||
"stop_price_prop": "stopPrice",
|
||||
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
||||
"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.
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||
}
|
||||
@@ -128,9 +129,6 @@ class Bitget(Exchange):
|
||||
|
||||
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
|
||||
def additional_exchange_init(self) -> None:
|
||||
"""
|
||||
|
||||
@@ -132,6 +132,7 @@ class Exchange:
|
||||
"stop_price_prop": "stopLossPrice", # Used for stoploss_on_exchange response parsing
|
||||
"stoploss_order_types": {},
|
||||
"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"],
|
||||
"ohlcv_params": {},
|
||||
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
|
||||
@@ -1687,7 +1688,24 @@ class Exchange:
|
||||
def fetch_stoploss_order(
|
||||
self, order_id: str, pair: str, params: dict | None = None
|
||||
) -> 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(
|
||||
self, order_id: str, pair: str, stoploss_order: bool = False
|
||||
@@ -1741,6 +1759,9 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
stoploss_order_types: dict[str, str]
|
||||
stoploss_blocks_assets: bool
|
||||
stoploss_query_requires_stop_flag: bool
|
||||
stoploss_algo_order_info_id: str
|
||||
# ohlcv
|
||||
ohlcv_params: dict
|
||||
ohlcv_candle_limit: int
|
||||
|
||||
@@ -30,6 +30,8 @@ class Gate(Exchange):
|
||||
"stoploss_order_types": {"limit": "limit"},
|
||||
"stop_price_param": "stopPrice",
|
||||
"stop_price_prop": "stopPrice",
|
||||
"stoploss_query_requires_stop_flag": True,
|
||||
"stoploss_algo_order_info_id": "fired_order_id",
|
||||
"l2_limit_upper": 1000,
|
||||
"marketOrderRequiresPrice": True,
|
||||
"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",
|
||||
"l2_limit_upper": 300,
|
||||
"stoploss_blocks_assets": False,
|
||||
"stoploss_algo_order_info_id": "trade_id",
|
||||
"stop_price_type_value_mapping": {
|
||||
PriceType.LAST: 0,
|
||||
PriceType.MARK: 1,
|
||||
@@ -132,25 +135,3 @@ class Gate(Exchange):
|
||||
|
||||
def get_order_id_conditional(self, order: CcxtOrder) -> str:
|
||||
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
|
||||
"stoploss_order_types": {"limit": "limit"},
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_query_requires_stop_flag": True,
|
||||
"trades_has_history": False, # Endpoint doesn't have a "since" parameter
|
||||
"ws_enabled": True,
|
||||
}
|
||||
@@ -263,9 +264,6 @@ class Okx(Exchange):
|
||||
return safe_value_fallback2(order, order, "id_stop", "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]:
|
||||
orders = []
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ def test_fetch_stoploss_order_gate(default_conf, mocker):
|
||||
|
||||
exchange.fetch_stoploss_order("1234", "ETH/BTC")
|
||||
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][1]["pair"] == "ETH/BTC"
|
||||
assert fetch_order_mock.call_args_list[0][1]["params"] == {"stop": True}
|
||||
assert fetch_order_mock.call_args_list[0][0][0] == "1234"
|
||||
assert fetch_order_mock.call_args_list[0][0][1] == "ETH/BTC"
|
||||
assert fetch_order_mock.call_args_list[0][0][2] == {"stop": True}
|
||||
|
||||
default_conf["trading_mode"] = "futures"
|
||||
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")
|
||||
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"
|
||||
|
||||
|
||||
def test_cancel_stoploss_order_gate(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange="gate")
|
||||
|
||||
cancel_order_mock = MagicMock()
|
||||
exchange.cancel_order = cancel_order_mock
|
||||
cancel_order_mock = mocker.patch.object(exchange, "cancel_order", autospec=True)
|
||||
|
||||
exchange.cancel_stoploss_order("1234", "ETH/BTC")
|
||||
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][1]["pair"] == "ETH/BTC"
|
||||
assert cancel_order_mock.call_args_list[0][1]["params"] == {"stop": True}
|
||||
assert cancel_order_mock.call_args_list[0][0][0] == "1234"
|
||||
assert cancel_order_mock.call_args_list[0][0][1] == "ETH/BTC"
|
||||
assert cancel_order_mock.call_args_list[0][0][2] == {"stop": True}
|
||||
|
||||
|
||||
@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):
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange="okx")
|
||||
|
||||
exchange.cancel_order = MagicMock()
|
||||
co_mock = mocker.patch.object(exchange, "cancel_order", autospec=True)
|
||||
|
||||
exchange.cancel_stoploss_order("1234", "ETH/USDT")
|
||||
assert exchange.cancel_order.call_count == 1
|
||||
assert exchange.cancel_order.call_args_list[0][1]["order_id"] == "1234"
|
||||
assert exchange.cancel_order.call_args_list[0][1]["pair"] == "ETH/USDT"
|
||||
assert exchange.cancel_order.call_args_list[0][1]["params"] == {"stop": True}
|
||||
assert co_mock.call_count == 1
|
||||
args, _ = co_mock.call_args
|
||||
assert args[0] == "1234"
|
||||
assert args[1] == "ETH/USDT"
|
||||
assert args[2] == {"stop": True}
|
||||
|
||||
|
||||
def test__get_stop_params_okx(mocker, default_conf):
|
||||
|
||||
Reference in New Issue
Block a user