Merge branch 'freqtrade:develop' into bt-metrics
This commit is contained in:
commit
329d95366a
@ -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}']
|
||||
))
|
||||
|
@ -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.
|
||||
|
@ -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']
|
||||
|
@ -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.
|
||||
|
@ -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"
|
||||
|
@ -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!
|
||||
|
@ -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]
|
||||
|
@ -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]
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user