Merge branch 'develop' into pr/Axel-CH/10062
This commit is contained in:
+27
-2
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
)
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user