mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-03-02 08:12:05 +00:00
Merge branch 'develop' into dependabot/pip/develop/pydantic-2.1.1
This commit is contained in:
@@ -34,26 +34,21 @@ def test_ohlcv_to_dataframe(ohlcv_history_list, caplog):
|
||||
assert log_has('Converting candle (OHLCV) data to dataframe for pair UNITTEST/BTC.', caplog)
|
||||
|
||||
|
||||
def test_trades_to_ohlcv(ohlcv_history_list, caplog):
|
||||
def test_trades_to_ohlcv(trades_history, caplog):
|
||||
|
||||
caplog.set_level(logging.DEBUG)
|
||||
with pytest.raises(ValueError, match="Trade-list empty."):
|
||||
trades_to_ohlcv([], '1m')
|
||||
|
||||
trades = [
|
||||
[1570752011620, "13519807", None, "sell", 0.00141342, 23.0, 0.03250866],
|
||||
[1570752011620, "13519808", None, "sell", 0.00141266, 54.0, 0.07628364],
|
||||
[1570752017964, "13519809", None, "sell", 0.00141266, 8.0, 0.01130128]]
|
||||
|
||||
df = trades_to_ohlcv(trades, '1m')
|
||||
df = trades_to_ohlcv(trades_history, '1m')
|
||||
assert not df.empty
|
||||
assert len(df) == 1
|
||||
assert 'open' in df.columns
|
||||
assert 'high' in df.columns
|
||||
assert 'low' in df.columns
|
||||
assert 'close' in df.columns
|
||||
assert df.loc[:, 'high'][0] == 0.00141342
|
||||
assert df.loc[:, 'low'][0] == 0.00141266
|
||||
assert df.loc[:, 'high'][0] == 0.019627
|
||||
assert df.loc[:, 'low'][0] == 0.019626
|
||||
|
||||
|
||||
def test_ohlcv_fill_up_missing_data(testdatadir, caplog):
|
||||
|
||||
@@ -129,9 +129,14 @@ def test_get_pair_dataframe(mocker, default_conf, ohlcv_history, candle_type):
|
||||
default_conf["runmode"] = RunMode.BACKTEST
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
assert dp.runmode == RunMode.BACKTEST
|
||||
assert isinstance(dp.get_pair_dataframe(
|
||||
"UNITTEST/BTC", timeframe, candle_type=candle_type), DataFrame)
|
||||
# assert dp.get_pair_dataframe("NONESENSE/AAA", timeframe).empty
|
||||
df = dp.get_pair_dataframe("UNITTEST/BTC", timeframe, candle_type=candle_type)
|
||||
assert isinstance(df, DataFrame)
|
||||
assert len(df) == 3 # ohlcv_history mock has just 3 rows
|
||||
|
||||
dp._set_dataframe_max_date(ohlcv_history.iloc[-1]['date'])
|
||||
df = dp.get_pair_dataframe("UNITTEST/BTC", timeframe, candle_type=candle_type)
|
||||
assert isinstance(df, DataFrame)
|
||||
assert len(df) == 2 # ohlcv_history is limited to 2 rows now
|
||||
|
||||
|
||||
def test_available_pairs(mocker, default_conf, ohlcv_history):
|
||||
|
||||
@@ -35,7 +35,7 @@ def test__get_params_binance(default_conf, mocker, side, type, time_in_force, ex
|
||||
])
|
||||
def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
||||
order_type = 'stop_loss_limit' if trademode == TradingMode.SPOT else 'stop'
|
||||
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
|
||||
@@ -3,7 +3,6 @@ from unittest.mock import MagicMock
|
||||
|
||||
from freqtrade.enums.marginmode import MarginMode
|
||||
from freqtrade.enums.tradingmode import TradingMode
|
||||
from freqtrade.exchange.exchange_utils import timeframe_to_msecs
|
||||
from tests.conftest import get_mock_coro, get_patched_exchange
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
@@ -37,12 +36,10 @@ async def test_bybit_fetch_funding_rate(default_conf, mocker):
|
||||
assert api_mock.fetch_funding_rate_history.call_count == 1
|
||||
assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT'
|
||||
kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1]
|
||||
assert kwargs['params'] == {}
|
||||
assert kwargs['since'] is None
|
||||
|
||||
api_mock.fetch_funding_rate_history.reset_mock()
|
||||
since_ms = 1610000000000
|
||||
since_ms_end = since_ms + (timeframe_to_msecs('4h') * limit)
|
||||
# Test fetch_funding_rate_history (current data)
|
||||
await exchange._fetch_funding_rate_history(
|
||||
pair='BTC/USDT:USDT',
|
||||
@@ -54,7 +51,6 @@ async def test_bybit_fetch_funding_rate(default_conf, mocker):
|
||||
assert api_mock.fetch_funding_rate_history.call_count == 1
|
||||
assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT'
|
||||
kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1]
|
||||
assert kwargs['params'] == {'until': since_ms_end}
|
||||
assert kwargs['since'] == since_ms
|
||||
|
||||
|
||||
|
||||
@@ -556,41 +556,6 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
|
||||
assert result == 4000
|
||||
|
||||
|
||||
def test_set_sandbox(default_conf, mocker):
|
||||
"""
|
||||
Test working scenario
|
||||
"""
|
||||
api_mock = MagicMock()
|
||||
api_mock.load_markets = MagicMock(return_value={
|
||||
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
|
||||
})
|
||||
url_mock = PropertyMock(return_value={'test': "api-public.sandbox.gdax.com",
|
||||
'api': 'https://api.gdax.com'})
|
||||
type(api_mock).urls = url_mock
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
liveurl = exchange._api.urls['api']
|
||||
default_conf['exchange']['sandbox'] = True
|
||||
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
|
||||
assert exchange._api.urls['api'] != liveurl
|
||||
|
||||
|
||||
def test_set_sandbox_exception(default_conf, mocker):
|
||||
"""
|
||||
Test Fail scenario
|
||||
"""
|
||||
api_mock = MagicMock()
|
||||
api_mock.load_markets = MagicMock(return_value={
|
||||
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
|
||||
})
|
||||
url_mock = PropertyMock(return_value={'api': 'https://api.gdax.com'})
|
||||
type(api_mock).urls = url_mock
|
||||
|
||||
with pytest.raises(OperationalException, match=r'does not provide a sandbox api'):
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
default_conf['exchange']['sandbox'] = True
|
||||
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
|
||||
|
||||
|
||||
def test__load_async_markets(default_conf, mocker, caplog):
|
||||
mocker.patch(f'{EXMS}._init_ccxt')
|
||||
mocker.patch(f'{EXMS}.validate_pairs')
|
||||
@@ -1372,7 +1337,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6))
|
||||
order_id = f'test_prod_{side}_{randint(0, 10 ** 6)}'
|
||||
api_mock.options = {} if not marketprice else {"createMarketBuyOrderRequiresPrice": True}
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
@@ -1452,7 +1417,7 @@ def test_buy_dry_run(default_conf, mocker, exchange_name):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_buy_prod(default_conf, mocker, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
||||
order_type = 'market'
|
||||
time_in_force = 'gtc'
|
||||
api_mock.options = {}
|
||||
@@ -1541,7 +1506,7 @@ def test_buy_prod(default_conf, mocker, exchange_name):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_buy_considers_time_in_force(default_conf, mocker, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
||||
api_mock.options = {}
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
@@ -1608,7 +1573,7 @@ def test_sell_dry_run(default_conf, mocker):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_sell_prod(default_conf, mocker, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
|
||||
order_id = f'test_prod_sell_{randint(0, 10 ** 6)}'
|
||||
order_type = 'market'
|
||||
api_mock.options = {}
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
@@ -1686,7 +1651,7 @@ def test_sell_prod(default_conf, mocker, exchange_name):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_sell_considers_time_in_force(default_conf, mocker, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
|
||||
order_id = f'test_prod_sell_{randint(0, 10 ** 6)}'
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
'symbol': 'ETH/BTC',
|
||||
@@ -2100,7 +2065,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_
|
||||
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
|
||||
# one_call calculation * 1.8 should do 2 calls
|
||||
|
||||
since = 5 * 60 * exchange.ohlcv_candle_limit('5m', CandleType.SPOT) * 1.8
|
||||
since = 5 * 60 * exchange.ohlcv_candle_limit('5m', candle_type) * 1.8
|
||||
ret = exchange.get_historic_ohlcv(
|
||||
pair,
|
||||
"5m",
|
||||
@@ -3557,7 +3522,7 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
|
||||
|
||||
assert ex.get_valid_pair_combination("ETH", "BTC") == "ETH/BTC"
|
||||
assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC"
|
||||
with pytest.raises(DependencyException, match=r"Could not combine.* to get a valid pair."):
|
||||
with pytest.raises(ValueError, match=r"Could not combine.* to get a valid pair."):
|
||||
ex.get_valid_pair_combination("NOPAIR", "ETH")
|
||||
|
||||
|
||||
@@ -5392,7 +5357,7 @@ def test_get_liquidation_price(
|
||||
])
|
||||
def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amount):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
||||
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
|
||||
@@ -16,7 +16,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
])
|
||||
def test_create_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
||||
order_type = 'stop-limit'
|
||||
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
|
||||
@@ -15,7 +15,7 @@ STOPLOSS_LIMIT_ORDERTYPE = 'stop-loss-limit'
|
||||
|
||||
def test_buy_kraken_trading_agreement(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
||||
order_type = 'limit'
|
||||
time_in_force = 'ioc'
|
||||
api_mock.options = {}
|
||||
@@ -56,7 +56,7 @@ def test_buy_kraken_trading_agreement(default_conf, mocker):
|
||||
|
||||
def test_sell_kraken_trading_agreement(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
|
||||
order_id = f'test_prod_sell_{randint(0, 10 ** 6)}'
|
||||
order_type = 'market'
|
||||
api_mock.options = {}
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
@@ -181,7 +181,7 @@ def test_get_balances_prod(default_conf, mocker):
|
||||
])
|
||||
def test_create_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedprice):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
||||
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
|
||||
@@ -17,7 +17,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
])
|
||||
def test_create_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side, order_type):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
||||
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
@@ -136,7 +136,7 @@ def test_stoploss_adjust_kucoin(mocker, default_conf):
|
||||
])
|
||||
def test_kucoin_create_order(default_conf, mocker, side, ordertype, rate):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6))
|
||||
order_id = f'test_prod_{side}_{randint(0, 10 ** 6)}'
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
'info': {
|
||||
|
||||
0
tests/exchange_online/__init__.py
Normal file
0
tests/exchange_online/__init__.py
Normal file
334
tests/exchange_online/conftest.py
Normal file
334
tests/exchange_online/conftest.py
Normal file
@@ -0,0 +1,334 @@
|
||||
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]
|
||||
|
||||
# Exchanges that should be tested online
|
||||
EXCHANGES = {
|
||||
'bittrex': {
|
||||
'pair': 'BTC/USDT',
|
||||
'stake_currency': 'USDT',
|
||||
'hasQuoteVolume': False,
|
||||
'timeframe': '1h',
|
||||
'leverage_tiers_public': False,
|
||||
'leverage_in_spot_market': False,
|
||||
},
|
||||
'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",
|
||||
}]
|
||||
},
|
||||
'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'
|
||||
}
|
||||
],
|
||||
},
|
||||
'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,
|
||||
'leverage_tiers_public': True,
|
||||
'leverage_in_spot_market': True,
|
||||
'sample_order': [
|
||||
{
|
||||
"orderId": "1274754916287346280",
|
||||
"orderLinkId": "1666798627015730",
|
||||
"symbol": "SOLUSDT",
|
||||
"createTime": "1674493798550",
|
||||
"orderPrice": "15.5",
|
||||
"orderQty": "1.1",
|
||||
"orderType": "LIMIT",
|
||||
"side": "BUY",
|
||||
"status": "NEW",
|
||||
"timeInForce": "GTC",
|
||||
"accountId": "5555555",
|
||||
"execQty": "0",
|
||||
"orderCategory": "0"
|
||||
}
|
||||
]
|
||||
},
|
||||
'huobi': {
|
||||
'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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@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,
|
||||
}
|
||||
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)
|
||||
|
||||
yield 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')
|
||||
|
||||
yield from get_exchange(exchange_name, exchange_conf)
|
||||
|
||||
|
||||
@pytest.fixture(params=EXCHANGES, scope="class")
|
||||
def exchange(request, exchange_conf):
|
||||
yield from get_exchange(request.param, exchange_conf)
|
||||
|
||||
|
||||
@pytest.fixture(params=EXCHANGES, scope="class")
|
||||
def exchange_futures(request, exchange_conf, class_mocker):
|
||||
|
||||
yield from get_futures_exchange(request.param, exchange_conf, class_mocker)
|
||||
@@ -5,338 +5,14 @@ However, these tests should give a good idea to determine if a new exchange is
|
||||
suitable to run with freqtrade.
|
||||
"""
|
||||
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||
from freqtrade.exchange.exchange import Exchange, timeframe_to_msecs
|
||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||
from tests.conftest import EXMS, get_default_conf_usdt
|
||||
|
||||
|
||||
EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str]
|
||||
|
||||
# Exchanges that should be tested
|
||||
EXCHANGES = {
|
||||
'bittrex': {
|
||||
'pair': 'BTC/USDT',
|
||||
'stake_currency': 'USDT',
|
||||
'hasQuoteVolume': False,
|
||||
'timeframe': '1h',
|
||||
'leverage_tiers_public': False,
|
||||
'leverage_in_spot_market': False,
|
||||
},
|
||||
'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",
|
||||
}]
|
||||
},
|
||||
'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'
|
||||
}
|
||||
],
|
||||
},
|
||||
'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,
|
||||
'leverage_tiers_public': True,
|
||||
'leverage_in_spot_market': True,
|
||||
'sample_order': [
|
||||
{
|
||||
"orderId": "1274754916287346280",
|
||||
"orderLinkId": "1666798627015730",
|
||||
"symbol": "SOLUSDT",
|
||||
"createTime": "1674493798550",
|
||||
"orderPrice": "15.5",
|
||||
"orderQty": "1.1",
|
||||
"orderType": "LIMIT",
|
||||
"side": "BUY",
|
||||
"status": "NEW",
|
||||
"timeInForce": "GTC",
|
||||
"accountId": "5555555",
|
||||
"execQty": "0",
|
||||
"orderCategory": "0"
|
||||
}
|
||||
]
|
||||
},
|
||||
'huobi': {
|
||||
'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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@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,
|
||||
}
|
||||
return config1
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture(params=EXCHANGES, scope="class")
|
||||
def exchange(request, exchange_conf):
|
||||
exchange_conf = set_test_proxy(
|
||||
exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False))
|
||||
exchange_conf['exchange']['name'] = request.param
|
||||
exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency']
|
||||
exchange = ExchangeResolver.load_exchange(exchange_conf, validate=True)
|
||||
|
||||
yield exchange, request.param
|
||||
|
||||
|
||||
@pytest.fixture(params=EXCHANGES, scope="class")
|
||||
def exchange_futures(request, exchange_conf, class_mocker):
|
||||
if EXCHANGES[request.param].get('futures') is not True:
|
||||
yield None, request.param
|
||||
else:
|
||||
exchange_conf = set_test_proxy(
|
||||
exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False))
|
||||
exchange_conf = deepcopy(exchange_conf)
|
||||
exchange_conf['exchange']['name'] = request.param
|
||||
exchange_conf['trading_mode'] = 'futures'
|
||||
exchange_conf['margin_mode'] = 'isolated'
|
||||
exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency']
|
||||
|
||||
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')
|
||||
|
||||
exchange = ExchangeResolver.load_exchange(
|
||||
exchange_conf, validate=True, load_leverage_tiers=True)
|
||||
|
||||
yield exchange, request.param
|
||||
from freqtrade.exchange.exchange import timeframe_to_msecs
|
||||
from tests.exchange_online.conftest import EXCHANGE_FIXTURE_TYPE, EXCHANGES
|
||||
|
||||
|
||||
@pytest.mark.longrun
|
||||
@@ -371,9 +47,6 @@ class TestCCXTExchange:
|
||||
|
||||
def test_load_markets_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
exchange, exchangename = exchange_futures
|
||||
if not exchange:
|
||||
# exchange_futures only returns values for supported exchanges
|
||||
return
|
||||
pair = EXCHANGES[exchangename]['pair']
|
||||
pair = EXCHANGES[exchangename].get('futures_pair', pair)
|
||||
markets = exchange.markets
|
||||
@@ -391,7 +64,7 @@ class TestCCXTExchange:
|
||||
assert po['id'] is not None
|
||||
if len(order.keys()) < 5:
|
||||
# Kucoin case
|
||||
assert po['status'] == 'closed'
|
||||
assert po['status'] is None
|
||||
continue
|
||||
assert po['timestamp'] == 1674493798550
|
||||
assert isinstance(po['datetime'], str)
|
||||
@@ -511,7 +184,8 @@ class TestCCXTExchange:
|
||||
now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
|
||||
assert exch.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
|
||||
|
||||
def ccxt__async_get_candle_history(self, exchange, exchangename, pair, timeframe, candle_type):
|
||||
def ccxt__async_get_candle_history(
|
||||
self, exchange, exchangename, pair, timeframe, candle_type, factor=0.9):
|
||||
|
||||
timeframe_ms = timeframe_to_msecs(timeframe)
|
||||
now = timeframe_to_prev_date(
|
||||
@@ -532,11 +206,11 @@ class TestCCXTExchange:
|
||||
assert res[1] == timeframe
|
||||
assert res[2] == candle_type
|
||||
candles = res[3]
|
||||
factor = 0.9
|
||||
candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * factor
|
||||
candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms * factor
|
||||
assert len(candles) >= min(candle_count, candle_count1), \
|
||||
f"{len(candles)} < {candle_count} in {timeframe}, Offset: {offset} {factor}"
|
||||
# Check if first-timeframe is either the start, or start + 1
|
||||
assert candles[0][0] == since_ms or (since_ms + timeframe_ms)
|
||||
|
||||
def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
@@ -544,8 +218,6 @@ class TestCCXTExchange:
|
||||
if exchangename in ('bittrex'):
|
||||
# For some weired reason, this test returns random lengths for bittrex.
|
||||
pytest.skip("Exchange doesn't provide stable ohlcv history")
|
||||
if exchangename in ('bitvavo'):
|
||||
pytest.skip("Exchange Downtime ")
|
||||
|
||||
if not exc._ft_has['ohlcv_has_history']:
|
||||
pytest.skip("Exchange does not support candle history")
|
||||
@@ -554,21 +226,29 @@ class TestCCXTExchange:
|
||||
self.ccxt__async_get_candle_history(
|
||||
exc, exchangename, pair, timeframe, CandleType.SPOT)
|
||||
|
||||
def test_ccxt__async_get_candle_history_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
@pytest.mark.parametrize('candle_type', [
|
||||
CandleType.FUTURES,
|
||||
CandleType.FUNDING_RATE,
|
||||
CandleType.MARK,
|
||||
])
|
||||
def test_ccxt__async_get_candle_history_futures(
|
||||
self, exchange_futures: EXCHANGE_FIXTURE_TYPE, candle_type):
|
||||
exchange, exchangename = exchange_futures
|
||||
if not exchange:
|
||||
# exchange_futures only returns values for supported exchanges
|
||||
return
|
||||
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
||||
timeframe = EXCHANGES[exchangename]['timeframe']
|
||||
if candle_type == CandleType.FUNDING_RATE:
|
||||
timeframe = exchange._ft_has.get('funding_fee_timeframe',
|
||||
exchange._ft_has['mark_ohlcv_timeframe'])
|
||||
self.ccxt__async_get_candle_history(
|
||||
exchange, exchangename, pair, timeframe, CandleType.FUTURES)
|
||||
exchange,
|
||||
exchangename,
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
candle_type=candle_type,
|
||||
)
|
||||
|
||||
def test_ccxt_fetch_funding_rate_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
exchange, exchangename = exchange_futures
|
||||
if not exchange:
|
||||
# exchange_futures only returns values for supported exchanges
|
||||
return
|
||||
|
||||
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
||||
since = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000)
|
||||
@@ -604,9 +284,6 @@ class TestCCXTExchange:
|
||||
|
||||
def test_ccxt_fetch_mark_price_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
exchange, exchangename = exchange_futures
|
||||
if not exchange:
|
||||
# exchange_futures only returns values for supported exchanges
|
||||
return
|
||||
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
||||
since = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000)
|
||||
pair_tf = (pair, '1h', CandleType.MARK)
|
||||
@@ -628,9 +305,6 @@ class TestCCXTExchange:
|
||||
|
||||
def test_ccxt__calculate_funding_fees(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
exchange, exchangename = exchange_futures
|
||||
if not exchange:
|
||||
# exchange_futures only returns values for supported exchanges
|
||||
return
|
||||
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
||||
since = datetime.now(timezone.utc) - timedelta(days=5)
|
||||
|
||||
@@ -677,31 +351,29 @@ class TestCCXTExchange:
|
||||
|
||||
def test_ccxt_get_max_leverage_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
futures, futures_name = exchange_futures
|
||||
if futures:
|
||||
leverage_tiers_public = EXCHANGES[futures_name].get('leverage_tiers_public')
|
||||
if leverage_tiers_public:
|
||||
futures_pair = EXCHANGES[futures_name].get(
|
||||
'futures_pair',
|
||||
EXCHANGES[futures_name]['pair']
|
||||
)
|
||||
futures_leverage = futures.get_max_leverage(futures_pair, 20)
|
||||
assert (isinstance(futures_leverage, float) or isinstance(futures_leverage, int))
|
||||
assert futures_leverage >= 1.0
|
||||
|
||||
def test_ccxt_get_contract_size(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
futures, futures_name = exchange_futures
|
||||
if futures:
|
||||
leverage_tiers_public = EXCHANGES[futures_name].get('leverage_tiers_public')
|
||||
if leverage_tiers_public:
|
||||
futures_pair = EXCHANGES[futures_name].get(
|
||||
'futures_pair',
|
||||
EXCHANGES[futures_name]['pair']
|
||||
)
|
||||
contract_size = futures.get_contract_size(futures_pair)
|
||||
assert (isinstance(contract_size, float) or isinstance(contract_size, int))
|
||||
assert contract_size >= 0.0
|
||||
futures_leverage = futures.get_max_leverage(futures_pair, 20)
|
||||
assert (isinstance(futures_leverage, float) or isinstance(futures_leverage, int))
|
||||
assert futures_leverage >= 1.0
|
||||
|
||||
def test_ccxt_get_contract_size(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
futures, futures_name = exchange_futures
|
||||
futures_pair = EXCHANGES[futures_name].get(
|
||||
'futures_pair',
|
||||
EXCHANGES[futures_name]['pair']
|
||||
)
|
||||
contract_size = futures.get_contract_size(futures_pair)
|
||||
assert (isinstance(contract_size, float) or isinstance(contract_size, int))
|
||||
assert contract_size >= 0.0
|
||||
|
||||
def test_ccxt_load_leverage_tiers(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
futures, futures_name = exchange_futures
|
||||
if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
|
||||
if EXCHANGES[futures_name].get('leverage_tiers_public'):
|
||||
leverage_tiers = futures.load_leverage_tiers()
|
||||
futures_pair = EXCHANGES[futures_name].get(
|
||||
'futures_pair',
|
||||
@@ -734,7 +406,7 @@ class TestCCXTExchange:
|
||||
|
||||
def test_ccxt_dry_run_liquidation_price(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
futures, futures_name = exchange_futures
|
||||
if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
|
||||
if EXCHANGES[futures_name].get('leverage_tiers_public'):
|
||||
|
||||
futures_pair = EXCHANGES[futures_name].get(
|
||||
'futures_pair',
|
||||
@@ -767,14 +439,13 @@ class TestCCXTExchange:
|
||||
|
||||
def test_ccxt_get_max_pair_stake_amount(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
futures, futures_name = exchange_futures
|
||||
if futures:
|
||||
futures_pair = EXCHANGES[futures_name].get(
|
||||
'futures_pair',
|
||||
EXCHANGES[futures_name]['pair']
|
||||
)
|
||||
max_stake_amount = futures.get_max_pair_stake_amount(futures_pair, 40000)
|
||||
assert (isinstance(max_stake_amount, float))
|
||||
assert max_stake_amount >= 0.0
|
||||
futures_pair = EXCHANGES[futures_name].get(
|
||||
'futures_pair',
|
||||
EXCHANGES[futures_name]['pair']
|
||||
)
|
||||
max_stake_amount = futures.get_max_pair_stake_amount(futures_pair, 40000)
|
||||
assert (isinstance(max_stake_amount, float))
|
||||
assert max_stake_amount >= 0.0
|
||||
|
||||
def test_private_method_presence(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchangename = exchange
|
||||
@@ -97,9 +97,9 @@ def mock_pytorch_mlp_model_training_parameters() -> Dict[str, Any]:
|
||||
return {
|
||||
"learning_rate": 3e-4,
|
||||
"trainer_kwargs": {
|
||||
"max_iters": 1,
|
||||
"n_steps": None,
|
||||
"batch_size": 64,
|
||||
"max_n_eval_batches": 1,
|
||||
"n_epochs": 1,
|
||||
},
|
||||
"model_kwargs": {
|
||||
"hidden_dim": 32,
|
||||
|
||||
@@ -20,7 +20,7 @@ from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.data.history import get_timerange
|
||||
from freqtrade.enums import CandleType, ExitType, RunMode
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
from freqtrade.exchange.exchange import timeframe_to_next_date
|
||||
from freqtrade.exchange import timeframe_to_next_date, timeframe_to_prev_date
|
||||
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename, get_strategy_run_id
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
from freqtrade.persistence import LocalTrade, Trade
|
||||
@@ -1122,10 +1122,10 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi
|
||||
processed = backtesting.strategy.advise_all_indicators(data)
|
||||
min_date, max_date = get_timerange(processed)
|
||||
|
||||
global count
|
||||
count = 0
|
||||
|
||||
def tmp_confirm_entry(pair, current_time, **kwargs):
|
||||
nonlocal count
|
||||
dp = backtesting.strategy.dp
|
||||
df, _ = dp.get_analyzed_dataframe(pair, backtesting.strategy.timeframe)
|
||||
current_candle = df.iloc[-1].squeeze()
|
||||
@@ -1135,8 +1135,13 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi
|
||||
assert candle_date == current_time
|
||||
# These asserts don't properly raise as they are nested,
|
||||
# therefore we increment count and assert for that.
|
||||
global count
|
||||
count = count + 1
|
||||
df = dp.get_pair_dataframe(pair, backtesting.strategy.timeframe)
|
||||
prior_time = timeframe_to_prev_date(backtesting.strategy.timeframe,
|
||||
candle_date - timedelta(seconds=1))
|
||||
assert prior_time == df.iloc[-1].squeeze()['date']
|
||||
assert df.iloc[-1].squeeze()['date'] < current_time
|
||||
|
||||
count += 1
|
||||
|
||||
backtesting.strategy.confirm_trade_entry = tmp_confirm_entry
|
||||
backtesting.backtest(
|
||||
@@ -1354,11 +1359,11 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
||||
|
||||
# Cached data correctly removed amounts
|
||||
offset = 1 if tres == 0 else 0
|
||||
removed_candles = len(data[pair]) - offset - backtesting.strategy.startup_candle_count
|
||||
removed_candles = len(data[pair]) - offset
|
||||
assert len(backtesting.dataprovider.get_analyzed_dataframe(pair, '5m')[0]) == removed_candles
|
||||
assert len(
|
||||
backtesting.dataprovider.get_analyzed_dataframe('NXT/BTC', '5m')[0]
|
||||
) == len(data['NXT/BTC']) - 1 - backtesting.strategy.startup_candle_count
|
||||
) == len(data['NXT/BTC']) - 1
|
||||
|
||||
backtesting.strategy.max_open_trades = 1
|
||||
backtesting.config.update({'max_open_trades': 1})
|
||||
|
||||
@@ -17,6 +17,8 @@ from tests.conftest import EXMS, get_args, log_has_re, patch_exchange
|
||||
def lookahead_conf(default_conf_usdt):
|
||||
default_conf_usdt['minimum_trade_amount'] = 10
|
||||
default_conf_usdt['targeted_trade_amount'] = 20
|
||||
default_conf_usdt['timerange'] = '20220101-20220501'
|
||||
|
||||
default_conf_usdt['strategy_path'] = str(
|
||||
Path(__file__).parent.parent / "strategy/strats/lookahead_bias")
|
||||
default_conf_usdt['strategy'] = 'strategy_test_v3_with_lookahead_bias'
|
||||
@@ -43,7 +45,9 @@ def test_start_lookahead_analysis(mocker):
|
||||
"--pairs",
|
||||
"UNITTEST/BTC",
|
||||
"--max-open-trades",
|
||||
"1"
|
||||
"1",
|
||||
"--timerange",
|
||||
"20220101-20220201"
|
||||
]
|
||||
pargs = get_args(args)
|
||||
pargs['config'] = None
|
||||
@@ -72,6 +76,24 @@ def test_start_lookahead_analysis(mocker):
|
||||
match=r"Targeted trade amount can't be smaller than minimum trade amount.*"):
|
||||
start_lookahead_analysis(pargs)
|
||||
|
||||
# Missing timerange
|
||||
args = [
|
||||
"lookahead-analysis",
|
||||
"--strategy",
|
||||
"strategy_test_v3_with_lookahead_bias",
|
||||
"--strategy-path",
|
||||
str(Path(__file__).parent.parent / "strategy/strats/lookahead_bias"),
|
||||
"--pairs",
|
||||
"UNITTEST/BTC",
|
||||
"--max-open-trades",
|
||||
"1",
|
||||
]
|
||||
pargs = get_args(args)
|
||||
pargs['config'] = None
|
||||
with pytest.raises(OperationalException,
|
||||
match=r"Please set a timerange\..*"):
|
||||
start_lookahead_analysis(pargs)
|
||||
|
||||
|
||||
def test_lookahead_helper_invalid_config(lookahead_conf) -> None:
|
||||
conf = deepcopy(lookahead_conf)
|
||||
|
||||
@@ -10,6 +10,7 @@ from unittest.mock import ANY, MagicMock, PropertyMock
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
import rapidjson
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, WebSocketDisconnect
|
||||
from fastapi.exceptions import HTTPException
|
||||
@@ -80,6 +81,16 @@ def client_post(client: TestClient, url, data={}):
|
||||
})
|
||||
|
||||
|
||||
def client_patch(client: TestClient, url, data={}):
|
||||
|
||||
return client.patch(url,
|
||||
json=data,
|
||||
headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
|
||||
'Origin': 'http://example.com',
|
||||
'content-type': 'application/json'
|
||||
})
|
||||
|
||||
|
||||
def client_get(client: TestClient, url):
|
||||
# Add fake Origin to ensure CORS kicks in
|
||||
return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
|
||||
@@ -1763,7 +1774,7 @@ def test_api_pairlists_evaluate(botclient, tmpdir, mocker):
|
||||
rc = client_get(client, f"{BASE_URI}/pairlists/evaluate/{job_id}")
|
||||
assert_response(rc)
|
||||
response = rc.json()
|
||||
assert response['result']['whitelist'] == ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC',]
|
||||
assert response['result']['whitelist'] == ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC']
|
||||
assert response['result']['length'] == 4
|
||||
|
||||
# Restart with additional filter, reducing the list to 2
|
||||
@@ -2010,6 +2021,7 @@ def test_api_backtest_history(botclient, mocker, testdatadir):
|
||||
assert len(result) == 3
|
||||
fn = result[0]['filename']
|
||||
assert fn == "backtest-result_multistrat"
|
||||
assert result[0]['notes'] == ''
|
||||
strategy = result[0]['strategy']
|
||||
rc = client_get(client, f"{BASE_URI}/backtest/history/result?filename={fn}&strategy={strategy}")
|
||||
assert_response(rc)
|
||||
@@ -2023,7 +2035,7 @@ def test_api_backtest_history(botclient, mocker, testdatadir):
|
||||
assert result2['backtest_result']['strategy'][strategy]
|
||||
|
||||
|
||||
def test_api_delete_backtest_history_entry(botclient, mocker, tmp_path: Path):
|
||||
def test_api_delete_backtest_history_entry(botclient, tmp_path: Path):
|
||||
ftbot, client = botclient
|
||||
|
||||
# Create a temporary directory and file
|
||||
@@ -2051,6 +2063,75 @@ def test_api_delete_backtest_history_entry(botclient, mocker, tmp_path: Path):
|
||||
assert not meta_path.exists()
|
||||
|
||||
|
||||
def test_api_patch_backtest_history_entry(botclient, tmp_path: Path):
|
||||
ftbot, client = botclient
|
||||
|
||||
# Create a temporary directory and file
|
||||
bt_results_base = tmp_path / "backtest_results"
|
||||
bt_results_base.mkdir()
|
||||
file_path = bt_results_base / "test.json"
|
||||
file_path.touch()
|
||||
meta_path = file_path.with_suffix('.meta.json')
|
||||
with meta_path.open('w') as metafile:
|
||||
rapidjson.dump({
|
||||
CURRENT_TEST_STRATEGY: {
|
||||
"run_id": "6e542efc8d5e62cef6e5be0ffbc29be81a6e751d",
|
||||
"backtest_start_time": 1690176003}
|
||||
}, metafile)
|
||||
|
||||
def read_metadata():
|
||||
with meta_path.open('r') as metafile:
|
||||
return rapidjson.load(metafile)
|
||||
|
||||
rc = client_patch(client, f"{BASE_URI}/backtest/history/randomFile.json")
|
||||
assert_response(rc, 503)
|
||||
|
||||
ftbot.config['user_data_dir'] = tmp_path
|
||||
ftbot.config['runmode'] = RunMode.WEBSERVER
|
||||
|
||||
rc = client_patch(client, f"{BASE_URI}/backtest/history/randomFile.json", {
|
||||
"strategy": CURRENT_TEST_STRATEGY,
|
||||
})
|
||||
assert rc.status_code == 404
|
||||
|
||||
# Nonexisting strategy
|
||||
rc = client_patch(client, f"{BASE_URI}/backtest/history/{file_path.name}", {
|
||||
"strategy": f"{CURRENT_TEST_STRATEGY}xxx",
|
||||
})
|
||||
assert rc.status_code == 400
|
||||
assert rc.json()['detail'] == 'Strategy not in metadata.'
|
||||
|
||||
# no Notes
|
||||
rc = client_patch(client, f"{BASE_URI}/backtest/history/{file_path.name}", {
|
||||
"strategy": CURRENT_TEST_STRATEGY,
|
||||
})
|
||||
assert rc.status_code == 200
|
||||
res = rc.json()
|
||||
assert isinstance(res, list)
|
||||
assert len(res) == 1
|
||||
assert res[0]['strategy'] == CURRENT_TEST_STRATEGY
|
||||
assert res[0]['notes'] == ''
|
||||
|
||||
fileres = read_metadata()
|
||||
assert fileres[CURRENT_TEST_STRATEGY]['run_id'] == res[0]['run_id']
|
||||
assert fileres[CURRENT_TEST_STRATEGY]['notes'] == ''
|
||||
|
||||
rc = client_patch(client, f"{BASE_URI}/backtest/history/{file_path.name}", {
|
||||
"strategy": CURRENT_TEST_STRATEGY,
|
||||
"notes": "FooBar",
|
||||
})
|
||||
assert rc.status_code == 200
|
||||
res = rc.json()
|
||||
assert isinstance(res, list)
|
||||
assert len(res) == 1
|
||||
assert res[0]['strategy'] == CURRENT_TEST_STRATEGY
|
||||
assert res[0]['notes'] == 'FooBar'
|
||||
|
||||
fileres = read_metadata()
|
||||
assert fileres[CURRENT_TEST_STRATEGY]['run_id'] == res[0]['run_id']
|
||||
assert fileres[CURRENT_TEST_STRATEGY]['notes'] == 'FooBar'
|
||||
|
||||
|
||||
def test_health(botclient):
|
||||
ftbot, client = botclient
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ class StrategyTestV3(IStrategy):
|
||||
|
||||
if current_profit < -0.0075:
|
||||
orders = trade.select_filled_orders(trade.entry_side)
|
||||
return round(orders[0].safe_cost, 0)
|
||||
return round(orders[0].stake_amount, 0)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -25,13 +25,13 @@ def test_strategy_test_v3(dataframe_1m, fee, is_short, side):
|
||||
strategy = StrategyTestV3({})
|
||||
|
||||
metadata = {'pair': 'ETH/BTC'}
|
||||
assert type(strategy.minimal_roi) is dict
|
||||
assert type(strategy.stoploss) is float
|
||||
assert type(strategy.timeframe) is str
|
||||
assert isinstance(strategy.minimal_roi, dict)
|
||||
assert isinstance(strategy.stoploss, float)
|
||||
assert isinstance(strategy.timeframe, str)
|
||||
indicators = strategy.populate_indicators(dataframe_1m, metadata)
|
||||
assert type(indicators) is DataFrame
|
||||
assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame
|
||||
assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame
|
||||
assert isinstance(indicators, DataFrame)
|
||||
assert isinstance(strategy.populate_buy_trend(indicators, metadata), DataFrame)
|
||||
assert isinstance(strategy.populate_sell_trend(indicators, metadata), DataFrame)
|
||||
|
||||
trade = Trade(
|
||||
open_rate=19_000,
|
||||
|
||||
@@ -1038,8 +1038,7 @@ def test_load_config_stoploss_exchange_limit_ratio(all_conf) -> None:
|
||||
validate_config_schema(all_conf)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("keys", [("exchange", "sandbox", False),
|
||||
("exchange", "key", ""),
|
||||
@pytest.mark.parametrize("keys", [("exchange", "key", ""),
|
||||
("exchange", "secret", ""),
|
||||
("exchange", "password", ""),
|
||||
])
|
||||
|
||||
@@ -1508,15 +1508,15 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_create_stoploss_order_invalid_order(
|
||||
mocker, default_conf_usdt, caplog, fee, is_short, limit_order, limit_order_open
|
||||
mocker, default_conf_usdt, caplog, fee, is_short, limit_order
|
||||
):
|
||||
open_order = limit_order_open[entry_side(is_short)]
|
||||
open_order = limit_order[entry_side(is_short)]
|
||||
order = limit_order[exit_side(is_short)]
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
create_order_mock = MagicMock(side_effect=[
|
||||
open_order,
|
||||
{'id': order['id']}
|
||||
order,
|
||||
])
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
@@ -1541,6 +1541,7 @@ def test_create_stoploss_order_invalid_order(
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
trade.is_short = is_short
|
||||
caplog.clear()
|
||||
rpc_mock.reset_mock()
|
||||
freqtrade.create_stoploss_order(trade, 200)
|
||||
assert trade.stoploss_order_id is None
|
||||
assert trade.exit_reason == ExitType.EMERGENCY_EXIT.value
|
||||
@@ -1554,9 +1555,11 @@ def test_create_stoploss_order_invalid_order(
|
||||
assert create_order_mock.call_args[1]['amount'] == trade.amount
|
||||
|
||||
# Rpc is sending first buy, then sell
|
||||
assert rpc_mock.call_count == 3
|
||||
assert rpc_mock.call_args_list[2][0][0]['sell_reason'] == ExitType.EMERGENCY_EXIT.value
|
||||
assert rpc_mock.call_args_list[2][0][0]['order_type'] == 'market'
|
||||
assert rpc_mock.call_count == 2
|
||||
assert rpc_mock.call_args_list[0][0][0]['sell_reason'] == ExitType.EMERGENCY_EXIT.value
|
||||
assert rpc_mock.call_args_list[0][0][0]['order_type'] == 'market'
|
||||
assert rpc_mock.call_args_list[0][0][0]['type'] == 'exit'
|
||||
assert rpc_mock.call_args_list[1][0][0]['type'] == 'exit_fill'
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
|
||||
@@ -9,8 +9,7 @@ import pytest
|
||||
|
||||
from freqtrade.misc import (dataframe_to_json, decimals_per_coin, deep_merge_dicts, file_dump_json,
|
||||
file_load_json, is_file_in_dir, json_to_dataframe, pair_to_filename,
|
||||
parse_db_uri_for_logging, plural, render_template,
|
||||
render_template_with_fallback, round_coin_value, safe_value_fallback,
|
||||
parse_db_uri_for_logging, plural, round_coin_value, safe_value_fallback,
|
||||
safe_value_fallback2)
|
||||
|
||||
|
||||
@@ -177,20 +176,6 @@ def test_plural() -> None:
|
||||
assert plural(-1.5, "ox", "oxen") == "oxen"
|
||||
|
||||
|
||||
def test_render_template_fallback(mocker):
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
with pytest.raises(TemplateNotFound):
|
||||
val = render_template(
|
||||
templatefile='subtemplates/indicators_does-not-exist.j2',)
|
||||
|
||||
val = render_template_with_fallback(
|
||||
templatefile='strategy_subtemplates/indicators_does-not-exist.j2',
|
||||
templatefallbackfile='strategy_subtemplates/indicators_minimal.j2',
|
||||
)
|
||||
assert isinstance(val, str)
|
||||
assert 'if self.dp' in val
|
||||
|
||||
|
||||
@pytest.mark.parametrize('conn_url,expected', [
|
||||
("postgresql+psycopg2://scott123:scott123@host:1245/dbname",
|
||||
"postgresql+psycopg2://scott123:*****@host:1245/dbname"),
|
||||
|
||||
@@ -63,7 +63,7 @@ def test_format_ms_time() -> None:
|
||||
# Date 2018-04-10 18:02:01
|
||||
date_in_epoch_ms = 1523383321000
|
||||
date = format_ms_time(date_in_epoch_ms)
|
||||
assert type(date) is str
|
||||
assert isinstance(date, str)
|
||||
res = datetime(2018, 4, 10, 18, 2, 1, tzinfo=timezone.utc)
|
||||
assert date == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S')
|
||||
res = datetime(2017, 12, 13, 8, 2, 1, tzinfo=timezone.utc)
|
||||
|
||||
17
tests/utils/test_rendering_utils.py
Normal file
17
tests/utils/test_rendering_utils.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import pytest
|
||||
|
||||
from freqtrade.util import render_template, render_template_with_fallback
|
||||
|
||||
|
||||
def test_render_template_fallback():
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
with pytest.raises(TemplateNotFound):
|
||||
val = render_template(
|
||||
templatefile='subtemplates/indicators_does-not-exist.j2',)
|
||||
|
||||
val = render_template_with_fallback(
|
||||
templatefile='strategy_subtemplates/indicators_does-not-exist.j2',
|
||||
templatefallbackfile='strategy_subtemplates/indicators_minimal.j2',
|
||||
)
|
||||
assert isinstance(val, str)
|
||||
assert 'if self.dp' in val
|
||||
Reference in New Issue
Block a user