• 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.71
/freqtrade/exchange/exchange_utils.py
1
"""
2
Exchange support utils
3
"""
4
from datetime import datetime, timedelta, timezone
1✔
5
from math import ceil, floor
1✔
6
from typing import Any, Dict, List, Optional, Tuple
1✔
7

8
import ccxt
1✔
9
from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE,
1✔
10
                  TRUNCATE, decimal_to_precision)
11

12
from freqtrade.exchange.common import (BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED,
1✔
13
                                       SUPPORTED_EXCHANGES)
14
from freqtrade.types import ValidExchangesType
1✔
15
from freqtrade.util import FtPrecise
1✔
16
from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts
1✔
17

18

19
CcxtModuleType = Any
1✔
20

21

22
def is_exchange_known_ccxt(
1✔
23
        exchange_name: str, ccxt_module: Optional[CcxtModuleType] = None) -> bool:
24
    return exchange_name in ccxt_exchanges(ccxt_module)
1✔
25

26

27
def ccxt_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[str]:
1✔
28
    """
29
    Return the list of all exchanges known to ccxt
30
    """
31
    return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
1✔
32

33

34
def available_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[str]:
1✔
35
    """
36
    Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
37
    """
38
    exchanges = ccxt_exchanges(ccxt_module)
1✔
39
    return [x for x in exchanges if validate_exchange(x)[0]]
1✔
40

41

42
def validate_exchange(exchange: str) -> Tuple[bool, str]:
1✔
43
    ex_mod = getattr(ccxt, exchange.lower())()
1✔
44
    if not ex_mod or not ex_mod.has:
1✔
45
        return False, ''
×
46
    missing = [k for k in EXCHANGE_HAS_REQUIRED if ex_mod.has.get(k) is not True]
1✔
47
    if missing:
1✔
48
        return False, f"missing: {', '.join(missing)}"
1✔
49

50
    missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)]
1✔
51

52
    if exchange.lower() in BAD_EXCHANGES:
1✔
53
        return False, BAD_EXCHANGES.get(exchange.lower(), '')
1✔
54
    if missing_opt:
1✔
55
        return True, f"missing opt: {', '.join(missing_opt)}"
1✔
56

57
    return True, ''
1✔
58

59

60
def _build_exchange_list_entry(
1✔
61
        exchange_name: str, exchangeClasses: Dict[str, Any]) -> ValidExchangesType:
62
    valid, comment = validate_exchange(exchange_name)
1✔
63
    result: ValidExchangesType = {
1✔
64
        'name': exchange_name,
65
        'valid': valid,
66
        'supported': exchange_name.lower() in SUPPORTED_EXCHANGES,
67
        'comment': comment,
68
        'trade_modes': [{'trading_mode': 'spot', 'margin_mode': ''}],
69
    }
70
    if resolved := exchangeClasses.get(exchange_name.lower()):
1✔
71
        supported_modes = [{'trading_mode': 'spot', 'margin_mode': ''}] + [
1✔
72
            {'trading_mode': tm.value, 'margin_mode': mm.value}
73
            for tm, mm in resolved['class']._supported_trading_mode_margin_pairs
74
        ]
75
        result.update({
1✔
76
            'trade_modes': supported_modes,
77
        })
78

79
    return result
1✔
80

81

82
def list_available_exchanges(all_exchanges: bool) -> List[ValidExchangesType]:
1✔
83
    """
84
    :return: List of tuples with exchangename, valid, reason.
85
    """
86
    exchanges = ccxt_exchanges() if all_exchanges else available_exchanges()
1✔
87
    from freqtrade.resolvers.exchange_resolver import ExchangeResolver
1✔
88

89
    subclassed = {e['name'].lower(): e for e in ExchangeResolver.search_all_objects({}, False)}
1✔
90

91
    exchanges_valid: List[ValidExchangesType] = [
1✔
92
        _build_exchange_list_entry(e, subclassed) for e in exchanges
93
    ]
94

95
    return exchanges_valid
1✔
96

97

98
def timeframe_to_seconds(timeframe: str) -> int:
1✔
99
    """
100
    Translates the timeframe interval value written in the human readable
101
    form ('1m', '5m', '1h', '1d', '1w', etc.) to the number
102
    of seconds for one timeframe interval.
103
    """
104
    return ccxt.Exchange.parse_timeframe(timeframe)
1✔
105

106

107
def timeframe_to_minutes(timeframe: str) -> int:
1✔
108
    """
109
    Same as timeframe_to_seconds, but returns minutes.
110
    """
111
    return ccxt.Exchange.parse_timeframe(timeframe) // 60
1✔
112

113

114
def timeframe_to_msecs(timeframe: str) -> int:
1✔
115
    """
116
    Same as timeframe_to_seconds, but returns milliseconds.
117
    """
118
    return ccxt.Exchange.parse_timeframe(timeframe) * 1000
