mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-01-26 17:00:23 +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).
|
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
|
## Hyperliquid
|
||||||
|
|
||||||
!!! Tip "Stoploss on Exchange"
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ The Order-type will be ignored if only one mode is available.
|
|||||||
| Binance | limit |
|
| Binance | limit |
|
||||||
| Binance Futures | market, limit |
|
| Binance Futures | market, limit |
|
||||||
| Bingx | market, limit |
|
| Bingx | market, limit |
|
||||||
|
| Bitget | market, limit |
|
||||||
| HTX | limit |
|
| HTX | limit |
|
||||||
| kraken | market, limit |
|
| kraken | market, limit |
|
||||||
| Gate | limit |
|
| Gate | limit |
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
|
from freqtrade.exceptions import (
|
||||||
|
DDosProtection,
|
||||||
|
OperationalException,
|
||||||
|
RetryableOrderError,
|
||||||
|
TemporaryError,
|
||||||
|
)
|
||||||
from freqtrade.exchange import Exchange
|
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
|
from freqtrade.util.datetime_helpers import dt_now, dt_ts
|
||||||
|
|
||||||
|
|
||||||
@@ -21,6 +30,10 @@ class Bitget(Exchange):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_ft_has: FtHas = {
|
_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.
|
"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"],
|
||||||
}
|
}
|
||||||
@@ -44,9 +57,72 @@ class Bitget(Exchange):
|
|||||||
timeframe_map = self._api.options["fetchOHLCV"]["maxRecentDaysPerTimeframe"]
|
timeframe_map = self._api.options["fetchOHLCV"]["maxRecentDaysPerTimeframe"]
|
||||||
days = timeframe_map.get(timeframe, 30)
|
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
|
not since_ms or dt_ts(dt_now() - timedelta(days=days)) < since_ms
|
||||||
):
|
):
|
||||||
return 1000
|
return 1000
|
||||||
|
|
||||||
return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
|
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:
|
for timeframe in timeframes:
|
||||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT) == 1000
|
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.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
|
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE) == 200
|
||||||
|
|
||||||
start_time = dt_ts(dt_now() - timedelta(days=17))
|
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.SPOT, start_time) == 1000
|
||||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES, 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
|
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 200
|
||||||
start_time = dt_ts(dt_now() - timedelta(days=48))
|
start_time = dt_ts(dt_now() - timedelta(days=48))
|
||||||
length = 200 if timeframe in ("1m", "5m") else 1000
|
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.SPOT, start_time) == length
|
||||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES, 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
|
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 200
|
||||||
|
|
||||||
start_time = dt_ts(dt_now() - timedelta(days=61))
|
start_time = dt_ts(dt_now() - timedelta(days=61))
|
||||||
length = 200
|
length = 200
|
||||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == length
|
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.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
|
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 200
|
||||||
|
|||||||
Reference in New Issue
Block a user