Skip to content

Commit 503eb1e

Browse files
authored
Merge pull request freqtrade#11120 from freqtrade/feat/BNFCR
Add support for BNFCR
2 parents d2beb07 + 10063b2 commit 503eb1e

File tree

14 files changed

+179
-36
lines changed

14 files changed

+179
-36
lines changed

build_helpers/schema.json

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
"description": "The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). \nUsually specified in the strategy and missing in the configuration.",
1414
"type": "string"
1515
},
16+
"proxy_coin": {
17+
"description": "Proxy coin - must be used for specific futures modes (e.g. BNFCR)",
18+
"type": "string"
19+
},
1620
"stake_currency": {
1721
"description": "Currency used for staking.",
1822
"type": "string"

docs/exchanges.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ When trading on Binance Futures market, orderbook must be used because there is
118118
},
119119
```
120120

121-
#### Binance futures settings
121+
#### Binance isolated futures settings
122122

123123
Users will also have to have the futures-setting "Position Mode" set to "One-way Mode", and "Asset Mode" set to "Single-Asset Mode".
124124
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
127127

128128
Freqtrade will not attempt to change these settings.
129129

130+
#### Binance BNFCR futures
131+
132+
BNFCR mode are a special type of futures mode on Binance to work around regulatory issues in Europe.
133+
To use BNFCR futures, you will have to have the following combination of settings:
134+
135+
``` jsonc
136+
{
137+
// ...
138+
"trading_mode": "futures",
139+
"margin_mode": "cross",
140+
"proxy_coin": "BNFCR",
141+
"stake_currency": "USDT" // or "USDC"
142+
// ...
143+
}
144+
```
145+
146+
The `stake_currency` setting defines the markets the bot will be operating in. This choice is really arbitrary.
147+
148+
On the exchange, you'll have to use "Multi-asset Mode" - and "Position Mode set to "One-way Mode".
149+
Freqtrade will check these settings on startup, but won't attempt to change them.
150+
130151
## Bingx
131152

132153
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.

docs/leverage.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Each market(trading pair), keeps collateral in a separate account
8282
"margin_mode": "isolated"
8383
```
8484

85-
#### Cross margin mode (currently unavailable)
85+
#### Cross margin mode
8686

8787
One account is used to share collateral between markets (trading pairs). Margin is taken from total account balance to avoid liquidation when needed.
8888

freqtrade/configuration/config_schema.py

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
),
4141
"type": "string",
4242
},
43+
"proxy_coin": {
44+
"description": "Proxy coin - must be used for specific futures modes (e.g. BNFCR)",
45+
"type": "string",
46+
},
4347
"stake_currency": {
4448
"description": "Currency used for staking.",
4549
"type": "string",

freqtrade/exchange/binance.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,29 @@ class Binance(Exchange):
4848
PriceType.MARK: "MARK_PRICE",
4949
},
5050
"ws_enabled": False,
51+
"proxy_coin_mapping": {
52+
"BNFCR": "USDC",
53+
"BFUSD": "USDT",
54+
},
5155
}
5256

5357
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
5458
# TradingMode.SPOT always supported and not required in this list
5559
# (TradingMode.MARGIN, MarginMode.CROSS),
56-
# (TradingMode.FUTURES, MarginMode.CROSS),
57-
(TradingMode.FUTURES, MarginMode.ISOLATED)
60+
(TradingMode.FUTURES, MarginMode.CROSS),
61+
(TradingMode.FUTURES, MarginMode.ISOLATED),
5862
]
5963

