diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 82ff56c3a..e6ca0bd8a 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -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") diff --git a/tests/data/test_metrics.py b/tests/data/test_metrics.py new file mode 100644 index 000000000..d19d9e328 --- /dev/null +++ b/tests/data/test_metrics.py @@ -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