test: split test_btanalysis and test_metrics

This commit is contained in:
Matthias
2026-04-11 16:41:07 +02:00
parent f838db6409
commit 68d514db91
2 changed files with 398 additions and 387 deletions
+3 -387
View File
@@ -1,10 +1,10 @@
from datetime import UTC, datetime, timedelta
from datetime import UTC, datetime
from pathlib import Path
from unittest.mock import MagicMock
from zipfile import ZipFile
import pytest
from pandas import DataFrame, DateOffset, Timestamp, to_datetime
from pandas import DataFrame, to_datetime
from freqtrade.configuration import TimeRange
from freqtrade.constants import LAST_BT_RESULT_FN
@@ -21,22 +21,7 @@ from freqtrade.data.btanalysis import (
load_trades,
load_trades_from_db,
)
from freqtrade.data.history import load_data, load_pair_history
from freqtrade.data.metrics import (
calculate_cagr,
calculate_calmar,
calculate_csum,
calculate_expectancy,
calculate_market_change,
calculate_max_drawdown,
calculate_sharpe,
calculate_sortino,
calculate_sqn,
calculate_underwater,
combine_dataframes_with_mean,
combined_dataframes_with_rel_mean,
create_cum_profit,
)
from freqtrade.data.history import load_pair_history
from freqtrade.exceptions import OperationalException
from freqtrade.util import dt_utc
from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades
@@ -253,375 +238,6 @@ def test_load_trades(default_conf, mocker):
assert bt_mock.call_count == 0
def test_calculate_market_change(testdatadir):
pairs = ["ETH/BTC", "ADA/BTC"]
data = load_data(datadir=testdatadir, pairs=pairs, timeframe="5m")
result = calculate_market_change(data)
assert isinstance(result, float)
assert pytest.approx(result) == 0.01100002
result = calculate_market_change(data, min_date=dt_utc(2018, 1, 20))
assert isinstance(result, float)
assert pytest.approx(result) == 0.0375149
# Move min-date after the last date
result = calculate_market_change(data, min_date=dt_utc(2018, 2, 20))
assert pytest.approx(result) == 0.0
def test_combine_dataframes_with_mean(testdatadir):
pairs = ["ETH/BTC", "ADA/BTC"]
data = load_data(datadir=testdatadir, pairs=pairs, timeframe="5m")
df = combine_dataframes_with_mean(data)
assert isinstance(df, DataFrame)
assert "ETH/BTC" in df.columns
assert "ADA/BTC" in df.columns
assert "mean" in df.columns
def test_combined_dataframes_with_rel_mean(testdatadir):
pairs = ["BTC/USDT", "XRP/USDT"]
data = load_data(datadir=testdatadir, pairs=pairs, timeframe="5m")
df = combined_dataframes_with_rel_mean(
data,
fromdt=data["BTC/USDT"].at[0, "date"],
todt=data["BTC/USDT"].at[data["BTC/USDT"].index[-1], "date"],
)
assert isinstance(df, DataFrame)
assert "BTC/USDT" not in df.columns
assert "XRP/USDT" not in df.columns
assert "mean" in df.columns
assert "rel_mean" in df.columns
assert "count" in df.columns
assert df.iloc[0]["count"] == 2
assert df.iloc[-1]["count"] == 2
assert len(df) < len(data["BTC/USDT"])
assert df["rel_mean"].between(-0.5, 0.5).all()
def test_combine_dataframes_with_mean_no_data(testdatadir):
pairs = ["ETH/BTC", "ADA/BTC"]
data = load_data(datadir=testdatadir, pairs=pairs, timeframe="6m")
with pytest.raises(ValueError, match=r"No data provided\."):
combine_dataframes_with_mean(data)
def test_create_cum_profit(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
timerange = TimeRange.parse_timerange("20180110-20180112")
df = load_pair_history(pair="TRX/BTC", timeframe="5m", datadir=testdatadir, timerange=timerange)
cum_profits = create_cum_profit(
df.set_index("date"), bt_data[bt_data["pair"] == "TRX/BTC"], "cum_profits", timeframe="5m"
)
assert "cum_profits" in cum_profits.columns
assert cum_profits.iloc[0]["cum_profits"] == 0
assert pytest.approx(cum_profits.iloc[-1]["cum_profits"]) == 9.0225563e-05
def test_create_cum_profit1(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
# Move close-time to "off" the candle, to make sure the logic still works
bt_data["close_date"] = bt_data.loc[:, "close_date"] + DateOffset(seconds=20)
timerange = TimeRange.parse_timerange("20180110-20180112")
df = load_pair_history(pair="TRX/BTC", timeframe="5m", datadir=testdatadir, timerange=timerange)
cum_profits = create_cum_profit(
df.set_index("date"), bt_data[bt_data["pair"] == "TRX/BTC"], "cum_profits", timeframe="5m"
)
assert "cum_profits" in cum_profits.columns
assert cum_profits.iloc[0]["cum_profits"] == 0
assert pytest.approx(cum_profits.iloc[-1]["cum_profits"]) == 9.0225563e-05
with pytest.raises(ValueError, match=r"Trade dataframe empty\."):
create_cum_profit(
df.set_index("date"),
bt_data[bt_data["pair"] == "NOTAPAIR"],
"cum_profits",
timeframe="5m",
)
def test_calculate_max_drawdown(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
drawdown = calculate_max_drawdown(bt_data, value_col="profit_abs")
assert isinstance(drawdown.relative_account_drawdown, float)
assert pytest.approx(drawdown.relative_account_drawdown) == 0.29753914
assert isinstance(drawdown.high_date, Timestamp)
assert isinstance(drawdown.low_date, Timestamp)
assert isinstance(drawdown.high_value, float)
assert isinstance(drawdown.low_value, float)
assert drawdown.high_date == Timestamp("2018-01-16 19:30:00", tz="UTC")
assert drawdown.low_date == Timestamp("2018-01-16 22:25:00", tz="UTC")
underwater = calculate_underwater(bt_data)
assert isinstance(underwater, DataFrame)
with pytest.raises(ValueError, match=r"Trade dataframe empty\."):
calculate_max_drawdown(DataFrame())
with pytest.raises(ValueError, match=r"Trade dataframe empty\."):
calculate_underwater(DataFrame())
def test_calculate_csum(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
csum_min, csum_max = calculate_csum(bt_data)
assert isinstance(csum_min, float)
assert isinstance(csum_max, float)
assert csum_min < csum_max
assert csum_min < 0.0001
assert csum_max > 0.0002
csum_min1, csum_max1 = calculate_csum(bt_data, 5)
assert csum_min1 == csum_min + 5
assert csum_max1 == csum_max + 5
with pytest.raises(ValueError, match=r"Trade dataframe empty\."):
csum_min, csum_max = calculate_csum(DataFrame())
def test_calculate_expectancy(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
expectancy, expectancy_ratio = calculate_expectancy(DataFrame())
assert expectancy == 0.0
assert expectancy_ratio == 100
expectancy, expectancy_ratio = calculate_expectancy(bt_data)
assert isinstance(expectancy, float)
assert isinstance(expectancy_ratio, float)
assert pytest.approx(expectancy) == 5.820687070932315e-06
assert pytest.approx(expectancy_ratio) == 0.07151374226574791
data = {"profit_abs": [100, 200, 50, -150, 300, -100, 80, -30]}
df = DataFrame(data)
expectancy, expectancy_ratio = calculate_expectancy(df)
assert pytest.approx(expectancy) == 56.25
assert pytest.approx(expectancy_ratio) == 0.60267857
def test_calculate_sortino(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
sortino = calculate_sortino(DataFrame(), None, None, 0)
assert sortino == 0.0
sortino = calculate_sortino(
bt_data,
bt_data["open_date"].min(),
bt_data["close_date"].max(),
0.01,
)
assert isinstance(sortino, float)
assert pytest.approx(sortino) == 35.17722
def test_calculate_sharpe(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
sharpe = calculate_sharpe(DataFrame(), None, None, 0)
assert sharpe == 0.0
sharpe = calculate_sharpe(
bt_data,
bt_data["open_date"].min(),
bt_data["close_date"].max(),
0.01,
)
assert isinstance(sharpe, float)
assert pytest.approx(sharpe) == 44.5078669
def test_calculate_calmar(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
calmar = calculate_calmar(DataFrame(), None, None, 0)
assert calmar == 0.0
calmar = calculate_calmar(
bt_data,
bt_data["open_date"].min(),
bt_data["close_date"].max(),
0.01,
)
assert isinstance(calmar, float)
assert pytest.approx(calmar) == 559.040508
def test_calculate_sqn(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
sqn = calculate_sqn(DataFrame(), 0)
assert sqn == 0.0
sqn = calculate_sqn(
bt_data,
0.01,
)
assert isinstance(sqn, float)
assert pytest.approx(sqn) == 3.2991
@pytest.mark.parametrize(
"profits,starting_balance,expected_sqn,description",
[
([1.0, -0.5, 2.0, -1.0, 0.5, 1.5, -0.5, 1.0], 100, 1.3229, "Mixed profits/losses"),
([], 100, 0.0, "Empty dataframe"),
([1.0, 0.5, 2.0, 1.5, 0.8], 100, 4.3657, "All winning trades"),
([-1.0, -0.5, -2.0, -1.5, -0.8], 100, -4.3657, "All losing trades"),
([1.0], 100, -100, "Single trade"),
],
)
def test_calculate_sqn_cases(profits, starting_balance, expected_sqn, description):
"""
Test SQN calculation with various scenarios:
"""
trades = DataFrame({"profit_abs": profits})
sqn = calculate_sqn(trades, starting_balance=starting_balance)
assert isinstance(sqn, float)
assert pytest.approx(sqn, rel=1e-4) == expected_sqn
@pytest.mark.parametrize(
"start,end,days, expected",
[
(64900, 176000, 3 * 365, 0.3945),
(64900, 176000, 365, 1.7119),
(1000, 1000, 365, 0.0),
(1000, 1500, 365, 0.5),
(1000, 1500, 100, 3.3927), # sub year
(0.01000000, 0.01762792, 120, 4.6087), # sub year BTC values
(1000, 1010, 0, 0.0), # zero days
(-100, 100, 365, 0.0), # negative starting balance
],
)
def test_calculate_cagr(start, end, days, expected):
assert round(calculate_cagr(days, start, end), 4) == expected
def test_calculate_max_drawdown2():
values = [
0.011580,
0.010048,
0.011340,
0.012161,
0.010416,
0.010009,
0.020024,
-0.024662,
-0.022350,
0.020496,
-0.029859,
-0.030511,
0.010041,
0.010872,
-0.025782,
0.010400,
0.012374,
0.012467,
0.114741,
0.010303,
0.010088,
-0.033961,
0.010680,
0.010886,
-0.029274,
0.011178,
0.010693,
0.010711,
]
dates = [dt_utc(2020, 1, 1) + timedelta(days=i) for i in range(len(values))]
df = DataFrame(zip(values, dates, strict=False), columns=["profit", "open_date"])
# sort by profit and reset index
df = df.sort_values("profit").reset_index(drop=True)
df1 = df.copy()
drawdown = calculate_max_drawdown(
df, date_col="open_date", starting_balance=0.2, value_col="profit"
)
# Ensure df has not been altered.
assert df.equals(df1)
assert isinstance(drawdown.drawdown_abs, float)
assert isinstance(drawdown.relative_account_drawdown, float)
# High must be before low
assert drawdown.high_date < drawdown.low_date
# High value must be higher than low value
assert drawdown.high_value > drawdown.low_value
assert drawdown.drawdown_abs == 0.091755
assert pytest.approx(drawdown.relative_account_drawdown) == 0.32129575
df = DataFrame(zip(values[:5], dates[:5], strict=False), columns=["profit", "open_date"])
# No losing trade ...
drawdown = calculate_max_drawdown(df, date_col="open_date", value_col="profit")
assert drawdown.drawdown_abs == 0.0
assert drawdown.low_value == 0.0
assert drawdown.current_high_value >= 0.0
assert drawdown.current_drawdown_abs == 0.0
df1 = DataFrame(zip(values[:5], dates[:5], strict=False), columns=["profit", "open_date"])
df1.loc[:, "profit"] = df1["profit"] * -1
# No winning trade ...
drawdown = calculate_max_drawdown(df1, date_col="open_date", value_col="profit")
assert drawdown.drawdown_abs == 0.055545
assert drawdown.high_value == 0.0
assert drawdown.current_high_value == 0.0
assert drawdown.current_drawdown_abs == 0.055545
@pytest.mark.parametrize(
"profits,relative,highd,lowdays,result,result_rel",
[
([0.0, -500.0, 500.0, 10000.0, -1000.0], False, 3, 4, 1000.0, 0.090909),
([0.0, -500.0, 500.0, 10000.0, -1000.0], True, 0, 1, 500.0, 0.5),
],
)
def test_calculate_max_drawdown_abs(profits, relative, highd, lowdays, result, result_rel):
"""
Test case from issue https://github.com/freqtrade/freqtrade/issues/6655
[1000, 500, 1000, 11000, 10000] # absolute results
[1000, 50%, 0%, 0%, ~9%] # Relative drawdowns
"""
init_date = datetime(2020, 1, 1, tzinfo=UTC)
dates = [init_date + timedelta(days=i) for i in range(len(profits))]
df = DataFrame(zip(profits, dates, strict=False), columns=["profit_abs", "open_date"])
# sort by profit and reset index
df = df.sort_values("profit_abs").reset_index(drop=True)
df1 = df.copy()
drawdown = calculate_max_drawdown(
df, date_col="open_date", starting_balance=1000, relative=relative
)
# Ensure df has not been altered.
assert df.equals(df1)
assert isinstance(drawdown.drawdown_abs, float)
assert isinstance(drawdown.relative_account_drawdown, float)
assert drawdown.high_date == init_date + timedelta(days=highd)
assert drawdown.low_date == init_date + timedelta(days=lowdays)
# High must be before low
assert drawdown.high_date < drawdown.low_date
# High value must be higher than low value
assert drawdown.high_value > drawdown.low_value
assert drawdown.drawdown_abs == result
assert pytest.approx(drawdown.relative_account_drawdown) == result_rel
def test_load_file_from_zip(tmp_path):
with pytest.raises(ValueError, match=r"Zip file .* not found\."):
load_file_from_zip(tmp_path / "test.zip", "testfile.txt")
+395
View File
@@ -0,0 +1,395 @@
from datetime import UTC, datetime, timedelta
import pytest
from pandas import DataFrame, DateOffset, Timestamp
from freqtrade.configuration import TimeRange
from freqtrade.data.btanalysis import (
load_backtest_data,
)
from freqtrade.data.history import load_data, load_pair_history
from freqtrade.data.metrics import (
calculate_cagr,
calculate_calmar,
calculate_csum,
calculate_expectancy,
calculate_market_change,
calculate_max_drawdown,
calculate_sharpe,
calculate_sortino,
calculate_sqn,
calculate_underwater,
combine_dataframes_with_mean,
combined_dataframes_with_rel_mean,
create_cum_profit,
)
from freqtrade.util import dt_utc
def test_calculate_market_change(testdatadir):
pairs = ["ETH/BTC", "ADA/BTC"]
data = load_data(datadir=testdatadir, pairs=pairs, timeframe="5m")
result = calculate_market_change(data)
assert isinstance(result, float)
assert pytest.approx(result) == 0.01100002
result = calculate_market_change(data, min_date=dt_utc(2018, 1, 20))
assert isinstance(result, float)
assert pytest.approx(result) == 0.0375149
# Move min-date after the last date
result = calculate_market_change(data, min_date=dt_utc(2018, 2, 20))
assert pytest.approx(result) == 0.0
def test_combine_dataframes_with_mean(testdatadir):
pairs = ["ETH/BTC", "ADA/BTC"]
data = load_data(datadir=testdatadir, pairs=pairs, timeframe="5m")
df = combine_dataframes_with_mean(data)
assert isinstance(df, DataFrame)
assert "ETH/BTC" in df.columns
assert "ADA/BTC" in df.columns
assert "mean" in df.columns
def test_combined_dataframes_with_rel_mean(testdatadir):
pairs = ["BTC/USDT", "XRP/USDT"]
data = load_data(datadir=testdatadir, pairs=pairs, timeframe="5m")
df = combined_dataframes_with_rel_mean(
data,
fromdt=data["BTC/USDT"].at[0, "date"],
todt=data["BTC/USDT"].at[data["BTC/USDT"].index[-1], "date"],
)
assert isinstance(df, DataFrame)
assert "BTC/USDT" not in df.columns
assert "XRP/USDT" not in df.columns
assert "mean" in df.columns
assert "rel_mean" in df.columns
assert "count" in df.columns
assert df.iloc[0]["count"] == 2
assert df.iloc[-1]["count"] == 2
assert len(df) < len(data["BTC/USDT"])
assert df["rel_mean"].between(-0.5, 0.5).all()
def test_combine_dataframes_with_mean_no_data(testdatadir):
pairs = ["ETH/BTC", "ADA/BTC"]
data = load_data(datadir=testdatadir, pairs=pairs, timeframe="6m")
with pytest.raises(ValueError, match=r"No data provided\."):
combine_dataframes_with_mean(data)
def test_create_cum_profit(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
timerange = TimeRange.parse_timerange("20180110-20180112")
df = load_pair_history(pair="TRX/BTC", timeframe="5m", datadir=testdatadir, timerange=timerange)
cum_profits = create_cum_profit(
df.set_index("date"), bt_data[bt_data["pair"] == "TRX/BTC"], "cum_profits", timeframe="5m"
)
assert "cum_profits" in cum_profits.columns
assert cum_profits.iloc[0]["cum_profits"] == 0
assert pytest.approx(cum_profits.iloc[-1]["cum_profits"]) == 9.0225563e-05
def test_create_cum_profit1(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
# Move close-time to "off" the candle, to make sure the logic still works
bt_data["close_date"] = bt_data.loc[:, "close_date"] + DateOffset(seconds=20)
timerange = TimeRange.parse_timerange("20180110-20180112")
df = load_pair_history(pair="TRX/BTC", timeframe="5m", datadir=testdatadir, timerange=timerange)
cum_profits = create_cum_profit(
df.set_index("date"), bt_data[bt_data["pair"] == "TRX/BTC"], "cum_profits", timeframe="5m"
)
assert "cum_profits" in cum_profits.columns
assert cum_profits.iloc[0]["cum_profits"] == 0
assert pytest.approx(cum_profits.iloc[-1]["cum_profits"]) == 9.0225563e-05
with pytest.raises(ValueError, match=r"Trade dataframe empty\."):
create_cum_profit(
df.set_index("date"),
bt_data[bt_data["pair"] == "NOTAPAIR"],
"cum_profits",
timeframe="5m",
)
def test_calculate_max_drawdown(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
drawdown = calculate_max_drawdown(bt_data, value_col="profit_abs")
assert isinstance(drawdown.relative_account_drawdown, float)
assert pytest.approx(drawdown.relative_account_drawdown) == 0.29753914
assert isinstance(drawdown.high_date, Timestamp)
assert isinstance(drawdown.low_date, Timestamp)
assert isinstance(drawdown.high_value, float)
assert isinstance(drawdown.low_value, float)
assert drawdown.high_date == Timestamp("2018-01-16 19:30:00", tz="UTC")
assert drawdown.low_date == Timestamp("2018-01-16 22:25:00", tz="UTC")
underwater = calculate_underwater(bt_data)
assert isinstance(underwater, DataFrame)
with pytest.raises(ValueError, match=r"Trade dataframe empty\."):
calculate_max_drawdown(DataFrame())
with pytest.raises(ValueError, match=r"Trade dataframe empty\."):
calculate_underwater(DataFrame())
def test_calculate_csum(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
csum_min, csum_max = calculate_csum(bt_data)
assert isinstance(csum_min, float)
assert isinstance(csum_max, float)
assert csum_min < csum_max
assert csum_min < 0.0001
assert csum_max > 0.0002
csum_min1, csum_max1 = calculate_csum(bt_data, 5)
assert csum_min1 == csum_min + 5
assert csum_max1 == csum_max + 5
with pytest.raises(ValueError, match=r"Trade dataframe empty\."):
csum_min, csum_max = calculate_csum(DataFrame())
def test_calculate_expectancy(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
expectancy, expectancy_ratio = calculate_expectancy(DataFrame())
assert expectancy == 0.0
assert expectancy_ratio == 100
expectancy, expectancy_ratio = calculate_expectancy(bt_data)
assert isinstance(expectancy, float)
assert isinstance(expectancy_ratio, float)
assert pytest.approx(expectancy) == 5.820687070932315e-06
assert pytest.approx(expectancy_ratio) == 0.07151374226574791
data = {"profit_abs": [100, 200, 50, -150, 300, -100, 80, -30]}
df = DataFrame(data)
expectancy, expectancy_ratio = calculate_expectancy(df)
assert pytest.approx(expectancy) == 56.25
assert pytest.approx(expectancy_ratio) == 0.60267857
def test_calculate_sortino(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
sortino = calculate_sortino(DataFrame(), None, None, 0)
assert sortino == 0.0
sortino = calculate_sortino(
bt_data,
bt_data["open_date"].min(),
bt_data["close_date"].max(),
0.01,
)
assert isinstance(sortino, float)
assert pytest.approx(sortino) == 35.17722
def test_calculate_sharpe(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
sharpe = calculate_sharpe(DataFrame(), None, None, 0)
assert sharpe == 0.0
sharpe = calculate_sharpe(
bt_data,
bt_data["open_date"].min(),
bt_data["close_date"].max(),
0.01,
)
assert isinstance(sharpe, float)
assert pytest.approx(sharpe) == 44.5078669
def test_calculate_calmar(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
calmar = calculate_calmar(DataFrame(), None, None, 0)
assert calmar == 0.0
calmar = calculate_calmar(
bt_data,
bt_data["open_date"].min(),
bt_data["close_date"].max(),
0.01,
)
assert isinstance(calmar, float)
assert pytest.approx(calmar) == 559.040508
def test_calculate_sqn(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
sqn = calculate_sqn(DataFrame(), 0)
assert sqn == 0.0
sqn = calculate_sqn(
bt_data,
0.01,
)
assert isinstance(sqn, float)
assert pytest.approx(sqn) == 3.2991
@pytest.mark.parametrize(
"profits,starting_balance,expected_sqn,description",
[
([1.0, -0.5, 2.0, -1.0, 0.5, 1.5, -0.5, 1.0], 100, 1.3229, "Mixed profits/losses"),
([], 100, 0.0, "Empty dataframe"),
([1.0, 0.5, 2.0, 1.5, 0.8], 100, 4.3657, "All winning trades"),
([-1.0, -0.5, -2.0, -1.5, -0.8], 100, -4.3657, "All losing trades"),
([1.0], 100, -100, "Single trade"),
],
)
def test_calculate_sqn_cases(profits, starting_balance, expected_sqn, description):
"""
Test SQN calculation with various scenarios:
"""
trades = DataFrame({"profit_abs": profits})
sqn = calculate_sqn(trades, starting_balance=starting_balance)
assert isinstance(sqn, float)
assert pytest.approx(sqn, rel=1e-4) == expected_sqn
@pytest.mark.parametrize(
"start,end,days, expected",
[
(64900, 176000, 3 * 365, 0.3945),
(64900, 176000, 365, 1.7119),
(1000, 1000, 365, 0.0),
(1000, 1500, 365, 0.5),
(1000, 1500, 100, 3.3927), # sub year
(0.01000000, 0.01762792, 120, 4.6087), # sub year BTC values
(1000, 1010, 0, 0.0), # zero days
(-100, 100, 365, 0.0), # negative starting balance
],
)
def test_calculate_cagr(start, end, days, expected):
assert round(calculate_cagr(days, start, end), 4) == expected
def test_calculate_max_drawdown2():
values = [
0.011580,
0.010048,
0.011340,
0.012161,
0.010416,
0.010009,
0.020024,
-0.024662,
-0.022350,
0.020496,
-0.029859,
-0.030511,
0.010041,
0.010872,
-0.025782,
0.010400,
0.012374,
0.012467,
0.114741,
0.010303,
0.010088,
-0.033961,
0.010680,
0.010886,
-0.029274,
0.011178,
0.010693,
0.010711,
]
dates = [dt_utc(2020, 1, 1) + timedelta(days=i) for i in range(len(values))]
df = DataFrame(zip(values, dates, strict=False), columns=["profit", "open_date"])
# sort by profit and reset index
df = df.sort_values("profit").reset_index(drop=True)
df1 = df.copy()
drawdown = calculate_max_drawdown(
df, date_col="open_date", starting_balance=0.2, value_col="profit"
)
# Ensure df has not been altered.
assert df.equals(df1)
assert isinstance(drawdown.drawdown_abs, float)
assert isinstance(drawdown.relative_account_drawdown, float)
# High must be before low
assert drawdown.high_date < drawdown.low_date
# High value must be higher than low value
assert drawdown.high_value > drawdown.low_value
assert drawdown.drawdown_abs == 0.091755
assert pytest.approx(drawdown.relative_account_drawdown) == 0.32129575
df = DataFrame(zip(values[:5], dates[:5], strict=False), columns=["profit", "open_date"])
# No losing trade ...
drawdown = calculate_max_drawdown(df, date_col="open_date", value_col="profit")
assert drawdown.drawdown_abs == 0.0
assert drawdown.low_value == 0.0
assert drawdown.current_high_value >= 0.0
assert drawdown.current_drawdown_abs == 0.0
df1 = DataFrame(zip(values[:5], dates[:5], strict=False), columns=["profit", "open_date"])
df1.loc[:, "profit"] = df1["profit"] * -1
# No winning trade ...
drawdown = calculate_max_drawdown(df1, date_col="open_date", value_col="profit")
assert drawdown.drawdown_abs == 0.055545
assert drawdown.high_value == 0.0
assert drawdown.current_high_value == 0.0
assert drawdown.current_drawdown_abs == 0.055545
@pytest.mark.parametrize(
"profits,relative,highd,lowdays,result,result_rel",
[
([0.0, -500.0, 500.0, 10000.0, -1000.0], False, 3, 4, 1000.0, 0.090909),
([0.0, -500.0, 500.0, 10000.0, -1000.0], True, 0, 1, 500.0, 0.5),
],
)
def test_calculate_max_drawdown_abs(profits, relative, highd, lowdays, result, result_rel):
"""
Test case from issue https://github.com/freqtrade/freqtrade/issues/6655
[1000, 500, 1000, 11000, 10000] # absolute results
[1000, 50%, 0%, 0%, ~9%] # Relative drawdowns
"""
init_date = datetime(2020, 1, 1, tzinfo=UTC)
dates = [init_date + timedelta(days=i) for i in range(len(profits))]
df = DataFrame(zip(profits, dates, strict=False), columns=["profit_abs", "open_date"])
# sort by profit and reset index
df = df.sort_values("profit_abs").reset_index(drop=True)
df1 = df.copy()
drawdown = calculate_max_drawdown(
df, date_col="open_date", starting_balance=1000, relative=relative
)
# Ensure df has not been altered.
assert df.equals(df1)
assert isinstance(drawdown.drawdown_abs, float)
assert isinstance(drawdown.relative_account_drawdown, float)
assert drawdown.high_date == init_date + timedelta(days=highd)
assert drawdown.low_date == init_date + timedelta(days=lowdays)
# High must be before low
assert drawdown.high_date < drawdown.low_date
# High value must be higher than low value
assert drawdown.high_value > drawdown.low_value
assert drawdown.drawdown_abs == result
assert pytest.approx(drawdown.relative_account_drawdown) == result_rel