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

freqtrade / freqtrade / 6181253459

08 Sep 2023 06:04AM UTC coverage: 94.614% (+0.06%) from 94.556%
6181253459

push

github-actions

web-flow
Merge pull request #9159 from stash86/fix-adjust

remove old codes when we only can do partial entries

2 of 2 new or added lines in 1 file covered. (100.0%)

19114 of 20202 relevant lines covered (94.61%)

0.95 hits per line

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

97.47
/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 MarginMode, PriceType, TradingMode
1✔
10
from freqtrade.enums.candletype import CandleType
1✔
11
from freqtrade.exceptions import DDosProtection, 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
    _ft_has: Dict = {
1✔
31
        "ohlcv_candle_limit": 1000,
32
        "ohlcv_has_history": True,
33
    }
34
    _ft_has_futures: Dict = {
1✔
35
        "ohlcv_has_history": True,
36
        "mark_ohlcv_timeframe": "4h",
37
        "funding_fee_timeframe": "8h",
38
        "stoploss_on_exchange": True,
39
        "stoploss_order_types": {"limit": "limit", "market": "market"},
40
        "stop_price_type_field": "triggerBy",
41
        "stop_price_type_value_mapping": {
42
            PriceType.LAST: "LastPrice",
43
            PriceType.MARK: "MarkPrice",
44
            PriceType.INDEX: "IndexPrice",
45
        },
46
    }
47

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

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

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

75
    @retrier
1✔
76
    def additional_exchange_init(self) -> None:
1✔
77
        """
78
        Additional exchange initialization logic.
79
        .api will be available at this point.
80
        Must be overridden in child methods if required.
81
        """
82
        try:
1✔
83
            if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']:
1✔
84
                position_mode = self._api.set_position_mode(False)
1✔
85
                self._log_exchange_response('set_position_mode', position_mode)
1✔
86
        except ccxt.DDoSProtection as e:
1✔
87
            raise DDosProtection(e) from e
1✔
88
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
89
            raise TemporaryError(
1✔
90
                f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}'
91
                ) from e
92
        except ccxt.BaseError as e:
1✔
93
            raise OperationalException(e) from e
1✔
94

95
    def ohlcv_candle_limit(
1✔
96
            self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
97

98
        if candle_type in (CandleType.FUNDING_RATE):
1✔
99
            return 200
1✔
100

101
        return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
1✔
102

103
    def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
1✔
104
        if self.trading_mode != TradingMode.SPOT:
1✔
105
            params = {'leverage': leverage}
1✔
106
            self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params)
1✔
107
            self._set_leverage(leverage, pair, accept_fail=True)
1✔
108

109
    def _get_params(
1✔
110
        self,
111
        side: BuySell,
112
        ordertype: str,
113
        leverage: float,
114
        reduceOnly: bool,
115
        time_in_force: str = 'GTC',
116
    ) -> Dict:
117
        params = super()._get_params(
1✔
118
            side=side,
119
            ordertype=ordertype,
120
            leverage=leverage,
121
            reduceOnly=reduceOnly,
122
            time_in_force=time_in_force,
123
        )
124
        if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
1✔
125
            params['position_idx'] = 0
1✔
126
        return params
1✔
127

128
    def dry_run_liquidation_price(
1✔
129
        self,
130
        pair: str,
131
        open_rate: float,   # Entry price of position
132
        is_short: bool,
133
        amount: float,
134
        stake_amount: float,
135
        leverage: float,
136
        wallet_balance: float,  # Or margin balance
137
        mm_ex_1: float = 0.0,  # (Binance) Cross only
138
        upnl_ex_1: float = 0.0,  # (Binance) Cross only
139
    ) -> Optional[float]:
140
        """
141
        Important: Must be fetching data from cached values as this is used by backtesting!
142
        PERPETUAL:
143
         bybit:
144
          https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067
145

146
        Long:
147
        Liquidation Price = (
148
            Entry Price * (1 - Initial Margin Rate + Maintenance Margin Rate)
149
            - Extra Margin Added/ Contract)
150
        Short:
151
        Liquidation Price = (
152
            Entry Price * (1 + Initial Margin Rate - Maintenance Margin Rate)
153
            + Extra Margin Added/ Contract)
154

155
        Implementation Note: Extra margin is currently not used.
156

157
        :param pair: Pair to calculate liquidation price for
158
        :param open_rate: Entry price of position
159
        :param is_short: True if the trade is a short, false otherwise
160
        :param amount: Absolute value of position size incl. leverage (in base currency)
161
        :param stake_amount: Stake amount - Collateral in settle currency.
162
        :param leverage: Leverage used for this position.
163
        :param trading_mode: SPOT, MARGIN, FUTURES, etc.
164
        :param margin_mode: Either ISOLATED or CROSS
165
        :param wallet_balance: Amount of margin_mode in the wallet being used to trade
166
            Cross-Margin Mode: crossWalletBalance
167
            Isolated-Margin Mode: isolatedWalletBalance
168
        """
169

170
        market = self.markets[pair]
1✔
171
        mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount)
1✔
172

173
        if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED:
1✔
174

175
            if market['inverse']:
1✔
176
                raise OperationalException(
×
177
                    "Freqtrade does not yet support inverse contracts")
178
            initial_margin_rate = 1 / leverage
1✔
179

180
            # See docstring - ignores extra margin!
181
            if is_short:
1✔
182
                return open_rate * (1 + initial_margin_rate - mm_ratio)
1✔
183
            else:
184
                return open_rate * (1 - initial_margin_rate + mm_ratio)
1✔
185

186
        else:
187
            raise OperationalException(
×
188
                "Freqtrade only supports isolated futures for leverage trading")
189

190
    def get_funding_fees(
1✔
191
            self, pair: str, amount: float, is_short: bool, open_date: datetime) -> float:
192
        """
193
        Fetch funding fees, either from the exchange (live) or calculates them
194
        based on funding rate/mark price history
195
        :param pair: The quote/base pair of the trade
196
        :param is_short: trade direction
197
        :param amount: Trade amount
198
        :param open_date: Open date of the trade
199
        :return: funding fee since open_date
200
        :raises: ExchangeError if something goes wrong.
201
        """
202
        # Bybit does not provide "applied" funding fees per position.
203
        if self.trading_mode == TradingMode.FUTURES:
1✔
204
            return self._fetch_and_calculate_funding_fees(
1✔
205
                    pair, amount, is_short, open_date)
206
        return 0.0
1✔
207

208
    def fetch_orders(self, pair: str, since: datetime, params: Optional[Dict] = None) -> List[Dict]:
1✔
209
        """
210
        Fetch all orders for a pair "since"
211
        :param pair: Pair for the query
212
        :param since: Starting time for the query
213
        """
214
        # On bybit, the distance between since and "until" can't exceed 7 days.
215
        # we therefore need to split the query into multiple queries.
216
        orders = []
1✔
217

218
        while since < dt_now():
1✔
219
            until = since + timedelta(days=7, minutes=-1)
1✔
220
            orders += super().fetch_orders(pair, since, params={'until': dt_ts(until)})
1✔
221
            since = until
1✔
222

223
        return orders
1✔
224

225
    def fetch_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
1✔
226
        order = super().fetch_order(order_id, pair, params)
1✔
227
        if (
1✔
228
            order.get('status') == 'canceled'
229
            and order.get('filled') == 0.0
230
            and order.get('remaining') == 0.0
231
        ):
232
            # Canceled orders will have "remaining=0" on bybit.
233
            order['remaining'] = None
1✔
234
        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