• 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.74
/freqtrade/exchange/okx.py
1
import logging
1✔
2
from datetime import timedelta
1✔
3
from typing import Any, Dict, List, Optional, Tuple
1✔
4

5
import ccxt
1✔
6

7
from freqtrade.constants import BuySell
1✔
8
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
1✔
9
from freqtrade.exceptions import (DDosProtection, OperationalException, RetryableOrderError,
1✔
10
                                  TemporaryError)
11
from freqtrade.exchange import Exchange, date_minus_candles
1✔
12
from freqtrade.exchange.common import retrier
1✔
13
from freqtrade.misc import safe_value_fallback2
1✔
14
from freqtrade.util import dt_now, dt_ts
1✔
15

16

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

19

20
class Okx(Exchange):
1✔
21
    """Okx exchange class.
22

23
    Contains adjustments needed for Freqtrade to work with this exchange.
24
    """
25

26
    _ft_has: Dict = {
1✔
27
        "ohlcv_candle_limit": 100,  # Warning, special case with data prior to X months
28
        "mark_ohlcv_timeframe": "4h",
29
        "funding_fee_timeframe": "8h",
30
        "stoploss_order_types": {"limit": "limit"},
31
        "stoploss_on_exchange": True,
32
    }
33
    _ft_has_futures: Dict = {
1✔
34
        "tickers_have_quoteVolume": False,
35
        "stop_price_type_field": "slTriggerPxType",
36
        "stop_price_type_value_mapping": {
37
            PriceType.LAST: "last",
38
            PriceType.MARK: "index",
39
            PriceType.INDEX: "mark",
40
            },
41
    }
42

43
    _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
1✔
44
        # TradingMode.SPOT always supported and not required in this list
45
        # (TradingMode.MARGIN, MarginMode.CROSS),
46
        # (TradingMode.FUTURES, MarginMode.CROSS),
47
        (TradingMode.FUTURES, MarginMode.ISOLATED),
48
    ]
49

50
    net_only = True
1✔
51

52
    _ccxt_params: Dict = {'options': {'brokerId': 'ffb5405ad327SUDE'}}
1✔
53

54
    def ohlcv_candle_limit(
1✔
55
            self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
56
        """
57
        Exchange ohlcv candle limit
58
        OKX has the following behaviour:
59
        * 300 candles for up-to-date data
60
        * 100 candles for historic data
61
        * 100 candles for additional candles (not futures or spot).
62
        :param timeframe: Timeframe to check
63
        :param candle_type: Candle-type
64
        :param since_ms: Starting timestamp
65
        :return: Candle limit as integer
66
        """
67
        if (
1✔
68
            candle_type in (CandleType.FUTURES, CandleType.SPOT) and
69
            (not since_ms or since_ms > (date_minus_candles(timeframe, 300).timestamp() * 1000))
70
        ):
71
            return 300
1✔
72

73
        return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
1✔
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
                accounts = self._api.fetch_accounts()
1✔
85
                self._log_exchange_response('fetch_accounts', accounts)
1✔
86
                if len(accounts) > 0:
1✔
87
                    self.net_only = accounts[0].get('info', {}).get('posMode') == 'net_mode'
1✔
88
        except ccxt.DDoSProtection as e:
1✔
89
            raise DDosProtection(e) from e
1✔
90
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
91
            raise TemporaryError(
1✔
92
                f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}'
93
                ) from e
94
        except ccxt.BaseError as e:
1✔
95
            raise OperationalException(e) from e
1✔
96

97
    def _get_posSide(self, side: BuySell, reduceOnly: bool):
1✔
98
        if self.net_only:
1✔
99
            return 'net'
1✔
100
        if not reduceOnly:
1✔
101
            # Enter
102
            return 'long' if side == 'buy' else 'short'
1✔
103
        else:
104
            # Exit
105
            return 'long' if side == 'sell' else 'short'
1✔
106

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

127
    def __fetch_leverage_already_set(self, pair: str, leverage: float, side: BuySell) -> bool:
1✔
128
        try:
1✔
129
            res_lev = self._api.fetch_leverage(symbol=pair, params={
1✔
130
                    "mgnMode": self.margin_mode.value,
131
                    "posSide": self._get_posSide(side, False),
132
                })
133
            self._log_exchange_response('get_leverage', res_lev)
1✔
134
            already_set = all(float(x['lever']) == leverage for x in res_lev['data'])
1✔
135
            return already_set
1✔
136

137
        except ccxt.BaseError:
1✔
138
            # Assume all errors as "not set yet"
139
            return False
1✔
140

141
    @retrier
1✔
142
    def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
1✔
143
        if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
1✔
144
            try:
1✔
145
                res = self._api.set_leverage(
1✔
146
                    leverage=leverage,
147
                    symbol=pair,
148
                    params={
149
                        "mgnMode": self.margin_mode.value,
150
                        "posSide": self._get_posSide(side, False),
151
                    })
152
                self._log_exchange_response('set_leverage', res)
