Merge branch 'develop' into pr/Axel-CH/10062

This commit is contained in:
Matthias
2024-12-22 18:07:32 +01:00
95 changed files with 7101 additions and 2360 deletions
+27 -2
View File
@@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring
import json
import logging
import platform
import re
from copy import deepcopy
from datetime import datetime, timedelta, timezone
@@ -517,6 +518,30 @@ def patch_gc(mocker) -> None:
mocker.patch("freqtrade.main.gc_set_threshold")
def is_arm() -> bool:
machine = platform.machine()
return "arm" in machine or "aarch64" in machine
def is_mac() -> bool:
machine = platform.system()
return "Darwin" in machine
@pytest.fixture(autouse=True)
def patch_torch_initlogs(mocker) -> None:
if is_mac():
# Mock torch import completely
import sys
import types
module_name = "torch"
mocked_module = types.ModuleType(module_name)
sys.modules[module_name] = mocked_module
else:
mocker.patch("torch._logging._init_logs")
@pytest.fixture(autouse=True)
def user_dir(mocker, tmp_path) -> Path:
user_dir = tmp_path / "user_data"
@@ -2235,7 +2260,7 @@ def tickers():
"first": None,
"last": 8603.67,
"change": -0.879,
"percentage": None,
"percentage": -8.95,
"average": None,
"baseVolume": 30414.604298,
"quoteVolume": 259629896.48584127,
@@ -2279,7 +2304,7 @@ def tickers():
"first": None,
"last": 129.28,
"change": 1.795,
"percentage": None,
"percentage": -2.5,
"average": None,
"baseVolume": 59698.79897,
"quoteVolume": 29132399.743954,
+102 -8
View File
@@ -1,13 +1,17 @@
from collections import OrderedDict
import numpy as np
import pandas as pd
import pytest
from freqtrade.constants import DEFAULT_TRADES_COLUMNS
from freqtrade.data.converter import populate_dataframe_with_trades
from freqtrade.data.converter.orderflow import trades_to_volumeprofile_with_total_delta_bid_ask
from freqtrade.data.converter.orderflow import (
ORDERFLOW_ADDED_COLUMNS,
timeframe_to_DateOffset,
trades_to_volumeprofile_with_total_delta_bid_ask,
)
from freqtrade.data.converter.trade_converter import trades_list_to_df
from freqtrade.data.dataprovider import DataProvider
from tests.strategy.strats.strategy_test_v3 import StrategyTestV3
BIN_SIZE_SCALE = 0.5
@@ -37,6 +41,7 @@ def populate_dataframe_with_trades_trades(testdatadir):
@pytest.fixture
def candles(testdatadir):
# TODO: this fixture isn't really necessary and could be removed
return pd.read_json(testdatadir / "orderflow/candles.json").copy()
@@ -102,7 +107,7 @@ def test_public_trades_mock_populate_dataframe_with_trades__check_orderflow(
},
}
# Apply the function to populate the data frame with order flow data
df, _ = populate_dataframe_with_trades(OrderedDict(), config, dataframe, trades)
df, _ = populate_dataframe_with_trades(None, config, dataframe, trades)
# Extract results from the first row of the DataFrame
results = df.iloc[0]
t = results["trades"]
@@ -243,7 +248,7 @@ def test_public_trades_trades_mock_populate_dataframe_with_trades__check_trades(
}
# Populate the DataFrame with trades and order flow data
df, _ = populate_dataframe_with_trades(OrderedDict(), config, dataframe, trades)
df, _ = populate_dataframe_with_trades(None, config, dataframe, trades)
# --- DataFrame and Trade Data Validation ---
@@ -401,9 +406,7 @@ def test_public_trades_config_max_trades(
},
}
df, _ = populate_dataframe_with_trades(
OrderedDict(), default_conf | orderflow_config, dataframe, trades
)
df, _ = populate_dataframe_with_trades(None, default_conf | orderflow_config, dataframe, trades)
assert df.delta.count() == 1
@@ -482,3 +485,94 @@ def test_public_trades_testdata_sanity(
"cost",
"date",
]
def test_analyze_with_orderflow(
default_conf_usdt,
mocker,
populate_dataframe_with_trades_dataframe,
populate_dataframe_with_trades_trades,
):
ohlcv_history = populate_dataframe_with_trades_dataframe
# call without orderflow
strategy = StrategyTestV3(config=default_conf_usdt)
strategy.dp = DataProvider(default_conf_usdt, None, None)
mocker.patch.object(strategy.dp, "trades", return_value=populate_dataframe_with_trades_trades)
import freqtrade.data.converter.orderflow as orderflow_module
spy = mocker.spy(orderflow_module, "trades_to_volumeprofile_with_total_delta_bid_ask")
pair = "ETH/BTC"
df = strategy.advise_indicators(ohlcv_history, {"pair:": pair})
assert len(df) == len(ohlcv_history)
assert "open" in df.columns
assert spy.call_count == 0
# Not expected to run - shouldn't have added orderflow columns
for col in ORDERFLOW_ADDED_COLUMNS:
assert col not in df.columns, f"Column {col} found in df.columns"
default_conf_usdt["exchange"]["use_public_trades"] = True
default_conf_usdt["orderflow"] = {
"cache_size": 5,
"max_candles": 5,
"scale": 0.005,
"imbalance_volume": 0,
"imbalance_ratio": 3,
"stacked_imbalance_range": 3,
}
strategy.config = default_conf_usdt
# First round - builds cache
df1 = strategy.advise_indicators(ohlcv_history, {"pair": pair})
assert len(df1) == len(ohlcv_history)
assert "open" in df1.columns
assert spy.call_count == 5
for col in ORDERFLOW_ADDED_COLUMNS:
assert col in df1.columns, f"Column {col} not found in df.columns"
if col not in ("stacked_imbalances_bid", "stacked_imbalances_ask"):
assert df1[col].count() == 5, f"Column {col} has {df1[col].count()} non-NaN values"
assert len(strategy._cached_grouped_trades_per_pair[pair]) == 5
lastval_trades = df1.at[len(df1) - 1, "trades"]
assert isinstance(lastval_trades, list)
assert len(lastval_trades) == 122
lastval_of = df1.at[len(df1) - 1, "orderflow"]
assert isinstance(lastval_of, dict)
spy.reset_mock()
# Ensure caching works - call the same logic again.
df2 = strategy.advise_indicators(ohlcv_history, {"pair": pair})
assert len(df2) == len(ohlcv_history)
assert "open" in df2.columns
assert spy.call_count == 0
for col in ORDERFLOW_ADDED_COLUMNS:
assert col in df2.columns, f"Round2: Column {col} not found in df.columns"
if col not in ("stacked_imbalances_bid", "stacked_imbalances_ask"):
assert (
df2[col].count() == 5
), f"Round2: Column {col} has {df2[col].count()} non-NaN values"
lastval_trade2 = df2.at[len(df2) - 1, "trades"]
assert isinstance(lastval_trade2, list)
assert len(lastval_trade2) == 122
lastval_of2 = df2.at[len(df2) - 1, "orderflow"]
assert isinstance(lastval_of2, dict)
def test_timeframe_to_DateOffset():
assert timeframe_to_DateOffset("1s") == pd.DateOffset(seconds=1)
assert timeframe_to_DateOffset("1m") == pd.DateOffset(minutes=1)
assert timeframe_to_DateOffset("5m") == pd.DateOffset(minutes=5)
assert timeframe_to_DateOffset("1h") == pd.DateOffset(hours=1)
assert timeframe_to_DateOffset("1d") == pd.DateOffset(days=1)
assert timeframe_to_DateOffset("1w") == pd.DateOffset(weeks=1)
assert timeframe_to_DateOffset("1M") == pd.DateOffset(months=1)
assert timeframe_to_DateOffset("1y") == pd.DateOffset(years=1)
+3 -3
View File
@@ -128,8 +128,8 @@ def test_load_data_with_new_pair_1min(
"""
Test load_pair_history() with 1 min timeframe
"""
mocker.patch(f"{EXMS}.get_historic_ohlcv", return_value=ohlcv_history)
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.object(exchange, "get_historic_ohlcv", return_value=ohlcv_history)
file = tmp_path / "MEME_BTC-1m.feather"
# do not download a new pair if refresh_pairs isn't set
@@ -306,8 +306,8 @@ def test_load_cached_data_for_updating(testdatadir) -> None:
def test_download_pair_history(
ohlcv_history, mocker, default_conf, tmp_path, candle_type, subdir, file_tail
) -> None:
mocker.patch(f"{EXMS}.get_historic_ohlcv", return_value=ohlcv_history)
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.object(exchange, "get_historic_ohlcv", return_value=ohlcv_history)
file1_1 = tmp_path / f"{subdir}MEME_BTC-1m{file_tail}.feather"
file1_5 = tmp_path / f"{subdir}MEME_BTC-5m{file_tail}.feather"
file2_1 = tmp_path / f"{subdir}CFI_BTC-1m{file_tail}.feather"
@@ -357,8 +357,8 @@ def test_download_pair_history2(mocker, default_conf, testdatadir, ohlcv_history
"freqtrade.data.history.datahandlers.featherdatahandler.FeatherDataHandler.ohlcv_store",
return_value=None,
)
mocker.patch(f"{EXMS}.get_historic_ohlcv", return_value=ohlcv_history)
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.object(exchange, "get_historic_ohlcv", return_value=ohlcv_history)
_download_pair_history(
datadir=testdatadir,
exchange=exchange,
+237 -32
View File
@@ -1,14 +1,17 @@
from datetime import datetime, timezone
from datetime import datetime, timedelta
from random import randint
from unittest.mock import MagicMock, PropertyMock
import ccxt
import pandas as pd
import pytest
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_seconds
from freqtrade.persistence import Trade
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re
from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts, dt_utc
from tests.conftest import EXMS, get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers
@@ -290,6 +293,7 @@ def test_liquidation_price_binance(
default_conf["trading_mode"] = trading_mode
default_conf["margin_mode"] = margin_mode
default_conf["liquidation_buffer"] = 0.0
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
exchange = get_patched_exchange(mocker, default_conf, exchange="binance")
def get_maint_ratio(pair_, stake_amount):
@@ -731,42 +735,243 @@ def test__set_leverage_binance(mocker, default_conf):
)
@pytest.mark.parametrize("candle_type", [CandleType.MARK, ""])
async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, candle_type):
ohlcv = [
[
int((datetime.now(timezone.utc).timestamp() - 1000) * 1000),
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
def patch_binance_vision_ohlcv(mocker, start, archive_end, api_end, timeframe):
def make_storage(start: datetime, end: datetime, timeframe: str):
date = pd.date_range(start, end, freq=timeframe.replace("m", "min"))
df = pd.DataFrame(
data=dict(date=date, open=1.0, high=1.0, low=1.0, close=1.0),
)
return df
archive_storage = make_storage(start, archive_end, timeframe)
api_storage = make_storage(start, api_end, timeframe)
ohlcv = [[dt_ts(start), 1, 1, 1, 1]]
# (pair, timeframe, candle_type, ohlcv, True)
candle_history = [None, None, None, ohlcv, None]
def get_historic_ohlcv(
# self,
pair: str,
timeframe: str,
since_ms: int,
candle_type: CandleType,
is_new_pair: bool = False,
until_ms: int | None = None,
):
since = dt_from_ts(since_ms)
until = dt_from_ts(until_ms) if until_ms else api_end + timedelta(seconds=1)
return api_storage.loc[(api_storage["date"] >= since) & (api_storage["date"] < until)]
async def download_archive_ohlcv(
candle_type,
pair,
timeframe,
since_ms,
until_ms,
markets=None,
stop_on_404=False,
):
since = dt_from_ts(since_ms)
until = dt_from_ts(until_ms) if until_ms else archive_end + timedelta(seconds=1)
if since < start:
pass
return archive_storage.loc[
(archive_storage["date"] >= since) & (archive_storage["date"] < until)
]
]
candle_mock = mocker.patch(f"{EXMS}._async_get_candle_history", return_value=candle_history)
api_mock = mocker.patch(f"{EXMS}.get_historic_ohlcv", side_effect=get_historic_ohlcv)
archive_mock = mocker.patch(
"freqtrade.exchange.binance.download_archive_ohlcv", side_effect=download_archive_ohlcv
)
return candle_mock, api_mock, archive_mock
@pytest.mark.parametrize(
"timeframe,is_new_pair,since,until,first_date,last_date,candle_called,archive_called,"
"api_called",
[
(
"1m",
True,
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23, 59),
True,
True,
False,
),
(
"1m",
True,
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 3),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 2, 23, 59),
True,
True,
True,
),
(
"1m",
True,
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 2, 1),
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 2, 0, 59),
True,
False,
True,
),
(
"1m",
False,
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23, 59),
False,
True,
False,
),
(
"1m",
True,
dt_utc(2019, 1, 1),
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23, 59),
True,
True,
False,
),
(
"1m",
False,
dt_utc(2019, 1, 1),
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23, 59),
False,
True,
False,
),
(
"1m",
False,
dt_utc(2019, 1, 1),
dt_utc(2019, 1, 2),
None,
None,
False,
True,
True,
),
(
"1m",
True,
dt_utc(2019, 1, 1),
dt_utc(2019, 1, 2),
None,
None,
True,
False,
False,
),
(
"1m",
False,
dt_utc(2021, 1, 1),
dt_utc(2021, 1, 2),
None,
None,
False,
False,
False,
),
(
"1m",
True,
dt_utc(2021, 1, 1),
dt_utc(2021, 1, 2),
None,
None,
True,
False,
False,
),
(
"1h",
False,
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23),
False,
False,
True,
),
(
"1m",
False,
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 3, 50, 30),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 3, 50),
False,
True,
False,
),
],
)
def test_get_historic_ohlcv_binance(
mocker,
default_conf,
timeframe,
is_new_pair,
since,
until,
first_date,
last_date,
candle_called,
archive_called,
api_called,
):
exchange = get_patched_exchange(mocker, default_conf, exchange="binance")
# Monkey-patch async function
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
pair = "ETH/BTC"
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"
assert restype == candle_type
# Call with very old timestamp - causes tons of requests
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(
pair, "5m", 1500000000000, is_new_pair=True, candle_type=candle_type
start = dt_utc(2020, 1, 1)
archive_end = dt_utc(2020, 1, 2)
api_end = dt_utc(2020, 1, 3)
candle_mock, api_mock, archive_mock = patch_binance_vision_ohlcv(
mocker, start=start, archive_end=archive_end, api_end=api_end, timeframe=timeframe
)
# Called twice - one "init" call - and one to get the actual data.
assert exchange._api_async.fetch_ohlcv.call_count == 2
assert res == ohlcv
assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog)
candle_type = CandleType.SPOT
pair = "BTC/USDT"
since_ms = dt_ts(since)
until_ms = dt_ts(until)
df = exchange.get_historic_ohlcv(pair, timeframe, since_ms, candle_type, is_new_pair, until_ms)
if df.empty:
assert first_date is None
assert last_date is None
else:
assert df["date"].iloc[0] == first_date
assert df["date"].iloc[-1] == last_date
assert (
df["date"].diff().iloc[1:] == timedelta(seconds=timeframe_to_seconds(timeframe))
).all()
if candle_called:
candle_mock.assert_called_once()
if archive_called:
archive_mock.assert_called_once()
if api_called:
api_mock.assert_called_once()
@pytest.mark.parametrize(
+337
View File
@@ -0,0 +1,337 @@
import asyncio
import datetime
import io
import re
import sys
import zipfile
from datetime import timedelta
import aiohttp
import pandas as pd
import pytest
from freqtrade.enums import CandleType
from freqtrade.exchange.binance_public_data import (
BadHttpStatus,
Http404,
binance_vision_zip_name,
download_archive_ohlcv,
get_daily_ohlcv,
)
from freqtrade.util.datetime_helpers import dt_ts, dt_utc
@pytest.fixture(scope="module")
def event_loop_policy(request):
if sys.platform == "win32":
return asyncio.WindowsSelectorEventLoopPolicy()
else:
return asyncio.DefaultEventLoopPolicy()
class MockResponse:
"""AioHTTP response mock"""
def __init__(self, content, status, reason=""):
self._content = content
self.status = status
self.reason = reason
async def read(self):
return self._content
async def __aexit__(self, exc_type, exc, tb):
pass
async def __aenter__(self):
return self
# spot klines archive csv file format, the futures/um klines don't have the header line
#
# open_time,open,high,low,close,volume,close_time,quote_volume,count,taker_buy_volume,taker_buy_quote_volume,ignore # noqa: E501
# 1698364800000,34161.6,34182.5,33977.4,34024.2,409953,1698368399999,1202.97118037,15095,192220,564.12041453,0 # noqa: E501
# 1698368400000,34024.2,34060.1,33776.4,33848.4,740960,1698371999999,2183.75671155,23938,368266,1085.17080793,0 # noqa: E501
# 1698372000000,33848.5,34150.0,33815.1,34094.2,390376,1698375599999,1147.73267094,13854,231446,680.60405822,0 # noqa: E501
def make_response_from_url(start_date, end_date):
def make_daily_df(date, timeframe):
start = dt_utc(date.year, date.month, date.day)
end = start + timedelta(days=1)
date_col = pd.date_range(start, end, freq=timeframe.replace("m", "min"), inclusive="left")
cols = (
"open_time,open,high,low,close,volume,close_time,quote_volume,count,taker_buy_volume,"
"taker_buy_quote_volume,ignore"
)
df = pd.DataFrame(columns=cols.split(","), dtype=float)
df["open_time"] = date_col.astype("int64") // 10**6
df["open"] = df["high"] = df["low"] = df["close"] = df["volume"] = 1.0
return df
def make_daily_zip(asset_type_url_segment, symbol, timeframe, date) -> bytes:
df = make_daily_df(date, timeframe)
if asset_type_url_segment == "spot":
header = True
elif asset_type_url_segment == "futures/um":
header = None
else:
raise ValueError
csv = df.to_csv(index=False, header=header)
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w") as zipf:
zipf.writestr(binance_vision_zip_name(symbol, timeframe, date), csv)
return zip_buffer.getvalue()
def make_response(url):
pattern = (
r"https://data.binance.vision/data/(?P<asset_type_url_segment>spot|futures/um)"
r"/daily/klines/(?P<symbol>.*?)/(?P<timeframe>.*?)/(?P=symbol)-(?P=timeframe)-"
r"(?P<date>\d{4}-\d{2}-\d{2}).zip"
)
m = re.match(pattern, url)
if not m:
return MockResponse(content="", status=404)
date = datetime.datetime.strptime(m["date"], "%Y-%m-%d").date()
if date < start_date or date > end_date:
return MockResponse(content="", status=404)
zip_file = make_daily_zip(m["asset_type_url_segment"], m["symbol"], m["timeframe"], date)
return MockResponse(content=zip_file, status=200)
return make_response
@pytest.mark.parametrize(
"candle_type,pair,since,until,first_date,last_date,stop_on_404",
[
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23),
False,
),
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23, 59, 59),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23),
False,
),
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 5),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 3, 23),
False,
),
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2019, 12, 25),
dt_utc(2020, 1, 5),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 3, 23),
False,
),
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2019, 1, 1),
dt_utc(2019, 1, 5),
None,
None,
False,
),
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2021, 1, 1),
dt_utc(2021, 1, 5),
None,
None,
False,
),
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2020, 1, 2),
None,
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 3, 23),
False,
),
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2020, 1, 5),
dt_utc(2020, 1, 1),
None,
None,
False,
),
(
CandleType.FUTURES,
"BTC/USDT:USDT",
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23, 59, 59),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23),
False,
),
(
CandleType.INDEX,
"N/A",
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23, 59, 59),
None,
None,
False,
),
# stop_on_404 = True
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2019, 12, 25),
dt_utc(2020, 1, 5),
None,
None,
True,
),
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 5),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 3, 23),
True,
),
(
CandleType.FUTURES,
"BTC/USDT:USDT",
dt_utc(2019, 12, 25),
dt_utc(2020, 1, 5),
None,
None,
True,
),
],
)
async def test_download_archive_ohlcv(
mocker, candle_type, pair, since, until, first_date, last_date, stop_on_404
):
history_start = dt_utc(2020, 1, 1).date()
history_end = dt_utc(2020, 1, 3).date()
timeframe = "1h"
since_ms = dt_ts(since)
until_ms = dt_ts(until)
mocker.patch(
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
side_effect=make_response_from_url(history_start, history_end),
)
markets = {"BTC/USDT": {"id": "BTCUSDT"}, "BTC/USDT:USDT": {"id": "BTCUSDT"}}
df = await download_archive_ohlcv(
candle_type,
pair,
timeframe,
since_ms=since_ms,
until_ms=until_ms,
markets=markets,
stop_on_404=stop_on_404,
)
if df.empty:
assert first_date is None and last_date is None
else:
assert candle_type in [CandleType.SPOT, CandleType.FUTURES]
assert df["date"].iloc[0] == first_date
assert df["date"].iloc[-1] == last_date
async def test_download_archive_ohlcv_exception(mocker):
timeframe = "1h"
pair = "BTC/USDT"
since_ms = dt_ts(dt_utc(2020, 1, 1))
until_ms = dt_ts(dt_utc(2020, 1, 2))
markets = {"BTC/USDT": {"id": "BTCUSDT"}, "BTC/USDT:USDT": {"id": "BTCUSDT"}}
mocker.patch(
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get", side_effect=RuntimeError
)
df = await download_archive_ohlcv(
CandleType.SPOT, pair, timeframe, since_ms=since_ms, until_ms=until_ms, markets=markets
)
assert df.empty
async def test_get_daily_ohlcv(mocker, testdatadir):
symbol = "BTCUSDT"
timeframe = "1h"
date = dt_utc(2024, 10, 28).date()
first_date = dt_utc(2024, 10, 28)
last_date = dt_utc(2024, 10, 28, 23)
async with aiohttp.ClientSession() as session:
spot_path = (
testdatadir / "binance/binance_public_data/spot-klines-BTCUSDT-1h-2024-10-28.zip"
)
get = mocker.patch(
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
return_value=MockResponse(spot_path.read_bytes(), 200),
)
df = await get_daily_ohlcv("spot", symbol, timeframe, date, session)
assert get.call_count == 1
assert df["date"].iloc[0] == first_date
assert df["date"].iloc[-1] == last_date
futures_path = (
testdatadir / "binance/binance_public_data/futures-um-klines-BTCUSDT-1h-2024-10-28.zip"
)
get = mocker.patch(
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
return_value=MockResponse(futures_path.read_bytes(), 200),
)
df = await get_daily_ohlcv("futures/um", symbol, timeframe, date, session)
assert get.call_count == 1
assert df["date"].iloc[0] == first_date
assert df["date"].iloc[-1] == last_date
get = mocker.patch(
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
return_value=MockResponse(b"", 404),
)
with pytest.raises(Http404):
df = await get_daily_ohlcv("spot", symbol, timeframe, date, session, retry_delay=0)
assert get.call_count == 1
get = mocker.patch(
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
return_value=MockResponse(b"", 500),
)
mocker.patch("asyncio.sleep")
with pytest.raises(BadHttpStatus):
df = await get_daily_ohlcv("spot", symbol, timeframe, date, session)
assert get.call_count == 4 # 1 + 3 default retries
get = mocker.patch(
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
return_value=MockResponse(b"nop", 200),
)
with pytest.raises(zipfile.BadZipFile):
df = await get_daily_ohlcv("spot", symbol, timeframe, date, session)
assert get.call_count == 4 # 1 + 3 default retries
+54 -6
View File
@@ -2006,6 +2006,46 @@ def test_get_tickers(default_conf, mocker, exchange_name, caplog):
assert exchange.get_tickers() == {}
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_conversion_rate(default_conf_usdt, mocker, exchange_name):
api_mock = MagicMock()
tick = {
"ETH/USDT": {
"last": 42,
},
"BCH/USDT": {
"last": 41,
},
"ETH/BTC": {
"last": 250,
},
}
tick2 = {
"ADA/USDT:USDT": {
"last": 2.5,
}
}
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
api_mock.fetch_tickers = MagicMock(side_effect=[tick, tick2])
api_mock.fetch_bids_asks = MagicMock(return_value={})
exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, exchange=exchange_name)
# retrieve original ticker
assert exchange.get_conversion_rate("USDT", "USDT") == 1
assert api_mock.fetch_tickers.call_count == 0
assert exchange.get_conversion_rate("ETH", "USDT") == 42
assert exchange.get_conversion_rate("ETH", "USDC") is None
assert exchange.get_conversion_rate("ETH", "BTC") == 250
assert exchange.get_conversion_rate("BTC", "ETH") == 0.004
assert api_mock.fetch_tickers.call_count == 1
api_mock.fetch_tickers.reset_mock()
assert exchange.get_conversion_rate("ADA", "USDT") == 2.5
# Only the call to the "others" market
assert api_mock.fetch_tickers.call_count == 1
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_fetch_ticker(default_conf, mocker, exchange_name):
api_mock = MagicMock()
@@ -2091,6 +2131,7 @@ def test___now_is_time_to_refresh(default_conf, mocker, exchange_name, time_mach
@pytest.mark.parametrize("candle_type", ["mark", ""])
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_type):
caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name)
pair = "ETH/BTC"
calls = 0
@@ -2123,7 +2164,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_
assert exchange._async_get_candle_history.call_count == 2
# Returns twice the above OHLCV data after truncating the open candle.
assert len(ret) == 2
assert log_has_re(r"Downloaded data for .* with length .*\.", caplog)
assert log_has_re(r"Downloaded data for .* from ccxt with length .*\.", caplog)
caplog.clear()
@@ -2156,7 +2197,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
pair = "ETH/USDT"
respair, restf, _, res, _ = await exchange._async_get_historic_ohlcv(
pair, "5m", 1500000000000, candle_type=candle_type, is_new_pair=False
pair, "5m", 1500000000000, candle_type=candle_type
)
assert respair == pair
assert restf == "5m"
@@ -2168,7 +2209,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
end_ts = 1_500_500_000_000
start_ts = 1_500_000_000_000
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
pair, "5m", since_ms=start_ts, candle_type=candle_type, until_ms=end_ts
)
# Required candles
candles = (end_ts - start_ts) / 300_000
@@ -4078,10 +4119,16 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
)
ex = Exchange(default_conf)
assert ex.get_valid_pair_combination("ETH", "BTC") == "ETH/BTC"
assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC"
assert next(ex.get_valid_pair_combination("ETH", "BTC")) == "ETH/BTC"
assert next(ex.get_valid_pair_combination("BTC", "ETH")) == "ETH/BTC"
multicombs = list(ex.get_valid_pair_combination("ETH", "USDT"))
assert len(multicombs) == 2
assert "ETH/USDT" in multicombs
assert "ETH/USDT:USDT" in multicombs
with pytest.raises(ValueError, match=r"Could not combine.* to get a valid pair."):
ex.get_valid_pair_combination("NOPAIR", "ETH")
for x in ex.get_valid_pair_combination("NOPAIR", "ETH"):
pass
@pytest.mark.parametrize(
@@ -6130,6 +6177,7 @@ def test_get_liquidation_price(
default_conf_usdt["exchange"]["name"] = exchange_name
default_conf_usdt["margin_mode"] = margin_mode
mocker.patch("freqtrade.exchange.gate.Gate.validate_ordertypes")
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange=exchange_name)
exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01))
-25
View File
@@ -1,4 +1,3 @@
import platform
import sys
from copy import deepcopy
from pathlib import Path
@@ -20,30 +19,6 @@ def is_py12() -> bool:
return sys.version_info >= (3, 12)
def is_mac() -> bool:
machine = platform.system()
return "Darwin" in machine
def is_arm() -> bool:
machine = platform.machine()
return "arm" in machine or "aarch64" in machine
@pytest.fixture(autouse=True)
def patch_torch_initlogs(mocker) -> None:
if is_mac():
# Mock torch import completely
import sys
import types
module_name = "torch"
mocked_module = types.ModuleType(module_name)
sys.modules[module_name] = mocked_module
else:
mocker.patch("torch._logging._init_logs")
@pytest.fixture(scope="function")
def freqai_conf(default_conf, tmp_path):
freqaiconf = deepcopy(default_conf)
+1 -2
View File
@@ -10,11 +10,10 @@ from freqtrade.configuration import TimeRange
from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import OperationalException
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
from tests.conftest import get_patched_exchange
from tests.conftest import get_patched_exchange, is_mac
from tests.freqai.conftest import (
get_patched_data_kitchen,
get_patched_freqai_strategy,
is_mac,
make_unfiltered_dataframe,
)
+8 -3
View File
@@ -13,11 +13,16 @@ from freqtrade.freqai.utils import download_all_data_for_training, get_required_
from freqtrade.optimize.backtesting import Backtesting
from freqtrade.persistence import Trade
from freqtrade.plugins.pairlistmanager import PairListManager
from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_has_re
from tests.freqai.conftest import (
get_patched_freqai_strategy,
from tests.conftest import (
EXMS,
create_mock_trades,
get_patched_exchange,
is_arm,
is_mac,
log_has_re,
)
from tests.freqai.conftest import (
get_patched_freqai_strategy,
make_rl_config,
mock_pytorch_mlp_model_training_parameters,
)
+8 -1
View File
@@ -4022,7 +4022,7 @@ def test_get_real_amount_fees_order(
default_conf_usdt, market_buy_order_usdt_doublefee, fee, mocker
):
tfo_mock = mocker.patch(f"{EXMS}.get_trades_for_order", return_value=[])
mocker.patch(f"{EXMS}.get_valid_pair_combination", return_value="BNB/USDT")
mocker.patch(f"{EXMS}.get_valid_pair_combination", return_value=["BNB/USDT"])
mocker.patch(f"{EXMS}.fetch_ticker", return_value={"last": 200})
trade = Trade(
pair="LTC/USDT",
@@ -5191,6 +5191,13 @@ def test_update_funding_fees(
open_exit_order = limit_order_open[exit_side(is_short)]
bid = 0.11
enter_rate_mock = MagicMock(return_value=bid)
open_order.update(
{
"status": "closed",
"filled": open_order["amount"],
"remaining": 0,
}
)
enter_mm = MagicMock(return_value=open_order)
patch_RPCManager(mocker)
patch_exchange(mocker)
@@ -29,7 +29,7 @@ def test_update_liquidation_prices(mocker, margin_mode, dry_run):
assert trade_mock.set_liquidation_price.call_count == 1
assert wallets.get_total.call_count == (
assert wallets.get_collateral.call_count == (
0 if margin_mode == MarginMode.ISOLATED or not dry_run else 1
)
+5 -3
View File
@@ -371,8 +371,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
mocker.patch("freqtrade.optimize.backtesting.Backtesting.backtest")
mocker.patch("freqtrade.optimize.backtesting.generate_backtest_stats")
mocker.patch("freqtrade.optimize.backtesting.show_backtest_results")
sbs = mocker.patch("freqtrade.optimize.backtesting.store_backtest_stats")
sbc = mocker.patch("freqtrade.optimize.backtesting.store_backtest_analysis_results")
sbs = mocker.patch("freqtrade.optimize.backtesting.store_backtest_results")
mocker.patch(
"freqtrade.plugins.pairlistmanager.PairListManager.whitelist",
PropertyMock(return_value=["UNITTEST/BTC"]),
@@ -397,7 +396,6 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
assert backtesting.strategy.bot_start.call_count == 1
assert backtesting.strategy.bot_loop_start.call_count == 0
assert sbs.call_count == 1
assert sbc.call_count == 1
def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None:
@@ -568,6 +566,9 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
mocker.patch(f"{EXMS}.get_fee", fee)
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float("inf"))
mocker.patch(
"freqtrade.persistence.trade_model.price_to_precision", lambda p, *args, **kwargs: p
)
mocker.patch(f"{EXMS}.get_max_leverage", return_value=100)
mocker.patch("freqtrade.optimize.backtesting.price_to_precision", lambda p, *args: p)
patch_exchange(mocker)
@@ -1842,6 +1843,7 @@ def test_backtest_multi_pair_long_short_switch(
if use_detail:
default_conf_usdt["timeframe_detail"] = "1m"
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float("inf"))
mocker.patch(f"{EXMS}.get_fee", fee)
+74 -11
View File
@@ -39,13 +39,34 @@ def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_res
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
correct = hl.hyperopt_loss_function(
hyperopt_results, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=hyperopt_results,
trade_count=600,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
over = hl.hyperopt_loss_function(
hyperopt_results, 600 + 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=hyperopt_results,
trade_count=600 + 100,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
under = hl.hyperopt_loss_function(
hyperopt_results, 600 - 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=hyperopt_results,
trade_count=600 - 100,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
assert over > correct
assert under > correct
@@ -58,9 +79,25 @@ def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results)
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
longer = hl.hyperopt_loss_function(
hyperopt_results, 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=hyperopt_results,
trade_count=100,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
shorter = hl.hyperopt_loss_function(
results=resultsb,
trade_count=100,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": resultsb["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
shorter = hl.hyperopt_loss_function(resultsb, 100, datetime(2019, 1, 1), datetime(2019, 5, 1))
assert shorter < longer
@@ -73,11 +110,34 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) ->
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
correct = hl.hyperopt_loss_function(
hyperopt_results, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=hyperopt_results,
trade_count=600,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
over = hl.hyperopt_loss_function(
results=results_over,
trade_count=600,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": results_over["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
over = hl.hyperopt_loss_function(results_over, 600, datetime(2019, 1, 1), datetime(2019, 5, 1))
under = hl.hyperopt_loss_function(
results_under, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=results_under,
trade_count=600,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": results_under["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
assert over < correct
assert under > correct
@@ -109,31 +169,34 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct
default_conf.update({"hyperopt_loss": lossfunction})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
correct = hl.hyperopt_loss_function(
hyperopt_results,
results=hyperopt_results,
trade_count=len(hyperopt_results),
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=default_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=default_conf["dry_run_wallet"],
)
over = hl.hyperopt_loss_function(
results_over,
results=results_over,
trade_count=len(results_over),
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=default_conf,
processed=None,
backtest_stats={"profit_total": results_over["profit_abs"].sum()},
starting_balance=default_conf["dry_run_wallet"],
)
under = hl.hyperopt_loss_function(
results_under,
results=results_under,
trade_count=len(results_under),
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=default_conf,
processed=None,
backtest_stats={"profit_total": results_under["profit_abs"].sum()},
starting_balance=default_conf["dry_run_wallet"],
)
assert over < correct
assert under > correct
+46 -16
View File
@@ -26,8 +26,7 @@ from freqtrade.optimize.optimize_reports import (
generate_strategy_comparison,
generate_trading_stats,
show_sorted_pairlist,
store_backtest_analysis_results,
store_backtest_stats,
store_backtest_results,
text_table_bt_results,
text_table_strategy,
)
@@ -226,8 +225,9 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmp_path):
filename_last = tmp_path / LAST_BT_RESULT_FN
_backup_file(filename_last, copy_file=True)
assert not filename.is_file()
default_conf["exportfilename"] = filename
store_backtest_stats(filename, stats, "2022_01_01_15_05_13")
store_backtest_results(default_conf, stats, "2022_01_01_15_05_13")
# get real Filename (it's btresult-<date>.json)
last_fn = get_latest_backtest_filename(filename_last.parent)
@@ -246,11 +246,12 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmp_path):
filename1.unlink()
def test_store_backtest_stats(testdatadir, mocker):
def test_store_backtest_results(testdatadir, mocker):
dump_mock = mocker.patch("freqtrade.optimize.optimize_reports.bt_storage.file_dump_json")
data = {"metadata": {}, "strategy": {}, "strategy_comparison": []}
store_backtest_stats(testdatadir, data, "2022_01_01_15_05_13")
store_backtest_results({"exportfilename": testdatadir}, data, "2022_01_01_15_05_13")
assert dump_mock.call_count == 3
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
@@ -258,16 +259,16 @@ def test_store_backtest_stats(testdatadir, mocker):
dump_mock.reset_mock()
filename = testdatadir / "testresult.json"
store_backtest_stats(filename, data, "2022_01_01_15_05_13")
store_backtest_results({"exportfilename": filename}, data, "2022_01_01_15_05_13")
assert dump_mock.call_count == 3
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
# result will be testdatadir / testresult-<timestamp>.json
assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / "testresult"))
def test_store_backtest_stats_real(tmp_path):
def test_store_backtest_results_real(tmp_path):
data = {"metadata": {}, "strategy": {}, "strategy_comparison": []}
store_backtest_stats(tmp_path, data, "2022_01_01_15_05_13")
store_backtest_results({"exportfilename": tmp_path}, data, "2022_01_01_15_05_13")
assert (tmp_path / "backtest-result-2022_01_01_15_05_13.json").is_file()
assert (tmp_path / "backtest-result-2022_01_01_15_05_13.meta.json").is_file()
@@ -276,7 +277,9 @@ def test_store_backtest_stats_real(tmp_path):
fn = get_latest_backtest_filename(tmp_path)
assert fn == "backtest-result-2022_01_01_15_05_13.json"
store_backtest_stats(tmp_path, data, "2024_01_01_15_05_25", market_change_data=pd.DataFrame())
store_backtest_results(
{"exportfilename": tmp_path}, data, "2024_01_01_15_05_25", market_change_data=pd.DataFrame()
)
assert (tmp_path / "backtest-result-2024_01_01_15_05_25.json").is_file()
assert (tmp_path / "backtest-result-2024_01_01_15_05_25.meta.json").is_file()
assert (tmp_path / "backtest-result-2024_01_01_15_05_25_market_change.feather").is_file()
@@ -287,13 +290,27 @@ def test_store_backtest_stats_real(tmp_path):
assert fn == "backtest-result-2024_01_01_15_05_25.json"
def test_store_backtest_candles(testdatadir, mocker):
def test_store_backtest_candles(tmp_path, mocker):
mocker.patch("freqtrade.optimize.optimize_reports.bt_storage.file_dump_json")
dump_mock = mocker.patch("freqtrade.optimize.optimize_reports.bt_storage.file_dump_joblib")
candle_dict = {"DefStrat": {"UNITTEST/BTC": pd.DataFrame()}}
bt_results = {"metadata": {}, "strategy": {}, "strategy_comparison": []}
mock_conf = {
"exportfilename": tmp_path,
"export": "signals",
"runmode": "backtest",
}
# mock directory exporting
store_backtest_analysis_results(testdatadir, candle_dict, {}, {}, "2022_01_01_15_05_13")
data = {
"signals": candle_dict,
"rejected": {},
"exited": {},
}
store_backtest_results(mock_conf, bt_results, "2022_01_01_15_05_13", analysis_results=data)
assert dump_mock.call_count == 3
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
@@ -303,11 +320,12 @@ def test_store_backtest_candles(testdatadir, mocker):
dump_mock.reset_mock()
# mock file exporting
filename = Path(testdatadir / "testresult")
store_backtest_analysis_results(filename, candle_dict, {}, {}, "2022_01_01_15_05_13")
filename = Path(tmp_path / "testresult")
mock_conf["exportfilename"] = filename
store_backtest_results(mock_conf, bt_results, "2022_01_01_15_05_13", analysis_results=data)
assert dump_mock.call_count == 3
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
# result will be testdatadir / testresult-<timestamp>_signals.pkl
# result will be tmp_path / testresult-<timestamp>_signals.pkl
assert str(dump_mock.call_args_list[0][0][0]).endswith("_signals.pkl")
assert str(dump_mock.call_args_list[1][0][0]).endswith("_rejected.pkl")
assert str(dump_mock.call_args_list[2][0][0]).endswith("_exited.pkl")
@@ -317,10 +335,21 @@ def test_store_backtest_candles(testdatadir, mocker):
def test_write_read_backtest_candles(tmp_path):
candle_dict = {"DefStrat": {"UNITTEST/BTC": pd.DataFrame()}}
bt_results = {"metadata": {}, "strategy": {}, "strategy_comparison": []}
mock_conf = {
"exportfilename": tmp_path,
"export": "signals",
"runmode": "backtest",
}
# test directory exporting
sample_date = "2022_01_01_15_05_13"
store_backtest_analysis_results(tmp_path, candle_dict, {}, {}, sample_date)
data = {
"signals": candle_dict,
"rejected": {},
"exited": {},
}
store_backtest_results(mock_conf, bt_results, sample_date, analysis_results=data)
stored_file = tmp_path / f"backtest-result-{sample_date}_signals.pkl"
with stored_file.open("rb") as scp:
pickled_signal_candles = joblib.load(scp)
@@ -335,7 +364,8 @@ def test_write_read_backtest_candles(tmp_path):
# test file exporting
filename = tmp_path / "testresult"
store_backtest_analysis_results(filename, candle_dict, {}, {}, sample_date)
mock_conf["exportfilename"] = filename
store_backtest_results(mock_conf, bt_results, sample_date, analysis_results=data)
stored_file = tmp_path / f"testresult-{sample_date}_signals.pkl"
with stored_file.open("rb") as scp:
pickled_signal_candles = joblib.load(scp)
+5 -2
View File
@@ -37,6 +37,7 @@ def test_PairLocks(use_db):
assert not PairLocks.is_pair_locked(pair)
assert not PairLocks.is_pair_locked(pair, side="long")
assert PairLocks.is_pair_locked(pair, side="short")
assert len(PairLocks.get_pair_locks(pair)) == 1
# XRP/BTC should not be locked now
pair = "XRP/BTC"
@@ -47,9 +48,11 @@ def test_PairLocks(use_db):
PairLocks.lock_pair(pair, dt_now() + timedelta(minutes=4))
assert PairLocks.is_pair_locked(pair)
# Get both locks from above
# Get all locks from above
locks = PairLocks.get_pair_locks(None)
assert len(locks) == 2
assert len(locks) == 4
assert len(PairLocks.get_pair_locks(None, side="*")) == 2
# Unlock original pair
pair = "ETH/BTC"
+10 -3
View File
@@ -360,9 +360,16 @@ def test_gen_pairlist_from_tickers(mocker, rpl_config, tickers):
exchange = get_patched_exchange(mocker, rpl_config, exchange="binance")
pairlistmanager = PairListManager(exchange, rpl_config)
remote_pairlist = PercentChangePairList(
exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][0], 0
)
remote_pairlist = pairlistmanager._pairlist_handlers[0]
# The generator returns BTC ETH and TKN - filtering the first ensures removing pairs
# in this step ain't problematic.
def _validate_pair(pair, ticker):
if pair == "BTC/USDT":
return False
return True
remote_pairlist._validate_pair = _validate_pair
result = remote_pairlist.gen_pairlist(tickers.return_value)
+34 -6
View File
@@ -514,8 +514,13 @@ def test_rpc_balance_handle_error(default_conf, mocker):
patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter({})
with pytest.raises(RPCException, match="Error getting current tickers."):
rpc._rpc_balance(default_conf["stake_currency"], default_conf["fiat_display_currency"])
res = rpc._rpc_balance(default_conf["stake_currency"], default_conf["fiat_display_currency"])
assert res["stake"] == "BTC"
assert len(res["currencies"]) == 1
assert res["currencies"][0]["currency"] == "BTC"
# ETH has not been converted.
assert all(currency["currency"] != "ETH" for currency in res["currencies"])
def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
@@ -530,6 +535,13 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
"total": 5.0,
"used": 4.0,
},
# Invalid coin not in tickers list.
# This triggers a 2nd call to get_tickers
"NotACoin": {
"free": 0.0,
"total": 2.0,
"used": 0.0,
},
"USDT": {
"free": 50.0,
"total": 100.0,
@@ -574,7 +586,7 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
fetch_positions=MagicMock(return_value=mock_pos),
get_tickers=tickers,
get_valid_pair_combination=MagicMock(
side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}"
side_effect=lambda a, b: [f"{b}/{a}" if a == "USDT" else f"{a}/{b}"]
),
)
default_conf_usdt["dry_run"] = False
@@ -590,8 +602,10 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
assert pytest.approx(result["total"]) == 2824.83464
assert pytest.approx(result["value"]) == 2824.83464 * 1.2
assert tickers.call_count == 1
assert tickers.call_count == 4
assert tickers.call_args_list[0][1]["cached"] is True
# Testing futures - so we should get spot tickers
assert tickers.call_args_list[-1][1]["market_type"] == "spot"
assert "USD" == result["symbol"]
assert result["currencies"] == [
{
@@ -622,6 +636,20 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
"is_bot_managed": False,
"is_position": False,
},
{
"currency": "NotACoin",
"balance": 2.0,
"bot_owned": 0,
"est_stake": 0,
"est_stake_bot": 0,
"free": 0.0,
"is_bot_managed": False,
"is_position": False,
"position": 0,
"side": "long",
"stake": "USDT",
"used": 0.0,
},
{
"currency": "USDT",
"free": 50.0,
@@ -652,8 +680,8 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
]
assert pytest.approx(result["total_bot"]) == 69.5
assert pytest.approx(result["total"]) == 2824.83464 # ETH stake is missing.
assert result["starting_capital"] == 50
assert result["starting_capital_ratio"] == pytest.approx(0.3899999)
assert result["starting_capital"] == 50 * default_conf_usdt["tradable_balance_ratio"]
assert result["starting_capital_ratio"] == pytest.approx(0.4040404)
def test_rpc_start(mocker, default_conf) -> None:
+2 -1
View File
@@ -556,7 +556,7 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers):
ftbot.config["dry_run"] = False
mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance)
mocker.patch(f"{EXMS}.get_tickers", tickers)
mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: f"{a}/{b}")
mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"])
ftbot.wallets.update()
rc = client_get(client, f"{BASE_URI}/balance")
@@ -1056,6 +1056,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
)
def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected):
ftbot, client = botclient
ftbot.config["tradable_balance_ratio"] = 1
patch_get_signal(ftbot)
mocker.patch.multiple(
EXMS,
+3 -3
View File
@@ -960,7 +960,7 @@ async def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance
default_conf["dry_run"] = False
mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance)
mocker.patch(f"{EXMS}.get_tickers", tickers)
mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: f"{a}/{b}")
mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"])
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
@@ -1049,7 +1049,7 @@ async def test_telegram_balance_handle_futures(
mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance)
mocker.patch(f"{EXMS}.fetch_positions", return_value=mock_pos)
mocker.patch(f"{EXMS}.get_tickers", tickers)
mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: f"{a}/{b}")
mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"])
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
@@ -1087,7 +1087,7 @@ async def test_balance_handle_empty_response_dry(default_conf, update, mocker) -
result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1
assert "*Warning:* Simulated balances in Dry Mode." in result
assert "Starting capital: `1000 BTC`" in result
assert "Starting capital: `990 BTC`" in result
async def test_balance_handle_too_large_response(default_conf, update, mocker) -> None:
+10
View File
@@ -1481,6 +1481,12 @@ def test_flat_vars_to_nested_dict(caplog):
"FREQTRADE__STAKE_AMOUNT": "200.05",
"FREQTRADE__TELEGRAM__CHAT_ID": "2151",
"NOT_RELEVANT": "200.0", # Will be ignored
"FREQTRADE__ARRAY": '[{"name":"default","host":"xxx"}]',
"FREQTRADE__EXCHANGE__PAIR_WHITELIST": '["BTC/USDT", "ETH/USDT"]',
# Fails due to trailing comma
"FREQTRADE__ARRAY_TRAIL_COMMA": '[{"name":"default","host":"xxx",}]',
# Object fails
"FREQTRADE__OBJECT": '{"name":"default","host":"xxx"}',
}
expected = {
"stake_amount": 200.05,
@@ -1494,8 +1500,12 @@ def test_flat_vars_to_nested_dict(caplog):
},
"some_setting": True,
"some_false_setting": False,
"pair_whitelist": ["BTC/USDT", "ETH/USDT"],
},
"telegram": {"chat_id": "2151"},
"array": [{"name": "default", "host": "xxx"}],
"object": '{"name":"default","host":"xxx"}',
"array_trail_comma": '[{"name":"default","host":"xxx",}]',
}
res = _flat_vars_to_nested_dict(test_args, ENV_VAR_PREFIX)
assert res == expected
+158 -4
View File
@@ -168,7 +168,7 @@ def test_get_trade_stake_amount_unlimited_amount(
assert result == 0
freqtrade.config["dry_run_wallet"] = 200
freqtrade.wallets._start_cap = 200
freqtrade.wallets._start_cap["BTC"] = 200
result = freqtrade.wallets.get_trade_stake_amount("XRP/USDT", 3)
assert round(result, 4) == round(result2, 4)
@@ -244,7 +244,7 @@ def test_get_starting_balance(
freqtrade = get_patched_freqtradebot(mocker, default_conf)
assert freqtrade.wallets.get_starting_balance() == expected
assert freqtrade.wallets.get_starting_balance() == expected * (1 if available_capital else 0.99)
def test_sync_wallet_futures_live(mocker, default_conf):
@@ -373,7 +373,10 @@ def test_sync_wallet_dry(mocker, default_conf_usdt, fee):
# sum of used and free should be total.
assert usdt_bal.total == usdt_bal.free + usdt_bal.used
assert freqtrade.wallets.get_starting_balance() == default_conf_usdt["dry_run_wallet"]
assert (
freqtrade.wallets.get_starting_balance()
== default_conf_usdt["dry_run_wallet"] * default_conf_usdt["tradable_balance_ratio"]
)
total = freqtrade.wallets.get_total("LTC")
free = freqtrade.wallets.get_free("LTC")
used = freqtrade.wallets.get_used("LTC")
@@ -401,7 +404,10 @@ def test_sync_wallet_futures_dry(mocker, default_conf, fee):
assert positions["XRP/BTC"].side == "long"
assert positions["LTC/BTC"].side == "short"
assert freqtrade.wallets.get_starting_balance() == default_conf["dry_run_wallet"]
assert (
freqtrade.wallets.get_starting_balance()
== default_conf["dry_run_wallet"] * default_conf["tradable_balance_ratio"]
)
total = freqtrade.wallets.get_total("BTC")
free = freqtrade.wallets.get_free("BTC")
used = freqtrade.wallets.get_used("BTC")
@@ -451,3 +457,151 @@ def test_check_exit_amount_futures(mocker, default_conf, fee):
assert freqtrade.wallets.check_exit_amount(trade) is False
assert total_mock.call_count == 0
assert update_mock.call_count == 1
@pytest.mark.parametrize(
"config,wallets",
[
(
{"stake_currency": "USDT", "dry_run_wallet": 1000.0},
{"USDT": {"currency": "USDT", "free": 1000.0, "used": 0.0, "total": 1000.0}},
),
(
{"stake_currency": "USDT", "dry_run_wallet": {"USDT": 1000.0, "BTC": 0.1, "ETH": 2.0}},
{
"USDT": {"currency": "USDT", "free": 1000.0, "used": 0.0, "total": 1000.0},
"BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1},
"ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0},
},
),
(
{
"stake_currency": "USDT",
"margin_mode": "cross",
"dry_run_wallet": {"USDC": 1000.0, "BTC": 0.1, "ETH": 2.0},
},
{
# USDT wallet should be created with 0 balance, but Free balance, since
# it's converted from the other currencies
"USDT": {"currency": "USDT", "free": 4200.0, "used": 0.0, "total": 0.0},
"USDC": {"currency": "USDC", "free": 1000.0, "used": 0.0, "total": 1000.0},
"BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1},
"ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0},
},
),
(
{
"stake_currency": "USDT",
"margin_mode": "cross",
"dry_run_wallet": {"USDT": 500, "USDC": 1000.0, "BTC": 0.1, "ETH": 2.0},
},
{
# USDT wallet should be created with 500 balance, but Free balance, since
# it's converted from the other currencies
"USDT": {"currency": "USDT", "free": 4700.0, "used": 0.0, "total": 500.0},
"USDC": {"currency": "USDC", "free": 1000.0, "used": 0.0, "total": 1000.0},
"BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1},
"ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0},
},
),
(
# Same as above, but without cross
{
"stake_currency": "USDT",
"dry_run_wallet": {"USDT": 500, "USDC": 1000.0, "BTC": 0.1, "ETH": 2.0},
},
{
# No "free" transfer for USDT wallet
"USDT": {"currency": "USDT", "free": 500.0, "used": 0.0, "total": 500.0},
"USDC": {"currency": "USDC", "free": 1000.0, "used": 0.0, "total": 1000.0},
"BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1},
"ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0},
},
),
(
# Same as above, but with futures and cross
{
"stake_currency": "USDT",
"margin_mode": "cross",
"trading_mode": "futures",
"dry_run_wallet": {"USDT": 500, "USDC": 1000.0, "BTC": 0.1, "ETH": 2.0},
},
{
# USDT wallet should be created with 500 balance, but Free balance, since
# it's converted from the other currencies
"USDT": {"currency": "USDT", "free": 4700.0, "used": 0.0, "total": 500.0},
"USDC": {"currency": "USDC", "free": 1000.0, "used": 0.0, "total": 1000.0},
"BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1},
"ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0},
},
),
],
)
def test_dry_run_wallet_initialization(mocker, default_conf_usdt, config, wallets):
default_conf_usdt.update(config)
mocker.patch(
f"{EXMS}.get_tickers",
return_value={
"USDC/USDT": {"last": 1.0},
"BTC/USDT": {"last": 20_000.0},
"ETH/USDT": {"last": 1100.0},
},
)
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
stake_currency = config["stake_currency"]
# Verify each wallet matches the expected values
for currency, expected_wallet in wallets.items():
wallet = freqtrade.wallets._wallets[currency]
assert wallet.currency == expected_wallet["currency"]
assert wallet.free == expected_wallet["free"]
assert wallet.used == expected_wallet["used"]
assert wallet.total == expected_wallet["total"]
# Verify no extra wallets were created
assert len(freqtrade.wallets._wallets) == len(wallets)
# Create a trade and verify the new currency is added to the wallets
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.0)
mocker.patch(f"{EXMS}.get_rate", return_value=2.22)
mocker.patch(
f"{EXMS}.fetch_ticker",
return_value={
"bid": 0.20,
"ask": 0.22,
"last": 0.22,
},
)
# Without position, collateral will be the same as free
assert freqtrade.wallets.get_collateral() == freqtrade.wallets.get_free(stake_currency)
freqtrade.execute_entry("NEO/USDT", 100.0)
# Update wallets and verify NEO is now included
freqtrade.wallets.update()
if default_conf_usdt["trading_mode"] != "futures":
assert "NEO" in freqtrade.wallets._wallets
assert freqtrade.wallets._wallets["NEO"].total == 45.04504504 # 100 USDT / 0.22
assert freqtrade.wallets._wallets["NEO"].used == 0.0
assert freqtrade.wallets._wallets["NEO"].free == 45.04504504
assert freqtrade.wallets.get_collateral() == freqtrade.wallets.get_free(stake_currency)
# Verify USDT wallet was reduced by trade amount
assert (
pytest.approx(freqtrade.wallets._wallets[stake_currency].total)
== wallets[stake_currency]["total"] - 100.0
)
assert len(freqtrade.wallets._wallets) == len(wallets) + 1 # Original wallets + NEO
else:
# Futures mode
assert "NEO" not in freqtrade.wallets._wallets
assert freqtrade.wallets._positions["NEO/USDT"].position == 45.04504504
assert pytest.approx(freqtrade.wallets._positions["NEO/USDT"].collateral) == 100
# Verify USDT wallet's free was reduced by trade amount
assert (
pytest.approx(freqtrade.wallets.get_collateral())
== freqtrade.wallets.get_free(stake_currency) + 100
)
assert (
pytest.approx(freqtrade.wallets._wallets[stake_currency].free)
== wallets[stake_currency]["free"] - 100.0
)