• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

freqtrade / freqtrade / 9116662491

16 May 2024 05:25PM UTC coverage: 94.683% (+0.005%) from 94.678%
9116662491

push

github

xmatthias
Bump ccxt min-version

20336 of 21478 relevant lines covered (94.68%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.94
/freqtrade/exchange/bybit.py
1
"""Bybit exchange subclass"""
2

3
import logging
1✔
4
from datetime import datetime, timedelta
1✔
5
from typing import Any, Dict, List, Optional, Tuple
1✔
6

7
import ccxt
1✔
8

9
from freqtrade.constants import BuySell
1✔
10
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
1✔
11
from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalException, TemporaryError
1✔
12
from freqtrade.exchange import Exchange
1✔
13
from freqtrade.exchange.common import retrier
1✔
14
from freqtrade.util.datetime_helpers import dt_now, dt_ts
1✔
15

16

17
logger = logging.getLogger(__name__)
1✔
18

19

20
class Bybit(Exchange):
1✔
21
    """
22
    Bybit exchange class. Contains adjustments needed for Freqtrade to work
23
    with this exchange.
24

25
    Please note that this exchange is not included in the list of exchanges
26
    officially supported by the Freqtrade development team. So some features
27
    may still not work as expected.
28
    """
29

30
    unified_account = False
1✔
31

32
    _ft_has: Dict = {
1✔
33
        "ohlcv_candle_limit": 1000,
34
        "ohlcv_has_history": True,
35
        "order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
36
    }
37
    _ft_has_futures: Dict = {
1✔
38
        "ohlcv_has_history": True,
39
        "mark_ohlcv_timeframe": "4h",
40
        "funding_fee_timeframe": "8h",
41
        "stoploss_on_exchange": True,
42
        "stoploss_order_types": {"limit": "limit", "market": "market"},
43
        # bybit response parsing fails to populate stopLossPrice
44
        "stop_price_prop": "stopPrice",
45
        "stop_price_type_field": "triggerBy",
46
        "stop_price_type_value_mapping": {
47
            PriceType.LAST: "LastPrice",
48
            PriceType.MARK: "MarkPrice",
49
            PriceType.INDEX: "IndexPrice",
50
        },
51
    }
52

53
    _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
1✔
54
        # TradingMode.SPOT always supported and not required in this list
55
        # (TradingMode.FUTURES, MarginMode.CROSS),
56
        (TradingMode.FUTURES, MarginMode.ISOLATED)
57
    ]
58

59
    @property
1✔
60
    def _ccxt_config(self) -> Dict:
1✔
61
        # Parameters to add directly to ccxt sync/async initialization.
62
        # ccxt defaults to swap mode.
63
        config = {}
1✔
64
        if self.trading_mode == TradingMode.SPOT:
1✔
65
            config.update({"options": {"defaultType": "spot"}})
1✔
66
        config.update(super()._ccxt_config)
1✔
67
        return config
1✔
68

69
    def market_is_future(self, market: Dict[str, Any]) -> bool:
1✔
70
        main = super().market_is_future(market)
1✔
71
        # For ByBit, we'll only support USDT markets for now.
72
        return main and market["settle"] == "USDT"
1✔
73

74
    @retrier
1✔
75
    def additional_exchange_init(self) -> None:
1✔
76
        """
77
        Additional exchange initialization logic.
78
        .api will be available at this point.
79
        Must be overridden in child methods if required.
80
        """
81
        try:
1✔
82
            if not self._config["dry_run"]:
1✔
83
                if self.trading_mode == TradingMode.FUTURES:
1✔
84
                    position_mode = self._api.set_position_mode(False)
1✔
85
                    self._log_exchange_response("set_position_mode", position_mode)
1✔
86
                is_unified = self._api.is_unified_enabled()
1✔
87
                # Returns a tuple of bools, first for margin, second for Account
88
                if is_unified and len(is_unified) > 1 and is_unified[1]:
1✔
89
                    self.unified_account = True
1✔
90
                    logger.info("Bybit: Unified account.")
1✔
91
                    raise OperationalException(
1✔
92
                        "Bybit: Unified account is not supported. "
93
                        "Please use a standard (sub)account."
94
                    )
95
                else:
96
                    self.unified_account = False
1✔
97
                    logger.info("Bybit: Standard account.")
1✔
98
        except ccxt.DDoSProtection as e:
1✔
99
            raise DDosProtection(e) from e
1✔
100
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
101
            raise TemporaryError(
1✔
102
                f"Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}"
103
            ) from e
104
        except ccxt.BaseError as e:
1✔
105
            raise OperationalException(e) from e
1✔
106

107
    def ohlcv_candle_limit(
1✔
108
        self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None
109
    ) -> int:
110
        if candle_type in (CandleType.FUNDING_RATE):
1✔
111
            return 200
1✔
112

113
        return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
1✔
114

115
    def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
1✔
116
        if self.trading_mode != TradingMode.SPOT:
1✔
117
            params = {"leverage": leverage}
1✔
118
            self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params)
1✔
119
            self._set_leverage(leverage, pair, accept_fail=True)
1✔
120

121
    def _get_params(
1✔
122
        self,
123
        side: BuySell,
124
        ordertype: str,
125
        leverage: float,
126
        reduceOnly: bool,
127
        time_in_force: str = "GTC",
128
    ) -> Dict:
129
        params = super()._get_params(
1✔
130
            side=side,
131
            ordertype=ordertype,
132
            leverage=leverage,
133
            reduceOnly=reduceOnly,
134
            time_in_force=time_in_force,
135
        )
