diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index f84822ac80b..17c3702b2d3 100755 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -103,7 +103,13 @@ "backtest_breakdown", ] -ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all", "trading_mode", "dex_exchanges"] +ARGS_LIST_EXCHANGES = [ + "print_one_column", + "list_exchanges_all", + "trading_mode", + "dex_exchanges", + "list_exchanges_futures_options", +] ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column", "trading_mode"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 9fc5f2e11b0..8a035b6a0ea 100755 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -2,7 +2,7 @@ Definition of cli arguments used in arguments.py """ -from argparse import ArgumentTypeError +from argparse import SUPPRESS, ArgumentTypeError from freqtrade import constants from freqtrade.constants import ( @@ -388,6 +388,13 @@ def __init__(self, *args, fthelp: dict[str, str] | None = None, **kwargs): help="Print only DEX exchanges.", action="store_true", ), + "list_exchanges_futures_options": Arg( + "--ccxt-show-futures-options-exchanges", + help=SUPPRESS, + # Show compatibility with ccxt for futures functionality + # Doesn't show in help as it's an internal/debug option. + action="store_true", + ), # List pairs / markets "list_pairs_all": Arg( "-a", diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index a918afae3e3..8fc5923816d 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -38,13 +38,15 @@ def start_list_exchanges(args: dict[str, Any]) -> None: else: available_exchanges = [e for e in available_exchanges if e["valid"] is not False] title = f"Exchanges available for Freqtrade ({len(available_exchanges)} exchanges):" - + show_fut_reasons = args.get("list_exchanges_futures_options", False) table = Table(title=title) table.add_column("Exchange Name") table.add_column("Class Name") table.add_column("Markets") table.add_column("Reason") + if show_fut_reasons: + table.add_column("Futures Reason") trading_mode = args.get("trading_mode", None) dex_only = args.get("dex_exchanges", False) @@ -78,12 +80,14 @@ def start_list_exchanges(args: dict[str, Any]) -> None: if exchange["dex"]: trade_modes = Text("DEX: ") + trade_modes trade_modes.stylize("bold", 0, 3) + futcol = [] if not show_fut_reasons else [exchange["comment_futures"]] table.add_row( name, classname, trade_modes, exchange["comment"], + *futcol, style=None if exchange["valid"] else "red", ) # table.add_row(*[exchange[header] for header in headers]) diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index e18707a9938..d3bc91002c6 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -30,7 +30,7 @@ def log_config_error_range(path: str, errmsg: str) -> str: offsetlist = re.findall(r"(?<=Parse\serror\sat\soffset\s)\d+", errmsg) if offsetlist: offset = int(offsetlist[0]) - text = Path(path).read_text() + text = Path(path).read_text(encoding="utf-8") # Fetch an offset of 80 characters around the error line subtext = text[offset - min(80, offset) : offset + 80] segments = subtext.split("\n") diff --git a/freqtrade/exchange/check_exchange.py b/freqtrade/exchange/check_exchange.py index 1402e40ccb5..5838687445a 100644 --- a/freqtrade/exchange/check_exchange.py +++ b/freqtrade/exchange/check_exchange.py @@ -47,14 +47,16 @@ def check_exchange(config: Config, check_for_bad: bool = True) -> bool: f"{', '.join(available_exchanges())}" ) - valid, reason, _ = validate_exchange(exchange) + valid, reason, _, _ = validate_exchange(exchange) if not valid: if check_for_bad: raise OperationalException( - f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}' + f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}.' ) else: - logger.warning(f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}') + logger.warning( + f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}.' + ) if MAP_EXCHANGE_CHILDCLASS.get(exchange, exchange) in SUPPORTED_EXCHANGES: logger.info( diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 61206de9263..7f53c39ef4b 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -36,12 +36,13 @@ def _get_logging_mixin(): API_FETCH_ORDER_RETRY_COUNT = 5 BAD_EXCHANGES = { - "bitmex": "Various reasons.", - "probit": "Requires additional, regular calls to `signIn()`.", - "poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.", - "kucoinfutures": "Unsupported futures exchange.", - "poloniexfutures": "Unsupported futures exchange.", - "binancecoinm": "Unsupported futures exchange.", + "bitmex": "Various reasons", + "probit": "Requires additional, regular calls to `signIn()`", + "poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders", + "krakenfutures": "Unsupported futures exchange", + "kucoinfutures": "Unsupported futures exchange", + "poloniexfutures": "Unsupported futures exchange", + "binancecoinm": "Unsupported futures exchange", } MAP_EXCHANGE_CHILDCLASS = { @@ -78,31 +79,35 @@ def _get_logging_mixin(): "fetchOHLCV": [], } -EXCHANGE_HAS_OPTIONAL = [ +EXCHANGE_HAS_OPTIONAL: dict[str, list[str]] = { # Private - "fetchMyTrades", # Trades for order - fee detection - "createLimitOrder", - "createMarketOrder", # Either OR for orders - # 'setLeverage', # Margin/Futures trading - # 'setMarginMode', # Margin/Futures trading - # 'fetchFundingHistory', # Futures trading + "fetchMyTrades": [], # Trades for order - fee detection + "createLimitOrder": [], + "createMarketOrder": [], # Either OR for orders # Public - "fetchOrderBook", - "fetchL2OrderBook", - "fetchTicker", # OR for pricing - "fetchTickers", # For volumepairlist? - "fetchTrades", # Downloading trades data - # 'fetchFundingRateHistory', # Futures trading - # 'fetchPositions', # Futures trading - # 'fetchLeverageTiers', # Futures initialization - # 'fetchMarketLeverageTiers', # Futures initialization - # 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance... - # "fetchPremiumIndexOHLCV", # Futures additional data - # "fetchMarkOHLCV", # Futures additional data - # "fetchIndexOHLCV", # Futures additional data + "fetchOrderBook": [], + "fetchL2OrderBook": [], + "fetchTicker": [], # OR for pricing + "fetchTickers": [], # For volumepairlist? + "fetchTrades": [], # Downloading trades data + "fetchOrders": ["fetchOpenOrders", "fetchClosedOrders"], # , # Refinding balance... # ccxt.pro - "watchOHLCV", -] + "watchOHLCV": [], +} + +EXCHANGE_HAS_OPTIONAL_FUTURES: dict[str, list[str]] = { + # private + "setLeverage": [], # Margin/Futures trading + "setMarginMode": [], # Margin/Futures trading + "fetchFundingHistory": [], # Futures trading + # Public + "fetchFundingRateHistory": [], # Futures trading + "fetchPositions": [], # Futures trading + "fetchLeverageTiers": ["fetchMarketLeverageTiers"], # Futures initialization + "fetchMarkOHLCV": [], + "fetchIndexOHLCV": [], # Futures additional data + "fetchPremiumIndexOHLCV": [], +} def calculate_backoff(retrycount, max_retries): diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2860208fbf6..dd0ca73e7aa 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -105,7 +105,6 @@ file_dump_json, file_load_json, safe_value_fallback, - safe_value_fallback2, ) from freqtrade.util import FtTTLCache, PeriodicCache, dt_from_ts, dt_now from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts, format_ms_time @@ -2093,7 +2092,7 @@ def get_conversion_rate(self, coin: str, currency: str) -> float | None: ) ticker = tickers_other.get(pair, None) if ticker: - rate: float | None = safe_value_fallback2(ticker, ticker, "last", "ask", None) + rate: float | None = safe_value_fallback(ticker, "last", "ask", None) if rate and pair.startswith(currency) and not pair.endswith(currency): rate = 1.0 / rate return rate @@ -2393,6 +2392,16 @@ def get_trades_for_order( raise OperationalException(e) from e def get_order_id_conditional(self, order: CcxtOrder) -> str: + """ + Return order id or id_stop (for conditional orders) based on exchange settings + + :param order: ccxt order dict + :return: correct order id + """ + if self.get_option("stoploss_query_requires_stop_flag") and ( + order["type"] in ("stoploss", "stop") + ): + return safe_value_fallback(order, "id_stop", "id") return order["id"] @retrier diff --git a/freqtrade/exchange/exchange_utils.py b/freqtrade/exchange/exchange_utils.py index ddb57b1234e..43d7acbd67e 100644 --- a/freqtrade/exchange/exchange_utils.py +++ b/freqtrade/exchange/exchange_utils.py @@ -22,6 +22,7 @@ from freqtrade.exchange.common import ( BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, + EXCHANGE_HAS_OPTIONAL_FUTURES, EXCHANGE_HAS_REQUIRED, MAP_EXCHANGE_CHILDCLASS, SUPPORTED_EXCHANGES, @@ -53,7 +54,22 @@ def available_exchanges(ccxt_module: CcxtModuleType | None = None) -> list[str]: return [x for x in exchanges if validate_exchange(x)[0]] -def validate_exchange(exchange: str) -> tuple[bool, str, ccxt.Exchange | None]: +def _exchange_has_helper(ex_mod: ccxt.Exchange, required: dict[str, list[str]]) -> list[str]: + """ + Checks availability of methods (or their replacement)s in ex_mod.has + :param ex_mod: ccxt Exchange module + :param required: dict of required methods, with possible replacement methods as list + :return: list of missing required methods + """ + return [ + k + for k, v in required.items() + if ex_mod.has.get(k) is not True + and (len(v) == 0 or not (all(ex_mod.has.get(x) for x in v))) + ] + + +def validate_exchange(exchange: str) -> tuple[bool, str, str, ccxt.Exchange | None]: """ returns: can_use, reason, exchange_object with Reason including both missing and missing_opt @@ -64,36 +80,38 @@ def validate_exchange(exchange: str) -> tuple[bool, str, ccxt.Exchange | None]: ex_mod = getattr(ccxt.async_support, exchange.lower())() if not ex_mod or not ex_mod.has: - return False, "", None + return False, "", "", None result = True - reason = "" - missing = [ - k - for k, v in EXCHANGE_HAS_REQUIRED.items() - if ex_mod.has.get(k) is not True and not (all(ex_mod.has.get(x) for x in v)) - ] + reasons = [] + reasons_fut = "" + missing = _exchange_has_helper(ex_mod, EXCHANGE_HAS_REQUIRED) if missing: result = False - reason += f"missing: {', '.join(missing)}" + reasons.append(f"missing: {', '.join(missing)}") - missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)] + missing_opt = _exchange_has_helper(ex_mod, EXCHANGE_HAS_OPTIONAL) + + missing_futures = _exchange_has_helper(ex_mod, EXCHANGE_HAS_OPTIONAL_FUTURES) if exchange.lower() in BAD_EXCHANGES: result = False - reason = BAD_EXCHANGES.get(exchange.lower(), "") + reasons.append(BAD_EXCHANGES.get(exchange.lower(), "")) if missing_opt: - reason += f"{'. ' if reason else ''}missing opt: {', '.join(missing_opt)}. " + reasons.append(f"missing opt: {', '.join(missing_opt)}") + + if missing_futures: + reasons_fut = f"missing futures opt: {', '.join(missing_futures)}" - return result, reason, ex_mod + return result, "; ".join(reasons), reasons_fut, ex_mod def _build_exchange_list_entry( exchange_name: str, exchangeClasses: dict[str, Any] ) -> ValidExchangesType: exchange_name = exchange_name.lower() - valid, comment, ex_mod = validate_exchange(exchange_name) + valid, comment, comment_fut, ex_mod = validate_exchange(exchange_name) mapped_exchange_name = MAP_EXCHANGE_CHILDCLASS.get(exchange_name, exchange_name).lower() is_alias = getattr(ex_mod, "alias", False) result: ValidExchangesType = { @@ -102,6 +120,7 @@ def _build_exchange_list_entry( "valid": valid, "supported": mapped_exchange_name in SUPPORTED_EXCHANGES and not is_alias, "comment": comment, + "comment_futures": comment_fut, "dex": getattr(ex_mod, "dex", False), "is_alias": is_alias, "alias_for": inspect.getmro(ex_mod.__class__)[1]().id diff --git a/freqtrade/exchange/gate.py b/freqtrade/exchange/gate.py index 585dddad5a9..e48cf3af6b1 100644 --- a/freqtrade/exchange/gate.py +++ b/freqtrade/exchange/gate.py @@ -10,8 +10,7 @@ from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier -from freqtrade.exchange.exchange_types import CcxtOrder, FtHas -from freqtrade.misc import safe_value_fallback2 +from freqtrade.exchange.exchange_types import FtHas logger = logging.getLogger(__name__) @@ -132,6 +131,3 @@ def get_trades_for_order( "rate": pair_fees[takerOrMaker], } return trades - - def get_order_id_conditional(self, order: CcxtOrder) -> str: - return safe_value_fallback2(order, order, "id_stop", "id") diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 4640d564781..5347f241785 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -14,7 +14,6 @@ from freqtrade.exchange import Exchange from freqtrade.exchange.common import API_RETRY_COUNT, retrier from freqtrade.exchange.exchange_types import CcxtOrder, FtHas -from freqtrade.misc import safe_value_fallback2 from freqtrade.util import dt_now, dt_ts @@ -259,11 +258,6 @@ def _fetch_stop_order_fallback(self, order_id: str, pair: str) -> CcxtOrder: raise OperationalException(e) from e raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).") - def get_order_id_conditional(self, order: CcxtOrder) -> str: - if order.get("type", "") == "stop": - return safe_value_fallback2(order, order, "id_stop", "id") - return order["id"] - def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[CcxtOrder]: orders = [] diff --git a/freqtrade/ft_types/valid_exchanges_type.py b/freqtrade/ft_types/valid_exchanges_type.py index d6b93f4b2f5..2d0e6ae2f67 100644 --- a/freqtrade/ft_types/valid_exchanges_type.py +++ b/freqtrade/ft_types/valid_exchanges_type.py @@ -14,6 +14,7 @@ class ValidExchangesType(TypedDict): valid: bool supported: bool comment: str + comment_futures: str dex: bool is_alias: bool alias_for: str | None diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index b437166e7e5..b5129361508 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -148,7 +148,7 @@ def _search_object( logger.debug("Ignoring broken symlink %s", entry) continue module_path = entry.resolve() - if entry.read_text().find(f"class {object_name}(") == -1: + if entry.read_text(encoding="utf-8").find(f"class {object_name}(") == -1: logger.debug(f"Skipping {module_path} as it does not contain class {object_name}.") continue diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py index 5377865d74d..965f6bcd6c6 100644 --- a/freqtrade/strategy/strategyupdater.py +++ b/freqtrade/strategy/strategyupdater.py @@ -66,8 +66,7 @@ def start(self, config: Config, strategy_obj: dict) -> None: target_file = Path.joinpath(strategies_backup_folder, strategy_obj["location_rel"]) # read the file - with Path(source_file).open("r") as f: - old_code = f.read() + old_code = Path(source_file).read_text(encoding="utf-8") if not strategies_backup_folder.is_dir(): Path(strategies_backup_folder).mkdir(parents=True, exist_ok=True) @@ -80,8 +79,7 @@ def start(self, config: Config, strategy_obj: dict) -> None: # update the code new_code = self.update_code(old_code) # write the modified code to the destination folder - with Path(source_file).open("w") as f: - f.write(new_code) + Path(source_file).write_text(new_code, encoding="utf-8") # define the function to update the code def update_code(self, code): diff --git a/freqtrade/util/migrations/__init__.py b/freqtrade/util/migrations/__init__.py index 50f7dff014e..20aafb04b43 100644 --- a/freqtrade/util/migrations/__init__.py +++ b/freqtrade/util/migrations/__init__.py @@ -1,8 +1,4 @@ from freqtrade.exchange import Exchange -from freqtrade.util.migrations.binance_mig import ( - migrate_binance_futures_data, - migrate_binance_futures_names, -) from freqtrade.util.migrations.funding_rate_mig import migrate_funding_fee_timeframe @@ -10,7 +6,6 @@ def migrate_data(config, exchange: Exchange | None = None) -> None: """ Migrate persisted data from old formats to new formats """ - migrate_binance_futures_data(config) migrate_funding_fee_timeframe(config, exchange) @@ -20,4 +15,5 @@ def migrate_live_content(config, exchange: Exchange | None = None) -> None: Migrate database content from old formats to new formats Used for dry/live mode. """ - migrate_binance_futures_names(config) + # Currently not used + pass diff --git a/freqtrade/util/migrations/binance_mig.py b/freqtrade/util/migrations/binance_mig.py deleted file mode 100644 index b85ee721ddc..00000000000 --- a/freqtrade/util/migrations/binance_mig.py +++ /dev/null @@ -1,87 +0,0 @@ -import logging - -from packaging import version -from sqlalchemy import select - -from freqtrade.constants import DOCS_LINK, Config -from freqtrade.enums import TradingMode -from freqtrade.exceptions import OperationalException -from freqtrade.persistence import KeyValueStore, Trade -from freqtrade.persistence.pairlock import PairLock - - -logger = logging.getLogger(__name__) - - -def migrate_binance_futures_names(config: Config): - """ - Migrate binance futures names in both database and data files. - This is needed because ccxt naming changed from "BTC/USDT" to "BTC/USDT:USDT" - """ - if not ( - config.get("trading_mode", TradingMode.SPOT) == TradingMode.FUTURES - and config["exchange"]["name"] == "binance" - ): - # only act on new futures - return - if KeyValueStore.get_int_value("binance_migration"): - # already migrated - return - import ccxt - - if version.parse("2.6.26") > version.parse(ccxt.__version__): - raise OperationalException( - "Please follow the update instructions in the docs " - f"({DOCS_LINK}/updating/) to install a compatible ccxt version." - ) - _migrate_binance_futures_db(config) - migrate_binance_futures_data(config) - KeyValueStore.store_value("binance_migration", 1) - - -def _migrate_binance_futures_db(config: Config): - logger.info("Migrating binance futures pairs in database.") - trades = Trade.get_trades([Trade.exchange == "binance", Trade.trading_mode == "FUTURES"]).all() - for trade in trades: - if ":" in trade.pair: - # already migrated - continue - new_pair = f"{trade.pair}:{trade.stake_currency}" - trade.pair = new_pair - - for order in trade.orders: - order.ft_pair = new_pair - # Should symbol be migrated too? - # order.symbol = new_pair - Trade.commit() - pls = PairLock.session.scalars(select(PairLock).filter(PairLock.pair.notlike("%:%"))).all() - for pl in pls: - pl.pair = f"{pl.pair}:{config['stake_currency']}" - # print(pls) - # pls.update({'pair': concat(PairLock.pair,':USDT')}) - Trade.commit() - logger.info("Done migrating binance futures pairs in database.") - - -def migrate_binance_futures_data(config: Config): - if not ( - config.get("trading_mode", TradingMode.SPOT) == TradingMode.FUTURES - and config["exchange"]["name"] == "binance" - ): - # only act on new futures - return - - from freqtrade.data.history import get_datahandler - - dhc = get_datahandler(config["datadir"], config["dataformat_ohlcv"]) - - paircombs = dhc.ohlcv_get_available_data( - config["datadir"], config.get("trading_mode", TradingMode.SPOT) - ) - - for pair, timeframe, candle_type in paircombs: - if ":" in pair: - # already migrated - continue - new_pair = f"{pair}:{config['stake_currency']}" - dhc.rename_futures_data(pair, new_pair, timeframe, candle_type) diff --git a/tests/commands/test_startup_time.py b/tests/commands/test_startup_time.py index 32b4f80fd1b..29c51b9bf50 100644 --- a/tests/commands/test_startup_time.py +++ b/tests/commands/test_startup_time.py @@ -1,10 +1,10 @@ import subprocess # noqa: S404, RUF100 import time -from tests.conftest import is_arm, is_mac +from tests.conftest import is_mac -MAXIMUM_STARTUP_TIME = 0.7 if is_mac() and not is_arm(True) else 0.5 +MAXIMUM_STARTUP_TIME = 0.6 if is_mac() else 0.5 def test_startup_time(): diff --git a/tests/data/test_datahandler.py b/tests/data/test_datahandler.py index 9d40bf5cbea..9bab3e797aa 100644 --- a/tests/data/test_datahandler.py +++ b/tests/data/test_datahandler.py @@ -288,7 +288,7 @@ def test_jsondatahandler_trades_load(testdatadir, caplog): dh.trades_load("XRP/ETH", TradingMode.SPOT) assert not log_has(logmsg, caplog) - # Test conversation is happening + # Test conversion is happening dh.trades_load("XRP/OLD", TradingMode.SPOT) assert log_has(logmsg, caplog) diff --git a/tests/exchange/test_exchange_utils.py b/tests/exchange/test_exchange_utils.py index 47ad7cccb97..d0d35a9ddb8 100644 --- a/tests/exchange/test_exchange_utils.py +++ b/tests/exchange/test_exchange_utils.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring, protected-access, invalid-name from datetime import UTC, datetime, timedelta from math import isnan, nan +from unittest.mock import MagicMock import pytest from ccxt import ( @@ -28,6 +29,7 @@ timeframe_to_seconds, ) from freqtrade.exchange.check_exchange import check_exchange +from freqtrade.exchange.exchange_utils import _exchange_has_helper from tests.conftest import log_has_re @@ -385,3 +387,42 @@ def test_amount_to_contract_precision_standalone( ): res = amount_to_contract_precision(amount, precision, precision_mode, contract_size) assert pytest.approx(res) == expected + + +def test_exchange__exchange_has_helper(): + e_mod = MagicMock() + e_mod.has = { + "fetchTicker": True, + "fetchOHLCV": False, + "fetchTrades": True, + "fetchMyTrades": False, + "fetchOrder": True, + } + required = { + "fetchOHLCV": [], + "fetchTicker": [], + "fetchMyTrades": ["fetchTrades"], + "fetchOrder": ["fetchOpenOrder", "fetchClosedOrder"], + } + missing = _exchange_has_helper(e_mod, required) + assert set(missing) == {"fetchOHLCV"} + + e_mod.has = { + "fetchTicker": True, + "fetchOHLCV": False, + "fetchTrades": False, + "fetchMyTrades": False, + "fetchOrder": True, + } + missing = _exchange_has_helper(e_mod, required) + assert set(missing) == {"fetchOHLCV", "fetchMyTrades"} + + e_mod.has = { + "fetchTicker": True, + "fetchOHLCV": False, + "fetchTrades": False, + "fetchMyTrades": False, + "fetchOrder": False, + } + missing = _exchange_has_helper(e_mod, required) + assert set(missing) == {"fetchOHLCV", "fetchMyTrades", "fetchOrder"} diff --git a/tests/persistence/test_db_context.py b/tests/persistence/test_db_context.py index 164095d5df1..b879ccfd93a 100644 --- a/tests/persistence/test_db_context.py +++ b/tests/persistence/test_db_context.py @@ -6,19 +6,19 @@ @pytest.mark.parametrize("timeframe", ["", "5m", "1d"]) def test_FtNoDBContext(timeframe): PairLocks.timeframe = "" - assert Trade.use_db is True - assert PairLocks.use_db is True + assert Trade.use_db + assert PairLocks.use_db assert PairLocks.timeframe == "" with FtNoDBContext(timeframe): - assert Trade.use_db is False - assert PairLocks.use_db is False + assert not Trade.use_db + assert not PairLocks.use_db assert PairLocks.timeframe == timeframe with FtNoDBContext(): - assert Trade.use_db is False - assert PairLocks.use_db is False + assert not Trade.use_db + assert not PairLocks.use_db assert PairLocks.timeframe == "" - assert Trade.use_db is True - assert PairLocks.use_db is True + assert Trade.use_db + assert PairLocks.use_db diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 768858aacd3..e0944e872bc 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -2521,7 +2521,9 @@ def test_api_strategy(botclient, tmp_path, mocker): assert_response(rc) assert rc.json()["strategy"] == CURRENT_TEST_STRATEGY - data = (Path(__file__).parents[1] / "strategy/strats/strategy_test_v3.py").read_text() + data = (Path(__file__).parents[1] / "strategy/strats/strategy_test_v3.py").read_text( + encoding="utf-8" + ) assert rc.json()["code"] == data rc = client_get(client, f"{BASE_URI}/strategy/NoStrat") @@ -2554,6 +2556,7 @@ def test_api_exchanges(botclient): "valid": True, "supported": True, "comment": "", + "comment_futures": ANY, "dex": False, "is_alias": False, "alias_for": None, @@ -2571,6 +2574,7 @@ def test_api_exchanges(botclient): "supported": False, "dex": False, "comment": "", + "comment_futures": ANY, "is_alias": False, "alias_for": None, "trade_modes": [{"trading_mode": "spot", "margin_mode": ""}], @@ -2583,6 +2587,7 @@ def test_api_exchanges(botclient): "supported": False, "dex": True, "comment": ANY, + "comment_futures": ANY, "is_alias": False, "alias_for": None, "trade_modes": [{"trading_mode": "spot", "margin_mode": ""}], diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py index 58473e9c040..04d218bc64a 100644 --- a/tests/strategy/strats/strategy_test_v2.py +++ b/tests/strategy/strats/strategy_test_v2.py @@ -14,6 +14,16 @@ class StrategyTestV2(IStrategy): Please look at the SampleStrategy in the user_data/strategy directory or strategy repository https://github.com/freqtrade/freqtrade-strategies for samples and inspiration. + + --- + + Some test asian characters. + Ensures that unicode characters are handled correctly when reading strategy files. + Otherwise this may break on windows systems. + All roughly translate to "hello world". + chinese string: "你好世界" + korean string: "안녕하세요,세계" + japanese string: "こんにちは、世界" """ INTERFACE_VERSION = 2 diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index e87810111a9..993b04d4bdb 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -23,6 +23,16 @@ class StrategyTestV3(IStrategy): Please look at the SampleStrategy in the user_data/strategy directory or strategy repository https://github.com/freqtrade/freqtrade-strategies for samples and inspiration. + + --- + + Some test asian characters. + Ensures that unicode characters are handled correctly when reading strategy files. + Otherwise this may break on windows systems. + All roughly translate to "hello world". + chinese string: "你好世界" + korean string: "안녕하세요,세계" + japanese string: "こんにちは、世界" """ INTERFACE_VERSION = 3 diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 1cad0e2b439..5eb933f4480 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1047,7 +1047,7 @@ def test_pandas_warning_direct(ohlcv_history, function, raises, recwarn): # Fixed in 2.2.x getattr(_STRATEGY, function)(df, {"pair": "ETH/BTC"}) else: - assert len(recwarn) == 0 + assert len(recwarn) == 0, f"warnings: {', '.join(recwarn.list)}" getattr(_STRATEGY, function)(df, {"pair": "ETH/BTC"}) @@ -1055,4 +1055,4 @@ def test_pandas_warning_direct(ohlcv_history, function, raises, recwarn): def test_pandas_warning_through_analyze_pair(ohlcv_history, mocker, recwarn): mocker.patch.object(_STRATEGY.dp, "ohlcv", return_value=ohlcv_history) _STRATEGY.analyze_pair("ETH/BTC") - assert len(recwarn) == 0 + assert len(recwarn) == 0, f"warnings: {', '.join(recwarn.list)}" diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py index 48f1d27d7d1..3c53f425e3f 100644 --- a/tests/test_strategy_updater.py +++ b/tests/test_strategy_updater.py @@ -15,7 +15,7 @@ def test_strategy_updater_start(user_dir, capsys) -> None: tmpdirp = Path(user_dir) / "strategies" tmpdirp.mkdir(parents=True, exist_ok=True) shutil.copy(teststrats / "strategy_test_v2.py", tmpdirp) - old_code = (teststrats / "strategy_test_v2.py").read_text() + old_code = (teststrats / "strategy_test_v2.py").read_text(encoding="utf-8") args = ["strategy-updater", "--userdir", str(user_dir), "--strategy-list", "StrategyTestV2"] pargs = get_args(args) @@ -29,7 +29,7 @@ def test_strategy_updater_start(user_dir, capsys) -> None: # updated file exists new_file = tmpdirp / "strategy_test_v2.py" assert new_file.exists() - new_code = new_file.read_text() + new_code = new_file.read_text(encoding="utf-8") assert "INTERFACE_VERSION = 3" in new_code assert "INTERFACE_VERSION = 2" in old_code captured = capsys.readouterr() diff --git a/tests/util/test_binance_mig.py b/tests/util/test_binance_mig.py deleted file mode 100644 index db3d8b282b3..00000000000 --- a/tests/util/test_binance_mig.py +++ /dev/null @@ -1,65 +0,0 @@ -import shutil - -import pytest - -from freqtrade.persistence import Trade -from freqtrade.util.migrations import migrate_binance_futures_data, migrate_data -from freqtrade.util.migrations.binance_mig import migrate_binance_futures_names -from tests.conftest import create_mock_trades_usdt, log_has - - -def test_binance_mig_data_conversion(default_conf_usdt, tmp_path, testdatadir): - # call doing nothing (spot mode) - migrate_binance_futures_data(default_conf_usdt) - default_conf_usdt["trading_mode"] = "futures" - pair_old = "XRP_USDT" - pair_unified = "XRP_USDT_USDT" - futures_src = testdatadir / "futures" - futures_dst = tmp_path / "futures" - futures_dst.mkdir() - files = [ - "-1h-mark.feather", - "-1h-futures.feather", - "-1h-funding_rate.feather", - "-1h-mark.feather", - ] - - # Copy files to tmpdir and rename to old naming - for file in files: - fn_after = futures_dst / f"{pair_old}{file}" - shutil.copy(futures_src / f"{pair_unified}{file}", fn_after) - - default_conf_usdt["datadir"] = tmp_path - # Migrate files to unified namings - migrate_binance_futures_data(default_conf_usdt) - - for file in files: - fn_after = futures_dst / f"{pair_unified}{file}" - assert fn_after.exists() - - -@pytest.mark.usefixtures("init_persistence") -def test_binance_mig_db_conversion(default_conf_usdt, fee, caplog): - # Does nothing in spot mode - migrate_binance_futures_names(default_conf_usdt) - - create_mock_trades_usdt(fee, None) - - for t in Trade.get_trades(): - t.trading_mode = "FUTURES" - t.exchange = "binance" - Trade.commit() - - default_conf_usdt["trading_mode"] = "futures" - migrate_binance_futures_names(default_conf_usdt) - assert log_has("Migrating binance futures pairs in database.", caplog) - - -def test_migration_wrapper(default_conf_usdt, mocker): - default_conf_usdt["trading_mode"] = "futures" - binmock = mocker.patch("freqtrade.util.migrations.migrate_binance_futures_data") - funding_mock = mocker.patch("freqtrade.util.migrations.migrate_funding_fee_timeframe") - migrate_data(default_conf_usdt) - - assert binmock.call_count == 1 - assert funding_mock.call_count == 1