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

freqtrade / freqtrade / 5247208638

pending completion
5247208638

push

github-actions

xmatthias
Improve behavior for when stoploss cancels without content

closes #8761

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

18708 of 19785 relevant lines covered (94.56%)

0.95 hits per line

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

97.54
/freqtrade/exchange/okx.py
1
import logging
1✔
2
from typing import Any, Dict, List, Optional, Tuple
1✔
3

4
import ccxt
1✔
5

6
from freqtrade.constants import BuySell
1✔
7
from freqtrade.enums import CandleType, MarginMode, TradingMode
1✔
8
from freqtrade.enums.pricetype import PriceType
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

15

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

18

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

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

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

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

51
    net_only = True
1✔
52

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

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

74
        return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
1✔
75

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

236
    def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
1✔
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
        )
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