136
        if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
1✔
137
            params["position_idx"] = 0
1✔
138
        return params
1✔
139

140
    def dry_run_liquidation_price(
1✔
141
        self,
142
        pair: str,
143
        open_rate: float,  # Entry price of position
144
        is_short: bool,
145
        amount: float,
146
        stake_amount: float,
147
        leverage: float,
148
        wallet_balance: float,  # Or margin balance
149
        mm_ex_1: float = 0.0,  # (Binance) Cross only
150
        upnl_ex_1: float = 0.0,  # (Binance) Cross only
151
    ) -> Optional[float]:
152
        """
153
        Important: Must be fetching data from cached values as this is used by backtesting!
154
        PERPETUAL:
155
         bybit:
156
          https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067
157

158
        Long:
159
        Liquidation Price = (
160
            Entry Price * (1 - Initial Margin Rate + Maintenance Margin Rate)
161
            - Extra Margin Added/ Contract)
162
        Short:
163
        Liquidation Price = (
164
            Entry Price * (1 + Initial Margin Rate - Maintenance Margin Rate)
165
            + Extra Margin Added/ Contract)
166

167
        Implementation Note: Extra margin is currently not used.
168

169
        :param pair: Pair to calculate liquidation price for
170
        :param open_rate: Entry price of position
171
        :param is_short: True if the trade is a short, false otherwise
172
        :param amount: Absolute value of position size incl. leverage (in base currency)
173
        :param stake_amount: Stake amount - Collateral in settle currency.
174
        :param leverage: Leverage used for this position.
175
        :param trading_mode: SPOT, MARGIN, FUTURES, etc.
176
        :param margin_mode: Either ISOLATED or CROSS
177
        :param wallet_balance: Amount of margin_mode in the wallet being used to trade
178
            Cross-Margin Mode: crossWalletBalance
179
            Isolated-Margin Mode: isolatedWalletBalance
180
        """
181

182
        market = self.markets[pair]
1✔
183
        mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount)
1✔
184

185
        if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED:
1✔
186
            if market["inverse"]:
1✔
187
                raise OperationalException("Freqtrade does not yet support inverse contracts")
×
188
            initial_margin_rate = 1 / leverage
1✔
189

190
            # See docstring - ignores extra margin!
191
            if is_short:
1✔
192
                return open_rate * (1 + initial_margin_rate - mm_ratio)
1✔
193
            else:
194
                return open_rate * (1 - initial_margin_rate + mm_ratio)
1✔
195

196
        else:
197
            raise OperationalException(
×
198
                "Freqtrade only supports isolated futures for leverage trading"
199
            )
200

201
    def get_funding_fees(
1✔
202
        self, pair: str, amount: float, is_short: bool, open_date: datetime
203
    ) -> float:
204
        """
205
        Fetch funding fees, either from the exchange (live) or calculates them
206
        based on funding rate/mark price history
207
        :param pair: The quote/base pair of the trade
208
        :param is_short: trade direction
209
        :param amount: Trade amount
210
        :param open_date: Open date of the trade
211
        :return: funding fee since open_date
212
        :raises: ExchangeError if something goes wrong.
213
        """
214
        # Bybit does not provide "applied" funding fees per position.
215
        if self.trading_mode == TradingMode.FUTURES:
1✔
216
            try:
1✔
217
                return self._fetch_and_calculate_funding_fees(pair, amount, is_short, open_date)
1✔
218
            except ExchangeError:
1✔
219
                logger.warning(f"Could not update funding fees for {pair}.")
1✔
220
        return 0.0
1✔
221

222
    def fetch_orders(self, pair: str, since: datetime, params: Optional[Dict] = None) -> List[Dict]:
1✔
223
        """
224
        Fetch all orders for a pair "since"
225
        :param pair: Pair for the query
226
        :param since: Starting time for the query
227
        """
228
        # On bybit, the distance between since and "until" can't exceed 7 days.
229
        # we therefore need to split the query into multiple queries.
230
        orders = []
1✔
231

232
        while since < dt_now():
1✔
233
            until = since + timedelta(days=7, minutes=-1)
1✔
234
            orders += super().fetch_orders(pair, since, params={"until": dt_ts(until)})
1✔
235
            since = until
1✔
236

237
        return orders
1✔
238

239
    def fetch_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
1✔
240
        order = super().fetch_order(order_id, pair, params)
1✔
241
        if (
1✔
242
            order.get("status") == "canceled"
243
            and order.get("filled") == 0.0
244
            and order.get("remaining") == 0.0
245
        ):
246
            # Canceled orders will have "remaining=0" on bybit.
247
            order["remaining"] = None
1✔
248
        return order
1✔
249

250
    @retrier
1✔
251
    def get_leverage_tiers(self) -> Dict[str, List[Dict]]:
1✔
252
        """
253
        Cache leverage tiers for 1 day, since they are not expected to change often, and
254
        bybit requires pagination to fetch all tiers.
255
        """
256

257
        # Load cached tiers
258
        tiers_cached = self.load_cached_leverage_tiers(
1✔
259
            self._config["stake_currency"], timedelta(days=1)
260
        )
261
        if tiers_cached:
1✔
262
            return tiers_cached
×
263

264
        # Fetch tiers from exchange
265
        tiers = super().get_leverage_tiers()
1✔
266

267
        self.cache_leverage_tiers(tiers, self._config["stake_currency"])
1✔
268
        return tiers
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc