From c3032feaf7e3e974cb9cc743f559ebba170a807c Mon Sep 17 00:00:00 2001 From: Alexander Malysh Date: Thu, 14 Nov 2024 10:25:14 +0000 Subject: [PATCH 01/65] * allow json in env variables --- freqtrade/configuration/environment_vars.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/freqtrade/configuration/environment_vars.py b/freqtrade/configuration/environment_vars.py index 37445538d..5baa2bfc3 100644 --- a/freqtrade/configuration/environment_vars.py +++ b/freqtrade/configuration/environment_vars.py @@ -1,3 +1,4 @@ +import json import logging import os from typing import Any @@ -20,6 +21,11 @@ def _get_var_typed(val): return True elif val.lower() in ("f", "false"): return False + # try to convert from json + try: + return json.loads(val) + except json.decoder.JSONDecodeError: + pass # keep as string return val From 92e64927b01b3c7d295ea953a140caa76d4e0bc5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Nov 2024 07:02:54 +0100 Subject: [PATCH 02/65] feat: allow object as dry-run balance --- build_helpers/schema.json | 13 +++++++++++-- freqtrade/configuration/config_schema.py | 4 +++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/build_helpers/schema.json b/build_helpers/schema.json index e12b0bf0d..73c06ba46 100644 --- a/build_helpers/schema.json +++ b/build_helpers/schema.json @@ -102,8 +102,17 @@ }, "dry_run_wallet": { "description": "Initial wallet balance for dry run mode.", - "type": "number", - "default": 1000 + "type": [ + "number", + "object" + ], + "default": 1000, + "patternProperties": { + "^[a-zA-Z0-9]+$": { + "type": "number" + } + }, + "additionalProperties": false }, "cancel_open_orders_on_exit": { "description": "Cancel open orders when exiting.", diff --git a/freqtrade/configuration/config_schema.py b/freqtrade/configuration/config_schema.py index 30f1f6f28..d31069e7b 100644 --- a/freqtrade/configuration/config_schema.py +++ b/freqtrade/configuration/config_schema.py @@ -85,8 +85,10 @@ CONF_SCHEMA = { }, "dry_run_wallet": { "description": "Initial wallet balance for dry run mode.", - "type": "number", + "type": ["number", "object"], "default": DRY_RUN_WALLET, + "patternProperties": {r"^[a-zA-Z0-9]+$": {"type": "number"}}, + "additionalProperties": False, }, "cancel_open_orders_on_exit": { "description": "Cancel open orders when exiting.", From 5b0be7e1a9f94a31d1336dcb30f280c712d442c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Nov 2024 07:03:54 +0100 Subject: [PATCH 03/65] feat: support dict like dry_run_wallet --- freqtrade/wallets.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 2b26fb8b6..f2cff8792 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -41,7 +41,14 @@ class Wallets: self._exchange = exchange self._wallets: dict[str, Wallet] = {} self._positions: dict[str, PositionWallet] = {} - self._start_cap = config["dry_run_wallet"] + self._start_cap: dict[str, float] = {} + self._stake_currency = config["stake_currency"] + + if isinstance(_start_cap := config["dry_run_wallet"], float | int): + self._start_cap[self._stake_currency] = _start_cap + else: + self._start_cap = _start_cap + self._last_wallet_refresh: datetime | None = None self.update() @@ -112,7 +119,7 @@ class Wallets: _wallets[curr] = Wallet(curr, trade.amount - pending, pending, trade.amount) - current_stake = self._start_cap + tot_profit - tot_in_trades + current_stake = self._start_cap[self._stake_currency] + tot_profit - tot_in_trades total_stake = current_stake + used_stake else: tot_in_trades = 0 @@ -129,12 +136,13 @@ class Wallets: collateral=collateral, side=position.trade_direction, ) - current_stake = self._start_cap + tot_profit - tot_in_trades + current_stake = self._start_cap[self._stake_currency] + tot_profit - tot_in_trades + used_stake = tot_in_trades total_stake = current_stake + tot_in_trades - _wallets[self._config["stake_currency"]] = Wallet( - currency=self._config["stake_currency"], + _wallets[self._stake_currency] = Wallet( + currency=self._stake_currency, free=current_stake, used=used_stake, total=total_stake, From b4b6de4e0da6598ce458db9f5115a3ac143e2eb9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Nov 2024 07:11:26 +0100 Subject: [PATCH 04/65] test: update test ... --- tests/test_wallets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 2c12d27b5..3b0a88bad 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -168,7 +168,7 @@ def test_get_trade_stake_amount_unlimited_amount( assert result == 0 freqtrade.config["dry_run_wallet"] = 200 - freqtrade.wallets._start_cap = 200 + freqtrade.wallets._start_cap["BTC"] = 200 result = freqtrade.wallets.get_trade_stake_amount("XRP/USDT", 3) assert round(result, 4) == round(result2, 4) From 15c1a8ee0bf98eab4909796ddd941fa9f7edbadb Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Nov 2024 07:13:05 +0100 Subject: [PATCH 05/65] chore: reduce dict lookups, reuse attribute --- freqtrade/wallets.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index f2cff8792..ec79e25e3 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -252,7 +252,7 @@ class Wallets: else: tot_profit = Trade.get_total_closed_profit() open_stakes = Trade.total_open_trades_stakes() - available_balance = self.get_free(self._config["stake_currency"]) + available_balance = self.get_free(self._stake_currency) return available_balance - tot_profit + open_stakes def get_total_stake_amount(self): @@ -272,9 +272,9 @@ class Wallets: # Ensure % is used from the overall balance # Otherwise we'd risk lowering stakes with each open trade. # (tied up + current free) * ratio) - tied up - available_amount = ( - val_tied_up + self.get_free(self._config["stake_currency"]) - ) * self._config["tradable_balance_ratio"] + available_amount = (val_tied_up + self.get_free(self._stake_currency)) * self._config[ + "tradable_balance_ratio" + ] return available_amount def get_available_stake_amount(self) -> float: @@ -285,7 +285,7 @@ class Wallets: ( + free amount) * tradable_balance_ratio - """ - free = self.get_free(self._config["stake_currency"]) + free = self.get_free(self._stake_currency) return min(self.get_total_stake_amount() - Trade.total_open_trades_stakes(), free) def _calculate_unlimited_stake_amount( @@ -344,8 +344,8 @@ class Wallets: if edge: stake_amount = edge.stake_amount( pair, - self.get_free(self._config["stake_currency"]), - self.get_total(self._config["stake_currency"]), + self.get_free(self._stake_currency), + self.get_total(self._stake_currency), val_tied_up, ) else: From 3fc259bb9b57995889feb0489f8fa00f1bc52ffa Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Nov 2024 07:23:57 +0100 Subject: [PATCH 06/65] feat: add non-trading balance to wallet --- freqtrade/wallets.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index ec79e25e3..39b7070ec 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -116,8 +116,14 @@ class Wallets: for o in trade.open_orders if o.amount and o.ft_order_side == trade.exit_side ) + curr_wallet_bal = self._start_cap.get(curr, 0) - _wallets[curr] = Wallet(curr, trade.amount - pending, pending, trade.amount) + _wallets[curr] = Wallet( + curr, + curr_wallet_bal + trade.amount - pending, + pending, + trade.amount + curr_wallet_bal, + ) current_stake = self._start_cap[self._stake_currency] + tot_profit - tot_in_trades total_stake = current_stake + used_stake @@ -147,6 +153,11 @@ class Wallets: used=used_stake, total=total_stake, ) + for currency in self._start_cap: + if currency not in _wallets: + bal = self._start_cap[currency] + _wallets[currency] = Wallet(currency, bal, 0, bal) + self._wallets = _wallets self._positions = _positions From 37aba6f7d5a96670a776a49ddb35089240b07e27 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Nov 2024 20:04:09 +0100 Subject: [PATCH 07/65] feat: Allow fetch_tickers from different marketsegment --- freqtrade/exchange/binance.py | 10 ++++++++-- freqtrade/exchange/exchange.py | 23 ++++++++++++++++++----- freqtrade/exchange/kraken.py | 10 ++++++++-- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 64b597fe8..151736e1b 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -52,8 +52,14 @@ class Binance(Exchange): (TradingMode.FUTURES, MarginMode.ISOLATED) ] - def get_tickers(self, symbols: list[str] | None = None, *, cached: bool = False) -> Tickers: - tickers = super().get_tickers(symbols=symbols, cached=cached) + def get_tickers( + self, + symbols: list[str] | None = None, + *, + cached: bool = False, + market_type: TradingMode | None = None, + ) -> Tickers: + tickers = super().get_tickers(symbols=symbols, cached=cached, market_type=market_type) if self.trading_mode == TradingMode.FUTURES: # Binance's future result has no bid/ask values. # Therefore we must fetch that from fetch_bids_asks and combine the two results. diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a3006c99b..e7be5394b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -201,7 +201,7 @@ class Exchange: self._cache_lock = Lock() # Cache for 10 minutes ... - self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=2, ttl=60 * 10) + self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=4, ttl=60 * 10) # Cache values for 300 to avoid frequent polling of the exchange for prices # Caching only applies to RPC methods, so prices for open trades are still # refreshed once every iteration. @@ -1801,24 +1801,37 @@ class Exchange: raise OperationalException(e) from e @retrier - def get_tickers(self, symbols: list[str] | None = None, *, cached: bool = False) -> Tickers: + def get_tickers( + self, + symbols: list[str] | None = None, + *, + cached: bool = False, + market_type: TradingMode | None = None, + ) -> Tickers: """ :param symbols: List of symbols to fetch :param cached: Allow cached result + :param market_type: Market type to fetch - either spot or futures. :return: fetch_tickers result """ tickers: Tickers if not self.exchange_has("fetchTickers"): return {} + cache_key = f"fetch_tickers_{market_type}" if market_type else "fetch_tickers" if cached: with self._cache_lock: - tickers = self._fetch_tickers_cache.get("fetch_tickers") # type: ignore + tickers = self._fetch_tickers_cache.get(cache_key) # type: ignore if tickers: return tickers try: - tickers = self._api.fetch_tickers(symbols) + # Re-map futures to swap + market_types = { + TradingMode.FUTURES: "swap", + } + params = {"type": market_types.get(market_type, market_type)} if market_type else {} + tickers = self._api.fetch_tickers(symbols, params) with self._cache_lock: - self._fetch_tickers_cache["fetch_tickers"] = tickers + self._fetch_tickers_cache[cache_key] = tickers return tickers except ccxt.NotSupported as e: raise OperationalException( diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index b2035fcef..53fce867b 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -50,11 +50,17 @@ class Kraken(Exchange): return parent_check and market.get("darkpool", False) is False - def get_tickers(self, symbols: list[str] | None = None, *, cached: bool = False) -> Tickers: + def get_tickers( + self, + symbols: list[str] | None = None, + *, + cached: bool = False, + market_type: TradingMode | None = None, + ) -> Tickers: # Only fetch tickers for current stake currency # Otherwise the request for kraken becomes too large. symbols = list(self.get_markets(quote_currencies=[self._config["stake_currency"]])) - return super().get_tickers(symbols=symbols, cached=cached) + return super().get_tickers(symbols=symbols, cached=cached, market_type=market_type) @retrier def get_balances(self) -> CcxtBalances: From 2bb111605c9daed19ae61200eb3b9cfb4dd31c35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Nov 2024 20:06:28 +0100 Subject: [PATCH 08/65] feat: update rpc_balance to fetch spot tickers Happens if there is no market for the given pair --- freqtrade/rpc/rpc.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index afa8baea2..d4c05d2d2 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -7,7 +7,7 @@ from abc import abstractmethod from collections.abc import Generator, Sequence from datetime import date, datetime, timedelta, timezone from math import isnan -from typing import Any, cast +from typing import Any import psutil from dateutil.relativedelta import relativedelta @@ -682,12 +682,23 @@ class RPC: est_bot_stake = amount else: pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) - rate: float | None = cast(Ticker, tickers.get(pair, {})).get("last", None) - if rate: - if pair.startswith(stake_currency) and not pair.endswith(stake_currency): - rate = 1.0 / rate - est_stake = rate * balance.total - est_bot_stake = rate * amount + ticker: Ticker | None = tickers.get(pair, None) + if not ticker: + tickers_spot: Tickers = self._freqtrade.exchange.get_tickers( + cached=True, + market_type=TradingMode.SPOT + if self._config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT + else TradingMode.FUTURES, + ) + ticker = tickers_spot.get(pair, None) + + if ticker: + rate: float | None = ticker.get("last", None) + if rate: + if pair.startswith(stake_currency) and not pair.endswith(stake_currency): + rate = 1.0 / rate + est_stake = rate * balance.total + est_bot_stake = rate * amount return est_stake, est_bot_stake From e6e193f2520a824276649d7c11f90413d1154d3f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Nov 2024 20:17:56 +0100 Subject: [PATCH 09/65] test: assert tickers is called a 2nd time if necessary --- tests/rpc/test_rpc.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index dd8c1bb9a..efdc18977 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -530,6 +530,13 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): "total": 5.0, "used": 4.0, }, + # Invalid coin not in tickers list. + # This triggers a 2nd call to get_tickers + "NotACoin": { + "free": 0.0, + "total": 2.0, + "used": 0.0, + }, "USDT": { "free": 50.0, "total": 100.0, @@ -590,8 +597,10 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): assert pytest.approx(result["total"]) == 2824.83464 assert pytest.approx(result["value"]) == 2824.83464 * 1.2 - assert tickers.call_count == 1 + assert tickers.call_count == 2 assert tickers.call_args_list[0][1]["cached"] is True + # Testing futures - so we should get spot tickers + assert tickers.call_args_list[1][1]["market_type"] == "spot" assert "USD" == result["symbol"] assert result["currencies"] == [ { @@ -622,6 +631,20 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): "is_bot_managed": False, "is_position": False, }, + { + "currency": "NotACoin", + "balance": 2.0, + "bot_owned": 0, + "est_stake": 0, + "est_stake_bot": 0, + "free": 0.0, + "is_bot_managed": False, + "is_position": False, + "position": 0, + "side": "long", + "stake": "USDT", + "used": 0.0, + }, { "currency": "USDT", "free": 50.0, From 7369331e2dbdc527ac24e9791e9815d418ba8fc9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Nov 2024 20:35:40 +0100 Subject: [PATCH 10/65] tests: add test for multi-pair dry-run wallets --- tests/test_wallets.py | 60 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 3b0a88bad..d5e49438a 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -451,3 +451,63 @@ def test_check_exit_amount_futures(mocker, default_conf, fee): assert freqtrade.wallets.check_exit_amount(trade) is False assert total_mock.call_count == 0 assert update_mock.call_count == 1 + + +@pytest.mark.parametrize( + "config,wallets", + [ + ( + {"stake_currency": "USDT", "dry_run_wallet": 1000.0}, + {"USDT": {"currency": "USDT", "free": 1000.0, "used": 0.0, "total": 1000.0}}, + ), + ( + {"stake_currency": "USDT", "dry_run_wallet": {"USDT": 1000.0, "BTC": 0.1, "ETH": 2.0}}, + { + "USDT": {"currency": "USDT", "free": 1000.0, "used": 0.0, "total": 1000.0}, + "BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1}, + "ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0}, + }, + ), + ], +) +def test_dry_run_wallet_initialization(mocker, default_conf_usdt, config, wallets): + default_conf_usdt.update(config) + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + + # Verify each wallet matches the expected values + for currency, expected_wallet in wallets.items(): + wallet = freqtrade.wallets._wallets[currency] + assert wallet.currency == expected_wallet["currency"] + assert wallet.free == expected_wallet["free"] + assert wallet.used == expected_wallet["used"] + assert wallet.total == expected_wallet["total"] + + # Verify no extra wallets were created + assert len(freqtrade.wallets._wallets) == len(wallets) + + # Create a trade and verify the new currency is added to the wallets + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.0) + mocker.patch(f"{EXMS}.get_rate", return_value=2.22) + mocker.patch( + f"{EXMS}.fetch_ticker", + return_value={ + "bid": 0.20, + "ask": 0.22, + "last": 0.22, + }, + ) + freqtrade.execute_entry("NEO/USDT", 100.0) + + # Update wallets and verify NEO is now included + freqtrade.wallets.update() + assert "NEO" in freqtrade.wallets._wallets + + assert freqtrade.wallets._wallets["NEO"].total == 45.04504504 # 100 USDT / 0.22 + assert freqtrade.wallets._wallets["NEO"].used == 0.0 + assert freqtrade.wallets._wallets["NEO"].free == 45.04504504 + + # Verify USDT wallet was reduced by trade amount + assert ( + pytest.approx(freqtrade.wallets._wallets["USDT"].total) == wallets["USDT"]["total"] - 100.0 + ) + assert len(freqtrade.wallets._wallets) == len(wallets) + 1 # Original wallets + NEO From 671821aeb3ec1a4fdee4c94b8b9931fb323ee976 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Nov 2024 06:48:07 +0100 Subject: [PATCH 11/65] docs: Add documentation for dry-run-wallet as dict --- docs/configuration.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 9c46b7546..cfce38176 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -168,7 +168,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `timeframe` | The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). Usually missing in configuration, and specified in the strategy. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** String | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency).
**Datatype:** String | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
*Defaults to `true`.*
**Datatype:** Boolean -| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.
*Defaults to `1000`.*
**Datatype:** Float +| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode. [More information below](#dry-run-wallet)
*Defaults to `1000`.*
**Datatype:** Float or Dict | `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions.
*Defaults to `false`.*
**Datatype:** Boolean | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `true`.*
**Datatype:** Boolean | `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to exit a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict @@ -323,6 +323,25 @@ To limit this calculation in case of large stoploss values, the calculated minim !!! Warning Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange. Freqtrade adjusts the stake-amount to this value, unless it's > 30% more than the calculated/desired stake-amount - in which case the trade is rejected. +#### Dry-run wallet + +When running in dry-run mode, the bot will use a simulated wallet to execute trades. The starting balance of this wallet is defined by `dry_run_wallet` (defaults to 1000). +For more complex scenarios, you can also assign a dictionary to `dry_run_wallet` to define the starting balance for each currency. + +```json +"dry_run_wallet": { + "BTC": 0.01, + "ETH": 2, + "USDT": 1000 +} +``` + +Command line options (`--dry-run-wallet`) can be used to override the configuration value, but only for the float value, not for the dictionary. If you'd like to use the dictionary, please adjust the configuration file. + +!!! Note + Balances not in stake-currency will not be used for trading, but are shown as part of the wallet balance. + On Cross-margin exchanges, the wallet balance may be used to calculate the available collateral for trading. + #### Tradable balance By default, the bot assumes that the `complete amount - 1%` is at it's disposal, and when using [dynamic stake amount](#dynamic-stake-amount), it will split the complete balance into `max_open_trades` buckets per trade. From 09308e568d1b8c40b71979b55f4417f48d3c593a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Nov 2024 07:14:11 +0100 Subject: [PATCH 12/65] fix: increase code reliability by not relying on stake-currency to be in the dict --- freqtrade/wallets.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 39b7070ec..9864d0604 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -125,7 +125,9 @@ class Wallets: trade.amount + curr_wallet_bal, ) - current_stake = self._start_cap[self._stake_currency] + tot_profit - tot_in_trades + current_stake = ( + self._start_cap.get(self._stake_currency, 0) + tot_profit - tot_in_trades + ) total_stake = current_stake + used_stake else: tot_in_trades = 0 @@ -142,7 +144,9 @@ class Wallets: collateral=collateral, side=position.trade_direction, ) - current_stake = self._start_cap[self._stake_currency] + tot_profit - tot_in_trades + current_stake = ( + self._start_cap.get(self._stake_currency, 0) + tot_profit - tot_in_trades + ) used_stake = tot_in_trades total_stake = current_stake + tot_in_trades From 6c9e6cc1983db4919743034b6d0c1bf119f554f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 06:00:19 +0000 Subject: [PATCH 13/65] chore(deps): bump xgboost from 2.0.3 to 2.1.3 Bumps [xgboost](https://github.com/dmlc/xgboost) from 2.0.3 to 2.1.3. - [Release notes](https://github.com/dmlc/xgboost/releases) - [Changelog](https://github.com/dmlc/xgboost/blob/master/NEWS.md) - [Commits](https://github.com/dmlc/xgboost/compare/v2.0.3...v2.1.3) --- updated-dependencies: - dependency-name: xgboost dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-freqai.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index ca7181004..47c69ff64 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -10,6 +10,6 @@ catboost==1.2.7; 'arm' not in platform_machine # Temporary downgrade of matplotlib due to https://github.com/matplotlib/matplotlib/issues/28551 matplotlib==3.9.3 lightgbm==4.5.0 -xgboost==2.0.3 +xgboost==2.1.3 tensorboard==2.18.0 datasieve==0.1.7 From c841146968528b4b56466b045e513d240ca0c407 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 Dec 2024 07:12:30 +0100 Subject: [PATCH 14/65] chore: Remove non-suppored callback from XGBRFRegressor According to the XGBoost docs, this probably never worked as it was never supported - but is now raising an error for user clarity. --- .../freqai/prediction_models/XGBoostRFRegressor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py index 12231ed13..140186075 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py @@ -5,7 +5,6 @@ from xgboost import XGBRFRegressor from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.tensorboard import TBCallback logger = logging.getLogger(__name__) @@ -45,7 +44,12 @@ class XGBoostRFRegressor(BaseRegressionModel): model = XGBRFRegressor(**self.model_training_parameters) - model.set_params(callbacks=[TBCallback(dk.data_path)]) + # Callbacks are not supported for XGBRFRegressor, and version 2.1.x started to throw + # the following error: + # NotImplementedError: `early_stopping_rounds` and `callbacks` are not implemented + # for random forest. + + # model.set_params(callbacks=[TBCallback(dk.data_path)]) model.fit( X=X, y=y, @@ -55,6 +59,6 @@ class XGBoostRFRegressor(BaseRegressionModel): xgb_model=xgb_model, ) # set the callbacks to empty so that we can serialize to disk later - model.set_params(callbacks=[]) + # model.set_params(callbacks=[]) return model From c082e5f6a6f5271bf26ba3ed4bc0ec94ed8d2c0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 Dec 2024 07:26:47 +0100 Subject: [PATCH 15/65] feat: add dry_run_wallet helper --- freqtrade/util/__init__.py | 2 ++ freqtrade/util/dry_run_wallet.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 freqtrade/util/dry_run_wallet.py diff --git a/freqtrade/util/__init__.py b/freqtrade/util/__init__.py index 7a4d4d119..a0b618b11 100644 --- a/freqtrade/util/__init__.py +++ b/freqtrade/util/__init__.py @@ -11,6 +11,7 @@ from freqtrade.util.datetime_helpers import ( format_ms_time, shorten_date, ) +from freqtrade.util.dry_run_wallet import get_dry_run_wallet from freqtrade.util.formatters import decimals_per_coin, fmt_coin, fmt_coin2, round_value from freqtrade.util.ft_precise import FtPrecise from freqtrade.util.measure_time import MeasureTime @@ -35,6 +36,7 @@ __all__ = [ "dt_utc", "format_date", "format_ms_time", + "get_dry_run_wallet", "FtPrecise", "PeriodicCache", "shorten_date", diff --git a/freqtrade/util/dry_run_wallet.py b/freqtrade/util/dry_run_wallet.py new file mode 100644 index 000000000..f994f0d4c --- /dev/null +++ b/freqtrade/util/dry_run_wallet.py @@ -0,0 +1,12 @@ +from pytest import Config + + +def get_dry_run_wallet(config: Config) -> int | float: + """ + Return dry-run wallet balance in stake currency from configuration. + This setup also supports dictionary mode for dry-run-wallet. + """ + if isinstance(_start_cap := config["dry_run_wallet"], float | int): + return _start_cap + else: + return _start_cap.get("stake_currency") From 7a8971b9b6051a20978355048f1929a03b0e46d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 Dec 2024 07:27:45 +0100 Subject: [PATCH 16/65] feat: use get_dry_run_wallet helper --- freqtrade/commands/optimize_commands.py | 3 ++- freqtrade/optimize/analysis/lookahead_helpers.py | 4 ++-- freqtrade/optimize/hyperopt_loss/hyperopt_loss_calmar.py | 3 ++- .../hyperopt_loss/hyperopt_loss_max_drawdown_relative.py | 3 ++- .../optimize/hyperopt_loss/hyperopt_loss_multi_metric.py | 3 ++- .../optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py | 3 ++- freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe.py | 3 ++- freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino.py | 3 ++- freqtrade/optimize/optimize_reports/optimize_reports.py | 4 ++-- freqtrade/plot/plotting.py | 3 ++- 10 files changed, 20 insertions(+), 12 deletions(-) diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index ea75f3bd1..788bca489 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -4,6 +4,7 @@ from typing import Any from freqtrade import constants from freqtrade.enums import RunMode from freqtrade.exceptions import ConfigurationError, OperationalException +from freqtrade.util import get_dry_run_wallet logger = logging.getLogger(__name__) @@ -26,7 +27,7 @@ def setup_optimize_configuration(args: dict[str, Any], method: RunMode) -> dict[ RunMode.HYPEROPT: "hyperoptimization", } if method in no_unlimited_runmodes.keys(): - wallet_size = config["dry_run_wallet"] * config["tradable_balance_ratio"] + wallet_size = get_dry_run_wallet(config) * config["tradable_balance_ratio"] # tradable_balance_ratio if ( config["stake_amount"] != constants.UNLIMITED_STAKE_AMOUNT diff --git a/freqtrade/optimize/analysis/lookahead_helpers.py b/freqtrade/optimize/analysis/lookahead_helpers.py index d664f9635..631a9549f 100644 --- a/freqtrade/optimize/analysis/lookahead_helpers.py +++ b/freqtrade/optimize/analysis/lookahead_helpers.py @@ -10,7 +10,7 @@ from freqtrade.constants import Config from freqtrade.exceptions import OperationalException from freqtrade.optimize.analysis.lookahead import LookaheadAnalysis from freqtrade.resolvers import StrategyResolver -from freqtrade.util import print_rich_table +from freqtrade.util import get_dry_run_wallet, print_rich_table logger = logging.getLogger(__name__) @@ -163,7 +163,7 @@ class LookaheadAnalysisSubFunctions: config["max_open_trades"] = len(config["pairs"]) min_dry_run_wallet = 1000000000 - if config["dry_run_wallet"] < min_dry_run_wallet: + if get_dry_run_wallet(config) < min_dry_run_wallet: logger.info( "Dry run wallet was not set to 1 billion, pushing it up there " "just to avoid false positives" diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_calmar.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_calmar.py index f22d59e50..6072629c4 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_calmar.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_calmar.py @@ -12,6 +12,7 @@ from pandas import DataFrame from freqtrade.constants import Config from freqtrade.data.metrics import calculate_calmar from freqtrade.optimize.hyperopt import IHyperOptLoss +from freqtrade.util import get_dry_run_wallet class CalmarHyperOptLoss(IHyperOptLoss): @@ -36,7 +37,7 @@ class CalmarHyperOptLoss(IHyperOptLoss): Uses Calmar Ratio calculation. """ - starting_balance = config["dry_run_wallet"] + starting_balance = get_dry_run_wallet(config) calmar_ratio = calculate_calmar(results, min_date, max_date, starting_balance) # print(expected_returns_mean, max_drawdown, calmar_ratio) return -calmar_ratio diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_relative.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_relative.py index ee7088d75..4bbbcf7d3 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_relative.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_relative.py @@ -10,6 +10,7 @@ from pandas import DataFrame from freqtrade.constants import Config from freqtrade.data.metrics import calculate_underwater from freqtrade.optimize.hyperopt import IHyperOptLoss +from freqtrade.util import get_dry_run_wallet class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss): @@ -31,7 +32,7 @@ class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss): total_profit = results["profit_abs"].sum() try: drawdown_df = calculate_underwater( - results, value_col="profit_abs", starting_balance=config["dry_run_wallet"] + results, value_col="profit_abs", starting_balance=get_dry_run_wallet(config) ) max_drawdown = abs(min(drawdown_df["drawdown"])) relative_drawdown = max(drawdown_df["drawdown_relative"]) diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_multi_metric.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_multi_metric.py index de8d117d6..dd5fa4a17 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_multi_metric.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_multi_metric.py @@ -36,6 +36,7 @@ from pandas import DataFrame from freqtrade.constants import Config from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown from freqtrade.optimize.hyperopt import IHyperOptLoss +from freqtrade.util import get_dry_run_wallet # smaller numbers penalize drawdowns more severely @@ -83,7 +84,7 @@ class MultiMetricHyperOptLoss(IHyperOptLoss): # Calculate drawdown try: drawdown = calculate_max_drawdown( - results, starting_balance=config["dry_run_wallet"], value_col="profit_abs" + results, starting_balance=get_dry_run_wallet(config), value_col="profit_abs" ) relative_account_drawdown = drawdown.relative_account_drawdown except ValueError: diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py index 61e2a6d32..5230408a9 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py @@ -13,6 +13,7 @@ from pandas import DataFrame from freqtrade.constants import Config from freqtrade.data.metrics import calculate_max_drawdown from freqtrade.optimize.hyperopt import IHyperOptLoss +from freqtrade.util import get_dry_run_wallet # smaller numbers penalize drawdowns more severely @@ -26,7 +27,7 @@ class ProfitDrawDownHyperOptLoss(IHyperOptLoss): try: drawdown = calculate_max_drawdown( - results, starting_balance=config["dry_run_wallet"], value_col="profit_abs" + results, starting_balance=get_dry_run_wallet(config), value_col="profit_abs" ) relative_account_drawdown = drawdown.relative_account_drawdown except ValueError: diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe.py index 2c7042a8a..4806ddb7f 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe.py @@ -12,6 +12,7 @@ from pandas import DataFrame from freqtrade.constants import Config from freqtrade.data.metrics import calculate_sharpe from freqtrade.optimize.hyperopt import IHyperOptLoss +from freqtrade.util.dry_run_wallet import get_dry_run_wallet class SharpeHyperOptLoss(IHyperOptLoss): @@ -36,7 +37,7 @@ class SharpeHyperOptLoss(IHyperOptLoss): Uses Sharpe Ratio calculation. """ - starting_balance = config["dry_run_wallet"] + starting_balance = get_dry_run_wallet(config) sharp_ratio = calculate_sharpe(results, min_date, max_date, starting_balance) # print(expected_returns_mean, up_stdev, sharp_ratio) return -sharp_ratio diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino.py index 32ff0c73f..c01be889d 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino.py @@ -12,6 +12,7 @@ from pandas import DataFrame from freqtrade.constants import Config from freqtrade.data.metrics import calculate_sortino from freqtrade.optimize.hyperopt import IHyperOptLoss +from freqtrade.util import get_dry_run_wallet class SortinoHyperOptLoss(IHyperOptLoss): @@ -36,7 +37,7 @@ class SortinoHyperOptLoss(IHyperOptLoss): Uses Sortino Ratio calculation. """ - starting_balance = config["dry_run_wallet"] + starting_balance = get_dry_run_wallet(config) sortino_ratio = calculate_sortino(results, min_date, max_date, starting_balance) # print(expected_returns_mean, down_stdev, sortino_ratio) return -sortino_ratio diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index 8f1f0140a..532abed39 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -18,7 +18,7 @@ from freqtrade.data.metrics import ( calculate_sortino, ) from freqtrade.ft_types import BacktestResultType -from freqtrade.util import decimals_per_coin, fmt_coin +from freqtrade.util import decimals_per_coin, fmt_coin, get_dry_run_wallet logger = logging.getLogger(__name__) @@ -373,7 +373,7 @@ def generate_strategy_stats( return {} config = content["config"] max_open_trades = min(config["max_open_trades"], len(pairlist)) - start_balance = config["dry_run_wallet"] + start_balance = get_dry_run_wallet(config) stake_currency = config["stake_currency"] pair_results = generate_pair_metrics( diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index cf86f070d..8ec7c7bae 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -28,6 +28,7 @@ from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy import IStrategy from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper +from freqtrade.util import get_dry_run_wallet logger = logging.getLogger(__name__) @@ -706,7 +707,7 @@ def plot_profit(config: Config) -> None: trades, config["timeframe"], config.get("stake_currency", ""), - config.get("available_capital", config["dry_run_wallet"]), + config.get("available_capital", get_dry_run_wallet(config)), ) store_plot_file( fig, From b0b73bf166d7545f361524ca3b714f582c50de79 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 Dec 2024 19:15:57 +0100 Subject: [PATCH 17/65] feat: add starting_balance as argument to hyperopt_loss_function --- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 2 ++ freqtrade/optimize/hyperopt_loss/hyperopt_loss_calmar.py | 3 +-- .../optimize/hyperopt_loss/hyperopt_loss_interface.py | 1 + .../hyperopt_loss/hyperopt_loss_max_drawdown_relative.py | 7 ++++--- .../optimize/hyperopt_loss/hyperopt_loss_multi_metric.py | 4 ++-- .../hyperopt_loss/hyperopt_loss_profit_drawdown.py | 7 ++++--- freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe.py | 6 +----- freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino.py | 6 +----- tests/optimize/test_hyperoptloss.py | 1 + 9 files changed, 17 insertions(+), 20 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index bd234aa57..c8d18d224 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -28,6 +28,7 @@ from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLo from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer, HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver +from freqtrade.util.dry_run_wallet import get_dry_run_wallet # Suppress scikit-learn FutureWarnings from skopt @@ -363,6 +364,7 @@ class HyperOptimizer: config=self.config, processed=processed, backtest_stats=strat_stats, + starting_balance=get_dry_run_wallet(self.config), ) return { "loss": loss, diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_calmar.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_calmar.py index 6072629c4..2a04d4070 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_calmar.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_calmar.py @@ -12,7 +12,6 @@ from pandas import DataFrame from freqtrade.constants import Config from freqtrade.data.metrics import calculate_calmar from freqtrade.optimize.hyperopt import IHyperOptLoss -from freqtrade.util import get_dry_run_wallet class CalmarHyperOptLoss(IHyperOptLoss): @@ -29,6 +28,7 @@ class CalmarHyperOptLoss(IHyperOptLoss): min_date: datetime, max_date: datetime, config: Config, + starting_balance: float, *args, **kwargs, ) -> float: @@ -37,7 +37,6 @@ class CalmarHyperOptLoss(IHyperOptLoss): Uses Calmar Ratio calculation. """ - starting_balance = get_dry_run_wallet(config) calmar_ratio = calculate_calmar(results, min_date, max_date, starting_balance) # print(expected_returns_mean, max_drawdown, calmar_ratio) return -calmar_ratio diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_interface.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_interface.py index a48fee731..ddc96d6a0 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_interface.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_interface.py @@ -31,6 +31,7 @@ class IHyperOptLoss(ABC): config: Config, processed: dict[str, DataFrame], backtest_stats: dict[str, Any], + starting_balance: float, **kwargs, ) -> float: """ diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_relative.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_relative.py index 4bbbcf7d3..a753263a3 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_relative.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_relative.py @@ -10,7 +10,6 @@ from pandas import DataFrame from freqtrade.constants import Config from freqtrade.data.metrics import calculate_underwater from freqtrade.optimize.hyperopt import IHyperOptLoss -from freqtrade.util import get_dry_run_wallet class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss): @@ -22,7 +21,9 @@ class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss): """ @staticmethod - def hyperopt_loss_function(results: DataFrame, config: Config, *args, **kwargs) -> float: + def hyperopt_loss_function( + results: DataFrame, config: Config, starting_balance: float, *args, **kwargs + ) -> float: """ Objective function. @@ -32,7 +33,7 @@ class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss): total_profit = results["profit_abs"].sum() try: drawdown_df = calculate_underwater( - results, value_col="profit_abs", starting_balance=get_dry_run_wallet(config) + results, value_col="profit_abs", starting_balance=starting_balance ) max_drawdown = abs(min(drawdown_df["drawdown"])) relative_drawdown = max(drawdown_df["drawdown_relative"]) diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_multi_metric.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_multi_metric.py index dd5fa4a17..7a10d279b 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_multi_metric.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_multi_metric.py @@ -36,7 +36,6 @@ from pandas import DataFrame from freqtrade.constants import Config from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown from freqtrade.optimize.hyperopt import IHyperOptLoss -from freqtrade.util import get_dry_run_wallet # smaller numbers penalize drawdowns more severely @@ -59,6 +58,7 @@ class MultiMetricHyperOptLoss(IHyperOptLoss): results: DataFrame, trade_count: int, config: Config, + starting_balance: float, **kwargs, ) -> float: total_profit = results["profit_abs"].sum() @@ -84,7 +84,7 @@ class MultiMetricHyperOptLoss(IHyperOptLoss): # Calculate drawdown try: drawdown = calculate_max_drawdown( - results, starting_balance=get_dry_run_wallet(config), value_col="profit_abs" + results, starting_balance=starting_balance, value_col="profit_abs" ) relative_account_drawdown = drawdown.relative_account_drawdown except ValueError: diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py index 5230408a9..2ab581520 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py @@ -13,7 +13,6 @@ from pandas import DataFrame from freqtrade.constants import Config from freqtrade.data.metrics import calculate_max_drawdown from freqtrade.optimize.hyperopt import IHyperOptLoss -from freqtrade.util import get_dry_run_wallet # smaller numbers penalize drawdowns more severely @@ -22,12 +21,14 @@ DRAWDOWN_MULT = 0.075 class ProfitDrawDownHyperOptLoss(IHyperOptLoss): @staticmethod - def hyperopt_loss_function(results: DataFrame, config: Config, *args, **kwargs) -> float: + def hyperopt_loss_function( + results: DataFrame, config: Config, starting_balance: float, *args, **kwargs + ) -> float: total_profit = results["profit_abs"].sum() try: drawdown = calculate_max_drawdown( - results, starting_balance=get_dry_run_wallet(config), value_col="profit_abs" + results, starting_balance=starting_balance, value_col="profit_abs" ) relative_account_drawdown = drawdown.relative_account_drawdown except ValueError: diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe.py index 4806ddb7f..20e4ee2b6 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe.py @@ -9,10 +9,8 @@ from datetime import datetime from pandas import DataFrame -from freqtrade.constants import Config from freqtrade.data.metrics import calculate_sharpe from freqtrade.optimize.hyperopt import IHyperOptLoss -from freqtrade.util.dry_run_wallet import get_dry_run_wallet class SharpeHyperOptLoss(IHyperOptLoss): @@ -25,10 +23,9 @@ class SharpeHyperOptLoss(IHyperOptLoss): @staticmethod def hyperopt_loss_function( results: DataFrame, - trade_count: int, min_date: datetime, max_date: datetime, - config: Config, + starting_balance: float, *args, **kwargs, ) -> float: @@ -37,7 +34,6 @@ class SharpeHyperOptLoss(IHyperOptLoss): Uses Sharpe Ratio calculation. """ - starting_balance = get_dry_run_wallet(config) sharp_ratio = calculate_sharpe(results, min_date, max_date, starting_balance) # print(expected_returns_mean, up_stdev, sharp_ratio) return -sharp_ratio diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino.py index c01be889d..935d038e5 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino.py @@ -9,10 +9,8 @@ from datetime import datetime from pandas import DataFrame -from freqtrade.constants import Config from freqtrade.data.metrics import calculate_sortino from freqtrade.optimize.hyperopt import IHyperOptLoss -from freqtrade.util import get_dry_run_wallet class SortinoHyperOptLoss(IHyperOptLoss): @@ -25,10 +23,9 @@ class SortinoHyperOptLoss(IHyperOptLoss): @staticmethod def hyperopt_loss_function( results: DataFrame, - trade_count: int, min_date: datetime, max_date: datetime, - config: Config, + starting_balance: float, *args, **kwargs, ) -> float: @@ -37,7 +34,6 @@ class SortinoHyperOptLoss(IHyperOptLoss): Uses Sortino Ratio calculation. """ - starting_balance = get_dry_run_wallet(config) sortino_ratio = calculate_sortino(results, min_date, max_date, starting_balance) # print(expected_returns_mean, down_stdev, sortino_ratio) return -sortino_ratio diff --git a/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py index 53de37a0e..9634bd117 100644 --- a/tests/optimize/test_hyperoptloss.py +++ b/tests/optimize/test_hyperoptloss.py @@ -116,6 +116,7 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct config=default_conf, processed=None, backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()}, + starting_balance=default_conf["dry_run_wallet"], ) over = hl.hyperopt_loss_function( results_over, From 18305a5bf6b163b6d7609ee4ff953f9a3b2dfcce Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 Dec 2024 19:16:48 +0100 Subject: [PATCH 18/65] docs: update hyperopt docs to include new argument --- docs/advanced-hyperopt.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 480b20daf..b12d8f5b6 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -39,6 +39,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss): config: Config, processed: dict[str, DataFrame], backtest_stats: dict[str, Any], + starting_balance: float, **kwargs, ) -> float: """ @@ -70,6 +71,7 @@ Currently, the arguments are: * `config`: Config object used (Note: Not all strategy-related parameters will be updated here if they are part of a hyperopt space). * `processed`: Dict of Dataframes with the pair as keys containing the data used for backtesting. * `backtest_stats`: Backtesting statistics using the same format as the backtesting file "strategy" substructure. Available fields can be seen in `generate_strategy_stats()` in `optimize_reports.py`. +* `starting_balance`: Starting balance used for backtesting. This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you. From ad8f62128778d2ddf6191485755f5c5356754c8f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 Dec 2024 19:17:01 +0100 Subject: [PATCH 19/65] tests: Improve hyperopt loss tests --- tests/optimize/test_hyperoptloss.py | 84 +++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py index 9634bd117..8f1b1c786 100644 --- a/tests/optimize/test_hyperoptloss.py +++ b/tests/optimize/test_hyperoptloss.py @@ -39,13 +39,34 @@ def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_res hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"}) hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) correct = hl.hyperopt_loss_function( - hyperopt_results, 600, datetime(2019, 1, 1), datetime(2019, 5, 1) + results=hyperopt_results, + trade_count=600, + min_date=datetime(2019, 1, 1), + max_date=datetime(2019, 5, 1), + config=hyperopt_conf, + processed=None, + backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()}, + starting_balance=hyperopt_conf["dry_run_wallet"], ) over = hl.hyperopt_loss_function( - hyperopt_results, 600 + 100, datetime(2019, 1, 1), datetime(2019, 5, 1) + results=hyperopt_results, + trade_count=600 + 100, + min_date=datetime(2019, 1, 1), + max_date=datetime(2019, 5, 1), + config=hyperopt_conf, + processed=None, + backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()}, + starting_balance=hyperopt_conf["dry_run_wallet"], ) under = hl.hyperopt_loss_function( - hyperopt_results, 600 - 100, datetime(2019, 1, 1), datetime(2019, 5, 1) + results=hyperopt_results, + trade_count=600 - 100, + min_date=datetime(2019, 1, 1), + max_date=datetime(2019, 5, 1), + config=hyperopt_conf, + processed=None, + backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()}, + starting_balance=hyperopt_conf["dry_run_wallet"], ) assert over > correct assert under > correct @@ -58,9 +79,25 @@ def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results) hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"}) hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) longer = hl.hyperopt_loss_function( - hyperopt_results, 100, datetime(2019, 1, 1), datetime(2019, 5, 1) + results=hyperopt_results, + trade_count=100, + min_date=datetime(2019, 1, 1), + max_date=datetime(2019, 5, 1), + config=hyperopt_conf, + processed=None, + backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()}, + starting_balance=hyperopt_conf["dry_run_wallet"], + ) + shorter = hl.hyperopt_loss_function( + results=resultsb, + trade_count=100, + min_date=datetime(2019, 1, 1), + max_date=datetime(2019, 5, 1), + config=hyperopt_conf, + processed=None, + backtest_stats={"profit_total": resultsb["profit_abs"].sum()}, + starting_balance=hyperopt_conf["dry_run_wallet"], ) - shorter = hl.hyperopt_loss_function(resultsb, 100, datetime(2019, 1, 1), datetime(2019, 5, 1)) assert shorter < longer @@ -73,11 +110,34 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"}) hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) correct = hl.hyperopt_loss_function( - hyperopt_results, 600, datetime(2019, 1, 1), datetime(2019, 5, 1) + results=hyperopt_results, + trade_count=600, + min_date=datetime(2019, 1, 1), + max_date=datetime(2019, 5, 1), + config=hyperopt_conf, + processed=None, + backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()}, + starting_balance=hyperopt_conf["dry_run_wallet"], + ) + over = hl.hyperopt_loss_function( + results=results_over, + trade_count=600, + min_date=datetime(2019, 1, 1), + max_date=datetime(2019, 5, 1), + config=hyperopt_conf, + processed=None, + backtest_stats={"profit_total": results_over["profit_abs"].sum()}, + starting_balance=hyperopt_conf["dry_run_wallet"], ) - over = hl.hyperopt_loss_function(results_over, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)) under = hl.hyperopt_loss_function( - results_under, 600, datetime(2019, 1, 1), datetime(2019, 5, 1) + results=results_under, + trade_count=600, + min_date=datetime(2019, 1, 1), + max_date=datetime(2019, 5, 1), + config=hyperopt_conf, + processed=None, + backtest_stats={"profit_total": results_under["profit_abs"].sum()}, + starting_balance=hyperopt_conf["dry_run_wallet"], ) assert over < correct assert under > correct @@ -109,7 +169,7 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct default_conf.update({"hyperopt_loss": lossfunction}) hl = HyperOptLossResolver.load_hyperoptloss(default_conf) correct = hl.hyperopt_loss_function( - hyperopt_results, + results=hyperopt_results, trade_count=len(hyperopt_results), min_date=datetime(2019, 1, 1), max_date=datetime(2019, 5, 1), @@ -119,22 +179,24 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct starting_balance=default_conf["dry_run_wallet"], ) over = hl.hyperopt_loss_function( - results_over, + results=results_over, trade_count=len(results_over), min_date=datetime(2019, 1, 1), max_date=datetime(2019, 5, 1), config=default_conf, processed=None, backtest_stats={"profit_total": results_over["profit_abs"].sum()}, + starting_balance=default_conf["dry_run_wallet"], ) under = hl.hyperopt_loss_function( - results_under, + results=results_under, trade_count=len(results_under), min_date=datetime(2019, 1, 1), max_date=datetime(2019, 5, 1), config=default_conf, processed=None, backtest_stats={"profit_total": results_under["profit_abs"].sum()}, + starting_balance=default_conf["dry_run_wallet"], ) assert over < correct assert under > correct From fe834f00a2b0aeb02e1bfc895279d258ce9bb41c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 Dec 2024 19:37:14 +0100 Subject: [PATCH 20/65] fix: import causing long startup time --- freqtrade/commands/optimize_commands.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index 788bca489..9695a313b 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -4,7 +4,6 @@ from typing import Any from freqtrade import constants from freqtrade.enums import RunMode from freqtrade.exceptions import ConfigurationError, OperationalException -from freqtrade.util import get_dry_run_wallet logger = logging.getLogger(__name__) @@ -18,7 +17,7 @@ def setup_optimize_configuration(args: dict[str, Any], method: RunMode) -> dict[ :return: Configuration """ from freqtrade.configuration import setup_utils_configuration - from freqtrade.util import fmt_coin + from freqtrade.util import fmt_coin, get_dry_run_wallet config = setup_utils_configuration(args, method) From ebae0a7248ca37d3af8519bce45d1c5b3b84a2af Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 Dec 2024 19:44:26 +0100 Subject: [PATCH 21/65] chore: improve typing of new functionality --- freqtrade/optimize/optimize_reports/optimize_reports.py | 6 +++--- freqtrade/util/dry_run_wallet.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index 532abed39..23119077b 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -69,7 +69,7 @@ def generate_rejected_signals( def _generate_result_line( - result: DataFrame, starting_balance: int, first_column: str | list[str] + result: DataFrame, starting_balance: float, first_column: str | list[str] ) -> dict: """ Generate one result dict, with "first_column" as key. @@ -111,7 +111,7 @@ def _generate_result_line( def generate_pair_metrics( pairlist: list[str], stake_currency: str, - starting_balance: int, + starting_balance: float, results: DataFrame, skip_nan: bool = False, ) -> list[dict]: @@ -144,7 +144,7 @@ def generate_pair_metrics( def generate_tag_metrics( tag_type: Literal["enter_tag", "exit_reason"] | list[Literal["enter_tag", "exit_reason"]], - starting_balance: int, + starting_balance: float, results: DataFrame, skip_nan: bool = False, ) -> list[dict]: diff --git a/freqtrade/util/dry_run_wallet.py b/freqtrade/util/dry_run_wallet.py index f994f0d4c..c904db6b3 100644 --- a/freqtrade/util/dry_run_wallet.py +++ b/freqtrade/util/dry_run_wallet.py @@ -1,4 +1,4 @@ -from pytest import Config +from freqtrade.constants import Config def get_dry_run_wallet(config: Config) -> int | float: From 1d0702e14364dcec0c091b1f34396844e1d3cbc2 Mon Sep 17 00:00:00 2001 From: xmatthias <5024695+xmatthias@users.noreply.github.com> Date: Thu, 5 Dec 2024 03:16:51 +0000 Subject: [PATCH 22/65] chore: update pre-commit hooks --- .../exchange/binance_leverage_tiers.json | 901 ++++++++++++++---- 1 file changed, 699 insertions(+), 202 deletions(-) diff --git a/freqtrade/exchange/binance_leverage_tiers.json b/freqtrade/exchange/binance_leverage_tiers.json index 4bcdab66d..69bfb8016 100644 --- a/freqtrade/exchange/binance_leverage_tiers.json +++ b/freqtrade/exchange/binance_leverage_tiers.json @@ -3668,6 +3668,161 @@ } } ], + "AERO/USDT:USDT": [ + { + "tier": 1.0, + "symbol": "AERO/USDT:USDT", + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 75.0, + "info": { + "bracket": "1", + "initialLeverage": "75", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "symbol": "AERO/USDT:USDT", + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "2", + "initialLeverage": "50", + "notionalCap": "10000", + "notionalFloor": "5000", + "maintMarginRatio": "0.015", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "symbol": "AERO/USDT:USDT", + "currency": "USDT", + "minNotional": 10000.0, + "maxNotional": 30000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "3", + "initialLeverage": "25", + "notionalCap": "30000", + "notionalFloor": "10000", + "maintMarginRatio": "0.02", + "cum": "75.0" + } + }, + { + "tier": 4.0, + "symbol": "AERO/USDT:USDT", + "currency": "USDT", + "minNotional": 30000.0, + "maxNotional": 60000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "4", + "initialLeverage": "20", + "notionalCap": "60000", + "notionalFloor": "30000", + "maintMarginRatio": "0.025", + "cum": "225.0" + } + }, + { + "tier": 5.0, + "symbol": "AERO/USDT:USDT", + "currency": "USDT", + "minNotional": 60000.0, + "maxNotional": 300000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "300000", + "notionalFloor": "60000", + "maintMarginRatio": "0.05", + "cum": "1725.0" + } + }, + { + "tier": 6.0, + "symbol": "AERO/USDT:USDT", + "currency": "USDT", + "minNotional": 300000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "600000", + "notionalFloor": "300000", + "maintMarginRatio": "0.1", + "cum": "16725.0" + } + }, + { + "tier": 7.0, + "symbol": "AERO/USDT:USDT", + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 750000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "750000", + "notionalFloor": "600000", + "maintMarginRatio": "0.125", + "cum": "31725.0" + } + }, + { + "tier": 8.0, + "symbol": "AERO/USDT:USDT", + "currency": "USDT", + "minNotional": 750000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "1500000", + "notionalFloor": "750000", + "maintMarginRatio": "0.25", + "cum": "125475.0" + } + }, + { + "tier": 9.0, + "symbol": "AERO/USDT:USDT", + "currency": "USDT", + "minNotional": 1500000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "9", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1500000", + "maintMarginRatio": "0.5", + "cum": "500475.0" + } + } + ], "AEVO/USDT:USDT": [ { "tier": 1.0, @@ -13777,14 +13932,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "75", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.02", + "maintMarginRatio": "0.01", "cum": "0.0" } }, @@ -13793,15 +13948,15 @@ "symbol": "CHR/USDT:USDT", "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, "info": { "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", + "initialLeverage": "50", + "notionalCap": "10000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.015", "cum": "25.0" } }, @@ -13809,68 +13964,119 @@ "tier": 3.0, "symbol": "CHR/USDT:USDT", "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 10000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "200000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "10000", + "maintMarginRatio": "0.02", + "cum": "75.0" } }, { "tier": 4.0, "symbol": "CHR/USDT:USDT", "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 50000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.1", - "cum": "10650.0" + "initialLeverage": "20", + "notionalCap": "100000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "325.0" } }, { "tier": 5.0, "symbol": "CHR/USDT:USDT", "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "minNotional": 100000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.125", - "cum": "23150.0" + "initialLeverage": "10", + "notionalCap": "500000", + "notionalFloor": "100000", + "maintMarginRatio": "0.05", + "cum": "2825.0" } }, { "tier": 6.0, "symbol": "CHR/USDT:USDT", "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.1", + "cum": "27825.0" + } + }, + { + "tier": 7.0, + "symbol": "CHR/USDT:USDT", + "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 1250000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "1250000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "52825.0" + } + }, + { + "tier": 8.0, + "symbol": "CHR/USDT:USDT", + "currency": "USDT", + "minNotional": 1250000.0, + "maxNotional": 2500000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "2500000", + "notionalFloor": "1250000", + "maintMarginRatio": "0.25", + "cum": "209075.0" + } + }, + { + "tier": 9.0, + "symbol": "CHR/USDT:USDT", + "currency": "USDT", + "minNotional": 2500000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "9", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "2500000", "maintMarginRatio": "0.5", - "cum": "398150.0" + "cum": "834075.0" } } ], @@ -26760,14 +26966,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "75", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.02", + "maintMarginRatio": "0.01", "cum": "0.0" } }, @@ -26776,15 +26982,15 @@ "symbol": "JOE/USDT:USDT", "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, "info": { "bracket": "2", - "initialLeverage": "15", - "notionalCap": "25000", + "initialLeverage": "50", + "notionalCap": "10000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.015", "cum": "25.0" } }, @@ -26792,85 +26998,119 @@ "tier": 3.0, "symbol": "JOE/USDT:USDT", "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 10000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "200000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "10000", + "maintMarginRatio": "0.02", + "cum": "75.0" } }, { "tier": 4.0, "symbol": "JOE/USDT:USDT", "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 50000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.1", - "cum": "10650.0" + "initialLeverage": "20", + "notionalCap": "100000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "325.0" } }, { "tier": 5.0, "symbol": "JOE/USDT:USDT", "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 100000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.125", - "cum": "23150.0" + "initialLeverage": "10", + "notionalCap": "500000", + "notionalFloor": "100000", + "maintMarginRatio": "0.05", + "cum": "2825.0" } }, { "tier": 6.0, "symbol": "JOE/USDT:USDT", "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "3000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.25", - "cum": "148150.0" + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.1", + "cum": "27825.0" } }, { "tier": 7.0, "symbol": "JOE/USDT:USDT", "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 1250000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "1250000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "52825.0" + } + }, + { + "tier": 8.0, + "symbol": "JOE/USDT:USDT", + "currency": "USDT", + "minNotional": 1250000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1250000", + "maintMarginRatio": "0.25", + "cum": "209075.0" + } + }, + { + "tier": 9.0, + "symbol": "JOE/USDT:USDT", + "currency": "USDT", "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "9", "initialLeverage": "1", "notionalCap": "5000000", "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "898150.0" + "cum": "959075.0" } } ], @@ -27184,6 +27424,161 @@ } } ], + "KAIA/USDT:USDT": [ + { + "tier": 1.0, + "symbol": "KAIA/USDT:USDT", + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 75.0, + "info": { + "bracket": "1", + "initialLeverage": "75", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "symbol": "KAIA/USDT:USDT", + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "2", + "initialLeverage": "50", + "notionalCap": "10000", + "notionalFloor": "5000", + "maintMarginRatio": "0.015", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "symbol": "KAIA/USDT:USDT", + "currency": "USDT", + "minNotional": 10000.0, + "maxNotional": 30000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "3", + "initialLeverage": "25", + "notionalCap": "30000", + "notionalFloor": "10000", + "maintMarginRatio": "0.02", + "cum": "75.0" + } + }, + { + "tier": 4.0, + "symbol": "KAIA/USDT:USDT", + "currency": "USDT", + "minNotional": 30000.0, + "maxNotional": 60000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "4", + "initialLeverage": "20", + "notionalCap": "60000", + "notionalFloor": "30000", + "maintMarginRatio": "0.025", + "cum": "225.0" + } + }, + { + "tier": 5.0, + "symbol": "KAIA/USDT:USDT", + "currency": "USDT", + "minNotional": 60000.0, + "maxNotional": 300000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "300000", + "notionalFloor": "60000", + "maintMarginRatio": "0.05", + "cum": "1725.0" + } + }, + { + "tier": 6.0, + "symbol": "KAIA/USDT:USDT", + "currency": "USDT", + "minNotional": 300000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "600000", + "notionalFloor": "300000", + "maintMarginRatio": "0.1", + "cum": "16725.0" + } + }, + { + "tier": 7.0, + "symbol": "KAIA/USDT:USDT", + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 750000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "750000", + "notionalFloor": "600000", + "maintMarginRatio": "0.125", + "cum": "31725.0" + } + }, + { + "tier": 8.0, + "symbol": "KAIA/USDT:USDT", + "currency": "USDT", + "minNotional": 750000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "1500000", + "notionalFloor": "750000", + "maintMarginRatio": "0.25", + "cum": "125475.0" + } + }, + { + "tier": 9.0, + "symbol": "KAIA/USDT:USDT", + "currency": "USDT", + "minNotional": 1500000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "9", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1500000", + "maintMarginRatio": "0.5", + "cum": "500475.0" + } + } + ], "KAS/USDT:USDT": [ { "tier": 1.0, @@ -29347,10 +29742,10 @@ "minNotional": 0.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.03, - "maxLeverage": 20.0, + "maxLeverage": 10.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "10", "notionalCap": "25000", "notionalFloor": "0", "maintMarginRatio": "0.03", @@ -29364,10 +29759,10 @@ "minNotional": 25000.0, "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxLeverage": 8.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "8", "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -29413,13 +29808,13 @@ "symbol": "LOOM/USDT:USDT", "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "5", "initialLeverage": "2", - "notionalCap": "3000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.25", "cum": "148000.0" @@ -29429,17 +29824,17 @@ "tier": 6.0, "symbol": "LOOM/USDT:USDT", "currency": "USDT", - "minNotional": 3000000.0, - "maxNotional": 3500000.0, + "minNotional": 1500000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "3500000", - "notionalFloor": "3000000", + "notionalCap": "2000000", + "notionalFloor": "1500000", "maintMarginRatio": "0.5", - "cum": "898000.0" + "cum": "523000.0" } } ], @@ -30244,14 +30639,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.015, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "75", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.015", + "maintMarginRatio": "0.01", "cum": "0.0" } }, @@ -30260,84 +30655,135 @@ "symbol": "LUNA2/USDT:USDT", "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, "info": { "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", + "initialLeverage": "50", + "notionalCap": "10000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "50.0" + "maintMarginRatio": "0.015", + "cum": "25.0" } }, { "tier": 3.0, "symbol": "LUNA2/USDT:USDT", "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "minNotional": 10000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "675.0" + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "10000", + "maintMarginRatio": "0.02", + "cum": "75.0" } }, { "tier": 4.0, "symbol": "LUNA2/USDT:USDT", "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 50000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5675.0" + "initialLeverage": "20", + "notionalCap": "100000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "325.0" } }, { "tier": 5.0, "symbol": "LUNA2/USDT:USDT", "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "minNotional": 100000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11925.0" + "initialLeverage": "10", + "notionalCap": "500000", + "notionalFloor": "100000", + "maintMarginRatio": "0.05", + "cum": "2825.0" } }, { "tier": 6.0, "symbol": "LUNA2/USDT:USDT", "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.1", + "cum": "27825.0" + } + }, + { + "tier": 7.0, + "symbol": "LUNA2/USDT:USDT", + "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 1250000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "1250000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "52825.0" + } + }, + { + "tier": 8.0, + "symbol": "LUNA2/USDT:USDT", + "currency": "USDT", + "minNotional": 1250000.0, + "maxNotional": 2500000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "2500000", + "notionalFloor": "1250000", + "maintMarginRatio": "0.25", + "cum": "209075.0" + } + }, + { + "tier": 9.0, + "symbol": "LUNA2/USDT:USDT", + "currency": "USDT", + "minNotional": 2500000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "9", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "2500000", "maintMarginRatio": "0.5", - "cum": "386925.0" + "cum": "834075.0" } } ], @@ -35954,10 +36400,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 75.0, + "maxLeverage": 20.0, "info": { "bracket": "1", - "initialLeverage": "75", + "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.01", @@ -35971,10 +36417,10 @@ "minNotional": 5000.0, "maxNotional": 10000.0, "maintenanceMarginRate": 0.015, - "maxLeverage": 50.0, + "maxLeverage": 16.0, "info": { "bracket": "2", - "initialLeverage": "50", + "initialLeverage": "16", "notionalCap": "10000", "notionalFloor": "5000", "maintMarginRatio": "0.015", @@ -35988,10 +36434,10 @@ "minNotional": 10000.0, "maxNotional": 30000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, + "maxLeverage": 14.0, "info": { "bracket": "3", - "initialLeverage": "25", + "initialLeverage": "14", "notionalCap": "30000", "notionalFloor": "10000", "maintMarginRatio": "0.02", @@ -36005,10 +36451,10 @@ "minNotional": 30000.0, "maxNotional": 60000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxLeverage": 12.0, "info": { "bracket": "4", - "initialLeverage": "20", + "initialLeverage": "12", "notionalCap": "60000", "notionalFloor": "30000", "maintMarginRatio": "0.025", @@ -36088,13 +36534,13 @@ "symbol": "ORBS/USDT:USDT", "currency": "USDT", "minNotional": 1500000.0, - "maxNotional": 3000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "9", "initialLeverage": "1", - "notionalCap": "3000000", + "notionalCap": "2000000", "notionalFloor": "1500000", "maintMarginRatio": "0.5", "cum": "500475.0" @@ -38419,10 +38865,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "75", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.01", @@ -38434,84 +38880,135 @@ "symbol": "QNT/USDT:USDT", "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, "info": { "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", + "initialLeverage": "50", + "notionalCap": "10000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" + "maintMarginRatio": "0.015", + "cum": "25.0" } }, { "tier": 3.0, "symbol": "QNT/USDT:USDT", "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "minNotional": 10000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "10000", + "maintMarginRatio": "0.02", + "cum": "75.0" } }, { "tier": 4.0, "symbol": "QNT/USDT:USDT", "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 50000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" + "initialLeverage": "20", + "notionalCap": "100000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "325.0" } }, { "tier": 5.0, "symbol": "QNT/USDT:USDT", "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "minNotional": 100000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" + "initialLeverage": "10", + "notionalCap": "500000", + "notionalFloor": "100000", + "maintMarginRatio": "0.05", + "cum": "2825.0" } }, { "tier": 6.0, "symbol": "QNT/USDT:USDT", "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.1", + "cum": "27825.0" + } + }, + { + "tier": 7.0, + "symbol": "QNT/USDT:USDT", + "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 1250000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "1250000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "52825.0" + } + }, + { + "tier": 8.0, + "symbol": "QNT/USDT:USDT", + "currency": "USDT", + "minNotional": 1250000.0, + "maxNotional": 2500000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "2500000", + "notionalFloor": "1250000", + "maintMarginRatio": "0.25", + "cum": "209075.0" + } + }, + { + "tier": 9.0, + "symbol": "QNT/USDT:USDT", + "currency": "USDT", + "minNotional": 2500000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "9", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "2500000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "834075.0" } } ], @@ -51060,10 +51557,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, + "maxLeverage": 10.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "10", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -51077,10 +51574,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxLeverage": 8.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "8", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -51094,10 +51591,10 @@ "minNotional": 25000.0, "maxNotional": 400000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "10", + "initialLeverage": "6", "notionalCap": "400000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -51143,13 +51640,13 @@ "symbol": "XEM/USDT:USDT", "currency": "USDT", "minNotional": 2000000.0, - "maxNotional": 6000000.0, + "maxNotional": 2100000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "6", "initialLeverage": "2", - "notionalCap": "6000000", + "notionalCap": "2100000", "notionalFloor": "2000000", "maintMarginRatio": "0.25", "cum": "295650.0" @@ -51159,17 +51656,17 @@ "tier": 7.0, "symbol": "XEM/USDT:USDT", "currency": "USDT", - "minNotional": 6000000.0, - "maxNotional": 10000000.0, + "minNotional": 2100000.0, + "maxNotional": 2200000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "10000000", - "notionalFloor": "6000000", + "notionalCap": "2200000", + "notionalFloor": "2100000", "maintMarginRatio": "0.5", - "cum": "1795650.0" + "cum": "820650.0" } } ], From e09d1b42f27b7484f0f6dd3abe8734b805ede34f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 4 Dec 2024 19:56:26 +0100 Subject: [PATCH 23/65] chore: eliminate duplicate trades data grouping --- freqtrade/data/converter/orderflow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/converter/orderflow.py b/freqtrade/data/converter/orderflow.py index 0bfd7a05f..52256e7b4 100644 --- a/freqtrade/data/converter/orderflow.py +++ b/freqtrade/data/converter/orderflow.py @@ -91,8 +91,6 @@ def populate_dataframe_with_trades( trades = trades.loc[trades["candle_start"] >= start_date] trades.reset_index(inplace=True, drop=True) - # group trades by candle start - trades_grouped_by_candle_start = trades.groupby("candle_start", group_keys=False) # Create Series to hold complex data trades_series = pd.Series(index=dataframe.index, dtype=object) orderflow_series = pd.Series(index=dataframe.index, dtype=object) @@ -100,7 +98,9 @@ def populate_dataframe_with_trades( stacked_imbalances_bid_series = pd.Series(index=dataframe.index, dtype=object) stacked_imbalances_ask_series = pd.Series(index=dataframe.index, dtype=object) + # group trades by candle start trades_grouped_by_candle_start = trades.groupby("candle_start", group_keys=False) + candle_start: datetime for candle_start, trades_grouped_df in trades_grouped_by_candle_start: is_between = candle_start == dataframe["date"] From 9f5cb3a07e65f8cd814c8b318684c1170b630f0d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 6 Dec 2024 20:11:09 +0100 Subject: [PATCH 24/65] chore: add exif-stripper to ensure exif data is removed --- .pre-commit-config.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3504f3b68..6f3d2ff27 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,6 +56,11 @@ repos: .*\.md )$ + - repo: https://github.com/stefmolin/exif-stripper + rev: 0.6.1 + hooks: + - id: strip-exif + - repo: https://github.com/codespell-project/codespell rev: v2.3.0 hooks: From 63f8217cd5ad6a105ff3e852eff9f97dc0ebe533 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 6 Dec 2024 20:11:46 +0100 Subject: [PATCH 25/65] chore: ensure exif-data is removed from images --- docs/assets/frequi-login-CORS-light.png | Bin 54458 -> 52311 bytes docs/assets/telegram_forcebuy.png | Bin 17895 -> 12035 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/assets/frequi-login-CORS-light.png b/docs/assets/frequi-login-CORS-light.png index 019ae4e8c90d768c050d0ba6308d40ab4c819c3b..cf6d7a700f73d3b72cdb5a97def3d2279acd88d8 100644 GIT binary patch literal 52311 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 tQDmsq~(e8}) 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 ZgSKN$zdSZA{0aqmNQuddRtOvX{x1OmZ&UyP literal 54458 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 Date: Sat, 7 Dec 2024 13:21:18 +0100 Subject: [PATCH 26/65] chore: improve type safety --- freqtrade/strategy/interface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1e415e9fc..1a032f3a2 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -1163,10 +1163,10 @@ class IStrategy(ABC, HyperStrategyMixin): logger.warning(f"Empty candle (OHLCV) data for pair {pair}") return None, None - latest_date = dataframe["date"].max() - latest = dataframe.loc[dataframe["date"] == latest_date].iloc[-1] + latest_date_pd = dataframe["date"].max() + latest = dataframe.loc[dataframe["date"] == latest_date_pd].iloc[-1] # Explicitly convert to datetime object to ensure the below comparison does not fail - latest_date = latest_date.to_pydatetime() + latest_date: datetime = latest_date_pd.to_pydatetime() # Check if dataframe is out of date timeframe_minutes = timeframe_to_minutes(timeframe) From 98e0a5f10101c365cac05311317e9018fcf6aaf6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 Dec 2024 15:51:37 +0100 Subject: [PATCH 27/65] chore: remove unused arguments in loss functions --- freqtrade/optimize/hyperopt_loss/hyperopt_loss_calmar.py | 3 --- .../hyperopt_loss/hyperopt_loss_max_drawdown_relative.py | 3 +-- freqtrade/optimize/hyperopt_loss/hyperopt_loss_multi_metric.py | 2 -- .../optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py | 3 +-- 4 files changed, 2 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_calmar.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_calmar.py index 2a04d4070..4f1a82e1e 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_calmar.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_calmar.py @@ -9,7 +9,6 @@ from datetime import datetime from pandas import DataFrame -from freqtrade.constants import Config from freqtrade.data.metrics import calculate_calmar from freqtrade.optimize.hyperopt import IHyperOptLoss @@ -24,10 +23,8 @@ class CalmarHyperOptLoss(IHyperOptLoss): @staticmethod def hyperopt_loss_function( results: DataFrame, - trade_count: int, min_date: datetime, max_date: datetime, - config: Config, starting_balance: float, *args, **kwargs, diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_relative.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_relative.py index a753263a3..3cd578cb4 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_relative.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_relative.py @@ -7,7 +7,6 @@ Hyperoptimization. from pandas import DataFrame -from freqtrade.constants import Config from freqtrade.data.metrics import calculate_underwater from freqtrade.optimize.hyperopt import IHyperOptLoss @@ -22,7 +21,7 @@ class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss): @staticmethod def hyperopt_loss_function( - results: DataFrame, config: Config, starting_balance: float, *args, **kwargs + results: DataFrame, starting_balance: float, *args, **kwargs ) -> float: """ Objective function. diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_multi_metric.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_multi_metric.py index 7a10d279b..adffdfb0b 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_multi_metric.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_multi_metric.py @@ -33,7 +33,6 @@ TARGET_TRADE_AMOUNT variable sets the minimum number of trades required to avoid import numpy as np from pandas import DataFrame -from freqtrade.constants import Config from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown from freqtrade.optimize.hyperopt import IHyperOptLoss @@ -57,7 +56,6 @@ class MultiMetricHyperOptLoss(IHyperOptLoss): def hyperopt_loss_function( results: DataFrame, trade_count: int, - config: Config, starting_balance: float, **kwargs, ) -> float: diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py index 2ab581520..af1b33dfb 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py @@ -10,7 +10,6 @@ individual needs. from pandas import DataFrame -from freqtrade.constants import Config from freqtrade.data.metrics import calculate_max_drawdown from freqtrade.optimize.hyperopt import IHyperOptLoss @@ -22,7 +21,7 @@ DRAWDOWN_MULT = 0.075 class ProfitDrawDownHyperOptLoss(IHyperOptLoss): @staticmethod def hyperopt_loss_function( - results: DataFrame, config: Config, starting_balance: float, *args, **kwargs + results: DataFrame, starting_balance: float, *args, **kwargs ) -> float: total_profit = results["profit_abs"].sum() From 0683ba3a54da1ec56252f732b680bf27f58823c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 08:30:45 +0100 Subject: [PATCH 28/65] feat: limit environment-variable json parsing to lists --- freqtrade/configuration/environment_vars.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/freqtrade/configuration/environment_vars.py b/freqtrade/configuration/environment_vars.py index 5baa2bfc3..8a825f59b 100644 --- a/freqtrade/configuration/environment_vars.py +++ b/freqtrade/configuration/environment_vars.py @@ -1,8 +1,9 @@ -import json import logging import os from typing import Any +import rapidjson + from freqtrade.constants import ENV_VAR_PREFIX from freqtrade.misc import deep_merge_dicts @@ -23,8 +24,11 @@ def _get_var_typed(val): return False # try to convert from json try: - return json.loads(val) - except json.decoder.JSONDecodeError: + value = rapidjson.loads(val) + # Limited to lists for now + if isinstance(value, list): + return value + except rapidjson.JSONDecodeError: pass # keep as string return val From 934bcf253eb4427ab8d372cb193e2346e49ca0ad Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 08:30:55 +0100 Subject: [PATCH 29/65] test: add tests for list parsing --- tests/test_configuration.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 8011ded96..3e9df382b 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1481,6 +1481,12 @@ def test_flat_vars_to_nested_dict(caplog): "FREQTRADE__STAKE_AMOUNT": "200.05", "FREQTRADE__TELEGRAM__CHAT_ID": "2151", "NOT_RELEVANT": "200.0", # Will be ignored + "FREQTRADE__ARRAY": '[{"name":"default","host":"xxx"}]', + "FREQTRADE__EXCHANGE__PAIR_WHITELIST": '["BTC/USDT", "ETH/USDT"]', + # Fails due to trailing comma + "FREQTRADE__ARRAY_TRAIL_COMMA": '[{"name":"default","host":"xxx",}]', + # Object fails + "FREQTRADE__OBJECT": '{"name":"default","host":"xxx"}', } expected = { "stake_amount": 200.05, @@ -1494,8 +1500,12 @@ def test_flat_vars_to_nested_dict(caplog): }, "some_setting": True, "some_false_setting": False, + "pair_whitelist": ["BTC/USDT", "ETH/USDT"], }, "telegram": {"chat_id": "2151"}, + "array": [{"name": "default", "host": "xxx"}], + "object": '{"name":"default","host":"xxx"}', + "array_trail_comma": '[{"name":"default","host":"xxx",}]', } res = _flat_vars_to_nested_dict(test_args, ENV_VAR_PREFIX) assert res == expected From 10b5d5e56bfb758db825cd5c2daadf3e0eaa4810 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 08:35:13 +0100 Subject: [PATCH 30/65] docs: add "list parsing" logic to documentation --- docs/configuration.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 16ae37603..0228f6234 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -39,13 +39,19 @@ Please note that Environment variables will overwrite corresponding settings in Common example: -``` +``` bash FREQTRADE__TELEGRAM__CHAT_ID= FREQTRADE__TELEGRAM__TOKEN= FREQTRADE__EXCHANGE__KEY= FREQTRADE__EXCHANGE__SECRET= ``` +Json lists are parsed as json - so you can use the following to set a list of pairs: + +``` bash +export FREQTRADE__EXCHANGE__PAIR_WHITELIST='["BTC/USDT", "ETH/USDT"]' +``` + !!! Note Environment variables detected are logged at startup - so if you can't find why a value is not what you think it should be based on the configuration, make sure it's not loaded from an environment variable. @@ -54,7 +60,7 @@ FREQTRADE__EXCHANGE__SECRET= ??? Warning "Loading sequence" Environment variables are loaded after the initial configuration. As such, you cannot provide the path to the configuration through environment variables. Please use `--config path/to/config.json` for that. - This also applies to user_dir to some degree. while the user directory can be set through environment variables - the configuration will **not** be loaded from that location. + This also applies to `user_dir` to some degree. while the user directory can be set through environment variables - the configuration will **not** be loaded from that location. ### Multiple configuration files From eee5d710e7c7b72fd2018be0033a20998e26958a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 09:59:40 +0100 Subject: [PATCH 31/65] chore: patch torch all the time - "list"tests do load the modules as well - so they need the same patch. --- tests/conftest.py | 25 +++++++++++++++++++++++++ tests/freqai/conftest.py | 25 ------------------------- tests/freqai/test_freqai_datakitchen.py | 3 +-- tests/freqai/test_freqai_interface.py | 11 ++++++++--- 4 files changed, 34 insertions(+), 30 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 517170860..df8a31974 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring import json import logging +import platform import re from copy import deepcopy from datetime import datetime, timedelta, timezone @@ -517,6 +518,30 @@ def patch_gc(mocker) -> None: mocker.patch("freqtrade.main.gc_set_threshold") +def is_arm() -> bool: + machine = platform.machine() + return "arm" in machine or "aarch64" in machine + + +def is_mac() -> bool: + machine = platform.system() + return "Darwin" in machine + + +@pytest.fixture(autouse=True) +def patch_torch_initlogs(mocker) -> None: + if is_mac(): + # Mock torch import completely + import sys + import types + + module_name = "torch" + mocked_module = types.ModuleType(module_name) + sys.modules[module_name] = mocked_module + else: + mocker.patch("torch._logging._init_logs") + + @pytest.fixture(autouse=True) def user_dir(mocker, tmp_path) -> Path: user_dir = tmp_path / "user_data" diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index 442f53020..9e14111af 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -1,4 +1,3 @@ -import platform import sys from copy import deepcopy from pathlib import Path @@ -20,30 +19,6 @@ def is_py12() -> bool: return sys.version_info >= (3, 12) -def is_mac() -> bool: - machine = platform.system() - return "Darwin" in machine - - -def is_arm() -> bool: - machine = platform.machine() - return "arm" in machine or "aarch64" in machine - - -@pytest.fixture(autouse=True) -def patch_torch_initlogs(mocker) -> None: - if is_mac(): - # Mock torch import completely - import sys - import types - - module_name = "torch" - mocked_module = types.ModuleType(module_name) - sys.modules[module_name] = mocked_module - else: - mocker.patch("torch._logging._init_logs") - - @pytest.fixture(scope="function") def freqai_conf(default_conf, tmp_path): freqaiconf = deepcopy(default_conf) diff --git a/tests/freqai/test_freqai_datakitchen.py b/tests/freqai/test_freqai_datakitchen.py index 27efc3a66..7a219e46e 100644 --- a/tests/freqai/test_freqai_datakitchen.py +++ b/tests/freqai/test_freqai_datakitchen.py @@ -10,11 +10,10 @@ from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from tests.conftest import get_patched_exchange +from tests.conftest import get_patched_exchange, is_mac from tests.freqai.conftest import ( get_patched_data_kitchen, get_patched_freqai_strategy, - is_mac, make_unfiltered_dataframe, ) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 2779ddcb8..0710591c9 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -13,11 +13,16 @@ from freqtrade.freqai.utils import download_all_data_for_training, get_required_ from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import Trade from freqtrade.plugins.pairlistmanager import PairListManager -from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_has_re -from tests.freqai.conftest import ( - get_patched_freqai_strategy, +from tests.conftest import ( + EXMS, + create_mock_trades, + get_patched_exchange, is_arm, is_mac, + log_has_re, +) +from tests.freqai.conftest import ( + get_patched_freqai_strategy, make_rl_config, mock_pytorch_mlp_model_training_parameters, ) From 8dc322d7f2c8717a1f5b102996a25ca396172633 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 10:20:21 +0100 Subject: [PATCH 32/65] chore: don't allow negative stake amounts --- freqtrade/wallets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 9864d0604..7fe78d935 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -339,7 +339,7 @@ class Wallets: f"lower than stake amount ({stake_amount} {self._config['stake_currency']})" ) - return stake_amount + return max(stake_amount, 0) def get_trade_stake_amount( self, pair: str, max_open_trades: IntOrInf, edge=None, update: bool = True From 193b989342f86fff00a61b2ecb13b1f10b5e9da0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 11:44:35 +0100 Subject: [PATCH 33/65] chore: Be more precise with binance account limitation --- freqtrade/exchange/binance.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 6959811c3..74a37b4f6 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -91,7 +91,10 @@ class Binance(Exchange): "\nHedge Mode is not supported by freqtrade. " "Please change 'Position Mode' on your binance futures account." ) - if assets_margin.get("multiAssetsMargin") is True: + if ( + assets_margin.get("multiAssetsMargin") is True + and self.margin_mode != MarginMode.CROSS + ): msg += ( "\nMulti-Asset Mode is not supported by freqtrade. " "Please change 'Asset Mode' on your binance futures account." From 6d698e584bf4501c5e7bc3e8cafada8850bcdd1b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 11:45:25 +0100 Subject: [PATCH 34/65] chore: return 0 total balances total may be 0, while still having "free" balance (cross futures scenarios) --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d4c05d2d2..bc4f84f24 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -726,7 +726,7 @@ class RPC: coin: str balance: Wallet for coin, balance in self._freqtrade.wallets.get_all_balances().items(): - if not balance.total: + if not balance.total and not balance.free: continue trade = open_assets.get(coin, None) From 3c88bdc60c30fbbad70090172db1c5b366b97ca5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 11:55:52 +0100 Subject: [PATCH 35/65] fix: round liquidation price to price precision --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 54f37d00d..9e18378c1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -3599,7 +3599,7 @@ class Exchange: liquidation_price_buffer = ( liquidation_price - buffer_amount if is_short else liquidation_price + buffer_amount ) - return max(liquidation_price_buffer, 0.0) + return self.price_to_precision(pair, max(liquidation_price_buffer, 0.0)) else: return None From ae1baf5789afafbb85e699c7bdf2871b0b95a5c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 13:07:34 +0100 Subject: [PATCH 36/65] test: mock price_to_precision for liquidation calculations --- tests/exchange/test_binance.py | 1 + tests/exchange/test_exchange.py | 1 + tests/optimize/test_backtesting.py | 2 ++ 3 files changed, 4 insertions(+) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index cc28760fa..3cd668074 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -293,6 +293,7 @@ def test_liquidation_price_binance( default_conf["trading_mode"] = trading_mode default_conf["margin_mode"] = margin_mode default_conf["liquidation_buffer"] = 0.0 + mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, exchange="binance") def get_maint_ratio(pair_, stake_amount): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 9d129601c..af95e5748 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -6131,6 +6131,7 @@ def test_get_liquidation_price( default_conf_usdt["exchange"]["name"] = exchange_name default_conf_usdt["margin_mode"] = margin_mode mocker.patch("freqtrade.exchange.gate.Gate.validate_ordertypes") + mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf_usdt, exchange=exchange_name) exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01)) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 05ebf2298..1b9945cb5 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -568,6 +568,7 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: mocker.patch(f"{EXMS}.get_fee", fee) mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float("inf")) + mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y) mocker.patch(f"{EXMS}.get_max_leverage", return_value=100) mocker.patch("freqtrade.optimize.backtesting.price_to_precision", lambda p, *args: p) patch_exchange(mocker) @@ -1842,6 +1843,7 @@ def test_backtest_multi_pair_long_short_switch( if use_detail: default_conf_usdt["timeframe_detail"] = "1m" + mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y) mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float("inf")) mocker.patch(f"{EXMS}.get_fee", fee) From 654d2ab63db47453f3f2e18bba022fb0f9949d91 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 13:45:03 +0100 Subject: [PATCH 37/65] fix: Round stoploss_dist to price_precision --- freqtrade/rpc/rpc.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index bc4f84f24..5d8b6a2e6 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -33,6 +33,7 @@ from freqtrade.enums import ( from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.exchange.exchange_types import Ticker, Tickers +from freqtrade.exchange.exchange_utils import price_to_precision from freqtrade.loggers import bufferHandler from freqtrade.persistence import KeyStoreKeys, KeyValueStore, PairLocks, Trade from freqtrade.persistence.models import PairLock @@ -243,7 +244,11 @@ class RPC: stoploss_entry_dist_ratio = stop_entry.profit_ratio # calculate distance to stoploss - stoploss_current_dist = trade.stop_loss - current_rate + stoploss_current_dist = price_to_precision( + trade.stop_loss - current_rate, + trade.price_precision, + trade.precision_mode_price, + ) stoploss_current_dist_ratio = stoploss_current_dist / current_rate trade_dict = trade.to_json() From 142ea68dceb853d967bb04dec39d63acd1981487 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 14:22:36 +0100 Subject: [PATCH 38/65] chore: fix oddity of price_to_precision --- freqtrade/exchange/exchange_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange_utils.py b/freqtrade/exchange/exchange_utils.py index 1c8a30001..1c9894242 100644 --- a/freqtrade/exchange/exchange_utils.py +++ b/freqtrade/exchange/exchange_utils.py @@ -308,7 +308,9 @@ def price_to_precision( decimal_to_precision( price, rounding_mode=rounding_mode, - precision=price_precision, + precision=int(price_precision) + if precisionMode != TICK_SIZE + else price_precision, counting_mode=precisionMode, ) ) From d0223e6f483f507e5897659ef30195164e265259 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 14:34:00 +0100 Subject: [PATCH 39/65] chore: simplify dry-run wallet update --- freqtrade/wallets.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 7fe78d935..f53c26255 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -130,18 +130,12 @@ class Wallets: ) total_stake = current_stake + used_stake else: - tot_in_trades = 0 for position in open_trades: - # size = self._exchange._contracts_to_amount(position.pair, position['contracts']) - size = position.amount - collateral = position.stake_amount - leverage = position.leverage - tot_in_trades += collateral _positions[position.pair] = PositionWallet( position.pair, - position=size, - leverage=leverage, - collateral=collateral, + position=position.amount, + leverage=position.leverage, + collateral=position.stake_amount, side=position.trade_direction, ) current_stake = ( From 58357a074641d885dcfc521e18b58f27c96b422f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 14:37:10 +0100 Subject: [PATCH 40/65] chore: calculate total_stake only once --- freqtrade/wallets.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index f53c26255..fb27d5e5d 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -124,11 +124,6 @@ class Wallets: pending, trade.amount + curr_wallet_bal, ) - - current_stake = ( - self._start_cap.get(self._stake_currency, 0) + tot_profit - tot_in_trades - ) - total_stake = current_stake + used_stake else: for position in open_trades: _positions[position.pair] = PositionWallet( @@ -138,12 +133,11 @@ class Wallets: collateral=position.stake_amount, side=position.trade_direction, ) - current_stake = ( - self._start_cap.get(self._stake_currency, 0) + tot_profit - tot_in_trades - ) used_stake = tot_in_trades - total_stake = current_stake + tot_in_trades + + current_stake = self._start_cap.get(self._stake_currency, 0) + tot_profit - tot_in_trades + total_stake = current_stake + used_stake _wallets[self._stake_currency] = Wallet( currency=self._stake_currency, From 4cd8e6b4444bcabb3833206d24aeb6a4fdae933c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 15:22:40 +0100 Subject: [PATCH 41/65] chore: simplify rpc_balance method by relying on exchange cache --- freqtrade/rpc/rpc.py | 48 +++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 5d8b6a2e6..a1a3df0db 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -675,7 +675,7 @@ class RPC: } def __balance_get_est_stake( - self, coin: str, stake_currency: str, amount: float, balance: Wallet, tickers: Tickers + self, coin: str, stake_currency: str, amount: float, balance: Wallet ) -> tuple[float, float]: est_stake = 0.0 est_bot_stake = 0.0 @@ -686,25 +686,31 @@ class RPC: est_stake = balance.free est_bot_stake = amount else: - pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) - ticker: Ticker | None = tickers.get(pair, None) - if not ticker: - tickers_spot: Tickers = self._freqtrade.exchange.get_tickers( - cached=True, - market_type=TradingMode.SPOT - if self._config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT - else TradingMode.FUTURES, - ) - ticker = tickers_spot.get(pair, None) + try: + tickers: Tickers = self._freqtrade.exchange.get_tickers(cached=True) + pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) + ticker: Ticker | None = tickers.get(pair, None) + if not ticker: + tickers_spot: Tickers = self._freqtrade.exchange.get_tickers( + cached=True, + market_type=TradingMode.SPOT + if self._config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT + else TradingMode.FUTURES, + ) + ticker = tickers_spot.get(pair, None) - if ticker: - rate: float | None = ticker.get("last", None) - if rate: - if pair.startswith(stake_currency) and not pair.endswith(stake_currency): - rate = 1.0 / rate - est_stake = rate * balance.total - est_bot_stake = rate * amount + if ticker: + rate: float | None = ticker.get("last", None) + if rate: + if pair.startswith(stake_currency) and not pair.endswith(stake_currency): + rate = 1.0 / rate + est_stake = rate * balance.total + est_bot_stake = rate * amount + return est_stake, est_bot_stake + except (ExchangeError, PricingError) as e: + logger.warning(f"Error {e} getting rate for {coin}") + pass return est_stake, est_bot_stake def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> dict: @@ -712,10 +718,6 @@ class RPC: currencies: list[dict] = [] total = 0.0 total_bot = 0.0 - try: - tickers: Tickers = self._freqtrade.exchange.get_tickers(cached=True) - except ExchangeError: - raise RPCException("Error getting current tickers.") open_trades: list[Trade] = Trade.get_open_trades() open_assets: dict[str, Trade] = {t.safe_base_currency: t for t in open_trades} @@ -742,7 +744,7 @@ class RPC: try: est_stake, est_stake_bot = self.__balance_get_est_stake( - coin, stake_currency, trade_amount, balance, tickers + coin, stake_currency, trade_amount, balance ) except ValueError: continue From 0276e65f396f9047de0f1492c8e15b1487bb8b25 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 15:29:23 +0100 Subject: [PATCH 42/65] test: update rpc test for new behavior --- tests/rpc/test_rpc.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index efdc18977..8fe59b5ea 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -514,8 +514,13 @@ def test_rpc_balance_handle_error(default_conf, mocker): patch_get_signal(freqtradebot) rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter({}) - with pytest.raises(RPCException, match="Error getting current tickers."): - rpc._rpc_balance(default_conf["stake_currency"], default_conf["fiat_display_currency"]) + res = rpc._rpc_balance(default_conf["stake_currency"], default_conf["fiat_display_currency"]) + assert res["stake"] == "BTC" + + assert len(res["currencies"]) == 1 + assert res["currencies"][0]["currency"] == "BTC" + # ETH has not been converted. + assert all(currency["currency"] != "ETH" for currency in res["currencies"]) def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): @@ -597,10 +602,10 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): assert pytest.approx(result["total"]) == 2824.83464 assert pytest.approx(result["value"]) == 2824.83464 * 1.2 - assert tickers.call_count == 2 + assert tickers.call_count == 4 assert tickers.call_args_list[0][1]["cached"] is True # Testing futures - so we should get spot tickers - assert tickers.call_args_list[1][1]["market_type"] == "spot" + assert tickers.call_args_list[-1][1]["market_type"] == "spot" assert "USD" == result["symbol"] assert result["currencies"] == [ { From 2ff0abc6e7c5c5f516e63c58fdbfc2c54fe18699 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 15:44:46 +0100 Subject: [PATCH 43/65] refactor: conversation rate retrieval to exchange class for better reusability across the bot. --- freqtrade/exchange/exchange.py | 31 ++++++++++++++++++++++++++++++- freqtrade/rpc/rpc.py | 25 ++++++------------------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9e18378c1..a0343147c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1855,7 +1855,36 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - # Pricing info + def get_conversion_rate(self, coin: str, currency: str) -> float: + """ + Quick and cached way to get conversion rate one currency to the other. + Can then be used as "rate * amount" to convert between currencies. + :param coin: Coin to convert + :param currency: Currency to convert to + :returns: Conversion rate from coin to currency + :raises: ExchangeErrors + """ + if coin == currency: + return 1.0 + tickers = self.get_tickers(cached=True) + pair = self.get_valid_pair_combination(coin, currency) + ticker: Ticker | None = tickers.get(pair, None) + if not ticker: + tickers_other: Tickers = self.get_tickers( + cached=True, + market_type=( + TradingMode.SPOT + if self.trading_mode != TradingMode.SPOT + else TradingMode.FUTURES + ), + ) + ticker = tickers_other.get(pair, None) + if ticker: + rate: float | None = ticker.get("last", None) + if rate and pair.startswith(currency) and not pair.endswith(currency): + rate = 1.0 / rate + return rate + return None @retrier def fetch_ticker(self, pair: str) -> Ticker: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index a1a3df0db..3c9a12c93 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -687,25 +687,12 @@ class RPC: est_bot_stake = amount else: try: - tickers: Tickers = self._freqtrade.exchange.get_tickers(cached=True) - pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) - ticker: Ticker | None = tickers.get(pair, None) - if not ticker: - tickers_spot: Tickers = self._freqtrade.exchange.get_tickers( - cached=True, - market_type=TradingMode.SPOT - if self._config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT - else TradingMode.FUTURES, - ) - ticker = tickers_spot.get(pair, None) - - if ticker: - rate: float | None = ticker.get("last", None) - if rate: - if pair.startswith(stake_currency) and not pair.endswith(stake_currency): - rate = 1.0 / rate - est_stake = rate * balance.total - est_bot_stake = rate * amount + rate: float | None = self._freqtrade.exchange.get_conversion_rate( + coin, stake_currency + ) + if rate: + est_stake = rate * balance.total + est_bot_stake = rate * amount return est_stake, est_bot_stake except (ExchangeError, PricingError) as e: From d898c80e65761e1338491681ede0674a5cd04c96 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 16:40:56 +0100 Subject: [PATCH 44/65] test: Add test for get_conversion_rate --- freqtrade/exchange/exchange.py | 6 ++++- tests/exchange/test_exchange.py | 43 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a0343147c..875cff6ee 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1867,7 +1867,11 @@ class Exchange: if coin == currency: return 1.0 tickers = self.get_tickers(cached=True) - pair = self.get_valid_pair_combination(coin, currency) + try: + pair = self.get_valid_pair_combination(coin, currency) + except ValueError: + return None + ticker: Ticker | None = tickers.get(pair, None) if not ticker: tickers_other: Tickers = self.get_tickers( diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index af95e5748..085982d1d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2006,6 +2006,49 @@ def test_get_tickers(default_conf, mocker, exchange_name, caplog): assert exchange.get_tickers() == {} +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_conversion_rate(default_conf_usdt, mocker, exchange_name): + api_mock = MagicMock() + tick = { + "ETH/USDT": { + "last": 42, + }, + "BCH/USDT": { + "last": 41, + }, + "ETH/BTC": { + "last": 250, + }, + } + tick2 = { + "XRP/USDT": { + "symbol": "XRP/USDT", + "bid": 0.5, + "ask": 1, + "last": 2.5, + } + } + mocker.patch(f"{EXMS}.exchange_has", return_value=True) + api_mock.fetch_tickers = MagicMock(side_effect=[tick, tick2]) + api_mock.fetch_bids_asks = MagicMock(return_value={}) + + exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, exchange=exchange_name) + # retrieve original ticker + assert exchange.get_conversion_rate("USDT", "USDT") == 1 + assert api_mock.fetch_tickers.call_count == 0 + assert exchange.get_conversion_rate("ETH", "USDT") == 42 + assert exchange.get_conversion_rate("ETH", "USDC") is None + assert exchange.get_conversion_rate("ETH", "BTC") == 250 + assert exchange.get_conversion_rate("BTC", "ETH") == 0.004 + + assert api_mock.fetch_tickers.call_count == 1 + api_mock.fetch_tickers.reset_mock() + + assert exchange.get_conversion_rate("XRP", "USDT") == 2.5 + # Only the call to the "others" market + assert api_mock.fetch_tickers.call_count == 1 + + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_fetch_ticker(default_conf, mocker, exchange_name): api_mock = MagicMock() From f07aec457a4f7f7fc3ca03ceac5c3baec53b3f03 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 16:50:10 +0100 Subject: [PATCH 45/65] chore: remove unnecessary imports --- freqtrade/rpc/rpc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 3c9a12c93..ac05f8714 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -32,7 +32,6 @@ from freqtrade.enums import ( ) from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs -from freqtrade.exchange.exchange_types import Ticker, Tickers from freqtrade.exchange.exchange_utils import price_to_precision from freqtrade.loggers import bufferHandler from freqtrade.persistence import KeyStoreKeys, KeyValueStore, PairLocks, Trade From 4d36aaff39e104b945fe478ed8efbf1e2f7aac1e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 16:54:12 +0100 Subject: [PATCH 46/65] feat: convert get_valid_pair_combination to generator --- freqtrade/exchange/exchange.py | 64 ++++++++++++++----------- tests/exchange/test_exchange.py | 19 ++++---- tests/freqtradebot/test_freqtradebot.py | 2 +- tests/rpc/test_rpc.py | 2 +- tests/rpc/test_rpc_apiserver.py | 2 +- tests/rpc/test_rpc_telegram.py | 4 +- 6 files changed, 53 insertions(+), 40 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 875cff6ee..55c7cff2b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -7,7 +7,7 @@ import asyncio import inspect import logging import signal -from collections.abc import Coroutine +from collections.abc import Coroutine, Generator from copy import deepcopy from datetime import datetime, timedelta, timezone from math import floor, isnan @@ -705,14 +705,22 @@ class Exchange: f"Available currencies are: {', '.join(quote_currencies)}" ) - def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> str: + def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> Generator[str, None, None]: """ Get valid pair combination of curr_1 and curr_2 by trying both combinations. """ - for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]: + yielded = False + for pair in ( + f"{curr_1}/{curr_2}", + f"{curr_2}/{curr_1}", + f"{curr_1}/{curr_2}:{curr_2}", + f"{curr_2}/{curr_1}:{curr_1}", + ): if pair in self.markets and self.markets[pair].get("active"): - return pair - raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") + yielded = True + yield pair + if not yielded: + raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") def validate_timeframes(self, timeframe: str | None) -> None: """ @@ -1868,26 +1876,25 @@ class Exchange: return 1.0 tickers = self.get_tickers(cached=True) try: - pair = self.get_valid_pair_combination(coin, currency) + for pair in self.get_valid_pair_combination(coin, currency): + ticker: Ticker | None = tickers.get(pair, None) + if not ticker: + tickers_other: Tickers = self.get_tickers( + cached=True, + market_type=( + TradingMode.SPOT + if self.trading_mode != TradingMode.SPOT + else TradingMode.FUTURES + ), + ) + ticker = tickers_other.get(pair, None) + if ticker: + rate: float | None = ticker.get("last", None) + if rate and pair.startswith(currency) and not pair.endswith(currency): + rate = 1.0 / rate + return rate except ValueError: return None - - ticker: Ticker | None = tickers.get(pair, None) - if not ticker: - tickers_other: Tickers = self.get_tickers( - cached=True, - market_type=( - TradingMode.SPOT - if self.trading_mode != TradingMode.SPOT - else TradingMode.FUTURES - ), - ) - ticker = tickers_other.get(pair, None) - if ticker: - rate: float | None = ticker.get("last", None) - if rate and pair.startswith(currency) and not pair.endswith(currency): - rate = 1.0 / rate - return rate return None @retrier @@ -2244,10 +2251,13 @@ class Exchange: # If cost is None or 0.0 -> falsy, return None return None try: - comb = self.get_valid_pair_combination(fee_curr, self._config["stake_currency"]) - tick = self.fetch_ticker(comb) - - fee_to_quote_rate = safe_value_fallback2(tick, tick, "last", "ask") + for comb in self.get_valid_pair_combination( + fee_curr, self._config["stake_currency"] + ): + tick = self.fetch_ticker(comb) + fee_to_quote_rate = safe_value_fallback2(tick, tick, "last", "ask") + if tick: + break except (ValueError, ExchangeError): fee_to_quote_rate = self._config["exchange"].get("unknown_fee_rate", None) if not fee_to_quote_rate: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 085982d1d..260dd8f0e 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2021,10 +2021,7 @@ def test_get_conversion_rate(default_conf_usdt, mocker, exchange_name): }, } tick2 = { - "XRP/USDT": { - "symbol": "XRP/USDT", - "bid": 0.5, - "ask": 1, + "ADA/USDT:USDT": { "last": 2.5, } } @@ -2044,7 +2041,7 @@ def test_get_conversion_rate(default_conf_usdt, mocker, exchange_name): assert api_mock.fetch_tickers.call_count == 1 api_mock.fetch_tickers.reset_mock() - assert exchange.get_conversion_rate("XRP", "USDT") == 2.5 + assert exchange.get_conversion_rate("ADA", "USDT") == 2.5 # Only the call to the "others" market assert api_mock.fetch_tickers.call_count == 1 @@ -4122,10 +4119,16 @@ def test_get_valid_pair_combination(default_conf, mocker, markets): ) ex = Exchange(default_conf) - assert ex.get_valid_pair_combination("ETH", "BTC") == "ETH/BTC" - assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC" + assert next(ex.get_valid_pair_combination("ETH", "BTC")) == "ETH/BTC" + assert next(ex.get_valid_pair_combination("BTC", "ETH")) == "ETH/BTC" + multicombs = list(ex.get_valid_pair_combination("ETH", "USDT")) + assert len(multicombs) == 2 + assert "ETH/USDT" in multicombs + assert "ETH/USDT:USDT" in multicombs + with pytest.raises(ValueError, match=r"Could not combine.* to get a valid pair."): - ex.get_valid_pair_combination("NOPAIR", "ETH") + for x in ex.get_valid_pair_combination("NOPAIR", "ETH"): + pass @pytest.mark.parametrize( diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index 21e5394a4..937bec8e5 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -4021,7 +4021,7 @@ def test_get_real_amount_fees_order( default_conf_usdt, market_buy_order_usdt_doublefee, fee, mocker ): tfo_mock = mocker.patch(f"{EXMS}.get_trades_for_order", return_value=[]) - mocker.patch(f"{EXMS}.get_valid_pair_combination", return_value="BNB/USDT") + mocker.patch(f"{EXMS}.get_valid_pair_combination", return_value=["BNB/USDT"]) mocker.patch(f"{EXMS}.fetch_ticker", return_value={"last": 200}) trade = Trade( pair="LTC/USDT", diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 8fe59b5ea..225c5e364 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -586,7 +586,7 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): fetch_positions=MagicMock(return_value=mock_pos), get_tickers=tickers, get_valid_pair_combination=MagicMock( - side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}" + side_effect=lambda a, b: [f"{b}/{a}" if a == "USDT" else f"{a}/{b}"] ), ) default_conf_usdt["dry_run"] = False diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index ee28bcc6c..221a8b666 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -556,7 +556,7 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers): ftbot.config["dry_run"] = False mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance) mocker.patch(f"{EXMS}.get_tickers", tickers) - mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: f"{a}/{b}") + mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"]) ftbot.wallets.update() rc = client_get(client, f"{BASE_URI}/balance") diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index bc7746aef..845bce37c 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -960,7 +960,7 @@ async def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance default_conf["dry_run"] = False mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance) mocker.patch(f"{EXMS}.get_tickers", tickers) - mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: f"{a}/{b}") + mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"]) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) @@ -1049,7 +1049,7 @@ async def test_telegram_balance_handle_futures( mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance) mocker.patch(f"{EXMS}.fetch_positions", return_value=mock_pos) mocker.patch(f"{EXMS}.get_tickers", tickers) - mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: f"{a}/{b}") + mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"]) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) From f529cfe8cf2b395e445f24af5943aeaad8ab5611 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 16:58:31 +0100 Subject: [PATCH 47/65] chore: fix wrong return typing --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 55c7cff2b..811426839 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1863,7 +1863,7 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def get_conversion_rate(self, coin: str, currency: str) -> float: + def get_conversion_rate(self, coin: str, currency: str) -> float | None: """ Quick and cached way to get conversion rate one currency to the other. Can then be used as "rate * amount" to convert between currencies. From d4fbdeee94fdb7af576da9cc9cd4e3d390ff3bff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 03:45:29 +0000 Subject: [PATCH 48/65] chore(deps-dev): bump the types group with 2 updates Bumps the types group with 2 updates: [types-tabulate](https://github.com/python/typeshed) and [types-python-dateutil](https://github.com/python/typeshed). Updates `types-tabulate` from 0.9.0.20240106 to 0.9.0.20241207 - [Commits](https://github.com/python/typeshed/commits) Updates `types-python-dateutil` from 2.9.0.20241003 to 2.9.0.20241206 - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-tabulate dependency-type: direct:development update-type: version-update:semver-patch dependency-group: types - dependency-name: types-python-dateutil dependency-type: direct:development update-type: version-update:semver-patch dependency-group: types ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 0640a2549..6591b4a57 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -28,5 +28,5 @@ nbconvert==7.16.4 types-cachetools==5.5.0.20240820 types-filelock==3.2.7 types-requests==2.32.0.20241016 -types-tabulate==0.9.0.20240106 -types-python-dateutil==2.9.0.20241003 +types-tabulate==0.9.0.20241207 +types-python-dateutil==2.9.0.20241206 From f5dceb0b09e515bbab53f6d830d77ebf74007e39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 03:46:15 +0000 Subject: [PATCH 49/65] chore(deps): bump mkdocs-material in the mkdocs group Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material). Updates `mkdocs-material` from 9.5.47 to 9.5.48 - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.47...9.5.48) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch dependency-group: mkdocs ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index e8e99f337..f342680e2 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.7 mkdocs==1.6.1 -mkdocs-material==9.5.47 +mkdocs-material==9.5.48 mdx_truly_sane_lists==1.3 pymdown-extensions==10.12 jinja2==3.1.4 From e97a647b5474c68200e75cabc4fbaaf9d02480ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 03:47:13 +0000 Subject: [PATCH 50/65] chore(deps-dev): bump ruff from 0.8.1 to 0.8.2 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.1 to 0.8.2. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.1...0.8.2) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 0640a2549..baaa0e06d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==4.0.1 -ruff==0.8.1 +ruff==0.8.2 mypy==1.13.0 pre-commit==4.0.1 pytest==8.3.4 From 83471228e7eeb70ec46f6a664c2c59488819a871 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 03:47:37 +0000 Subject: [PATCH 51/65] chore(deps): bump python-telegram-bot from 21.8 to 21.9 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 21.8 to 21.9. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v21.8...v21.9) --- updated-dependencies: - dependency-name: python-telegram-bot dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c3719777e..311a2f510 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ cryptography==42.0.8; platform_machine == 'armv7l' cryptography==44.0.0; platform_machine != 'armv7l' aiohttp==3.10.11 SQLAlchemy==2.0.36 -python-telegram-bot==21.8 +python-telegram-bot==21.9 # can't be hard-pinned due to telegram-bot pinning httpx with ~ httpx>=0.24.1 humanize==4.11.0 From 8aea01545231b13659ca35d97daac943ad3c9481 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 03:47:48 +0000 Subject: [PATCH 52/65] chore(deps): bump ccxt from 4.4.35 to 4.4.37 Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.4.35 to 4.4.37. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/4.4.35...4.4.37) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c3719777e..0597f25fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ bottleneck==1.4.2 numexpr==2.10.2 pandas-ta==0.3.14b -ccxt==4.4.35 +ccxt==4.4.37 cryptography==42.0.8; platform_machine == 'armv7l' cryptography==44.0.0; platform_machine != 'armv7l' aiohttp==3.10.11 From f1fabb07bf91205a7373578f29f6cba66debd23c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 03:47:54 +0000 Subject: [PATCH 53/65] chore(deps): bump fastapi from 0.115.5 to 0.115.6 Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.115.5 to 0.115.6. - [Release notes](https://github.com/fastapi/fastapi/releases) - [Commits](https://github.com/fastapi/fastapi/compare/0.115.5...0.115.6) --- updated-dependencies: - dependency-name: fastapi dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c3719777e..94e6c612b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,7 +39,7 @@ orjson==3.10.12 sdnotify==0.3.2 # API Server -fastapi==0.115.5 +fastapi==0.115.6 pydantic==2.10.2 uvicorn==0.32.1 pyjwt==2.10.1 From 678821467164dc87397cb0622bf92fc21f5bdecc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 05:32:50 +0000 Subject: [PATCH 54/65] chore(deps): bump pydantic from 2.10.2 to 2.10.3 Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.10.2 to 2.10.3. - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/compare/v2.10.2...v2.10.3) --- updated-dependencies: - dependency-name: pydantic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4a8d2c835..444539b65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,7 +40,7 @@ sdnotify==0.3.2 # API Server fastapi==0.115.6 -pydantic==2.10.2 +pydantic==2.10.3 uvicorn==0.32.1 pyjwt==2.10.1 aiofiles==24.1.0 From 8cb82df055b8013374d881a33cfbb14cb62ae402 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 17:20:24 +0100 Subject: [PATCH 55/65] chore: simplify dry_wallets call --- freqtrade/wallets.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index fb27d5e5d..75bd9a505 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -145,9 +145,8 @@ class Wallets: used=used_stake, total=total_stake, ) - for currency in self._start_cap: + for currency, bal in self._start_cap.items(): if currency not in _wallets: - bal = self._start_cap[currency] _wallets[currency] = Wallet(currency, bal, 0, bal) self._wallets = _wallets From 6887ed4bf7218d782cd0dc1f840413ab05a224c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 19:38:25 +0100 Subject: [PATCH 56/65] feat: add cross margin balance logic --- freqtrade/wallets.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 75bd9a505..4dcdb4ee4 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -136,12 +136,22 @@ class Wallets: used_stake = tot_in_trades + cross_margin = 0.0 + if self._config.get("margin_mode") == "cross": + # In cross-margin mode, the total balance is used as collateral. + for curr, bal in self._start_cap.items(): + if curr == self._stake_currency: + continue + rate = self._exchange.get_conversion_rate(curr, self._stake_currency) + if rate: + cross_margin += bal * rate + current_stake = self._start_cap.get(self._stake_currency, 0) + tot_profit - tot_in_trades total_stake = current_stake + used_stake _wallets[self._stake_currency] = Wallet( currency=self._stake_currency, - free=current_stake, + free=current_stake + cross_margin, used=used_stake, total=total_stake, ) From 0fc0b2a1be58e815f7927dd104464b6b121843ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2024 19:38:36 +0100 Subject: [PATCH 57/65] test: add test for cross balance logic --- tests/test_wallets.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index d5e49438a..fc217ab84 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -468,10 +468,33 @@ def test_check_exit_amount_futures(mocker, default_conf, fee): "ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0}, }, ), + ( + { + "stake_currency": "USDT", + "margin_mode": "cross", + "dry_run_wallet": {"USDC": 1000.0, "BTC": 0.1, "ETH": 2.0}, + }, + { + # USDT wallet should be created with 0 balance, but Free balance, since + # it's converted from the other currencies + "USDT": {"currency": "USDT", "free": 4200.0, "used": 0.0, "total": 0.0}, + "USDC": {"currency": "USDC", "free": 1000.0, "used": 0.0, "total": 1000.0}, + "BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1}, + "ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0}, + }, + ), ], ) def test_dry_run_wallet_initialization(mocker, default_conf_usdt, config, wallets): default_conf_usdt.update(config) + mocker.patch( + f"{EXMS}.get_tickers", + return_value={ + "USDC/USDT": {"last": 1.0}, + "BTC/USDT": {"last": 20_000.0}, + "ETH/USDT": {"last": 1100.0}, + }, + ) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) # Verify each wallet matches the expected values @@ -507,7 +530,9 @@ def test_dry_run_wallet_initialization(mocker, default_conf_usdt, config, wallet assert freqtrade.wallets._wallets["NEO"].free == 45.04504504 # Verify USDT wallet was reduced by trade amount + stake_currency = config["stake_currency"] assert ( - pytest.approx(freqtrade.wallets._wallets["USDT"].total) == wallets["USDT"]["total"] - 100.0 + pytest.approx(freqtrade.wallets._wallets[stake_currency].total) + == wallets[stake_currency]["total"] - 100.0 ) assert len(freqtrade.wallets._wallets) == len(wallets) + 1 # Original wallets + NEO From 6b1ac499ba8702a041785f9bbf6024eddf61287f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 9 Dec 2024 06:26:29 +0100 Subject: [PATCH 58/65] tests: more tests for cross wallet --- tests/test_wallets.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index fc217ab84..c4d84f547 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -483,6 +483,35 @@ def test_check_exit_amount_futures(mocker, default_conf, fee): "ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0}, }, ), + ( + { + "stake_currency": "USDT", + "margin_mode": "cross", + "dry_run_wallet": {"USDT": 500, "USDC": 1000.0, "BTC": 0.1, "ETH": 2.0}, + }, + { + # USDT wallet should be created with 500 balance, but Free balance, since + # it's converted from the other currencies + "USDT": {"currency": "USDT", "free": 4700.0, "used": 0.0, "total": 500.0}, + "USDC": {"currency": "USDC", "free": 1000.0, "used": 0.0, "total": 1000.0}, + "BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1}, + "ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0}, + }, + ), + ( + # Same as above, but without cross + { + "stake_currency": "USDT", + "dry_run_wallet": {"USDT": 500, "USDC": 1000.0, "BTC": 0.1, "ETH": 2.0}, + }, + { + # No "free" transfer for USDT wallet + "USDT": {"currency": "USDT", "free": 500.0, "used": 0.0, "total": 500.0}, + "USDC": {"currency": "USDC", "free": 1000.0, "used": 0.0, "total": 1000.0}, + "BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1}, + "ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0}, + }, + ), ], ) def test_dry_run_wallet_initialization(mocker, default_conf_usdt, config, wallets): From fe9d1a053e7b5ffb61307750cb71b339ea0bd90b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 9 Dec 2024 06:40:49 +0100 Subject: [PATCH 59/65] chore: bump pre-commit dependencies --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6f3d2ff27..f621489d0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,8 +17,8 @@ repos: - types-cachetools==5.5.0.20240820 - types-filelock==3.2.7 - types-requests==2.32.0.20241016 - - types-tabulate==0.9.0.20240106 - - types-python-dateutil==2.9.0.20241003 + - types-tabulate==0.9.0.20241207 + - types-python-dateutil==2.9.0.20241206 - SQLAlchemy==2.0.36 # stages: [push] From e9cf0a71d822d4048c300da4c37713d2be3799fd Mon Sep 17 00:00:00 2001 From: xmatthias <5024695+xmatthias@users.noreply.github.com> Date: Tue, 10 Dec 2024 05:05:39 +0000 Subject: [PATCH 60/65] chore: update pre-commit hooks --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f621489d0..745cc391f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.8.1' + rev: 'v0.8.2' hooks: - id: ruff - id: ruff-format From 097836d1930822a59abe990ea91e8f3dcde65b46 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 10 Dec 2024 20:49:43 +0100 Subject: [PATCH 61/65] feat: improve logic for liquidation price calc --- freqtrade/leverage/liquidation_price.py | 7 +++++-- freqtrade/wallets.py | 14 ++++++++++++++ tests/leverage/test_update_liquidation_price.py | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index aed8c9160..4b49e4551 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -27,9 +27,12 @@ def update_liquidation_prices( total_wallet_stake = 0.0 if dry_run: # Parameters only needed for cross margin - total_wallet_stake = wallets.get_total(stake_currency) + total_wallet_stake = wallets.get_collateral() - logger.info("Updating liquidation price for all open trades.") + logger.info( + "Updating liquidation price for all open trades. " + f"Collateral {total_wallet_stake} {stake_currency}." + ) open_trades = Trade.get_open_trades() for t in open_trades: # TODO: This should be done in a batch update diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 4dcdb4ee4..5a239dadd 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -73,6 +73,18 @@ class Wallets: else: return 0 + def get_collateral(self) -> float: + """ + Get total collateral for liquidation price calculation. + """ + if self._config.get("margin_mode") == "cross": + # free includes all balances and, combined with position collateral, + # is used as "wallet balance". + return self.get_free(self._stake_currency) + sum( + pos.collateral for pos in self._positions.values() + ) + return self.get_total(self._stake_currency) + def get_owned(self, pair: str, base_currency: str) -> float: """ Get currently owned value. @@ -139,6 +151,8 @@ class Wallets: cross_margin = 0.0 if self._config.get("margin_mode") == "cross": # In cross-margin mode, the total balance is used as collateral. + # This is moved as "free" into the stake currency balance. + # strongly tied to the get_collateral() implementation. for curr, bal in self._start_cap.items(): if curr == self._stake_currency: continue diff --git a/tests/leverage/test_update_liquidation_price.py b/tests/leverage/test_update_liquidation_price.py index 1b25babd0..68c35509f 100644 --- a/tests/leverage/test_update_liquidation_price.py +++ b/tests/leverage/test_update_liquidation_price.py @@ -29,7 +29,7 @@ def test_update_liquidation_prices(mocker, margin_mode, dry_run): assert trade_mock.set_liquidation_price.call_count == 1 - assert wallets.get_total.call_count == ( + assert wallets.get_collateral.call_count == ( 0 if margin_mode == MarginMode.ISOLATED or not dry_run else 1 ) From f3d7b249be98bb9776fe370536346d3e858dbed0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 10 Dec 2024 21:04:49 +0100 Subject: [PATCH 62/65] test: Improve wallet tests --- tests/test_wallets.py | 58 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index c4d84f547..36211dcab 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -512,6 +512,23 @@ def test_check_exit_amount_futures(mocker, default_conf, fee): "ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0}, }, ), + ( + # Same as above, but with futures and cross + { + "stake_currency": "USDT", + "margin_mode": "cross", + "trading_mode": "futures", + "dry_run_wallet": {"USDT": 500, "USDC": 1000.0, "BTC": 0.1, "ETH": 2.0}, + }, + { + # USDT wallet should be created with 500 balance, but Free balance, since + # it's converted from the other currencies + "USDT": {"currency": "USDT", "free": 4700.0, "used": 0.0, "total": 500.0}, + "USDC": {"currency": "USDC", "free": 1000.0, "used": 0.0, "total": 1000.0}, + "BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1}, + "ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0}, + }, + ), ], ) def test_dry_run_wallet_initialization(mocker, default_conf_usdt, config, wallets): @@ -525,7 +542,7 @@ def test_dry_run_wallet_initialization(mocker, default_conf_usdt, config, wallet }, ) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - + stake_currency = config["stake_currency"] # Verify each wallet matches the expected values for currency, expected_wallet in wallets.items(): wallet = freqtrade.wallets._wallets[currency] @@ -548,20 +565,37 @@ def test_dry_run_wallet_initialization(mocker, default_conf_usdt, config, wallet "last": 0.22, }, ) + # Without position, collateral will be the same as free + assert freqtrade.wallets.get_collateral() == freqtrade.wallets.get_free(stake_currency) freqtrade.execute_entry("NEO/USDT", 100.0) # Update wallets and verify NEO is now included freqtrade.wallets.update() - assert "NEO" in freqtrade.wallets._wallets + if default_conf_usdt["trading_mode"] != "futures": + assert "NEO" in freqtrade.wallets._wallets - assert freqtrade.wallets._wallets["NEO"].total == 45.04504504 # 100 USDT / 0.22 - assert freqtrade.wallets._wallets["NEO"].used == 0.0 - assert freqtrade.wallets._wallets["NEO"].free == 45.04504504 + assert freqtrade.wallets._wallets["NEO"].total == 45.04504504 # 100 USDT / 0.22 + assert freqtrade.wallets._wallets["NEO"].used == 0.0 + assert freqtrade.wallets._wallets["NEO"].free == 45.04504504 + assert freqtrade.wallets.get_collateral() == freqtrade.wallets.get_free(stake_currency) + # Verify USDT wallet was reduced by trade amount + assert ( + pytest.approx(freqtrade.wallets._wallets[stake_currency].total) + == wallets[stake_currency]["total"] - 100.0 + ) + assert len(freqtrade.wallets._wallets) == len(wallets) + 1 # Original wallets + NEO + else: + # Futures mode + assert "NEO" not in freqtrade.wallets._wallets + assert freqtrade.wallets._positions["NEO/USDT"].position == 45.04504504 + assert pytest.approx(freqtrade.wallets._positions["NEO/USDT"].collateral) == 100 - # Verify USDT wallet was reduced by trade amount - stake_currency = config["stake_currency"] - assert ( - pytest.approx(freqtrade.wallets._wallets[stake_currency].total) - == wallets[stake_currency]["total"] - 100.0 - ) - assert len(freqtrade.wallets._wallets) == len(wallets) + 1 # Original wallets + NEO + # Verify USDT wallet's free was reduced by trade amount + assert ( + pytest.approx(freqtrade.wallets.get_collateral()) + == freqtrade.wallets.get_free(stake_currency) + 100 + ) + assert ( + pytest.approx(freqtrade.wallets._wallets[stake_currency].free) + == wallets[stake_currency]["free"] - 100.0 + ) From d8fa782f13662e7f223405e8793b35a8c1adefa4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Dec 2024 07:07:47 +0100 Subject: [PATCH 63/65] test: Add test with faulty behavior part of #11074 --- freqtrade/plugins/pairlist/PercentChangePairList.py | 1 - tests/conftest.py | 4 ++-- tests/plugins/test_percentchangepairlist.py | 12 +++++++++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/freqtrade/plugins/pairlist/PercentChangePairList.py b/freqtrade/plugins/pairlist/PercentChangePairList.py index cbc86f259..984853591 100644 --- a/freqtrade/plugins/pairlist/PercentChangePairList.py +++ b/freqtrade/plugins/pairlist/PercentChangePairList.py @@ -191,7 +191,6 @@ class PercentChangePairList(IPairList): for k, v in tickers.items() if ( self._exchange.get_pair_quote_currency(k) == self._stake_currency - and (self._use_range or v.get("percentage") is not None) and v["symbol"] in _pairlist ) ] diff --git a/tests/conftest.py b/tests/conftest.py index df8a31974..69ea8878b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2237,7 +2237,7 @@ def tickers(): "first": None, "last": 8603.67, "change": -0.879, - "percentage": None, + "percentage": -8.95, "average": None, "baseVolume": 30414.604298, "quoteVolume": 259629896.48584127, @@ -2281,7 +2281,7 @@ def tickers(): "first": None, "last": 129.28, "change": 1.795, - "percentage": None, + "percentage": -2.5, "average": None, "baseVolume": 59698.79897, "quoteVolume": 29132399.743954, diff --git a/tests/plugins/test_percentchangepairlist.py b/tests/plugins/test_percentchangepairlist.py index df165cf98..9d661c9c4 100644 --- a/tests/plugins/test_percentchangepairlist.py +++ b/tests/plugins/test_percentchangepairlist.py @@ -360,9 +360,15 @@ def test_gen_pairlist_from_tickers(mocker, rpl_config, tickers): exchange = get_patched_exchange(mocker, rpl_config, exchange="binance") pairlistmanager = PairListManager(exchange, rpl_config) - remote_pairlist = PercentChangePairList( - exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][0], 0 - ) + remote_pairlist = pairlistmanager._pairlist_handlers[0] + + # The generator returns BTC ETH and TKN - filtering the first ensures removing pairs in this step ain't problematic. + def _validate_pair(pair, ticker): + if pair == "BTC/USDT": + return False + return True + + remote_pairlist._validate_pair = _validate_pair result = remote_pairlist.gen_pairlist(tickers.return_value) From b9aa78b987a299bc97dd2c440bcb6827a9b4d522 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Dec 2024 07:12:50 +0100 Subject: [PATCH 64/65] fix: crash in PercentChange pairlist Could happen if pairs were removed due to empty Ticker when the pairlist is not a generator. closes #11074 --- .../plugins/pairlist/PercentChangePairList.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/freqtrade/plugins/pairlist/PercentChangePairList.py b/freqtrade/plugins/pairlist/PercentChangePairList.py index 984853591..149e3977f 100644 --- a/freqtrade/plugins/pairlist/PercentChangePairList.py +++ b/freqtrade/plugins/pairlist/PercentChangePairList.py @@ -217,7 +217,7 @@ class PercentChangePairList(IPairList): self.fetch_percent_change_from_lookback_period(filtered_tickers) else: # Fetching 24h change by default from supported exchange tickers - self.fetch_percent_change_from_tickers(filtered_tickers, tickers) + filtered_tickers = self.fetch_percent_change_from_tickers(filtered_tickers, tickers) if self._min_value is not None: filtered_tickers = [v for v in filtered_tickers if v["percentage"] > self._min_value] @@ -261,7 +261,6 @@ class PercentChangePairList(IPairList): ) * 1000 ) - # todo: utc date output for starting date self.log_once( f"Using change range of {self._lookback_period} candles, timeframe: " f"{self._lookback_timeframe}, starting from {format_ms_time(since_ms)} " @@ -302,15 +301,21 @@ class PercentChangePairList(IPairList): else: filtered_tickers[i]["percentage"] = 0 - def fetch_percent_change_from_tickers(self, filtered_tickers: list[dict[str, Any]], tickers): - for i, p in enumerate(filtered_tickers): + def fetch_percent_change_from_tickers( + self, filtered_tickers: list[dict[str, Any]], tickers + ) -> list[dict[str, Any]]: + valid_tickers: list[dict[str, Any]] = [] + for p in filtered_tickers: # Filter out assets - if not self._validate_pair( - p["symbol"], tickers[p["symbol"]] if p["symbol"] in tickers else None + if ( + self._validate_pair( + p["symbol"], tickers[p["symbol"]] if p["symbol"] in tickers else None + ) + and p["symbol"] != "UNI/USDT" ): - filtered_tickers.remove(p) - else: - filtered_tickers[i]["percentage"] = tickers[p["symbol"]]["percentage"] + p["percentage"] = tickers[p["symbol"]]["percentage"] + valid_tickers.append(p) + return valid_tickers def _validate_pair(self, pair: str, ticker: Ticker | None) -> bool: """ From 34835752f815774f99848e9b36b34027fd9c9b14 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Dec 2024 07:25:07 +0100 Subject: [PATCH 65/65] chore: improve typing in percentChange Pairlist --- .../plugins/pairlist/PercentChangePairList.py | 28 +++++++++++++------ tests/plugins/test_percentchangepairlist.py | 3 +- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/freqtrade/plugins/pairlist/PercentChangePairList.py b/freqtrade/plugins/pairlist/PercentChangePairList.py index 149e3977f..4f91a47d1 100644 --- a/freqtrade/plugins/pairlist/PercentChangePairList.py +++ b/freqtrade/plugins/pairlist/PercentChangePairList.py @@ -8,7 +8,7 @@ defined period or as coming from ticker import logging from datetime import timedelta -from typing import Any +from typing import TypedDict from cachetools import TTLCache from pandas import DataFrame @@ -24,6 +24,11 @@ from freqtrade.util import dt_now, format_ms_time logger = logging.getLogger(__name__) +class SymbolWithPercentage(TypedDict): + symbol: str + percentage: float | None + + class PercentChangePairList(IPairList): is_pairlist_generator = True supports_backtesting = SupportsBacktesting.NO @@ -211,10 +216,12 @@ class PercentChangePairList(IPairList): :param tickers: Tickers (from exchange.get_tickers). May be cached. :return: new whitelist """ - filtered_tickers: list[dict[str, Any]] = [{"symbol": k} for k in pairlist] + filtered_tickers: list[SymbolWithPercentage] = [ + {"symbol": k, "percentage": None} for k in pairlist + ] if self._use_range: # calculating using lookback_period - self.fetch_percent_change_from_lookback_period(filtered_tickers) + filtered_tickers = self.fetch_percent_change_from_lookback_period(filtered_tickers) else: # Fetching 24h change by default from supported exchange tickers filtered_tickers = self.fetch_percent_change_from_tickers(filtered_tickers, tickers) @@ -227,7 +234,7 @@ class PercentChangePairList(IPairList): sorted_tickers = sorted( filtered_tickers, reverse=self._sort_direction == "desc", - key=lambda t: t["percentage"], + key=lambda t: t["percentage"], # type: ignore ) # Validate whitelist to only have active market pairs @@ -239,7 +246,7 @@ class PercentChangePairList(IPairList): return pairs def fetch_candles_for_lookback_period( - self, filtered_tickers: list[dict[str, str]] + self, filtered_tickers: list[SymbolWithPercentage] ) -> dict[PairWithTimeframe, DataFrame]: since_ms = ( int( @@ -275,7 +282,9 @@ class PercentChangePairList(IPairList): candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms) return candles - def fetch_percent_change_from_lookback_period(self, filtered_tickers: list[dict[str, Any]]): + def fetch_percent_change_from_lookback_period( + self, filtered_tickers: list[SymbolWithPercentage] + ) -> list[SymbolWithPercentage]: # get lookback period in ms, for exchange ohlcv fetch candles = self.fetch_candles_for_lookback_period(filtered_tickers) @@ -300,11 +309,12 @@ class PercentChangePairList(IPairList): filtered_tickers[i]["percentage"] = pct_change else: filtered_tickers[i]["percentage"] = 0 + return filtered_tickers def fetch_percent_change_from_tickers( - self, filtered_tickers: list[dict[str, Any]], tickers - ) -> list[dict[str, Any]]: - valid_tickers: list[dict[str, Any]] = [] + self, filtered_tickers: list[SymbolWithPercentage], tickers + ) -> list[SymbolWithPercentage]: + valid_tickers: list[SymbolWithPercentage] = [] for p in filtered_tickers: # Filter out assets if ( diff --git a/tests/plugins/test_percentchangepairlist.py b/tests/plugins/test_percentchangepairlist.py index 9d661c9c4..6f399fc9f 100644 --- a/tests/plugins/test_percentchangepairlist.py +++ b/tests/plugins/test_percentchangepairlist.py @@ -362,7 +362,8 @@ def test_gen_pairlist_from_tickers(mocker, rpl_config, tickers): remote_pairlist = pairlistmanager._pairlist_handlers[0] - # The generator returns BTC ETH and TKN - filtering the first ensures removing pairs in this step ain't problematic. + # The generator returns BTC ETH and TKN - filtering the first ensures removing pairs + # in this step ain't problematic. def _validate_pair(pair, ticker): if pair == "BTC/USDT": return False