64+
def get_proxy_coin(self) -> str:
65+
"""
66+
Get the proxy coin for the given coin
67+
Falls back to the stake currency if no proxy coin is found
68+
:return: Proxy coin or stake currency
69+
"""
70+
if self.margin_mode == MarginMode.CROSS:
71+
return self._config.get("proxy_coin", self._config["stake_currency"])
72+
return self._config["stake_currency"]
73+
6074
def get_tickers(
6175
self,
6276
symbols: list[str] | None = None,

freqtrade/exchange/exchange.py

+19-7
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ class Exchange:
156156
# Override createMarketBuyOrderRequiresPrice where ccxt has it wrong
157157
"marketOrderRequiresPrice": False,
158158
"exchange_has_overrides": {}, # Dictionary overriding ccxt's "has".
159+
"proxy_coin_mapping": {}, # Mapping for proxy coins
159160
# Expected to be in the format {"fetchOHLCV": True} or {"fetchOHLCV": False}
160161
"ws_enabled": False, # Set to true for exchanges with tested websocket support
161162
}
@@ -1863,6 +1864,14 @@ def get_tickers(
18631864
except ccxt.BaseError as e:
18641865
raise OperationalException(e) from e
18651866

1867+
def get_proxy_coin(self) -> str:
1868+
"""
1869+
Get the proxy coin for the given coin
1870+
Falls back to the stake currency if no proxy coin is found
1871+
:return: Proxy coin or stake currency
1872+
"""
1873+
return self._config["stake_currency"]
1874+
18661875
def get_conversion_rate(self, coin: str, currency: str) -> float | None:
18671876
"""
18681877
Quick and cached way to get conversion rate one currency to the other.
@@ -1872,6 +1881,11 @@ def get_conversion_rate(self, coin: str, currency: str) -> float | None:
18721881
:returns: Conversion rate from coin to currency
18731882
:raises: ExchangeErrors
18741883
"""
1884+
1885+
if (proxy_coin := self._ft_has["proxy_coin_mapping"].get(coin, None)) is not None:
1886+
coin = proxy_coin
1887+
if (proxy_currency := self._ft_has["proxy_coin_mapping"].get(currency, None)) is not None:
1888+
currency = proxy_currency
18751889
if coin == currency:
18761890
return 1.0
18771891
tickers = self.get_tickers(cached=True)
@@ -1889,7 +1903,7 @@ def get_conversion_rate(self, coin: str, currency: str) -> float | None:
18891903
)
18901904
ticker = tickers_other.get(pair, None)
18911905
if ticker:
1892-
rate: float | None = ticker.get("last", None)
1906+
rate: float | None = safe_value_fallback2(ticker, ticker, "last", "ask", None)
18931907
if rate and pair.startswith(currency) and not pair.endswith(currency):
18941908
rate = 1.0 / rate
18951909
return rate
@@ -2251,13 +2265,11 @@ def calculate_fee_rate(
22512265
# If cost is None or 0.0 -> falsy, return None
22522266
return None
22532267
try:
2254-
for comb in self.get_valid_pair_combination(
2268+
fee_to_quote_rate = self.get_conversion_rate(
22552269
fee_curr, self._config["stake_currency"]
2256-
):
2257-
tick = self.fetch_ticker(comb)
2258-
fee_to_quote_rate = safe_value_fallback2(tick, tick, "last", "ask")
2259-
if tick:
2260-
break
2270+
)
2271+
if not fee_to_quote_rate:
2272+
raise ValueError("Conversion rate not found.")
22612273
except (ValueError, ExchangeError):
22622274
fee_to_quote_rate = self._config["exchange"].get("unknown_fee_rate", None)
22632275
if not fee_to_quote_rate:

freqtrade/exchange/exchange_types.py

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ class FtHas(TypedDict, total=False):
4747
needs_trading_fees: bool
4848
order_props_in_contracts: list[Literal["amount", "cost", "filled", "remaining"]]
4949

50+
proxy_coin_mapping: dict[str, str]
51+
5052
# Websocket control
5153
ws_enabled: bool
5254

freqtrade/freqtradebot.py

+1
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ def __init__(self, config: Config) -> None:
162162

163163
def update():
164164
self.update_funding_fees()
165+
self.update_all_liquidation_prices()
165166
self.wallets.update()
166167

167168
# This would be more efficient if scheduled in utc time, and performed at each

freqtrade/leverage/liquidation_price.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ def update_liquidation_prices(
2828
if dry_run:
2929
# Parameters only needed for cross margin
3030
total_wallet_stake = wallets.get_collateral()
31+
logger.info(
32+
"Updating liquidation price for all open trades. "
33+
f"Collateral {total_wallet_stake} {stake_currency}."
34+
)
3135

32-
logger.info(
33-
"Updating liquidation price for all open trades. "
34-
f"Collateral {total_wallet_stake} {stake_currency}."
35-
)
3636
open_trades: list[Trade] = Trade.get_open_trades()
3737
for t in open_trades:
3838
# TODO: This should be done in a batch update

freqtrade/rpc/rpc.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -689,9 +689,10 @@ def __balance_get_est_stake(
689689
) -> tuple[float, float]:
690690
est_stake = 0.0
691691
est_bot_stake = 0.0
692-
if coin == stake_currency:
692+
is_futures = self._config.get("trading_mode", TradingMode.SPOT) == TradingMode.FUTURES
693+
if coin == self._freqtrade.exchange.get_proxy_coin():
693694
est_stake = balance.total
694-
if self._config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
695+
if is_futures:
695696
# in Futures, "total" includes the locked stake, and therefore all positions
696697
est_stake = balance.free
697698
est_bot_stake = amount
@@ -701,7 +702,7 @@ def __balance_get_est_stake(
701702
coin, stake_currency
702703
)
703704
if rate:
704-
est_stake = rate * balance.total
705+
est_stake = rate * (balance.free if is_futures else balance.total)
705706
est_bot_stake = rate * amount
706707

707708
return est_stake, est_bot_stake
@@ -733,10 +734,15 @@ def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> dict:
733734
if not balance.total and not balance.free:
734735
continue
735736

736-
trade = open_assets.get(coin, None)
737-
is_bot_managed = coin == stake_currency or trade is not None
737+
trade = (
738+
open_assets.get(coin, None)
739+
if self._freqtrade.trading_mode != TradingMode.FUTURES
740+
else None
741+
)
742+
is_stake_currency = coin == self._freqtrade.exchange.get_proxy_coin()
743+
is_bot_managed = is_stake_currency or trade is not None
738744
trade_amount = trade.amount if trade else 0
739-
if coin == stake_currency:
745+
if is_stake_currency:
740746
trade_amount = self._freqtrade.wallets.get_available_stake_amount()
741747

742748
try:

freqtrade/wallets.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ def __init__(self, config: Config, exchange: Exchange, is_backtest: bool = False
4141
self._wallets: dict[str, Wallet] = {}
4242
self._positions: dict[str, PositionWallet] = {}
4343
self._start_cap: dict[str, float] = {}
44-
self._stake_currency = config["stake_currency"]
44+
45+
self._stake_currency = self._exchange.get_proxy_coin()
4546

4647
if isinstance(_start_cap := config["dry_run_wallet"], float | int):
4748
self._start_cap[self._stake_currency] = _start_cap

tests/exchange/test_exchange.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -2028,6 +2028,7 @@ def test_get_conversion_rate(default_conf_usdt, mocker, exchange_name):
20282028
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
20292029
api_mock.fetch_tickers = MagicMock(side_effect=[tick, tick2])
20302030
api_mock.fetch_bids_asks = MagicMock(return_value={})
2031+
default_conf_usdt["trading_mode"] = "futures"
20312032

20322033
exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, exchange=exchange_name)
20332034
# retrieve original ticker
@@ -2045,6 +2046,13 @@ def test_get_conversion_rate(default_conf_usdt, mocker, exchange_name):
20452046
# Only the call to the "others" market
20462047
assert api_mock.fetch_tickers.call_count == 1
20472048

2049+
if exchange_name == "binance":
2050+
# Special binance case of BNFCR matching USDT.
2051+
assert exchange.get_conversion_rate("BNFCR", "USDT") is None
2052+
assert exchange.get_conversion_rate("BNFCR", "USDC") == 1
2053+
assert exchange.get_conversion_rate("USDT", "BNFCR") is None
2054+
assert exchange.get_conversion_rate("USDC", "BNFCR") == 1
2055+
20482056

20492057
@pytest.mark.parametrize("exchange_name", EXCHANGES)
20502058
def test_fetch_ticker(default_conf, mocker, exchange_name):
@@ -4721,7 +4729,7 @@ def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None:
47214729
],
47224730
)
47234731
def test_calculate_fee_rate(mocker, default_conf, order, expected, unknown_fee_rate) -> None:
4724-
mocker.patch(f"{EXMS}.fetch_ticker", return_value={"last": 0.081})
4732+
mocker.patch(f"{EXMS}.get_tickers", return_value={"NEO/BTC": {"last": 0.081}})
47254733
if unknown_fee_rate:
47264734
default_conf["exchange"]["unknown_fee_rate"] = unknown_fee_rate
47274735

