from copy import deepcopy from pathlib import Path from typing import Tuple import pytest from freqtrade.constants import Config from freqtrade.exchange.exchange import Exchange from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import EXMS, get_default_conf_usdt EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str] EXCHANGE_WS_FIXTURE_TYPE = Tuple[Exchange, str, str] # Exchanges that should be tested online EXCHANGES = { "binance": { "pair": "BTC/USDT", "stake_currency": "USDT", "use_ci_proxy": True, "hasQuoteVolume": True, "timeframe": "1h", "futures": True, "futures_pair": "BTC/USDT:USDT", "hasQuoteVolumeFutures": True, "leverage_tiers_public": False, "leverage_in_spot_market": False, "trades_lookback_hours": 4, "private_methods": ["fapiPrivateGetPositionSideDual", "fapiPrivateGetMultiAssetsMargin"], "sample_order": [ { "symbol": "SOLUSDT", "orderId": 3551312894, "orderListId": -1, "clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba", "transactTime": 1674493798550, "price": "15.50000000", "origQty": "1.10000000", "executedQty": "0.00000000", "cummulativeQuoteQty": "0.00000000", "status": "NEW", "timeInForce": "GTC", "type": "LIMIT", "side": "BUY", "workingTime": 1674493798550, "fills": [], "selfTradePreventionMode": "NONE", }, { "symbol": "SOLUSDT", "orderId": 3551312894, "orderListId": -1, "clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba", "transactTime": 1674493798550, "price": "15.50000000", "origQty": "1.10000000", "executedQty": "1.10000000", "cummulativeQuoteQty": "17.05", "status": "FILLED", "timeInForce": "GTC", "type": "LIMIT", "side": "BUY", "workingTime": 1674493798550, "fills": [], "selfTradePreventionMode": "NONE", }, ], }, "binanceus": { "pair": "BTC/USDT", "stake_currency": "USDT", "hasQuoteVolume": True, "timeframe": "1h", "futures": False, "sample_order": [ { "symbol": "SOLUSDT", "orderId": 3551312894, "orderListId": -1, "clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba", "transactTime": 1674493798550, "price": "15.50000000", "origQty": "1.10000000", "executedQty": "0.00000000", "cummulativeQuoteQty": "0.00000000", "status": "NEW", "timeInForce": "GTC", "type": "LIMIT", "side": "BUY", "workingTime": 1674493798550, "fills": [], "selfTradePreventionMode": "NONE", } ], }, "kraken": { "pair": "BTC/USD", "stake_currency": "USD", "hasQuoteVolume": True, "timeframe": "1h", "leverage_tiers_public": False, "leverage_in_spot_market": True, "trades_lookback_hours": 12, }, "kucoin": { "pair": "XRP/USDT", "stake_currency": "USDT", "hasQuoteVolume": True, "timeframe": "1h", "leverage_tiers_public": False, "leverage_in_spot_market": True, "sample_order": [ {"id": "63d6742d0adc5570001d2bbf7"}, # create order { "id": "63d6742d0adc5570001d2bbf7", "symbol": "SOL-USDT", "opType": "DEAL", "type": "limit", "side": "buy", "price": "15.5", "size": "1.1", "funds": "0", "dealFunds": "17.05", "dealSize": "1.1", "fee": "0.000065252", "feeCurrency": "USDT", "stp": "", "stop": "", "stopTriggered": False, "stopPrice": "0", "timeInForce": "GTC", "postOnly": False, "hidden": False, "iceberg": False, "visibleSize": "0", "cancelAfter": 0, "channel": "API", "clientOid": "0a053870-11bf-41e5-be61-b272a4cb62e1", "remark": None, "tags": "partner:ccxt", "isActive": False, "cancelExist": False, "createdAt": 1674493798550, "tradeType": "TRADE", }, ], }, "gate": { "pair": "BTC/USDT", "stake_currency": "USDT", "hasQuoteVolume": True, "timeframe": "1h", "futures": True, "futures_pair": "BTC/USDT:USDT", "hasQuoteVolumeFutures": True, "leverage_tiers_public": True, "leverage_in_spot_market": True, "sample_order": [ { "id": "276266139423", "text": "apiv4", "create_time": "1674493798", "update_time": "1674493798", "create_time_ms": "1674493798550", "update_time_ms": "1674493798550", "status": "closed", "currency_pair": "SOL_USDT", "type": "limit", "account": "spot", "side": "buy", "amount": "1.1", "price": "15.5", "time_in_force": "gtc", "iceberg": "0", "left": "0", "fill_price": "17.05", "filled_total": "17.05", "avg_deal_price": "15.5", "fee": "0.0000018", "fee_currency": "SOL", "point_fee": "0", "gt_fee": "0", "gt_maker_fee": "0", "gt_taker_fee": "0.0015", "gt_discount": True, "rebated_fee": "0", "rebated_fee_currency": "USDT", }, { # market order "id": "276401180529", "text": "apiv4", "create_time": "1674493798", "update_time": "1674493798", "create_time_ms": "1674493798550", "update_time_ms": "1674493798550", "status": "cancelled", "currency_pair": "SOL_USDT", "type": "market", "account": "spot", "side": "buy", "amount": "17.05", "price": "0", "time_in_force": "ioc", "iceberg": "0", "left": "0.0000000016228", "fill_price": "17.05", "filled_total": "17.05", "avg_deal_price": "15.5", "fee": "0", "fee_currency": "SOL", "point_fee": "0.0199999999967544", "gt_fee": "0", "gt_maker_fee": "0", "gt_taker_fee": "0", "gt_discount": False, "rebated_fee": "0", "rebated_fee_currency": "USDT", }, ], "sample_my_trades": [ { "id": "123412341234", "create_time": "167997798", "create_time_ms": "167997798825.566200", "currency_pair": "SOL_USDT", "side": "sell", "role": "taker", "amount": "0.0115", "price": "1712.63", "order_id": "1234123412", "fee": "0.0", "fee_currency": "USDT", "point_fee": "0.03939049", "gt_fee": "0.0", "amend_text": "-", } ], }, "okx": { "pair": "BTC/USDT", "stake_currency": "USDT", "hasQuoteVolume": True, "timeframe": "1h", "futures": True, "futures_pair": "BTC/USDT:USDT", "hasQuoteVolumeFutures": False, "leverage_tiers_public": True, "leverage_in_spot_market": True, "private_methods": ["fetch_accounts"], }, "bybit": { "pair": "BTC/USDT", "stake_currency": "USDT", "hasQuoteVolume": True, "use_ci_proxy": True, "timeframe": "1h", "futures_pair": "BTC/USDT:USDT", "futures": True, "orderbook_max_entries": 50, "leverage_tiers_public": True, "leverage_in_spot_market": True, "sample_order": [ { "orderId": "1274754916287346280", "orderLinkId": "1666798627015730", "symbol": "SOLUSDT", "createdTime": "1674493798550", "price": "15.5", "qty": "1.1", "orderType": "Limit", "side": "Buy", "orderStatus": "New", "timeInForce": "GTC", "accountId": "5555555", "execQty": "0", "orderCategory": "0", } ], }, "bitmart": { "pair": "BTC/USDT", "stake_currency": "USDT", "hasQuoteVolume": True, "timeframe": "1h", "orderbook_max_entries": 50, }, "htx": { "pair": "ETH/BTC", "stake_currency": "BTC", "hasQuoteVolume": True, "timeframe": "1h", "futures": False, }, "bitvavo": { "pair": "BTC/EUR", "stake_currency": "EUR", "hasQuoteVolume": True, "timeframe": "1h", "leverage_tiers_public": False, "leverage_in_spot_market": False, }, "bingx": { "pair": "BTC/USDT", "stake_currency": "USDT", "hasQuoteVolume": True, "timeframe": "1h", "futures": False, "sample_order": [ { "symbol": "SOL-USDT", "orderId": "1762393630149869568", "transactTime": "1674493798550", "price": "15.5", "stopPrice": "0", "origQty": "1.1", "executedQty": "1.1", "cummulativeQuoteQty": "17.05", "status": "FILLED", "type": "LIMIT", "side": "BUY", "clientOrderID": "", }, { "symbol": "SOL-USDT", "orderId": "1762393630149869568", "transactTime": "1674493798550", "price": "15.5", "stopPrice": "0", "origQty": "1.1", "executedQty": "1.1", "cummulativeQuoteQty": "17.05", "status": "FILLED", "type": "MARKET", "side": "BUY", "clientOrderID": "", }, ], }, } @pytest.fixture(scope="class") def exchange_conf(): config = get_default_conf_usdt((Path(__file__).parent / "testdata").resolve()) config["exchange"]["pair_whitelist"] = [] config["exchange"]["key"] = "" config["exchange"]["secret"] = "" config["dry_run"] = False config["entry_pricing"]["use_order_book"] = True config["exit_pricing"]["use_order_book"] = True return config def set_test_proxy(config: Config, use_proxy: bool) -> Config: # Set proxy to test in CI. import os if use_proxy and (proxy := os.environ.get("CI_WEB_PROXY")): config1 = deepcopy(config) config1["exchange"]["ccxt_config"] = { "httpsProxy": proxy, "wsProxy": proxy, } return config1 return config def get_exchange(exchange_name, exchange_conf): exchange_conf = set_test_proxy( exchange_conf, EXCHANGES[exchange_name].get("use_ci_proxy", False) ) exchange_conf["exchange"]["name"] = exchange_name exchange_conf["stake_currency"] = EXCHANGES[exchange_name]["stake_currency"] exchange = ExchangeResolver.load_exchange( exchange_conf, validate=True, load_leverage_tiers=True ) return exchange, exchange_name def get_futures_exchange(exchange_name, exchange_conf, class_mocker): if EXCHANGES[exchange_name].get("futures") is not True: pytest.skip(f"Exchange {exchange_name} does not support futures.") else: exchange_conf = deepcopy(exchange_conf) exchange_conf = set_test_proxy( exchange_conf, EXCHANGES[exchange_name].get("use_ci_proxy", False) ) exchange_conf["trading_mode"] = "futures" exchange_conf["margin_mode"] = "isolated" class_mocker.patch("freqtrade.exchange.binance.Binance.fill_leverage_tiers") class_mocker.patch(f"{EXMS}.fetch_trading_fees") class_mocker.patch("freqtrade.exchange.okx.Okx.additional_exchange_init") class_mocker.patch("freqtrade.exchange.binance.Binance.additional_exchange_init") class_mocker.patch("freqtrade.exchange.bybit.Bybit.additional_exchange_init") class_mocker.patch(f"{EXMS}.load_cached_leverage_tiers", return_value=None) class_mocker.patch(f"{EXMS}.cache_leverage_tiers") return get_exchange(exchange_name, exchange_conf) @pytest.fixture(params=EXCHANGES, scope="class") def exchange(request, exchange_conf, class_mocker): class_mocker.patch("freqtrade.exchange.bybit.Bybit.additional_exchange_init") return get_exchange(request.param, exchange_conf) @pytest.fixture(params=EXCHANGES, scope="class") def exchange_futures(request, exchange_conf, class_mocker): return get_futures_exchange(request.param, exchange_conf, class_mocker) @pytest.fixture(params=["spot", "futures"], scope="class") def exchange_mode(request): return request.param @pytest.fixture(params=EXCHANGES, scope="class") def exchange_ws(request, exchange_conf, exchange_mode, class_mocker): class_mocker.patch("freqtrade.exchange.bybit.Bybit.additional_exchange_init") exchange_conf["exchange"]["enable_ws"] = True if exchange_mode == "spot": exchange, name = get_exchange(request.param, exchange_conf) pair = EXCHANGES[request.param]["pair"] elif EXCHANGES[request.param].get("futures"): exchange, name = get_futures_exchange( request.param, exchange_conf, class_mocker=class_mocker ) pair = EXCHANGES[request.param]["futures_pair"] else: pytest.skip("Exchange does not support futures.") if not exchange._has_watch_ohlcv: pytest.skip("Exchange does not support watch_ohlcv.") yield exchange, name, pair exchange.close()