From c112b84d0f838d10b2f38e143bc124544908eaae Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Dec 2024 21:32:39 +0100 Subject: [PATCH 01/20] feat: support dynamic proxy coin conversion --- freqtrade/exchange/binance.py | 4 ++++ freqtrade/exchange/exchange.py | 4 ++++ freqtrade/exchange/exchange_types.py | 2 ++ 3 files changed, 10 insertions(+) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 74a37b4f6..9ac1b0d97 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -48,6 +48,10 @@ class Binance(Exchange): PriceType.MARK: "MARK_PRICE", }, "ws_enabled": False, + "proxy_coin_mapping": { + "BNFCR": "USDC", + "BFUSD": "USDT", + }, } _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [ diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index bba6c5aaf..932d74922 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -156,6 +156,7 @@ class Exchange: # Override createMarketBuyOrderRequiresPrice where ccxt has it wrong "marketOrderRequiresPrice": False, "exchange_has_overrides": {}, # Dictionary overriding ccxt's "has". + "proxy_coin_mapping": {}, # Mapping for proxy coins # Expected to be in the format {"fetchOHLCV": True} or {"fetchOHLCV": False} "ws_enabled": False, # Set to true for exchanges with tested websocket support } @@ -1872,6 +1873,9 @@ class Exchange: :returns: Conversion rate from coin to currency :raises: ExchangeErrors """ + + if (proxy_coin := self._ft_has["proxy_coin_mapping"].get(coin, None)) is not None: + coin = proxy_coin if coin == currency: return 1.0 tickers = self.get_tickers(cached=True) diff --git a/freqtrade/exchange/exchange_types.py b/freqtrade/exchange/exchange_types.py index 69741dc65..9687057bd 100644 --- a/freqtrade/exchange/exchange_types.py +++ b/freqtrade/exchange/exchange_types.py @@ -47,6 +47,8 @@ class FtHas(TypedDict, total=False): needs_trading_fees: bool order_props_in_contracts: list[Literal["amount", "cost", "filled", "remaining"]] + proxy_coin_mapping: dict[str, str] + # Websocket control ws_enabled: bool From e7b1a7e49317181f2dde6b84ed86b73e2333ae4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Dec 2024 21:32:54 +0100 Subject: [PATCH 02/20] test: add test for proxy coin conversion --- tests/exchange/test_exchange.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 260dd8f0e..99e403094 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2028,6 +2028,7 @@ def test_get_conversion_rate(default_conf_usdt, mocker, exchange_name): 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={}) + default_conf_usdt["trading_mode"] = "futures" exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, exchange=exchange_name) # retrieve original ticker @@ -2045,6 +2046,11 @@ def test_get_conversion_rate(default_conf_usdt, mocker, exchange_name): # Only the call to the "others" market assert api_mock.fetch_tickers.call_count == 1 + if exchange_name == "binance": + # Special binance case of BNFCR matching USDT. + assert exchange.get_conversion_rate("BNFCR", "USDT") is None + assert exchange.get_conversion_rate("BNFCR", "USDC") == 1 + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_fetch_ticker(default_conf, mocker, exchange_name): From ca9589b2e8e702dcc39d604764010c15765f9478 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2024 06:46:13 +0100 Subject: [PATCH 03/20] chore: reduce pointless verbosity of cross liquidation update --- freqtrade/leverage/liquidation_price.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index b48190d84..5440da64a 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -28,11 +28,11 @@ def update_liquidation_prices( if dry_run: # Parameters only needed for cross margin total_wallet_stake = wallets.get_collateral() + logger.info( + "Updating liquidation price for all open trades. " + f"Collateral {total_wallet_stake} {stake_currency}." + ) - logger.info( - "Updating liquidation price for all open trades. " - f"Collateral {total_wallet_stake} {stake_currency}." - ) open_trades: list[Trade] = Trade.get_open_trades() for t in open_trades: # TODO: This should be done in a batch update From 0b9cca27e20612073a7ec922385cf0d5f199586b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2024 06:48:03 +0100 Subject: [PATCH 04/20] feat: add "get_proxy_coin" method used for BNFCR mode --- freqtrade/exchange/binance.py | 12 +++++++++++- freqtrade/exchange/exchange.py | 8 ++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 9ac1b0d97..1b0b60824 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -58,9 +58,19 @@ class Binance(Exchange): # TradingMode.SPOT always supported and not required in this list # (TradingMode.MARGIN, MarginMode.CROSS), # (TradingMode.FUTURES, MarginMode.CROSS), - (TradingMode.FUTURES, MarginMode.ISOLATED) + (TradingMode.FUTURES, MarginMode.ISOLATED), ] + def get_proxy_coin(self) -> str: + """ + Get the proxy coin for the given coin + Falls back to the stake currency if no proxy coin is found + :return: Proxy coin or stake currency + """ + if self.margin_mode == MarginMode.CROSS: + return "BNFCR" + return self._config["stake_currency"] + def get_tickers( self, symbols: list[str] | None = None, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 932d74922..c28f072ea 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1864,6 +1864,14 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + def get_proxy_coin(self) -> str: + """ + Get the proxy coin for the given coin + Falls back to the stake currency if no proxy coin is found + :return: Proxy coin or stake currency + """ + return self._config["stake_currency"] + def get_conversion_rate(self, coin: str, currency: str) -> float | None: """ Quick and cached way to get conversion rate one currency to the other. From 478387531de6b6928977f2882b87067d1863e788 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2024 06:48:47 +0100 Subject: [PATCH 05/20] feat: Updates for proxy coin functionalities --- freqtrade/rpc/rpc.py | 12 +++++++----- freqtrade/wallets.py | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 0e492be86..38cf044b9 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -682,9 +682,10 @@ class RPC: ) -> tuple[float, float]: est_stake = 0.0 est_bot_stake = 0.0 - if coin == stake_currency: + is_futures = self._config.get("trading_mode", TradingMode.SPOT) == TradingMode.FUTURES + if coin == self._freqtrade.exchange.get_proxy_coin(): est_stake = balance.total - if self._config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT: + if is_futures: # in Futures, "total" includes the locked stake, and therefore all positions est_stake = balance.free est_bot_stake = amount @@ -694,7 +695,7 @@ class RPC: coin, stake_currency ) if rate: - est_stake = rate * balance.total + est_stake = rate * (balance.free if is_futures else balance.total) est_bot_stake = rate * amount return est_stake, est_bot_stake @@ -727,9 +728,10 @@ class RPC: continue trade = open_assets.get(coin, None) - is_bot_managed = coin == stake_currency or trade is not None + is_stake_currency = coin == self._freqtrade.exchange.get_proxy_coin() + is_bot_managed = is_stake_currency or trade is not None trade_amount = trade.amount if trade else 0 - if coin == stake_currency: + if is_stake_currency: trade_amount = self._freqtrade.wallets.get_available_stake_amount() try: diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index e0fb1f556..eea90ce33 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -41,7 +41,8 @@ class Wallets: self._wallets: dict[str, Wallet] = {} self._positions: dict[str, PositionWallet] = {} self._start_cap: dict[str, float] = {} - self._stake_currency = config["stake_currency"] + + self._stake_currency = self._exchange.get_proxy_coin() if isinstance(_start_cap := config["dry_run_wallet"], float | int): self._start_cap[self._stake_currency] = _start_cap From d05cae10f77f4da1c0c195ac6d87d38d7cd25678 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2024 07:10:50 +0100 Subject: [PATCH 06/20] test: remove duplicate test assertion --- tests/rpc/test_rpc.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 9bb8d64ea..cdc9ca624 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -600,8 +600,6 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): default_conf_usdt["stake_currency"], default_conf_usdt["fiat_display_currency"] ) - assert pytest.approx(result["total"]) == 2824.83464 - assert pytest.approx(result["value"]) == 2824.83464 * 1.2 assert tickers.call_count == 4 assert tickers.call_args_list[0][1]["cached"] is True # Testing futures - so we should get spot tickers @@ -680,6 +678,7 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): ] assert pytest.approx(result["total_bot"]) == 69.5 assert pytest.approx(result["total"]) == 2824.83464 # ETH stake is missing. + assert pytest.approx(result["value"]) == 2824.83464 * 1.2 assert result["starting_capital"] == 50 * default_conf_usdt["tradable_balance_ratio"] assert result["starting_capital_ratio"] == pytest.approx(0.4040404) From 1ef4b063ea3dc71425bea0c3e3bec14664ac42b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2024 07:12:44 +0100 Subject: [PATCH 07/20] test: adjust test to reflect that futures uses "free", not "total" --- tests/rpc/test_rpc.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index cdc9ca624..ec8cf15c9 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -612,7 +612,7 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): "balance": 0.012, "used": 0.002, "bot_owned": 0, - "est_stake": 103.78464, + "est_stake": 86.4872, "est_stake_bot": 0, "stake": "USDT", "side": "long", @@ -626,7 +626,7 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): "balance": 5.0, "used": 4.0, "bot_owned": 0, - "est_stake": 2651.05, + "est_stake": 530.21, "est_stake_bot": 0, "stake": "USDT", "side": "long", @@ -677,8 +677,8 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): }, ] assert pytest.approx(result["total_bot"]) == 69.5 - assert pytest.approx(result["total"]) == 2824.83464 # ETH stake is missing. - assert pytest.approx(result["value"]) == 2824.83464 * 1.2 + assert pytest.approx(result["total"]) == 686.6972 # ETH stake is missing. + assert pytest.approx(result["value"]) == 686.6972 * 1.2 assert result["starting_capital"] == 50 * default_conf_usdt["tradable_balance_ratio"] assert result["starting_capital_ratio"] == pytest.approx(0.4040404) From c5f89f34fd9557be8b05d905d45eb681c7bdd0e5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2024 19:25:14 +0100 Subject: [PATCH 08/20] tests: improve rpc_handle test --- tests/rpc/test_rpc.py | 87 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 8 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index ec8cf15c9..81a47b91b 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -523,7 +523,9 @@ def test_rpc_balance_handle_error(default_conf, mocker): assert all(currency["currency"] != "ETH" for currency in res["currencies"]) -def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): +@pytest.mark.parametrize("proxy_coin", [None, "BNFCR"]) +@pytest.mark.parametrize("margin_mode", ["isolated", "cross"]) +def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, margin_mode): mock_balance = { "BTC": { "free": 0.01, @@ -548,6 +550,14 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): "used": 5.0, }, } + if proxy_coin: + default_conf_usdt["proxy_coin"] = proxy_coin + mock_balance[proxy_coin] = { + "free": 1500.0, + "total": 0.0, + "used": 0.0, + } + mock_pos = [ { "symbol": "ETH/USDT:USDT", @@ -591,6 +601,7 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): ) default_conf_usdt["dry_run"] = False default_conf_usdt["trading_mode"] = "futures" + default_conf_usdt["margin_mode"] = margin_mode freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt) patch_get_signal(freqtradebot) rpc = RPC(freqtradebot) @@ -600,12 +611,12 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): default_conf_usdt["stake_currency"], default_conf_usdt["fiat_display_currency"] ) - assert tickers.call_count == 4 + assert tickers.call_count == 4 if not proxy_coin else 6 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"] == [ + expected_curr = [ { "currency": "BTC", "free": 0.01, @@ -676,11 +687,71 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): "is_position": True, }, ] - assert pytest.approx(result["total_bot"]) == 69.5 - assert pytest.approx(result["total"]) == 686.6972 # ETH stake is missing. - assert pytest.approx(result["value"]) == 686.6972 * 1.2 - assert result["starting_capital"] == 50 * default_conf_usdt["tradable_balance_ratio"] - assert result["starting_capital_ratio"] == pytest.approx(0.4040404) + if proxy_coin: + if margin_mode == "cross": + # Insert before ETH - as positions are always last. + expected_curr.insert( + len(expected_curr) - 1, + { + "currency": proxy_coin, + "free": 1500.0, + "balance": 0.0, + "used": 0.0, + "bot_owned": 1485.0, + "est_stake": 1500.0, + "est_stake_bot": 1485.0, + "stake": "USDT", + "side": "long", + "position": 0, + "is_bot_managed": True, + "is_position": False, + }, + ) + expected_curr[-3] = { + "currency": "USDT", + "free": 50.0, + "balance": 100.0, + "used": 5.0, + "bot_owned": 0, + "est_stake": 50.0, + "est_stake_bot": 0, + "stake": "USDT", + "side": "long", + "position": 0, + "is_bot_managed": False, + "is_position": False, + } + else: + expected_curr.insert( + len(expected_curr) - 1, + { + "currency": proxy_coin, + "free": 1500.0, + "balance": 0.0, + "used": 0.0, + "bot_owned": 0.0, + "est_stake": 0, + "est_stake_bot": 0, + "stake": "USDT", + "side": "long", + "position": 0, + "is_bot_managed": False, + "is_position": False, + }, + ) + + assert result["currencies"] == expected_curr + if proxy_coin and margin_mode == "cross": + assert pytest.approx(result["total_bot"]) == 1505.0 + assert pytest.approx(result["total"]) == 2186.6972 # ETH stake is missing. + assert result["starting_capital"] == 1500 * default_conf_usdt["tradable_balance_ratio"] + assert result["starting_capital_ratio"] == pytest.approx(0.013468013468013407) + else: + assert pytest.approx(result["total_bot"]) == 69.5 + assert pytest.approx(result["total"]) == 686.6972 # ETH stake is missing. + assert result["starting_capital"] == 50 * default_conf_usdt["tradable_balance_ratio"] + assert result["starting_capital_ratio"] == pytest.approx(0.4040404) + assert pytest.approx(result["value"]) == result["total"] * 1.2 def test_rpc_start(mocker, default_conf) -> None: From c492dcc7ba97bd2685b270a288e062dd8f5fab4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2024 19:30:15 +0100 Subject: [PATCH 09/20] feat: make proxy_coin dynamic (config setting) --- freqtrade/configuration/config_schema.py | 4 ++++ freqtrade/exchange/binance.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/configuration/config_schema.py b/freqtrade/configuration/config_schema.py index d31069e7b..aafb828cd 100644 --- a/freqtrade/configuration/config_schema.py +++ b/freqtrade/configuration/config_schema.py @@ -40,6 +40,10 @@ CONF_SCHEMA = { ), "type": "string", }, + "proxy_coin": { + "description": "Proxy coin - must be used for specific futures modes (e.g. BNFCR)", + "type": "string", + }, "stake_currency": { "description": "Currency used for staking.", "type": "string", diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 1b0b60824..22ece89c9 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -68,7 +68,7 @@ class Binance(Exchange): :return: Proxy coin or stake currency """ if self.margin_mode == MarginMode.CROSS: - return "BNFCR" + return self._config.get("proxy_coin", self._config["stake_currency"]) return self._config["stake_currency"] def get_tickers( From 5a24292d34603f33907deb4065f3e9372a0a2cc3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2024 19:30:58 +0100 Subject: [PATCH 10/20] chore: update config_schema --- build_helpers/schema.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build_helpers/schema.json b/build_helpers/schema.json index 73c06ba46..72c9131ce 100644 --- a/build_helpers/schema.json +++ b/build_helpers/schema.json @@ -13,6 +13,10 @@ "description": "The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). \nUsually specified in the strategy and missing in the configuration.", "type": "string" }, + "proxy_coin": { + "description": "Proxy coin - must be used for specific futures modes (e.g. BNFCR)", + "type": "string" + }, "stake_currency": { "description": "Currency used for staking.", "type": "string" From dcc8c8800f9a2d0feade4464461b5a3c036728a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2024 19:44:24 +0100 Subject: [PATCH 11/20] feat: conversion_rate should map both sides --- freqtrade/exchange/exchange.py | 2 ++ tests/exchange/test_exchange.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c28f072ea..8ac4900c8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1884,6 +1884,8 @@ class Exchange: if (proxy_coin := self._ft_has["proxy_coin_mapping"].get(coin, None)) is not None: coin = proxy_coin + if (proxy_currency := self._ft_has["proxy_coin_mapping"].get(currency, None)) is not None: + currency = proxy_currency if coin == currency: return 1.0 tickers = self.get_tickers(cached=True) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 99e403094..9b37e78eb 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2050,6 +2050,8 @@ def test_get_conversion_rate(default_conf_usdt, mocker, exchange_name): # Special binance case of BNFCR matching USDT. assert exchange.get_conversion_rate("BNFCR", "USDT") is None assert exchange.get_conversion_rate("BNFCR", "USDC") == 1 + assert exchange.get_conversion_rate("USDT", "BNFCR") is None + assert exchange.get_conversion_rate("USDC", "BNFCR") == 1 @pytest.mark.parametrize("exchange_name", EXCHANGES) From 85af1c91a2b3283268eb66aacc9406a63f12d9e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Dec 2024 06:14:37 +0100 Subject: [PATCH 12/20] feat: enable cross futures on binance --- freqtrade/exchange/binance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 22ece89c9..a5a986d1f 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -57,7 +57,7 @@ class Binance(Exchange): _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [ # TradingMode.SPOT always supported and not required in this list # (TradingMode.MARGIN, MarginMode.CROSS), - # (TradingMode.FUTURES, MarginMode.CROSS), + (TradingMode.FUTURES, MarginMode.CROSS), (TradingMode.FUTURES, MarginMode.ISOLATED), ] From 2c1400a9fffcd5e848811a241bb4601d531f9665 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Dec 2024 06:34:18 +0100 Subject: [PATCH 13/20] test: update trading mode test for binance cross support --- tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 9b37e78eb..bbf8bcbf9 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4906,7 +4906,7 @@ def test_set_margin_mode(mocker, default_conf, margin_mode): ("okx", TradingMode.FUTURES, MarginMode.ISOLATED, False), # * Remove once implemented ("binance", TradingMode.MARGIN, MarginMode.CROSS, True), - ("binance", TradingMode.FUTURES, MarginMode.CROSS, True), + ("binance", TradingMode.FUTURES, MarginMode.CROSS, False), ("kraken", TradingMode.MARGIN, MarginMode.CROSS, True), ("kraken", TradingMode.FUTURES, MarginMode.CROSS, True), ("gate", TradingMode.MARGIN, MarginMode.CROSS, True), From d5595727182dcd7045c2f166d898c12baf062f6e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Dec 2024 06:34:36 +0100 Subject: [PATCH 14/20] fix: exclude balances matching trades in futures mode --- freqtrade/rpc/rpc.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 38cf044b9..ed56436d4 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -727,7 +727,11 @@ class RPC: if not balance.total and not balance.free: continue - trade = open_assets.get(coin, None) + trade = ( + open_assets.get(coin, None) + if self._freqtrade.trading_mode != TradingMode.FUTURES + else None + ) is_stake_currency = coin == self._freqtrade.exchange.get_proxy_coin() is_bot_managed = is_stake_currency or trade is not None trade_amount = trade.amount if trade else 0 From 7b1ea81a275d927d3fa0c326a1665f6bc8567f3f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Dec 2024 06:34:51 +0100 Subject: [PATCH 15/20] chore: use safe_value_fallback to get currency rate --- 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 8ac4900c8..4c945ce86 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1903,7 +1903,7 @@ class Exchange: ) ticker = tickers_other.get(pair, None) if ticker: - rate: float | None = ticker.get("last", None) + rate: float | None = safe_value_fallback2(ticker, ticker, "last", "ask", None) if rate and pair.startswith(currency) and not pair.endswith(currency): rate = 1.0 / rate return rate From 2a45ff08e4045670de3009afd3e45aa0951f31bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Dec 2024 06:38:52 +0100 Subject: [PATCH 16/20] fix: update "calculate_fee_rate" to use get_conversation_rate --- freqtrade/exchange/exchange.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 4c945ce86..61340fb0e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2265,13 +2265,11 @@ class Exchange: # If cost is None or 0.0 -> falsy, return None return None try: - for comb in self.get_valid_pair_combination( + fee_to_quote_rate = self.get_conversion_rate( 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 + ) + if not fee_to_quote_rate: + raise ValueError("Conversion rate not found.") except (ValueError, ExchangeError): fee_to_quote_rate = self._config["exchange"].get("unknown_fee_rate", None) if not fee_to_quote_rate: From 44c29aa9983afbc985df11d614a6ab52db5fa041 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Dec 2024 07:09:35 +0100 Subject: [PATCH 17/20] test: update tests for new calculate_fee_rate logic --- tests/exchange/test_exchange.py | 2 +- tests/freqtradebot/test_freqtradebot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index bbf8bcbf9..63d29186c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4729,7 +4729,7 @@ def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None: ], ) def test_calculate_fee_rate(mocker, default_conf, order, expected, unknown_fee_rate) -> None: - mocker.patch(f"{EXMS}.fetch_ticker", return_value={"last": 0.081}) + mocker.patch(f"{EXMS}.get_tickers", return_value={"NEO/BTC": {"last": 0.081}}) if unknown_fee_rate: default_conf["exchange"]["unknown_fee_rate"] = unknown_fee_rate diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index 937bec8e5..8ddb7f6f4 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -3971,7 +3971,7 @@ def test_get_real_amount_multi( markets["BNB/ETH"] = markets["ETH/USDT"] freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets)) - mocker.patch(f"{EXMS}.fetch_ticker", return_value={"ask": 0.19, "last": 0.2}) + mocker.patch(f"{EXMS}.get_conversion_rate", return_value=0.2) # Amount is reduced by "fee" expected_amount = amount * fee_reduction_amount From daf655cd24ae45c1beffb347f76cdccf12266671 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Dec 2024 19:50:07 +0100 Subject: [PATCH 18/20] docs: add documentation for BNFCR futures --- docs/exchanges.md | 23 ++++++++++++++++++++++- docs/leverage.md | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 34a2e4d14..401bd8eea 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -118,7 +118,7 @@ When trading on Binance Futures market, orderbook must be used because there is }, ``` -#### Binance futures settings +#### Binance isolated futures settings Users will also have to have the futures-setting "Position Mode" set to "One-way Mode", and "Asset Mode" set to "Single-Asset Mode". These settings will be checked on startup, and freqtrade will show an error if this setting is wrong. @@ -127,6 +127,27 @@ These settings will be checked on startup, and freqtrade will show an error if t Freqtrade will not attempt to change these settings. +#### Binance BNFCR futures + +BNFCR mode are a special type of futures mode on Binance to work around regulatory issues in Europe. +To use BNFCR futures, you will have to have the following combination of settings: + +``` jsonc +{ + // ... + "trading_mode": "futures", + "margin_mode": "cross", + "proxy_coin": "BNFCR", + "stake_currency": "USDT" // or "USDC" + // ... +} +``` + +the `stake_currency` setting defines the markets the bot will be operating in. This choice is really arbitrary. + +On the exchange, you'll have to use "Multi-asset Mode" - and "Position Mode set to "One-way Mode". +Freqtrade will check these settings on startup, but won't attempt to change them. + ## Bingx BingX supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "IOC" (immediate-or-cancel) and "PO" (Post only) settings. diff --git a/docs/leverage.md b/docs/leverage.md index 2fbd13145..d1517e2d3 100644 --- a/docs/leverage.md +++ b/docs/leverage.md @@ -82,7 +82,7 @@ Each market(trading pair), keeps collateral in a separate account "margin_mode": "isolated" ``` -#### Cross margin mode (currently unavailable) +#### Cross margin mode One account is used to share collateral between markets (trading pairs). Margin is taken from total account balance to avoid liquidation when needed. From 0a27a7ad3853fea8e94f66d712b71b2d588e09bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Dec 2024 20:21:08 +0100 Subject: [PATCH 19/20] docs: fix typo --- docs/exchanges.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 401bd8eea..f253fb753 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -143,7 +143,7 @@ To use BNFCR futures, you will have to have the following combination of setting } ``` -the `stake_currency` setting defines the markets the bot will be operating in. This choice is really arbitrary. +The `stake_currency` setting defines the markets the bot will be operating in. This choice is really arbitrary. On the exchange, you'll have to use "Multi-asset Mode" - and "Position Mode set to "One-way Mode". Freqtrade will check these settings on startup, but won't attempt to change them. From 10063b205c2994b1bb276fb4e4e2ac5cdb05fd10 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 26 Dec 2024 16:55:54 +0100 Subject: [PATCH 20/20] chore: refresh liquidation prices at regular intervals --- freqtrade/freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ed08b99af..8a7385d72 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -162,6 +162,7 @@ class FreqtradeBot(LoggingMixin): def update(): self.update_funding_fees() + self.update_all_liquidation_prices() self.wallets.update() # This would be more efficient if scheduled in utc time, and performed at each