Skip to content

Commit e9847d9

Browse files
authored
Merge pull request freqtrade#12627 from freqtrade/unify/algo_stop_orders
refactor stoploss methods for exchanges with algo orders
2 parents 21d723b + 1143aba commit e9847d9

File tree

8 files changed

+46
-74
lines changed

8 files changed

+46
-74
lines changed

freqtrade/exchange/binance.py

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
download_archive_trades,
1818
)
1919
from freqtrade.exchange.common import retrier
20-
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas, Tickers
20+
from freqtrade.exchange.exchange_types import FtHas, Tickers
2121
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_msecs
2222
from freqtrade.misc import deep_merge_dicts, json_load
2323
from freqtrade.util import FtTTLCache
@@ -51,6 +51,8 @@ class Binance(Exchange):
5151
"funding_fee_candle_limit": 1000,
5252
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
5353
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
54+
"stoploss_query_requires_stop_flag": True,
55+
"stoploss_algo_order_info_id": "actualOrderId",
5456
"tickers_have_price": False,
5557
"floor_leverage": True,
5658
"fetch_orders_limit_minutes": 7 * 1440, # "fetch_orders" is limited to 7 days
@@ -145,34 +147,6 @@ def additional_exchange_init(self) -> None:
145147
except ccxt.BaseError as e:
146148
raise OperationalException(e) from e
147149

