diff --git a/README.md b/README.md index 80ccd871a..fcf28efe4 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,9 @@ hesitate to read the source code and understand the mechanism of this bot. Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange. - [X] [Binance](https://www.binance.com/) -- [X] [Bitmart](https://bitmart.com/) - [X] [BingX](https://bingx.com/invite/0EM9RX) +- [X] [Bitget](https://www.bitget.com/) +- [X] [Bitmart](https://bitmart.com/) - [X] [Bybit](https://bybit.com/) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [HTX](https://www.htx.com/) @@ -41,6 +42,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even ### Supported Futures Exchanges (experimental) - [X] [Binance](https://www.binance.com/) +- [X] [Bitget](https://www.bitget.com/) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX) - [X] [OKX](https://okx.com/) diff --git a/docs/exchanges.md b/docs/exchanges.md index 859a6cbaf..61d0628ec 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -298,7 +298,14 @@ Without these permissions, the bot will not start correctly and show errors like Bybit supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "FOK" (full-or-cancel), "IOC" (immediate-or-cancel) and "PO" (Post only) settings. -Futures trading on bybit is currently supported for isolated futures mode. +!!! Warning "Unified accounts" + Freqtrade assumes accounts to be dedicated to the bot. + We therefore recommend the usage of one subaccount per bot. This is especially important when using unified accounts. + Other configurations (multiple bots on one account, manual non-bot trades on the bot account) are not supported and may lead to unexpected behavior. + +### Bybit Futures + +Futures trading on bybit is supported for isolated futures mode. On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that manual changes to this setting may result in exceptions and errors. @@ -312,10 +319,6 @@ API Keys for live futures trading must have the following permissions: We do strongly recommend to limit all API keys to the IP you're going to use it from. -!!! Warning "Unified accounts" - Freqtrade assumes accounts to be dedicated to the bot. - We therefore recommend the usage of one subaccount per bot. This is especially important when using unified accounts. - Other configurations (multiple bots on one account, manual non-bot trades on the bot account) are not supported and may lead to unexpected behavior. ## Bitmart @@ -355,6 +358,12 @@ Bitget supports [time_in_force](configuration.md#understand-order_time_in_force) 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. +### Bitget Futures + +Futures trading on bitget is supported for isolated futures mode. + +On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that manual changes to this setting may result in exceptions and errors. + ## Hyperliquid !!! Tip "Stoploss on Exchange" @@ -478,3 +487,5 @@ For example, to test the order type `FOK` with Kraken, and modify candle limit t !!! Warning Please make sure to fully understand the impacts of these settings before modifying them. + Using `_ft_has_params` overrides may lead to unexpected behavior, and may even break your bot. + We will not be able to provide support for issues caused by custom settings in `_ft_has_params`. diff --git a/docs/includes/exchange-features.md b/docs/includes/exchange-features.md index 7aa6e6436..6833818b7 100644 --- a/docs/includes/exchange-features.md +++ b/docs/includes/exchange-features.md @@ -5,6 +5,8 @@ | [Binance](exchanges.md#binance) | futures | isolated, cross | market, limit | | [Bingx](exchanges.md#bingx) | spot | | market, limit | | [Bitmart](exchanges.md#bitmart) | spot | | ❌ (not supported) | +| [Bitget](exchanges.md#bitget) | spot | | market, limit | +| [Bitget](exchanges.md#bitget) | futures | isolated | market, limit | | [Bybit](exchanges.md#bybit) | spot | | ❌ (not supported) | | [Bybit](exchanges.md#bybit) | futures | isolated | market, limit | | [Gate.io](exchanges.md#gateio) | spot | | limit | diff --git a/docs/index.md b/docs/index.md index 7276432da..a97e43365 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,6 +39,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual, - [X] [Binance](https://www.binance.com/) - [X] [BingX](https://bingx.com/invite/0EM9RX) +- [X] [Bitget](https://www.bitget.com/) - [X] [Bitmart](https://bitmart.com/) - [X] [Bybit](https://bybit.com/) - [X] [Gate.io](https://www.gate.io/ref/6266643) @@ -52,6 +53,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual, ### Supported Futures Exchanges (experimental) - [X] [Binance](https://www.binance.com/) +- [X] [Bitget](https://www.bitget.com/) - [X] [Bybit](https://bybit.com/) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX) diff --git a/freqtrade/exchange/bitget.py b/freqtrade/exchange/bitget.py index b6b347c34..a1d6db255 100644 --- a/freqtrade/exchange/bitget.py +++ b/freqtrade/exchange/bitget.py @@ -3,7 +3,8 @@ from datetime import timedelta import ccxt -from freqtrade.enums import CandleType +from freqtrade.constants import BuySell +from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.exceptions import ( DDosProtection, OperationalException, @@ -20,27 +21,30 @@ logger = logging.getLogger(__name__) class Bitget(Exchange): - """ - Bitget exchange class. Contains adjustments needed for Freqtrade to work - with this exchange. - - Please note that this exchange is not included in the list of exchanges - officially supported by the Freqtrade development team. So some features - may still not work as expected. + """Bitget exchange class. + Contains adjustments needed for Freqtrade to work with this exchange. """ _ft_has: FtHas = { "stoploss_on_exchange": True, "stop_price_param": "stopPrice", "stop_price_prop": "stopPrice", + "stoploss_blocks_assets": False, # Stoploss orders do not block assets "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"], } _ft_has_futures: FtHas = { "mark_ohlcv_timeframe": "4h", + "funding_fee_candle_limit": 100, } + _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [ + (TradingMode.SPOT, MarginMode.NONE), + (TradingMode.FUTURES, MarginMode.ISOLATED), + # (TradingMode.FUTURES, MarginMode.CROSS), + ] + def ohlcv_candle_limit( self, timeframe: str, candle_type: CandleType, since_ms: int | None = None ) -> int: @@ -126,3 +130,109 @@ class Bitget(Exchange): 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: + """ + Additional exchange initialization logic. + .api will be available at this point. + Must be overridden in child methods if required. + """ + try: + if not self._config["dry_run"]: + if self.trading_mode == TradingMode.FUTURES: + position_mode = self._api.set_position_mode(False) + self._log_exchange_response("set_position_mode", position_mode) + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: + raise TemporaryError( + f"Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}" + ) from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False): + if self.trading_mode != TradingMode.SPOT: + # Explicitly setting margin_mode is not necessary as marginMode can be set per order. + # self.set_margin_mode(pair, self.margin_mode, accept_fail) + self._set_leverage(leverage, pair, accept_fail) + + def _get_params( + self, + side: BuySell, + ordertype: str, + leverage: float, + reduceOnly: bool, + time_in_force: str = "GTC", + ) -> dict: + params = super()._get_params( + side=side, + ordertype=ordertype, + leverage=leverage, + reduceOnly=reduceOnly, + time_in_force=time_in_force, + ) + if self.trading_mode == TradingMode.FUTURES and self.margin_mode: + params["marginMode"] = self.margin_mode.value.lower() + return params + + def dry_run_liquidation_price( + self, + pair: str, + open_rate: float, + is_short: bool, + amount: float, + stake_amount: float, + leverage: float, + wallet_balance: float, + open_trades: list, + ) -> float | None: + """ + Important: Must be fetching data from cached values as this is used by backtesting! + + + https://www.bitget.com/support/articles/12560603808759 + MMR: Maintenance margin rate of the trading pair. + + CoinMainIndexPrice: The index price for Coin-M futures. For USDT-M futures, + the index price is: 1. + + TakerFeeRatio: The fee rate applied when placing taker orders. + + Position direction: The current position direction of the trading pair. + 1 indicates a long position, and -1 indicates a short position. + + Formula: + + Estimated liquidation price = [ + position margin - position size x average entry price x position direction + ] ÷ [position size x (MMR + TakerFeeRatio - position direction)] + + :param pair: Pair to calculate liquidation price for + :param open_rate: Entry price of position + :param is_short: True if the trade is a short, false otherwise + :param amount: Absolute value of position size incl. leverage (in base currency) + :param stake_amount: Stake amount - Collateral in settle currency. + :param leverage: Leverage used for this position. + :param wallet_balance: Amount of margin_mode in the wallet being used to trade + Cross-Margin Mode: crossWalletBalance + Isolated-Margin Mode: isolatedWalletBalance + :param open_trades: List of other open trades in the same wallet + """ + market = self.markets[pair] + taker_fee_rate = market["taker"] or self._api.describe().get("fees", {}).get( + "trading", {} + ).get("taker", 0.001) + mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount) + + if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED: + position_direction = -1 if is_short else 1 + + return (wallet_balance - (amount * open_rate * position_direction)) / ( + amount * (mm_ratio + taker_fee_rate - position_direction) + ) + else: + raise OperationalException( + "Freqtrade currently only supports isolated futures for bitget" + ) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 3d08e5e8c..650db19b7 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -56,6 +56,7 @@ SUPPORTED_EXCHANGES = [ "binance", "bingx", "bitmart", + "bitget", "bybit", "gate", "htx", diff --git a/tests/exchange/test_bitget.py b/tests/exchange/test_bitget.py index fdfaf7a7e..ca32ba536 100644 --- a/tests/exchange/test_bitget.py +++ b/tests/exchange/test_bitget.py @@ -1,10 +1,10 @@ from datetime import timedelta -from unittest.mock import MagicMock +from unittest.mock import MagicMock, PropertyMock import pytest -from freqtrade.enums import CandleType -from freqtrade.exceptions import RetryableOrderError +from freqtrade.enums import CandleType, MarginMode, TradingMode +from freqtrade.exceptions import OperationalException, 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 @@ -120,3 +120,76 @@ def test_bitget_ohlcv_candle_limit(mocker, default_conf_usdt): 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 + + +def test_additional_exchange_init_bitget(default_conf, mocker): + default_conf["dry_run"] = False + default_conf["trading_mode"] = TradingMode.FUTURES + default_conf["margin_mode"] = MarginMode.ISOLATED + api_mock = MagicMock() + api_mock.set_position_mode = MagicMock(return_value={}) + + get_patched_exchange(mocker, default_conf, exchange="bitget", api_mock=api_mock) + assert api_mock.set_position_mode.call_count == 1 + + ccxt_exceptionhandlers( + mocker, default_conf, api_mock, "bitget", "additional_exchange_init", "set_position_mode" + ) + + +def test_dry_run_liquidation_price_cross_bitget(default_conf, mocker): + default_conf["dry_run"] = True + default_conf["trading_mode"] = TradingMode.FUTURES + default_conf["margin_mode"] = MarginMode.CROSS + api_mock = MagicMock() + mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", MagicMock(return_value=(0.005, 0.0))) + exchange = get_patched_exchange(mocker, default_conf, exchange="bitget", api_mock=api_mock) + + with pytest.raises( + OperationalException, match="Freqtrade currently only supports isolated futures for bitget" + ): + exchange.dry_run_liquidation_price( + "ETH/USDT:USDT", + 100_000, + False, + 0.1, + 100, + 10, + 100, + [], + ) + + +def test__lev_prep_bitget(default_conf, mocker): + api_mock = MagicMock() + api_mock.set_margin_mode = MagicMock() + api_mock.set_leverage = MagicMock() + type(api_mock).has = PropertyMock(return_value={"setMarginMode": True, "setLeverage": True}) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="bitget") + exchange._lev_prep("BTC/USDC:USDC", 3.2, "buy") + + assert api_mock.set_margin_mode.call_count == 0 + assert api_mock.set_leverage.call_count == 0 + + # test in futures mode + api_mock.set_margin_mode.reset_mock() + api_mock.set_leverage.reset_mock() + default_conf["dry_run"] = False + + default_conf["trading_mode"] = "futures" + default_conf["margin_mode"] = "isolated" + + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="bitget") + exchange._lev_prep("BTC/USDC:USDC", 3.2, "buy") + + assert api_mock.set_margin_mode.call_count == 0 + assert api_mock.set_leverage.call_count == 1 + api_mock.set_leverage.assert_called_with(symbol="BTC/USDC:USDC", leverage=3.2) + + api_mock.reset_mock() + + exchange._lev_prep("BTC/USDC:USDC", 19.99, "sell") + + assert api_mock.set_margin_mode.call_count == 0 + assert api_mock.set_leverage.call_count == 1 + api_mock.set_leverage.assert_called_with(symbol="BTC/USDC:USDC", leverage=19.99) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4315f95c3..e5fc6f574 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -5942,29 +5942,32 @@ def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers): assert exchange.get_max_leverage("TIA/USDT:USDT", 130.008) == 40 -@pytest.mark.parametrize("exchange_name", ["binance", "kraken", "gate", "okx", "bybit"]) -def test__get_params(mocker, default_conf, exchange_name): +@pytest.mark.parametrize( + "exchange_name, add_params_spot, add_params_futures", + [ + ("binance", {}, {}), + ("kraken", {}, {"leverage": 3.0}), + ("gate", {}, {}), + ("okx", {}, {"tdMode": "isolated", "posSide": "net"}), + ("bybit", {}, {"position_idx": 0}), + ("bitget", {}, {"marginMode": "isolated"}), + ], +) +def test__get_params(mocker, default_conf, exchange_name, add_params_spot, add_params_futures): api_mock = MagicMock() mocker.patch(f"{EXMS}.exchange_has", return_value=True) exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange._params = {"test": True} params1 = {"test": True} - params2 = { + params1.update(add_params_spot) + + params_fut = { "test": True, "timeInForce": "IOC", "reduceOnly": True, } - - if exchange_name == "kraken": - params2["leverage"] = 3.0 - - if exchange_name == "okx": - params2["tdMode"] = "isolated" - params2["posSide"] = "net" - - if exchange_name == "bybit": - params2["position_idx"] = 0 + params_fut.update(add_params_futures) assert ( exchange._get_params( @@ -6012,7 +6015,7 @@ def test__get_params(mocker, default_conf, exchange_name): time_in_force="IOC", leverage=3.0, ) - == params2 + == params_fut ) diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py index 0b5cccc8e..94ff4ac47 100644 --- a/tests/exchange_online/conftest.py +++ b/tests/exchange_online/conftest.py @@ -422,6 +422,10 @@ EXCHANGES = { "hasQuoteVolume": True, "timeframe": "1h", "candle_count": 1000, + "futures": True, + "futures_pair": "BTC/USDT:USDT", + "leverage_tiers_public": True, + "leverage_in_spot_market": True, }, "coinex": { "pair": "BTC/USDT",