From 624be0c469c0f06fab999412d37e3794acd51bbb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Apr 2026 17:10:05 +0200 Subject: [PATCH] feat: add calmar_from_balance --- freqtrade/data/metrics.py | 48 +++++++++++++++++++++++++++++++++++--- tests/data/test_metrics.py | 40 +++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 5aaf6fac2..4d66104ea 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -537,12 +537,12 @@ def calculate_calmar( starting_balance: float, ) -> float: """ - Calculate calmar + Calculate calmar from trades data. :param trades: DataFrame containing trades (requires columns close_date and profit_abs) :return: calmar """ if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date): - return 0 + return 0.0 total_profit = trades["profit_abs"].sum() / starting_balance days_period = max(1, (max_date - min_date).days) @@ -558,7 +558,49 @@ def calculate_calmar( ) max_drawdown = drawdown.relative_account_drawdown except ValueError: - max_drawdown = 0 + return 0.0 + + return _calculate_annualized_ratio(expected_returns_mean, max_drawdown) + + +def calculate_calmar_from_balance( + balance_history: pd.DataFrame, + date_col: str = "date", + balance_col: str = "total_quote", +) -> float: + """ + Calculate calmar ratio from historical balance snapshots. + + :param balance_history: DataFrame containing at least date and balance columns + :param date_col: Column containing timestamps + :param balance_col: Column containing historical balance values + :return: calmar + """ + wallet = _prepare_balance_history( + balance_history=balance_history, + date_col=date_col, + balance_col=balance_col, + ) + + if len(wallet) < 2: + return 0.0 + + starting_balance = float(wallet[balance_col].iloc[0]) + final_balance = float(wallet[balance_col].iloc[-1]) + days_period = max(1, (wallet[date_col].iloc[-1] - wallet[date_col].iloc[0]).days) + + total_profit = (final_balance - starting_balance) / starting_balance + expected_returns_mean = total_profit / days_period * 100 + + try: + drawdown = calculate_max_drawdown_from_balance( + wallet, + date_col=date_col, + balance_col=balance_col, + ) + max_drawdown = drawdown.relative_account_drawdown + except ValueError: + return 0.0 return _calculate_annualized_ratio(expected_returns_mean, max_drawdown) diff --git a/tests/data/test_metrics.py b/tests/data/test_metrics.py index 610cbd8ee..242700eef 100644 --- a/tests/data/test_metrics.py +++ b/tests/data/test_metrics.py @@ -12,6 +12,7 @@ from freqtrade.data.history import load_data, load_pair_history from freqtrade.data.metrics import ( calculate_cagr, calculate_calmar, + calculate_calmar_from_balance, calculate_csum, calculate_expectancy, calculate_market_change, @@ -366,6 +367,45 @@ def test_calculate_calmar(testdatadir): assert pytest.approx(calmar) == 559.040508 +def test_calculate_calmar_from_balance(): + balance_history = DataFrame( + { + "date": to_datetime( + [ + "2025-01-01 00:00:00+00:00", + "2025-01-01 12:00:00+00:00", + "2025-01-01 18:00:00+00:00", + "2025-01-04 00:00:00+00:00", + ], + utc=True, + ), + "total_quote": [100.0, 120.0, 80.0, 110.0], + } + ) + + calmar = calculate_calmar_from_balance(balance_history) + expected_returns_mean = ((110.0 - 100.0) / 100.0) / 3 * 100 + expected_calmar = expected_returns_mean / (1 / 3) * np.sqrt(365) + + assert isinstance(calmar, float) + assert pytest.approx(calmar) == expected_calmar + + +def test_calculate_calmar_from_balance_empty_or_flat(): + assert calculate_calmar_from_balance(DataFrame()) == 0.0 + + flat_balance_history = DataFrame( + { + "date": to_datetime( + ["2025-01-01 00:00:00+00:00", "2025-01-02 00:00:00+00:00"], + utc=True, + ), + "total_quote": [100.0, 100.0], + } + ) + assert calculate_calmar_from_balance(flat_balance_history) == -100 + + def test_calculate_sqn(testdatadir): filename = testdatadir / "backtest_results/backtest-result.json" bt_data = load_backtest_data(filename)