|
3 | 3 |
|
4 | 4 | import ccxt |
5 | 5 |
|
6 | | -from freqtrade.enums import CandleType |
| 6 | +from freqtrade.constants import BuySell |
| 7 | +from freqtrade.enums import CandleType, MarginMode, TradingMode |
7 | 8 | from freqtrade.exceptions import ( |
8 | 9 | DDosProtection, |
9 | 10 | OperationalException, |
|
20 | 21 |
|
21 | 22 |
|
22 | 23 | class Bitget(Exchange): |
23 | | - """ |
24 | | - Bitget exchange class. Contains adjustments needed for Freqtrade to work |
25 | | - with this exchange. |
26 | | -
|
27 | | - Please note that this exchange is not included in the list of exchanges |
28 | | - officially supported by the Freqtrade development team. So some features |
29 | | - may still not work as expected. |
| 24 | + """Bitget exchange class. |
| 25 | + Contains adjustments needed for Freqtrade to work with this exchange. |
30 | 26 | """ |
31 | 27 |
|
32 | 28 | _ft_has: FtHas = { |
33 | 29 | "stoploss_on_exchange": True, |
34 | 30 | "stop_price_param": "stopPrice", |
35 | 31 | "stop_price_prop": "stopPrice", |
| 32 | + "stoploss_blocks_assets": False, # Stoploss orders do not block assets |
36 | 33 | "stoploss_order_types": {"limit": "limit", "market": "market"}, |
37 | 34 | "ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones. |
38 | 35 | "order_time_in_force": ["GTC", "FOK", "IOC", "PO"], |
39 | 36 | } |
40 | 37 | _ft_has_futures: FtHas = { |
41 | 38 | "mark_ohlcv_timeframe": "4h", |
| 39 | + "funding_fee_candle_limit": 100, |
42 | 40 | } |
43 | 41 |
|
| 42 | + _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [ |
| 43 | + (TradingMode.SPOT, MarginMode.NONE), |
| 44 | + (TradingMode.FUTURES, MarginMode.ISOLATED), |
| 45 | + # (TradingMode.FUTURES, MarginMode.CROSS), |
| 46 | + ] |
| 47 | + |
44 | 48 | def ohlcv_candle_limit( |
45 | 49 | self, timeframe: str, candle_type: CandleType, since_ms: int | None = None |
46 | 50 | ) -> int: |
@@ -126,3 +130,109 @@ def fetch_stoploss_order( |
126 | 130 |
|
127 | 131 | def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict: |
128 | 132 | return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True}) |
| 133 | + |
| 134 | + @retrier |
| 135 | + def additional_exchange_init(self) -> None: |
| 136 | + """ |
| 137 | + Additional exchange initialization logic. |
| 138 | + .api will be available at this point. |
| 139 | + Must be overridden in child methods if required. |
| 140 | + """ |
| 141 | + try: |
| 142 | + if not self._config["dry_run"]: |
| 143 | + if self.trading_mode == TradingMode.FUTURES: |
| 144 | + position_mode = self._api.set_position_mode(False) |
| 145 | + self._log_exchange_response("set_position_mode", position_mode) |
| 146 | + except ccxt.DDoSProtection as e: |
| 147 | + raise DDosProtection(e) from e |
| 148 | + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: |
| 149 | + raise TemporaryError( |
| 150 | + f"Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}" |
| 151 | + ) from e |
| 152 | + except ccxt.BaseError as e: |
| 153 | + raise OperationalException(e) from e |
| 154 | + |
| 155 | + def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False): |
| 156 | + if self.trading_mode != TradingMode.SPOT: |
| 157 | + # Explicitly setting margin_mode is not necessary as marginMode can be set per order. |
| 158 | + # self.set_margin_mode(pair, self.margin_mode, accept_fail) |
| 159 | + self._set_leverage(leverage, pair, accept_fail) |
| 160 | + |
| 161 | + def _get_params( |
| 162 | + self, |
| 163 | + side: BuySell, |
| 164 | + ordertype: str, |
| 165 | + leverage: float, |
| 166 | + reduceOnly: bool, |
| 167 | + time_in_force: str = "GTC", |
| 168 | + ) -> dict: |
| 169 | + params = super()._get_params( |
| 170 | + side=side, |
| 171 | + ordertype=ordertype, |
| 172 | + leverage=leverage, |
| 173 | + reduceOnly=reduceOnly, |
| 174 | + time_in_force=time_in_force, |
| 175 | + ) |
| 176 | + if self.trading_mode == TradingMode.FUTURES and self.margin_mode: |
| 177 | + params["marginMode"] = self.margin_mode.value.lower() |
| 178 | + return params |
| 179 | + |
| 180 | + def dry_run_liquidation_price( |
| 181 | + self, |
| 182 | + pair: str, |
| 183 | + open_rate: float, |
| 184 | + is_short: bool, |
| 185 | + amount: float, |
| 186 | + stake_amount: float, |
| 187 | + leverage: float, |
| 188 | + wallet_balance: float, |
| 189 | + open_trades: list, |
| 190 | + ) -> float | None: |
| 191 | + """ |
| 192 | + Important: Must be fetching data from cached values as this is used by backtesting! |
| 193 | +
|
| 194 | +
|
| 195 | + https://www.bitget.com/support/articles/12560603808759 |
| 196 | + MMR: Maintenance margin rate of the trading pair. |
| 197 | +
|
| 198 | + CoinMainIndexPrice: The index price for Coin-M futures. For USDT-M futures, |
| 199 | + the index price is: 1. |
| 200 | +
|
| 201 | + TakerFeeRatio: The fee rate applied when placing taker orders. |
| 202 | +
|
| 203 | + Position direction: The current position direction of the trading pair. |
| 204 | + 1 indicates a long position, and -1 indicates a short position. |
| 205 | +
|
| 206 | + Formula: |
| 207 | +
|
| 208 | + Estimated liquidation price = [ |
| 209 | + position margin - position size x average entry price x position direction |
| 210 | + ] ÷ [position size x (MMR + TakerFeeRatio - position direction)] |
| 211 | +
|
| 212 | + :param pair: Pair to calculate liquidation price for |
| 213 | + :param open_rate: Entry price of position |
| 214 | + :param is_short: True if the trade is a short, false otherwise |
| 215 | + :param amount: Absolute value of position size incl. leverage (in base currency) |
| 216 | + :param stake_amount: Stake amount - Collateral in settle currency. |
| 217 | + :param leverage: Leverage used for this position. |
| 218 | + :param wallet_balance: Amount of margin_mode in the wallet being used to trade |
| 219 | + Cross-Margin Mode: crossWalletBalance |
| 220 | + Isolated-Margin Mode: isolatedWalletBalance |
| 221 | + :param open_trades: List of other open trades in the same wallet |
| 222 | + """ |
| 223 | + market = self.markets[pair] |
| 224 | + taker_fee_rate = market["taker"] or self._api.describe().get("fees", {}).get( |
| 225 | + "trading", {} |
| 226 | + ).get("taker", 0.001) |
| 227 | + mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount) |
| 228 | + |
| 229 | + if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED: |
| 230 | + position_direction = -1 if is_short else 1 |
| 231 | + |
| 232 | + return (wallet_balance - (amount * open_rate * position_direction)) / ( |
| 233 | + amount * (mm_ratio + taker_fee_rate - position_direction) |
| 234 | + ) |
| 235 | + else: |
| 236 | + raise OperationalException( |
| 237 | + "Freqtrade currently only supports isolated futures for bitget" |
| 238 | + ) |
0 commit comments