Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
033050e
read text encoding='utf-8'
dev-starlight Nov 11, 2025
8caf7a8
Add UTF-8 encoding to read_text method
dev-starlight Nov 11, 2025
33dbe32
Merge branch 'develop' into pr/dev-starlight/12506
xmatthias Nov 27, 2025
6cef739
chore: apply formatting
xmatthias Nov 27, 2025
1380940
chore: remove binance pair naming migration
xmatthias Jan 2, 2026
40f87c0
test: remove binance pair naming tests
xmatthias Jan 2, 2026
795df94
Merge pull request #12679 from freqtrade/maint/binance_mig
xmatthias Jan 2, 2026
9cdd865
fix: "missing" condition when no replacemet items are available
xmatthias Jan 2, 2026
6c16468
refactor: extract "has" checking logic to separate function
xmatthias Jan 2, 2026
a174461
test: add test for exchange_has validation
xmatthias Jan 2, 2026
f78aa7b
feat: has_optional should check for replacement methods
xmatthias Jan 2, 2026
34ad58e
feat: enable fetch_orders optional check
xmatthias Jan 2, 2026
04f4f32
refactor: improve punctuation handling on bad exchanges
xmatthias Jan 2, 2026
2ec2626
feat: blacklist kraken-futures - it's futures only
xmatthias Jan 2, 2026
0e82c04
feat: improve output of list-exchanges
xmatthias Jan 2, 2026
f2dfe17
feat: split futures "has" parameters to a separate variable
xmatthias Jan 2, 2026
4d4fee9
feat: add futures validation to validate_exchange
xmatthias Jan 2, 2026
a09ca38
feat: list-exchanges to show futures "has" problematics
xmatthias Jan 2, 2026
8a33b71
test: update exchange api test
xmatthias Jan 2, 2026
4b27894
test: small improvements to test clarity
xmatthias Jan 2, 2026
3543437
test: add test chinese comment to strategy_test file
xmatthias Jan 2, 2026
bc2be74
fix: further fixes for utf8 reading on windows
xmatthias Jan 2, 2026
00bd3e5
fix: strategy-updater should support non-english characters on windows
xmatthias Jan 2, 2026
577464c
Merge pull request #12506 from dev-starlight/develop
xmatthias Jan 2, 2026
f82d2fe
chore: simplify conversion rate usage
xmatthias Jan 2, 2026
5813057
fix: use "triggered order" id where necessary
xmatthias Jan 2, 2026
fcab946
test: reduce startup time failure rates
xmatthias Jan 2, 2026
9a9b4e1
test: pandas warning tests to show warnings
xmatthias Jan 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion freqtrade/commands/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down
9 changes: 8 additions & 1 deletion freqtrade/commands/cli_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion freqtrade/commands/list_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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])
Expand Down
2 changes: 1 addition & 1 deletion freqtrade/configuration/load_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
8 changes: 5 additions & 3 deletions freqtrade/exchange/check_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
61 changes: 33 additions & 28 deletions freqtrade/exchange/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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):
Expand Down
13 changes: 11 additions & 2 deletions freqtrade/exchange/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
47 changes: 33 additions & 14 deletions freqtrade/exchange/exchange_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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 = {
Expand All @@ -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
Expand Down
6 changes: 1 addition & 5 deletions freqtrade/exchange/gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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")
6 changes: 0 additions & 6 deletions freqtrade/exchange/okx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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 = []

Expand Down
1 change: 1 addition & 0 deletions freqtrade/ft_types/valid_exchanges_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion freqtrade/resolvers/iresolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 2 additions & 4 deletions freqtrade/strategy/strategyupdater.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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):
Expand Down
Loading
Loading