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

freqtrade / freqtrade / 9394559170

26 Apr 2024 06:36AM UTC coverage: 94.656% (-0.02%) from 94.674%
9394559170

push

github

xmatthias
Loader should be passed as kwarg for clarity

20280 of 21425 relevant lines covered (94.66%)

0.95 hits per line

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

97.78
/freqtrade/exchange/bybit.py
1
""" Bybit exchange subclass """
2
import logging
1✔
3
from datetime import datetime, timedelta
1✔
4
from typing import Any, Dict, List, Optional, Tuple
1✔
5

6
import ccxt
1✔
7

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

15

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

18

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

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

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

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

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

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

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

109
    def ohlcv_candle_limit(
1✔
110
            self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
111

112
        if candle_type in (CandleType.FUNDING_RATE):
1✔
113
            return 200
1✔
114

115
        return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
1✔
116

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

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

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

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

169
        Implementation Note: Extra margin is currently not used.
170

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

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

187
        if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED:
1✔
188

189
            if market['inverse']:
1✔
190
                raise OperationalException(
×
191
                    "Freqtrade does not yet support inverse contracts")
192
            initial_margin_rate = 1 / leverage
1✔
193

194
            # See docstring - ignores extra margin!
195
            if is_short:
1✔
196
                return open_rate * (1 + initial_margin_rate - mm_ratio)
1✔
197
            else:
198
                return open_rate * (1 - initial_margin_rate + mm_ratio)
1✔
199

200
        else:
201
            raise OperationalException(
×
202
                "Freqtrade only supports isolated futures for leverage trading")
203

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

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

235
        while since < dt_now():
1✔
236
            until = since + timedelta(days=7, minutes=-1)
1✔
237
            orders += super().fetch_orders(pair, since, params={'until': dt_ts(until)})
1✔
238
            since = until
1✔
239

240
        return orders
1✔
241

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