Merge pull request #12098 from freqtrade/feat/bitget_stoploss

add bitget stoploss  support
This commit is contained in:
Matthias
2025-08-12 06:57:36 +02:00
committed by GitHub
5 changed files with 209 additions and 6 deletions

View File

@@ -344,6 +344,10 @@ Bitget requires a passphrase for each api key, you will therefore need to add th
Bitget supports [time_in_force](configuration.md#understand-order_time_in_force).
!!! Tip "Stoploss on Exchange"
Bitget supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used.
## Hyperliquid
!!! Tip "Stoploss on Exchange"

View File

@@ -31,6 +31,7 @@ The Order-type will be ignored if only one mode is available.
| Binance | limit |
| Binance Futures | market, limit |
| Bingx | market, limit |
| Bitget | market, limit |
| HTX | limit |
| kraken | market, limit |
| Gate | limit |

View File

@@ -1,9 +1,18 @@
import logging
from datetime import timedelta
import ccxt
from freqtrade.enums import CandleType
from freqtrade.exceptions import (
DDosProtection,
OperationalException,
RetryableOrderError,
TemporaryError,
)
from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange_types import FtHas
from freqtrade.exchange.common import API_RETRY_COUNT, retrier
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
from freqtrade.util.datetime_helpers import dt_now, dt_ts
@@ -21,6 +30,10 @@ class Bitget(Exchange):
"""
_ft_has: FtHas = {
"stoploss_on_exchange": True,
"stop_price_param": "stopPrice",
"stop_price_prop": "stopPrice",
"stoploss_order_types": {"limit": "limit", "market": "market"},
"ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones.
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
}
@@ -44,9 +57,72 @@ class Bitget(Exchange):
timeframe_map = self._api.options["fetchOHLCV"]["maxRecentDaysPerTimeframe"]
days = timeframe_map.get(timeframe, 30)
if candle_type in (CandleType.FUTURES, CandleType.SPOT) and (
if candle_type in (CandleType.FUTURES, CandleType.SPOT, CandleType.MARK) and (
not since_ms or dt_ts(dt_now() - timedelta(days=days)) < since_ms
):
return 1000
return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
def _convert_stop_order(self, pair: str, order_id: str, order: CcxtOrder) -> CcxtOrder:
if order.get("status", "open") == "closed":
# Use orderID as cliendOrderId filter to fetch the regular followup order.
# Could be done with "fetch_order" - but clientOid as filter doesn't seem to work
# https://www.bitget.com/api-doc/spot/trade/Get-Order-Info
for method in (
self._api.fetch_canceled_and_closed_orders,
self._api.fetch_open_orders,
):
orders = method(pair)
orders_f = [order for order in orders if order["clientOrderId"] == order_id]
if orders_f:
order_reg = orders_f[0]
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 = self._order_contracts_to_amount(order)
order["type"] = "stoploss"
return order
def _fetch_stop_order_fallback(self, order_id: str, pair: str) -> CcxtOrder:
params2 = {
"stop": True,
}
for method in (
self._api.fetch_open_orders,
self._api.fetch_canceled_and_closed_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]
self._log_exchange_response("get_stop_order_fallback", order)
return self._convert_stop_order(pair, order_id, order)
except (ccxt.OrderNotFound, ccxt.InvalidOrder):
pass
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError(
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
) from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).")
@retrier(retries=API_RETRY_COUNT)
def fetch_stoploss_order(
self, order_id: str, pair: str, params: dict | None = None
) -> CcxtOrder:
if self._config["dry_run"]:
return self.fetch_dry_run_order(order_id)
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})

View File

@@ -0,0 +1,122 @@
from datetime import timedelta
from unittest.mock import MagicMock
import pytest
from freqtrade.enums import CandleType
from freqtrade.exceptions import RetryableOrderError
from freqtrade.exchange.common import API_RETRY_COUNT
from freqtrade.util import dt_now, dt_ts
from tests.conftest import EXMS, get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers
@pytest.mark.usefixtures("init_persistence")
def test_fetch_stoploss_order_bitget(default_conf, mocker):
default_conf["dry_run"] = False
mocker.patch("freqtrade.exchange.common.time.sleep")
api_mock = MagicMock()
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="bitget")
api_mock.fetch_open_orders = MagicMock(return_value=[])
api_mock.fetch_canceled_and_closed_orders = MagicMock(return_value=[])
with pytest.raises(RetryableOrderError):
exchange.fetch_stoploss_order("1234", "ETH/BTC")
assert api_mock.fetch_open_orders.call_count == API_RETRY_COUNT + 1
assert api_mock.fetch_canceled_and_closed_orders.call_count == API_RETRY_COUNT + 1
api_mock.fetch_open_orders.reset_mock()
api_mock.fetch_canceled_and_closed_orders.reset_mock()
api_mock.fetch_canceled_and_closed_orders = MagicMock(
return_value=[{"id": "1234", "status": "closed", "clientOrderId": "123455"}]
)
api_mock.fetch_open_orders = MagicMock(return_value=[{"id": "50110", "clientOrderId": "1234"}])
resp = exchange.fetch_stoploss_order("1234", "ETH/BTC")
assert api_mock.fetch_open_orders.call_count == 2
assert api_mock.fetch_canceled_and_closed_orders.call_count == 2
assert resp["id"] == "1234"
assert resp["id_stop"] == "50110"
assert resp["type"] == "stoploss"
default_conf["dry_run"] = True
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="bitget")
dro_mock = mocker.patch(f"{EXMS}.fetch_dry_run_order", MagicMock(return_value={"id": "123455"}))
api_mock.fetch_open_orders.reset_mock()
api_mock.fetch_canceled_and_closed_orders.reset_mock()
resp = exchange.fetch_stoploss_order("1234", "ETH/BTC")
assert api_mock.fetch_open_orders.call_count == 0
assert api_mock.fetch_canceled_and_closed_orders.call_count == 0
assert dro_mock.call_count == 1
def test_fetch_stoploss_order_bitget_exceptions(default_conf_usdt, mocker):
default_conf_usdt["dry_run"] = False
api_mock = MagicMock()
# Test emulation of the stoploss getters
api_mock.fetch_canceled_and_closed_orders = MagicMock(return_value=[])
ccxt_exceptionhandlers(
mocker,
default_conf_usdt,
api_mock,
"bitget",
"fetch_stoploss_order",
"fetch_open_orders",
retries=API_RETRY_COUNT + 1,
order_id="12345",
pair="ETH/USDT",
)
def test_bitget_ohlcv_candle_limit(mocker, default_conf_usdt):
# This test is also a live test - so we're sure our limits are correct.
api_mock = MagicMock()
api_mock.options = {
"fetchOHLCV": {
"maxRecentDaysPerTimeframe": {
"1m": 30,
"5m": 30,
"15m": 30,
"30m": 30,
"1h": 60,
"4h": 60,
"1d": 60,
}
}
}
exch = get_patched_exchange(mocker, default_conf_usdt, api_mock, exchange="bitget")
timeframes = ("1m", "5m", "1h")
for timeframe in timeframes:
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT) == 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES) == 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK) == 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE) == 200
start_time = dt_ts(dt_now() - timedelta(days=17))
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 200
start_time = dt_ts(dt_now() - timedelta(days=48))
length = 200 if timeframe in ("1m", "5m") else 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == length
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == length
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == length
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 200
start_time = dt_ts(dt_now() - timedelta(days=61))
length = 200
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == length
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == length
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == length
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 200

View File

@@ -541,24 +541,24 @@ class TestCCXTExchange:
for timeframe in timeframes:
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT) == 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES) == 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK) == 200
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK) == 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE) == 200
start_time = dt_ts(dt_now() - timedelta(days=17))
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == 200
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 200
start_time = dt_ts(dt_now() - timedelta(days=48))
length = 200 if timeframe in ("1m", "5m") else 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == length
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == length
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == 200
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == length
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 200
start_time = dt_ts(dt_now() - timedelta(days=61))
length = 200
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == length
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == length
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == 200
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == length
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 200