@@ -4898,7 +4906,7 @@ def test_set_margin_mode(mocker, default_conf, margin_mode):
48984906
("okx", TradingMode.FUTURES, MarginMode.ISOLATED, False),
48994907
# * Remove once implemented
49004908
("binance", TradingMode.MARGIN, MarginMode.CROSS, True),
4901-
("binance", TradingMode.FUTURES, MarginMode.CROSS, True),
4909+
("binance", TradingMode.FUTURES, MarginMode.CROSS, False),
49024910
("kraken", TradingMode.MARGIN, MarginMode.CROSS, True),
49034911
("kraken", TradingMode.FUTURES, MarginMode.CROSS, True),
49044912
("gate", TradingMode.MARGIN, MarginMode.CROSS, True),

tests/freqtradebot/test_freqtradebot.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3980,7 +3980,7 @@ def test_get_real_amount_multi(
39803980
markets["BNB/ETH"] = markets["ETH/USDT"]
39813981
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
39823982
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets))
3983-
mocker.patch(f"{EXMS}.fetch_ticker", return_value={"ask": 0.19, "last": 0.2})
3983+
mocker.patch(f"{EXMS}.get_conversion_rate", return_value=0.2)
39843984

39853985
# Amount is reduced by "fee"
39863986
expected_amount = amount * fee_reduction_amount

0 commit comments

Comments
 (0)