1✔
153

154
            except ccxt.DDoSProtection as e:
1✔
155
                raise DDosProtection(e) from e
1✔
156
            except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
157
                already_set = self.__fetch_leverage_already_set(pair, leverage, side)
1✔
158
                if not already_set:
1✔
159
                    raise TemporaryError(
1✔
160
                        f'Could not set leverage due to {e.__class__.__name__}. Message: {e}'
161
                        ) from e
162
            except ccxt.BaseError as e:
1✔
163
                raise OperationalException(e) from e
1✔
164

165
    def get_max_pair_stake_amount(
1✔
166
        self,
167
        pair: str,
168
        price: float,
169
        leverage: float = 1.0
170
    ) -> float:
171

172
        if self.trading_mode == TradingMode.SPOT:
1✔
173
            return float('inf')  # Not actually inf, but this probably won't matter for SPOT
1✔
174

175
        if pair not in self._leverage_tiers:
1✔
176
            return float('inf')
1✔
177

178
        pair_tiers = self._leverage_tiers[pair]
1✔
179
        return pair_tiers[-1]['maxNotional'] / leverage
1✔
180

181
    def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
1✔
182
        params = super()._get_stop_params(side, ordertype, stop_price)
1✔
183
        if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
1✔
184
            params['tdMode'] = self.margin_mode.value
1✔
185
            params['posSide'] = self._get_posSide(side, True)
1✔
186
        return params
1✔
187

188
    def _convert_stop_order(self, pair: str, order_id: str, order: Dict) -> Dict:
1✔
189
        if (
1✔
190
            order.get('status', 'open') == 'closed'
191
            and (real_order_id := order.get('info', {}).get('ordId')) is not None
192
        ):
193
            # Once a order triggered, we fetch the regular followup order.
194
            order_reg = self.fetch_order(real_order_id, pair)
1✔
195
            self._log_exchange_response('fetch_stoploss_order1', order_reg)
1✔
196
            order_reg['id_stop'] = order_reg['id']
1✔
197
            order_reg['id'] = order_id
1✔
198
            order_reg['type'] = 'stoploss'
1✔
199
            order_reg['status_stop'] = 'triggered'
1✔
200
            return order_reg
1✔
201
        order = self._order_contracts_to_amount(order)
1✔
202
        order['type'] = 'stoploss'
1✔
203
        return order
1✔
204

205
    def fetch_stoploss_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
1✔
206
        if self._config['dry_run']:
1✔
207
            return self.fetch_dry_run_order(order_id)
1✔
208

209
        try:
1✔
210
            params1 = {'stop': True}
1✔
211
            order_reg = self._api.fetch_order(order_id, pair, params=params1)
1✔
212
            self._log_exchange_response('fetch_stoploss_order', order_reg)
1✔
213
            return self._convert_stop_order(pair, order_id, order_reg)
1✔
214
        except ccxt.OrderNotFound:
1✔
215
            pass
1✔
216
        params2 = {'stop': True, 'ordType': 'conditional'}
1✔
217
        for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders,
1✔
218
                       self._api.fetch_canceled_orders):
219
            try:
1✔
220
                orders = method(pair, params=params2)
1✔
221
                orders_f = [order for order in orders if order['id'] == order_id]
1✔
222
                if orders_f:
1✔
223
                    order = orders_f[0]
1✔
224
                    return self._convert_stop_order(pair, order_id, order)
1✔
225
            except ccxt.BaseError:
×
226
                pass
×
227
        raise RetryableOrderError(
1✔
228
                f'StoplossOrder not found (pair: {pair} id: {order_id}).')
229

230
    def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
1✔
231
        if order.get('type', '') == 'stop':
1✔
232
            return safe_value_fallback2(order, order, 'id_stop', 'id')
×
233
        return order['id']
1✔
234

235
    def cancel_stoploss_order(
1✔
236
            self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
237
        params1 = {'stop': True}
1✔
238
        # 'ordType': 'conditional'
239
        #
240
        return self.cancel_order(
1✔
241
            order_id=order_id,
242
            pair=pair,
243
            params=params1,
244
        )
245

246
    def _fetch_orders_emulate(self, pair: str, since_ms: int) -> List[Dict]:
1✔
247
        orders = []
1✔
248

249
        orders = self._api.fetch_closed_orders(pair, since=since_ms)
1✔
250
        if (since_ms < dt_ts(dt_now() - timedelta(days=6, hours=23))):
1✔
251
            # Regular fetch_closed_orders only returns 7 days of data.
252
            # Force usage of "archive" endpoint, which returns 3 months of data.
253
            params = {'method': 'privateGetTradeOrdersHistoryArchive'}
1✔
254
            orders_hist = self._api.fetch_closed_orders(pair, since=since_ms, params=params)
1✔
255
            orders.extend(orders_hist)
1✔
256

257
        orders_open = self._api.fetch_open_orders(pair, since=since_ms)
1✔
258
        orders.extend(orders_open)
1✔
259
        return orders
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