diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 6b6c2a772..e72b850ca 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -365,7 +365,7 @@ class MyAwesomeStrategy(IStrategy): timeframe = '15m' minimal_roi = { "0": 0.10 - }, + } # Define the parameter spaces buy_ema_short = IntParameter(3, 50, default=5) buy_ema_long = IntParameter(15, 200, default=50) @@ -400,7 +400,7 @@ class MyAwesomeStrategy(IStrategy): return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - conditions = [] + conditions = [] conditions.append(qtpylib.crossed_above( dataframe[f'ema_long_{self.buy_ema_long.value}'], dataframe[f'ema_short_{self.buy_ema_short.value}'] )) diff --git a/docs/leverage.md b/docs/leverage.md index 429aff86c..0a265277e 100644 --- a/docs/leverage.md +++ b/docs/leverage.md @@ -92,6 +92,8 @@ One account is used to share collateral between markets (trading pairs). Margin "margin_mode": "cross" ``` +Please read the [exchange specific notes](exchanges.md) for exchanges that support this mode and how they differ. + ## Set leverage to use Different strategies and risk profiles will require different levels of leverage. diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py index 565a279b1..baa1cca3a 100755 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -52,7 +52,7 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand return analysed_trades_dict -def _analyze_candles_and_indicators(pair, trades, signal_candles): +def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles: pd.DataFrame): buyf = signal_candles if len(buyf) > 0: @@ -120,7 +120,7 @@ def _do_group_table_output(bigdf, glist): else: agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'], - 'profit_ratio': ['sum', 'median', 'mean']} + 'profit_ratio': ['median', 'mean', 'sum']} agg_cols = ['num_buys', 'profit_abs_sum', 'profit_abs_median', 'profit_abs_mean', 'median_profit_pct', 'mean_profit_pct', 'total_profit_pct'] diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index f08e2693d..138903b57 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -239,7 +239,7 @@ def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: dateti down_stdev = np.std(trades.loc[trades['profit_abs'] < 0, 'profit_abs'] / starting_balance) - if down_stdev != 0: + if down_stdev != 0 and not np.isnan(down_stdev): sortino_ratio = expected_returns_mean / down_stdev * np.sqrt(365) else: # Define high (negative) sortino ratio to be clear that this is NOT optimal. diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 7462e4f81..9942a4268 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -11,7 +11,7 @@ from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier -from freqtrade.exchange.types import Tickers +from freqtrade.exchange.types import OHLCVResponse, Tickers from freqtrade.misc import deep_merge_dicts, json_load @@ -112,7 +112,7 @@ class Binance(Exchange): since_ms: int, candle_type: CandleType, is_new_pair: bool = False, raise_: bool = False, until_ms: Optional[int] = None - ) -> Tuple[str, str, str, List]: + ) -> OHLCVResponse: """ Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date Does not work for other exchanges, which don't return the earliest data when called with "0" diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 51341588d..b4b5f8342 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -36,7 +36,7 @@ from freqtrade.exchange.exchange_utils import (CcxtModuleType, amount_to_contrac price_to_precision, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) -from freqtrade.exchange.types import Ticker, Tickers +from freqtrade.exchange.types import OHLCVResponse, Ticker, Tickers from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json, safe_value_fallback2) from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist @@ -1813,32 +1813,18 @@ class Exchange: :param candle_type: '', mark, index, premiumIndex, or funding_rate :return: List with candle (OHLCV) data """ - pair, _, _, data = self.loop.run_until_complete( + pair, _, _, data, _ = self.loop.run_until_complete( self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe, since_ms=since_ms, until_ms=until_ms, is_new_pair=is_new_pair, candle_type=candle_type)) logger.info(f"Downloaded data for {pair} with length {len(data)}.") return data - def get_historic_ohlcv_as_df(self, pair: str, timeframe: str, - since_ms: int, candle_type: CandleType) -> DataFrame: - """ - Minimal wrapper around get_historic_ohlcv - converting the result into a dataframe - :param pair: Pair to download - :param timeframe: Timeframe to get data for - :param since_ms: Timestamp in milliseconds to get history from - :param candle_type: Any of the enum CandleType (must match trading mode!) - :return: OHLCV DataFrame - """ - ticks = self.get_historic_ohlcv(pair, timeframe, since_ms=since_ms, candle_type=candle_type) - return ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True, - drop_incomplete=self._ohlcv_partial_candle) - async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int, candle_type: CandleType, is_new_pair: bool = False, raise_: bool = False, until_ms: Optional[int] = None - ) -> Tuple[str, str, str, List]: + ) -> OHLCVResponse: """ Download historic ohlcv :param is_new_pair: used by binance subclass to allow "fast" new pair downloading @@ -1869,15 +1855,16 @@ class Exchange: continue else: # Deconstruct tuple if it's not an exception - p, _, c, new_data = res + p, _, c, new_data, _ = res if p == pair and c == candle_type: data.extend(new_data) # Sort data again after extending the result - above calls return in "async order" data = sorted(data, key=lambda x: x[0]) - return pair, timeframe, candle_type, data + return pair, timeframe, candle_type, data, self._ohlcv_partial_candle - def _build_coroutine(self, pair: str, timeframe: str, candle_type: CandleType, - since_ms: Optional[int], cache: bool) -> Coroutine: + def _build_coroutine( + self, pair: str, timeframe: str, candle_type: CandleType, + since_ms: Optional[int], cache: bool) -> Coroutine[Any, Any, OHLCVResponse]: not_all_data = cache and self.required_candle_call_count > 1 if cache and (pair, timeframe, candle_type) in self._klines: candle_limit = self.ohlcv_candle_limit(timeframe, candle_type) @@ -1914,7 +1901,7 @@ class Exchange: """ Build Coroutines to execute as part of refresh_latest_ohlcv """ - input_coroutines = [] + input_coroutines: List[Coroutine[Any, Any, OHLCVResponse]] = [] cached_pairs = [] for pair, timeframe, candle_type in set(pair_list): if (timeframe not in self.timeframes @@ -1978,7 +1965,6 @@ class Exchange: :return: Dict of [{(pair, timeframe): Dataframe}] """ logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list)) - drop_incomplete = self._ohlcv_partial_candle if drop_incomplete is None else drop_incomplete # Gather coroutines to run input_coroutines, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache) @@ -1996,8 +1982,9 @@ class Exchange: if isinstance(res, Exception): logger.warning(f"Async code raised an exception: {repr(res)}") continue - # Deconstruct tuple (has 4 elements) - pair, timeframe, c_type, ticks = res + # Deconstruct tuple (has 5 elements) + pair, timeframe, c_type, ticks, drop_hint = res + drop_incomplete = drop_hint if drop_incomplete is None else drop_incomplete ohlcv_df = self._process_ohlcv_df( pair, timeframe, c_type, ticks, cache, drop_incomplete) @@ -2025,7 +2012,7 @@ class Exchange: timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None, - ) -> Tuple[str, str, str, List]: + ) -> OHLCVResponse: """ Asynchronously get candle history data using fetch_ohlcv :param candle_type: '', mark, index, premiumIndex, or funding_rate @@ -2065,9 +2052,9 @@ class Exchange: data = sorted(data, key=lambda x: x[0]) except IndexError: logger.exception("Error loading %s. Result was %s.", pair, data) - return pair, timeframe, candle_type, [] + return pair, timeframe, candle_type, [], self._ohlcv_partial_candle logger.debug("Done fetching pair %s, interval %s ...", pair, timeframe) - return pair, timeframe, candle_type, data + return pair, timeframe, candle_type, data, self._ohlcv_partial_candle except ccxt.NotSupported as e: raise OperationalException( @@ -2764,11 +2751,16 @@ class Exchange: """ Important: Must be fetching data from cached values as this is used by backtesting! PERPETUAL: - gateio: https://www.gate.io/help/futures/perpetual/22160/calculation-of-liquidation-price + gateio: https://www.gate.io/help/futures/futures/27724/liquidation-price-bankruptcy-price + > Liquidation Price = (Entry Price ± Margin / Contract Multiplier / Size) / + [ 1 ± (Maintenance Margin Ratio + Taker Rate)] + Wherein, "+" or "-" depends on whether the contract goes long or short: + "-" for long, and "+" for short. + okex: https://www.okex.com/support/hc/en-us/articles/ 360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin - :param exchange_name: + :param pair: Pair to calculate liquidation price for :param open_rate: Entry price of position :param is_short: True if the trade is a short, false otherwise :param amount: Absolute value of position size incl. leverage (in base currency) @@ -2808,7 +2800,7 @@ class Exchange: def get_maintenance_ratio_and_amt( self, pair: str, - nominal_value: float = 0.0, + nominal_value: float, ) -> Tuple[float, Optional[float]]: """ Important: Must be fetching data from cached values as this is used by backtesting! diff --git a/freqtrade/exchange/types.py b/freqtrade/exchange/types.py index a60b454d4..813b09297 100644 --- a/freqtrade/exchange/types.py +++ b/freqtrade/exchange/types.py @@ -1,4 +1,6 @@ -from typing import Dict, Optional, TypedDict +from typing import Dict, List, Optional, Tuple, TypedDict + +from freqtrade.enums import CandleType class Ticker(TypedDict): @@ -14,3 +16,6 @@ class Ticker(TypedDict): Tickers = Dict[str, Ticker] + +# pair, timeframe, candleType, OHLCV, drop last?, +OHLCVResponse = Tuple[str, str, CandleType, List, bool] diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 2b8b96cba..81e05ade0 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1177,6 +1177,7 @@ class Backtesting: open_trade_count_start = self.backtest_loop( row, pair, current_time, end_date, max_open_trades, open_trade_count_start) + continue detail_data.loc[:, 'enter_long'] = row[LONG_IDX] detail_data.loc[:, 'exit_long'] = row[ELONG_IDX] detail_data.loc[:, 'enter_short'] = row[SHORT_IDX] diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index fc39b0ab4..4690a6ccb 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -28,7 +28,7 @@ class FreqaiExampleStrategy(IStrategy): plot_config = { "main_plot": {}, "subplots": { - "prediction": {"prediction": {"color": "blue"}}, + "&-s_close": {"prediction": {"color": "blue"}}, "do_predict": { "do_predict": {"color": "brown"}, }, @@ -140,7 +140,8 @@ class FreqaiExampleStrategy(IStrategy): # If user wishes to use multiple targets, they can add more by # appending more columns with '&'. User should keep in mind that multi targets # requires a multioutput prediction model such as - # templates/CatboostPredictionMultiModel.py, + # freqai/prediction_models/CatboostRegressorMultiTarget.py, + # freqtrade trade --freqaimodel CatboostRegressorMultiTarget # df["&-s_range"] = ( # df["close"] diff --git a/requirements-dev.txt b/requirements-dev.txt index c1fd160ee..cf7a75d98 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -18,7 +18,7 @@ pytest-mock==3.10.0 pytest-random-order==1.1.0 isort==5.11.4 # For datetime mocking -time-machine==2.8.2 +time-machine==2.9.0 # fastapi testing httpx==0.23.1 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index fcae2cbdd..0cfd6cfa1 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -5,5 +5,5 @@ scipy==1.9.3 scikit-learn==1.1.3 scikit-optimize==0.9.0 -filelock==3.8.2 +filelock==3.9.0 progressbar2==4.2.0 diff --git a/requirements.txt b/requirements.txt index cf6b270ab..b132971df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,7 +37,7 @@ sdnotify==0.3.2 # API Server fastapi==0.88.0 -pydantic==1.10.2 +pydantic==1.10.4 uvicorn==0.20.0 pyjwt==2.6.0 aiofiles==22.1.0 diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index d568f48f6..967dbe296 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -746,9 +746,7 @@ def test_download_data_no_exchange(mocker, caplog): start_download_data(pargs) -def test_download_data_no_pairs(mocker, caplog): - - mocker.patch.object(Path, "exists", MagicMock(return_value=False)) +def test_download_data_no_pairs(mocker): mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) @@ -770,8 +768,6 @@ def test_download_data_no_pairs(mocker, caplog): def test_download_data_all_pairs(mocker, markets): - mocker.patch.object(Path, "exists", MagicMock(return_value=False)) - dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 306a30985..189f0488d 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -557,7 +557,7 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, c exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) pair = 'ETH/BTC' - respair, restf, restype, res = await exchange._async_get_historic_ohlcv( + respair, restf, restype, res, _ = await exchange._async_get_historic_ohlcv( pair, "5m", 1500000000000, is_new_pair=False, candle_type=candle_type) assert respair == pair assert restf == '5m' @@ -566,7 +566,7 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, c assert exchange._api_async.fetch_ohlcv.call_count > 400 # assert res == ohlcv exchange._api_async.fetch_ohlcv.reset_mock() - _, _, _, res = await exchange._async_get_historic_ohlcv( + _, _, _, res, _ = await exchange._async_get_historic_ohlcv( pair, "5m", 1500000000000, is_new_pair=True, candle_type=candle_type) # Called twice - one "init" call - and one to get the actual data. diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 7f23c2031..e721ee2c9 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -8,16 +8,19 @@ 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.enums import CandleType from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date -from freqtrade.exchange.exchange import timeframe_to_msecs +from freqtrade.exchange.exchange import Exchange, timeframe_to_msecs from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import get_default_conf_usdt +EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str] + # Exchanges that should be tested EXCHANGES = { 'bittrex': { @@ -141,19 +144,19 @@ def exchange_futures(request, exchange_conf, class_mocker): @pytest.mark.longrun class TestCCXTExchange(): - def test_load_markets(self, exchange): - exchange, exchangename = exchange + def test_load_markets(self, exchange: EXCHANGE_FIXTURE_TYPE): + exch, exchangename = exchange pair = EXCHANGES[exchangename]['pair'] - markets = exchange.markets + markets = exch.markets assert pair in markets assert isinstance(markets[pair], dict) - assert exchange.market_is_spot(markets[pair]) + assert exch.market_is_spot(markets[pair]) - def test_has_validations(self, exchange): + def test_has_validations(self, exchange: EXCHANGE_FIXTURE_TYPE): - exchange, exchangename = exchange + exch, exchangename = exchange - exchange.validate_ordertypes({ + exch.validate_ordertypes({ 'entry': 'limit', 'exit': 'limit', 'stoploss': 'limit', @@ -162,13 +165,13 @@ class TestCCXTExchange(): if exchangename == 'gateio': # gateio doesn't have market orders on spot return - exchange.validate_ordertypes({ + exch.validate_ordertypes({ 'entry': 'market', 'exit': 'market', 'stoploss': 'market', }) - def test_load_markets_futures(self, exchange_futures): + 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 @@ -181,11 +184,11 @@ class TestCCXTExchange(): assert exchange.market_is_future(markets[pair]) - def test_ccxt_fetch_tickers(self, exchange): - exchange, exchangename = exchange + def test_ccxt_fetch_tickers(self, exchange: EXCHANGE_FIXTURE_TYPE): + exch, exchangename = exchange pair = EXCHANGES[exchangename]['pair'] - tickers = exchange.get_tickers() + tickers = exch.get_tickers() assert pair in tickers assert 'ask' in tickers[pair] assert tickers[pair]['ask'] is not None @@ -195,11 +198,11 @@ class TestCCXTExchange(): if EXCHANGES[exchangename].get('hasQuoteVolume'): assert tickers[pair]['quoteVolume'] is not None - def test_ccxt_fetch_ticker(self, exchange): - exchange, exchangename = exchange + def test_ccxt_fetch_ticker(self, exchange: EXCHANGE_FIXTURE_TYPE): + exch, exchangename = exchange pair = EXCHANGES[exchangename]['pair'] - ticker = exchange.fetch_ticker(pair) + ticker = exch.fetch_ticker(pair) assert 'ask' in ticker assert ticker['ask'] is not None assert 'bid' in ticker @@ -208,21 +211,21 @@ class TestCCXTExchange(): if EXCHANGES[exchangename].get('hasQuoteVolume'): assert ticker['quoteVolume'] is not None - def test_ccxt_fetch_l2_orderbook(self, exchange): - exchange, exchangename = exchange + def test_ccxt_fetch_l2_orderbook(self, exchange: EXCHANGE_FIXTURE_TYPE): + exch, exchangename = exchange pair = EXCHANGES[exchangename]['pair'] - l2 = exchange.fetch_l2_order_book(pair) + l2 = exch.fetch_l2_order_book(pair) assert 'asks' in l2 assert 'bids' in l2 assert len(l2['asks']) >= 1 assert len(l2['bids']) >= 1 - l2_limit_range = exchange._ft_has['l2_limit_range'] - l2_limit_range_required = exchange._ft_has['l2_limit_range_required'] + l2_limit_range = exch._ft_has['l2_limit_range'] + l2_limit_range_required = exch._ft_has['l2_limit_range_required'] if exchangename == 'gateio': # TODO: Gateio is unstable here at the moment, ignoring the limit partially. return for val in [1, 2, 5, 25, 100]: - l2 = exchange.fetch_l2_order_book(pair, val) + l2 = exch.fetch_l2_order_book(pair, val) if not l2_limit_range or val in l2_limit_range: if val > 50: # Orderbooks are not always this deep. @@ -232,7 +235,7 @@ class TestCCXTExchange(): assert len(l2['asks']) == val assert len(l2['bids']) == val else: - next_limit = exchange.get_next_limit_in_list( + next_limit = exch.get_next_limit_in_list( val, l2_limit_range, l2_limit_range_required) if next_limit is None: assert len(l2['asks']) > 100 @@ -245,23 +248,23 @@ class TestCCXTExchange(): assert len(l2['asks']) == next_limit assert len(l2['asks']) == next_limit - def test_ccxt_fetch_ohlcv(self, exchange): - exchange, exchangename = exchange + def test_ccxt_fetch_ohlcv(self, exchange: EXCHANGE_FIXTURE_TYPE): + exch, exchangename = exchange pair = EXCHANGES[exchangename]['pair'] timeframe = EXCHANGES[exchangename]['timeframe'] pair_tf = (pair, timeframe, CandleType.SPOT) - ohlcv = exchange.refresh_latest_ohlcv([pair_tf]) + ohlcv = exch.refresh_latest_ohlcv([pair_tf]) assert isinstance(ohlcv, dict) - assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf)) - # assert len(exchange.klines(pair_tf)) > 200 + assert len(ohlcv[pair_tf]) == len(exch.klines(pair_tf)) + # assert len(exch.klines(pair_tf)) > 200 # Assume 90% uptime ... - assert len(exchange.klines(pair_tf)) > exchange.ohlcv_candle_limit( + assert len(exch.klines(pair_tf)) > exch.ohlcv_candle_limit( timeframe, CandleType.SPOT) * 0.90 # Check if last-timeframe is within the last 2 intervals now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2)) - assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now) + 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): @@ -289,17 +292,17 @@ class TestCCXTExchange(): assert len(candles) >= min(candle_count, candle_count1) assert candles[0][0] == since_ms or (since_ms + timeframe_ms) - def test_ccxt__async_get_candle_history(self, exchange): - exchange, exchangename = exchange + def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE): + exc, exchangename = exchange # For some weired reason, this test returns random lengths for bittrex. - if not exchange._ft_has['ohlcv_has_history'] or exchangename in ('bittrex'): + if not exc._ft_has['ohlcv_has_history'] or exchangename in ('bittrex'): return pair = EXCHANGES[exchangename]['pair'] timeframe = EXCHANGES[exchangename]['timeframe'] self.ccxt__async_get_candle_history( - exchange, exchangename, pair, timeframe, CandleType.SPOT) + exc, exchangename, pair, timeframe, CandleType.SPOT) - def test_ccxt__async_get_candle_history_futures(self, exchange_futures): + def test_ccxt__async_get_candle_history_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): exchange, exchangename = exchange_futures if not exchange: # exchange_futures only returns values for supported exchanges @@ -309,7 +312,7 @@ class TestCCXTExchange(): self.ccxt__async_get_candle_history( exchange, exchangename, pair, timeframe, CandleType.FUTURES) - def test_ccxt_fetch_funding_rate_history(self, exchange_futures): + 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 @@ -347,7 +350,7 @@ class TestCCXTExchange(): (rate['open'].min() != rate['open'].max()) ) - def test_ccxt_fetch_mark_price_history(self, exchange_futures): + 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 @@ -371,7 +374,7 @@ class TestCCXTExchange(): assert mark_candles[mark_candles['date'] == prev_hour].iloc[0]['open'] != 0.0 assert mark_candles[mark_candles['date'] == this_hour].iloc[0]['open'] != 0.0 - def test_ccxt__calculate_funding_fees(self, exchange_futures): + 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 @@ -387,16 +390,16 @@ class TestCCXTExchange(): # TODO: tests fetch_trades (?) - def test_ccxt_get_fee(self, exchange): - exchange, exchangename = exchange + def test_ccxt_get_fee(self, exchange: EXCHANGE_FIXTURE_TYPE): + exch, exchangename = exchange pair = EXCHANGES[exchangename]['pair'] threshold = 0.01 - assert 0 < exchange.get_fee(pair, 'limit', 'buy') < threshold - assert 0 < exchange.get_fee(pair, 'limit', 'sell') < threshold - assert 0 < exchange.get_fee(pair, 'market', 'buy') < threshold - assert 0 < exchange.get_fee(pair, 'market', 'sell') < threshold + assert 0 < exch.get_fee(pair, 'limit', 'buy') < threshold + assert 0 < exch.get_fee(pair, 'limit', 'sell') < threshold + assert 0 < exch.get_fee(pair, 'market', 'buy') < threshold + assert 0 < exch.get_fee(pair, 'market', 'sell') < threshold - def test_ccxt_get_max_leverage_spot(self, exchange): + def test_ccxt_get_max_leverage_spot(self, exchange: EXCHANGE_FIXTURE_TYPE): spot, spot_name = exchange if spot: leverage_in_market_spot = EXCHANGES[spot_name].get('leverage_in_spot_market') @@ -406,7 +409,7 @@ class TestCCXTExchange(): assert (isinstance(spot_leverage, float) or isinstance(spot_leverage, int)) assert spot_leverage >= 1.0 - def test_ccxt_get_max_leverage_futures(self, exchange_futures): + 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') @@ -419,7 +422,7 @@ class TestCCXTExchange(): assert (isinstance(futures_leverage, float) or isinstance(futures_leverage, int)) assert futures_leverage >= 1.0 - def test_ccxt_get_contract_size(self, exchange_futures): + def test_ccxt_get_contract_size(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): futures, futures_name = exchange_futures if futures: futures_pair = EXCHANGES[futures_name].get( @@ -430,7 +433,7 @@ class TestCCXTExchange(): assert (isinstance(contract_size, float) or isinstance(contract_size, int)) assert contract_size >= 0.0 - def test_ccxt_load_leverage_tiers(self, exchange_futures): + 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'): leverage_tiers = futures.load_leverage_tiers() @@ -463,7 +466,7 @@ class TestCCXTExchange(): oldminNotional = tier['minNotional'] oldmaxNotional = tier['maxNotional'] - def test_ccxt_dry_run_liquidation_price(self, exchange_futures): + 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'): @@ -494,7 +497,7 @@ class TestCCXTExchange(): assert (isinstance(liquidation_price, float)) assert liquidation_price >= 0.0 - def test_ccxt_get_max_pair_stake_amount(self, exchange_futures): + 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( diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 280e20ff0..3714291d1 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1955,7 +1955,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_ pair = 'ETH/BTC' async def mock_candle_hist(pair, timeframe, candle_type, since_ms): - return pair, timeframe, candle_type, ohlcv + return pair, timeframe, candle_type, ohlcv, True exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) # one_call calculation * 1.8 should do 2 calls @@ -1988,62 +1988,6 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_ assert log_has_re(r"Async code raised an exception: .*", caplog) -@pytest.mark.parametrize("exchange_name", EXCHANGES) -@pytest.mark.parametrize('candle_type', ['mark', '']) -def test_get_historic_ohlcv_as_df(default_conf, mocker, exchange_name, candle_type): - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - ohlcv = [ - [ - arrow.utcnow().int_timestamp * 1000, # unix timestamp ms - 1, # open - 2, # high - 3, # low - 4, # close - 5, # volume (in quote currency) - ], - [ - arrow.utcnow().shift(minutes=5).int_timestamp * 1000, # unix timestamp ms - 1, # open - 2, # high - 3, # low - 4, # close - 5, # volume (in quote currency) - ], - [ - arrow.utcnow().shift(minutes=10).int_timestamp * 1000, # unix timestamp ms - 1, # open - 2, # high - 3, # low - 4, # close - 5, # volume (in quote currency) - ] - ] - pair = 'ETH/BTC' - - async def mock_candle_hist(pair, timeframe, candle_type, since_ms): - return pair, timeframe, candle_type, ohlcv - - 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 - ret = exchange.get_historic_ohlcv_as_df( - pair, - "5m", - int((arrow.utcnow().int_timestamp - since) * 1000), - candle_type=candle_type - ) - - assert exchange._async_get_candle_history.call_count == 2 - # Returns twice the above OHLCV data - assert len(ret) == 2 - assert isinstance(ret, DataFrame) - assert 'date' in ret.columns - assert 'open' in ret.columns - assert 'close' in ret.columns - assert 'high' in ret.columns - - @pytest.mark.asyncio @pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize('candle_type', [CandleType.MARK, CandleType.SPOT]) @@ -2063,7 +2007,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_ exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) pair = 'ETH/USDT' - respair, restf, _, res = await exchange._async_get_historic_ohlcv( + respair, restf, _, res, _ = await exchange._async_get_historic_ohlcv( pair, "5m", 1500000000000, candle_type=candle_type, is_new_pair=False) assert respair == pair assert restf == '5m' @@ -2074,7 +2018,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_ exchange._api_async.fetch_ohlcv.reset_mock() end_ts = 1_500_500_000_000 start_ts = 1_500_000_000_000 - respair, restf, _, res = await exchange._async_get_historic_ohlcv( + respair, restf, _, res, _ = await exchange._async_get_historic_ohlcv( pair, "5m", since_ms=start_ts, candle_type=candle_type, is_new_pair=False, until_ms=end_ts ) @@ -2306,7 +2250,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ pair = 'ETH/BTC' res = await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT) assert type(res) is tuple - assert len(res) == 4 + assert len(res) == 5 assert res[0] == pair assert res[1] == "5m" assert res[2] == CandleType.SPOT @@ -2393,7 +2337,7 @@ async def test__async_get_candle_history_empty(default_conf, mocker, caplog): pair = 'ETH/BTC' res = await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT) assert type(res) is tuple - assert len(res) == 4 + assert len(res) == 5 assert res[0] == pair assert res[1] == "5m" assert res[2] == CandleType.SPOT