1✔
119

120

121
def timeframe_to_prev_date(timeframe: str, date: Optional[datetime] = None) -> datetime:
1✔
122
    """
123
    Use Timeframe and determine the candle start date for this date.
124
    Does not round when given a candle start date.
125
    :param timeframe: timeframe in string format (e.g. "5m")
126
    :param date: date to use. Defaults to now(utc)
127
    :returns: date of previous candle (with utc timezone)
128
    """
129
    if not date:
1✔
130
        date = datetime.now(timezone.utc)
1✔
131

132
    new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_DOWN) // 1000
1✔
133
    return dt_from_ts(new_timestamp)
1✔
134

135

136
def timeframe_to_next_date(timeframe: str, date: Optional[datetime] = None) -> datetime:
1✔
137
    """
138
    Use Timeframe and determine next candle.
139
    :param timeframe: timeframe in string format (e.g. "5m")
140
    :param date: date to use. Defaults to now(utc)
141
    :returns: date of next candle (with utc timezone)
142
    """
143
    if not date:
1✔
144
        date = datetime.now(timezone.utc)
1✔
145
    new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_UP) // 1000
1✔
146
    return dt_from_ts(new_timestamp)
1✔
147

148

149
def date_minus_candles(
1✔
150
        timeframe: str, candle_count: int, date: Optional[datetime] = None) -> datetime:
151
    """
152
    subtract X candles from a date.
153
    :param timeframe: timeframe in string format (e.g. "5m")
154
    :param candle_count: Amount of candles to subtract.
155
    :param date: date to use. Defaults to now(utc)
156

157
    """
158
    if not date:
1✔
159
        date = datetime.now(timezone.utc)
1✔
160

161
    tf_min = timeframe_to_minutes(timeframe)
1✔
162
    new_date = timeframe_to_prev_date(timeframe, date) - timedelta(minutes=tf_min * candle_count)
1✔
163
    return new_date
1✔
164

165

166
def market_is_active(market: Dict) -> bool:
1✔
167
    """
168
    Return True if the market is active.
169
    """
170
    # "It's active, if the active flag isn't explicitly set to false. If it's missing or
171
    # true then it's true. If it's undefined, then it's most likely true, but not 100% )"
172
    # See https://github.com/ccxt/ccxt/issues/4874,
173
    # https://github.com/ccxt/ccxt/issues/4075#issuecomment-434760520
174
    return market.get('active', True) is not False
1✔
175

176

177
def amount_to_contracts(amount: float, contract_size: Optional[float]) -> float:
1✔
178
    """
179
    Convert amount to contracts.
180
    :param amount: amount to convert
181
    :param contract_size: contract size - taken from exchange.get_contract_size(pair)
182
    :return: num-contracts
183
    """
184
    if contract_size and contract_size != 1:
1✔
185
        return float(FtPrecise(amount) / FtPrecise(contract_size))
1✔
186
    else:
187
        return amount
1✔
188

189

190
def contracts_to_amount(num_contracts: float, contract_size: Optional[float]) -> float:
1✔
191
    """
192
    Takes num-contracts and converts it to contract size
193
    :param num_contracts: number of contracts
194
    :param contract_size: contract size - taken from exchange.get_contract_size(pair)
195
    :return: Amount
196
    """
197

198
    if contract_size and contract_size != 1:
1✔
199
        return float(FtPrecise(num_contracts) * FtPrecise(contract_size))
1✔
200
    else:
201
        return num_contracts
1✔
202

203

204
def amount_to_precision(amount: float, amount_precision: Optional[float],
1✔
205
                        precisionMode: Optional[int]) -> float:
206
    """
207
    Returns the amount to buy or sell to a precision the Exchange accepts
208
    Re-implementation of ccxt internal methods - ensuring we can test the result is correct
209
    based on our definitions.
210
    :param amount: amount to truncate
211
    :param amount_precision: amount precision to use.
212
                             should be retrieved from markets[pair]['precision']['amount']
213
    :param precisionMode: precision mode to use. Should be used from precisionMode
214
                          one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
215
    :return: truncated amount
216
    """
217
    if amount_precision is not None and precisionMode is not None:
1✔
218
        precision = int(amount_precision) if precisionMode != TICK_SIZE else amount_precision
1✔
219
        # precision must be an int for non-ticksize inputs.
220
        amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
1✔
221
                                            precision=precision,
222
                                            counting_mode=precisionMode,
223
                                            ))
224

225
    return amount
1✔
226

227

228
def amount_to_contract_precision(
1✔
229
        amount, amount_precision: Optional[float], precisionMode: Optional[int],
230
        contract_size: Optional[float]) -> float:
