mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-12-02 01:53:05 +00:00
Merge pull request #12098 from freqtrade/feat/bitget_stoploss
add bitget stoploss support
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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})
|
||||
|
||||
122
tests/exchange/test_bitget.py
Normal file
122
tests/exchange/test_bitget.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user