diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index a9d3963d2..00101c5ea 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -390,10 +390,55 @@ def calculate_sharpe( # Define high (negative) sharpe ratio to be clear that this is NOT optimal. sharp_ratio = -100 - # print(expected_returns_mean, up_stdev, sharp_ratio) return sharp_ratio +def calculate_sharpe_from_balance( + balance_history: pd.DataFrame, + date_col: str = "date", + balance_col: str = "total_quote", +) -> float: + """ + Calculate sharpe 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: sharpe + """ + if ( + len(balance_history) == 0 + or date_col not in balance_history + or balance_col not in balance_history + ): + return 0.0 + + wallet = balance_history.loc[:, [date_col, balance_col]].copy() + wallet[date_col] = pd.to_datetime(wallet[date_col], utc=True, errors="coerce") + wallet = wallet.dropna(subset=[date_col, balance_col]).sort_values(date_col) + + if len(wallet) < 2: + return 0.0 + + # Sample balance to daily end-of-day values to normalize variable snapshot frequency. + daily_balance = wallet.set_index(date_col)[balance_col].resample("1D").last().dropna() + daily_returns = daily_balance.pct_change().dropna() + + if len(daily_returns) == 0: + return 0.0 + + expected_returns_mean = daily_returns.mean() + up_stdev = daily_returns.std(ddof=0) + + if up_stdev != 0 and not np.isnan(up_stdev): + sharp_ratio = expected_returns_mean / up_stdev * np.sqrt(365) + else: + # Define high (negative) sharpe ratio to be clear that this is NOT optimal. + sharp_ratio = -100 + + return float(sharp_ratio) + + def calculate_calmar( trades: pd.DataFrame, min_date: datetime | None, diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index 23963038c..149f886a2 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -308,6 +308,13 @@ def text_table_add_metrics(strat_results: dict) -> None: ), ] ) + if "sharpe" in wallet_stats: + wallet_metrics.append( + ( + "Sharpe ratio balance", + f"{wallet_stats['sharpe']:.2f}", + ) + ) # Newly added fields should be ignored if they are missing in strat_results. hyperopt-show # command stores these results and newer version of freqtrade must be able to handle old diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index ba84d8834..24c683482 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -15,6 +15,7 @@ from freqtrade.data.metrics import ( calculate_market_change, calculate_max_drawdown, calculate_sharpe, + calculate_sharpe_from_balance, calculate_sortino, calculate_sqn, ) @@ -59,11 +60,13 @@ def generate_wallet_stats(wallet_df: DataFrame, stake_currency: str) -> dict[str low_balance = total_quote.loc[low_idx] low_date = wallet.loc[low_idx, "date"] high_date = wallet.loc[high_idx, "date"] + sharpe = calculate_sharpe_from_balance(wallet) return { "start_balance": start_balance, "end_balance": end_balance, "high_balance": high_balance, "low_balance": low_balance, + "sharpe": sharpe, "low_date": low_date.strftime(DATETIME_PRINT_FORMAT), "low_ts": int(low_date.timestamp() * 1000), "high_date": high_date.strftime(DATETIME_PRINT_FORMAT),