148-
def fetch_stoploss_order(
149-
self, order_id: str, pair: str, params: dict | None = None
150-
) -> CcxtOrder:
151-
if self.trading_mode == TradingMode.FUTURES:
152-
params = params or {}
153-
params.update({"stop": True})
154-
order = self.fetch_order(order_id, pair, params)
155-
if self.trading_mode == TradingMode.FUTURES and order.get("status", "open") == "closed":
156-
# Places a real order - which we need to fetch explicitly.
157-
158-
if new_orderid := order.get("info", {}).get("actualOrderId"):
159-
order1 = self.fetch_order(order_id=new_orderid, pair=pair, params={})
160-
order1["id_stop"] = order1["id"]
161-
order1["id"] = order_id
162-
order1["type"] = "stoploss"
163-
order1["stopPrice"] = order.get("stopPrice")
164-
order1["status_stop"] = "triggered"
165-
166-
return order1
167-
168-
return order
169-
170-
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
171-
if self.trading_mode == TradingMode.FUTURES:
172-
params = params or {}
173-
params.update({"stop": True})
174-
return self.cancel_order(order_id=order_id, pair=pair, params=params)
175-
176150
def get_historic_ohlcv(
177151
self,
178152
pair: str,

freqtrade/exchange/bitget.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class Bitget(Exchange):
3131
"stop_price_prop": "stopPrice",
3232
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
3333
"stoploss_order_types": {"limit": "limit", "market": "market"},
34+
"stoploss_query_requires_stop_flag": True,
3435
"ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones.
3536
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
3637
}
@@ -128,9 +129,6 @@ def fetch_stoploss_order(
128129

129130
return self._fetch_stop_order_fallback(order_id, pair)
130131

131-
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
132-
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
133-
134132
@retrier
135133
def additional_exchange_init(self) -> None:
136134
"""

freqtrade/exchange/exchange.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ class Exchange:
132132
"stop_price_prop": "stopLossPrice", # Used for stoploss_on_exchange response parsing
133133
"stoploss_order_types": {},
134134
"stoploss_blocks_assets": True, # By default stoploss orders block assets
135+
"stoploss_query_requires_stop_flag": False, # Require "stop": True" to fetch stop orders
135136
"order_time_in_force": ["GTC"],
136137
"ohlcv_params": {},
137138
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
@@ -1687,7 +1688,24 @@ def fetch_order(self, order_id: str, pair: str, params: dict | None = None) -> C
16871688
def fetch_stoploss_order(
16881689
self, order_id: str, pair: str, params: dict | None = None
16891690
) -> CcxtOrder:
1690-
return self.fetch_order(order_id, pair, params)
1691+
if self.get_option("stoploss_query_requires_stop_flag"):
1692+
params = params or {}
1693+
params["stop"] = True
1694+
order = self.fetch_order(order_id, pair, params)
1695+
val = self.get_option("stoploss_algo_order_info_id")
1696+
if val and order.get("status", "open") == "closed":
1697+
if new_orderid := order.get("info", {}).get(val):
1698+
# Fetch real order, which was placed by the algo order.
1699+
actual_order = self.fetch_order(order_id=new_orderid, pair=pair, params=None)
1700+
actual_order["id_stop"] = actual_order["id"]
1701+
actual_order["id"] = order_id
1702+
actual_order["type"] = "stoploss"
1703+
actual_order["stopPrice"] = order.get("stopPrice")
1704+
actual_order["status_stop"] = "triggered"
1705+
1706+
return actual_order
1707+
1708+
return order
16911709

16921710
def fetch_order_or_stoploss_order(
16931711
self, order_id: str, pair: str, stoploss_order: bool = False
@@ -1741,6 +1759,9 @@ def cancel_order(self, order_id: str, pair: str, params: dict | None = None) ->
17411759
raise OperationalException(e) from e
17421760

17431761
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
1762+
if self.get_option("stoploss_query_requires_stop_flag"):
1763+
params = params or {}
1764+
params["stop"] = True
17441765
return self.cancel_order(order_id, pair, params)
17451766

17461767
def is_cancel_order_result_suitable(self, corder) -> TypeGuard[CcxtOrder]:

freqtrade/exchange/exchange_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class FtHas(TypedDict, total=False):
1919
stop_price_type_value_mapping: dict
2020
stoploss_order_types: dict[str, str]
2121
stoploss_blocks_assets: bool
22+
stoploss_query_requires_stop_flag: bool
23+
stoploss_algo_order_info_id: str
2224
# ohlcv
2325
ohlcv_params: dict
2426
ohlcv_candle_limit: int

freqtrade/exchange/gate.py

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ class Gate(Exchange):
3030
"stoploss_order_types": {"limit": "limit"},
3131
"stop_price_param": "stopPrice",
3232
"stop_price_prop": "stopPrice",
33+
"stoploss_query_requires_stop_flag": True,
34+
"stoploss_algo_order_info_id": "fired_order_id",
3335
"l2_limit_upper": 1000,
3436
"marketOrderRequiresPrice": True,
3537
"trades_has_history": False, # Endpoint would support this - but ccxt doesn't.
@@ -42,6 +44,7 @@ class Gate(Exchange):
4244
"stop_price_type_field": "price_type",
4345
"l2_limit_upper": 300,
4446
"stoploss_blocks_assets": False,
47+
"stoploss_algo_order_info_id": "trade_id",
4548
"stop_price_type_value_mapping": {
4649
PriceType.LAST: 0,
4750
PriceType.MARK: 1,
@@ -132,25 +135,3 @@ def get_trades_for_order(
132135

133136
def get_order_id_conditional(self, order: CcxtOrder) -> str:
134137
return safe_value_fallback2(order, order, "id_stop", "id")
135-
136-
def fetch_stoploss_order(
137-
self, order_id: str, pair: str, params: dict | None = None
138-
) -> CcxtOrder:
139-
order = self.fetch_order(order_id=order_id, pair=pair, params={"stop": True})
140-
if order.get("status", "open") == "closed":
141-
# Places a real order - which we need to fetch explicitly.
142-
val = "trade_id" if self.trading_mode == TradingMode.FUTURES else "fired_order_id"
143-
144-
if new_orderid := order.get("info", {}).get(val):
145-
order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params)
146-
order1["id_stop"] = order1["id"]
147-
order1["id"] = order_id
148-
order1["type"] = "stoploss"
149-
order1["stopPrice"] = order.get("stopPrice")
150-
order1["status_stop"] = "triggered"
151-
152-
return order1
153-
return order
154-
155-
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
156-
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})

freqtrade/exchange/okx.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class Okx(Exchange):
3131
"ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
3232
"stoploss_order_types": {"limit": "limit"},
3333
"stoploss_on_exchange": True,
34+
"stoploss_query_requires_stop_flag": True,
3435
"trades_has_history": False, # Endpoint doesn't have a "since" parameter
3536
"ws_enabled": True,
3637
}
@@ -263,9 +264,6 @@ def get_order_id_conditional(self, order: CcxtOrder) -> str:
263264
return safe_value_fallback2(order, order, "id_stop", "id")
264265
return order["id"]
265266

266-
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
267-
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
268-
269267
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[CcxtOrder]:
270268
orders = []
271269

tests/exchange/test_gate.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ def test_fetch_stoploss_order_gate(default_conf, mocker):
1616

1717
exchange.fetch_stoploss_order("1234", "ETH/BTC")
1818
assert fetch_order_mock.call_count == 1
19-
assert fetch_order_mock.call_args_list[0][1]["order_id"] == "1234"
20-
assert fetch_order_mock.call_args_list[0][1]["pair"] == "ETH/BTC"
21-
assert fetch_order_mock.call_args_list[0][1]["params"] == {"stop": True}
19+
assert fetch_order_mock.call_args_list[0][0][0] == "1234"
20+
assert fetch_order_mock.call_args_list[0][0][1] == "ETH/BTC"
21+
assert fetch_order_mock.call_args_list[0][0][2] == {"stop": True}
2222

2323
default_conf["trading_mode"] = "futures"
2424
default_conf["margin_mode"] = "isolated"
@@ -36,21 +36,19 @@ def test_fetch_stoploss_order_gate(default_conf, mocker):
3636

3737
exchange.fetch_stoploss_order("1234", "ETH/BTC")
3838
assert exchange.fetch_order.call_count == 2
39-
assert exchange.fetch_order.call_args_list[0][1]["order_id"] == "1234"
39+
assert exchange.fetch_order.call_args_list[0][0][0] == "1234"
4040
assert exchange.fetch_order.call_args_list[1][1]["order_id"] == "222555"
4141

4242

4343
def test_cancel_stoploss_order_gate(default_conf, mocker):
4444
exchange = get_patched_exchange(mocker, default_conf, exchange="gate")
45-
46-
cancel_order_mock = MagicMock()
47-
exchange.cancel_order = cancel_order_mock
45+
cancel_order_mock = mocker.patch.object(exchange, "cancel_order", autospec=True)
4846

4947
exchange.cancel_stoploss_order("1234", "ETH/BTC")
5048
assert cancel_order_mock.call_count == 1
51-
assert cancel_order_mock.call_args_list[0][1]["order_id"] == "1234"
52-
assert cancel_order_mock.call_args_list[0][1]["pair"] == "ETH/BTC"
53-
assert cancel_order_mock.call_args_list[0][1]["params"] == {"stop": True}
49+
assert cancel_order_mock.call_args_list[0][0][0] == "1234"
50+
assert cancel_order_mock.call_args_list[0][0][1] == "ETH/BTC"
51+
assert cancel_order_mock.call_args_list[0][0][2] == {"stop": True}
5452

5553

5654
@pytest.mark.parametrize(

tests/exchange/test_okx.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -661,14 +661,14 @@ def test_stoploss_adjust_okx(mocker, default_conf, sl1, sl2, sl3, side):
661661

662662
def test_stoploss_cancel_okx(mocker, default_conf):
663663
exchange = get_patched_exchange(mocker, default_conf, exchange="okx")
664-
665-
exchange.cancel_order = MagicMock()
664+
co_mock = mocker.patch.object(exchange, "cancel_order", autospec=True)
666665

667666
exchange.cancel_stoploss_order("1234", "ETH/USDT")
668-
assert exchange.cancel_order.call_count == 1
669-
assert exchange.cancel_order.call_args_list[0][1]["order_id"] == "1234"
670-
assert exchange.cancel_order.call_args_list[0][1]["pair"] == "ETH/USDT"
671-
assert exchange.cancel_order.call_args_list[0][1]["params"] == {"stop": True}
667+
assert co_mock.call_count == 1
668+
args, _ = co_mock.call_args
669+
assert args[0] == "1234"
670+
assert args[1] == "ETH/USDT"
671+
assert args[2] == {"stop": True}
672672

673673

674674
def test__get_stop_params_okx(mocker, default_conf):

0 commit comments

Comments
 (0)