231
    """
232
    Returns the amount to buy or sell to a precision the Exchange accepts
233
    including calculation to and from contracts.
234
    Re-implementation of ccxt internal methods - ensuring we can test the result is correct
235
    based on our definitions.
236
    :param amount: amount to truncate
237
    :param amount_precision: amount precision to use.
238
                             should be retrieved from markets[pair]['precision']['amount']
239
    :param precisionMode: precision mode to use. Should be used from precisionMode
240
                          one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
241
    :param contract_size: contract size - taken from exchange.get_contract_size(pair)
242
    :return: truncated amount
243
    """
244
    if amount_precision is not None and precisionMode is not None:
1✔
245
        contracts = amount_to_contracts(amount, contract_size)
1✔
246
        amount_p = amount_to_precision(contracts, amount_precision, precisionMode)
1✔
247
        return contracts_to_amount(amount_p, contract_size)
1✔
248
    return amount
1✔
249

250

251
def __price_to_precision_significant_digits(
1✔
252
    price: float,
253
    price_precision: float,
254
    *,
255
    rounding_mode: int = ROUND,
256
) -> float:
257
    """
258
    Implementation of ROUND_UP/Round_down for significant digits mode.
259
    """
260
    from decimal import ROUND_DOWN as dec_ROUND_DOWN
1✔
261
    from decimal import ROUND_UP as dec_ROUND_UP
1✔
262
    from decimal import Decimal
1✔
263
    dec = Decimal(str(price))
1✔
264
    string = f'{dec:f}'
1✔
265
    precision = round(price_precision)
1✔
266

267
    q = precision - dec.adjusted() - 1
1✔
268
    sigfig = Decimal('10') ** -q
1✔
269
    if q < 0:
1✔
270
        string_to_precision = string[:precision]
1✔
271
        # string_to_precision is '' when we have zero precision
272
        below = sigfig * Decimal(string_to_precision if string_to_precision else '0')
1✔
273
        above = below + sigfig
1✔
274
        res = above if rounding_mode == ROUND_UP else below
1✔
275
        precise = f'{res:f}'
1✔
276
    else:
277
        precise = '{:f}'.format(dec.quantize(
1✔
278
            sigfig,
279
            rounding=dec_ROUND_DOWN if rounding_mode == ROUND_DOWN else dec_ROUND_UP)
280
        )
281
    return float(precise)
1✔
282

283

284
def price_to_precision(
1✔
285
    price: float,
286
    price_precision: Optional[float],
287
    precisionMode: Optional[int],
288
    *,
289
    rounding_mode: int = ROUND,
290
) -> float:
291
    """
292
    Returns the price rounded to the precision the Exchange accepts.
293
    Partial Re-implementation of ccxt internal method decimal_to_precision(),
294
    which does not support rounding up.
295
    For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
296

297
    TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
298
    align with amount_to_precision().
299
    :param price: price to convert
300
    :param price_precision: price precision to use. Used from markets[pair]['precision']['price']
301
    :param precisionMode: precision mode to use. Should be used from precisionMode
302
                          one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
303
    :param rounding_mode: rounding mode to use. Defaults to ROUND
304
    :return: price rounded up to the precision the Exchange accepts
305
    """
306
    if price_precision is not None and precisionMode is not None:
1✔
307
        if rounding_mode not in (ROUND_UP, ROUND_DOWN):
1✔
308
            # Use CCXT code where possible.
309
            return float(decimal_to_precision(price, rounding_mode=rounding_mode,
1✔
310
                                              precision=price_precision,
311
                                              counting_mode=precisionMode
312
                                              ))
313

314
        if precisionMode == TICK_SIZE:
1✔
315
            precision = FtPrecise(price_precision)
1✔
316
            price_str = FtPrecise(price)
1✔
317
            missing = price_str % precision
1✔
318
            if not missing == FtPrecise("0"):
1✔
319
                if rounding_mode == ROUND_UP:
1✔
320
                    res = price_str - missing + precision
1✔
321
                elif rounding_mode == ROUND_DOWN:
1✔
322
                    res = price_str - missing
1✔
323
                return round(float(str(res)), 14)
1✔
324
            return price
1✔
325
        elif precisionMode == DECIMAL_PLACES:
1✔
326

327
            ndigits = round(price_precision)
1✔
328
            ticks = price * (10**ndigits)
1✔
329
            if rounding_mode == ROUND_UP:
1✔
330
                return ceil(ticks) / (10**ndigits)
1✔
331
            if rounding_mode == ROUND_DOWN:
1✔
332
                return floor(ticks) / (10**ndigits)
1✔
333

334
            raise ValueError(f"Unknown rounding_mode {rounding_mode}")
×
335
        elif precisionMode == SIGNIFICANT_DIGITS:
1✔
336
            if rounding_mode in (ROUND_UP, ROUND_DOWN):
1✔
337
                return __price_to_precision_significant_digits(
1✔
338
                    price, price_precision, rounding_mode=rounding_mode
339
                )
340

341
        raise ValueError(f"Unknown precisionMode {precisionMode}")
×
342
    return price
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