diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 138903b57..fe0d03522 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -203,21 +203,48 @@ def calculate_expectancy(trades: pd.DataFrame) -> float: if len(trades) == 0: return 0 - expectancy = 1 + expectancy = 0 - profit_sum = trades.loc[trades['profit_abs'] > 0, 'profit_abs'].sum() - loss_sum = abs(trades.loc[trades['profit_abs'] < 0, 'profit_abs'].sum()) - nb_win_trades = len(trades.loc[trades['profit_abs'] > 0]) - nb_loss_trades = len(trades.loc[trades['profit_abs'] < 0]) + winning_trades = trades.loc[trades['profit_abs'] > 0] + losing_trades = trades.loc[trades['profit_abs'] < 0] + profit_sum = winning_trades['profit_abs'].sum() + loss_sum = abs(losing_trades['profit_abs'].sum()) + nb_win_trades = len(winning_trades) + nb_loss_trades = len(losing_trades) - if (nb_win_trades > 0) and (nb_loss_trades > 0): - average_win = profit_sum / nb_win_trades - average_loss = loss_sum / nb_loss_trades - risk_reward_ratio = average_win / average_loss - winrate = nb_win_trades / len(trades) - expectancy = ((1 + risk_reward_ratio) * winrate) - 1 - elif nb_win_trades == 0: - expectancy = 0 + average_win = profit_sum / nb_win_trades + average_loss = loss_sum / nb_loss_trades + winrate = nb_win_trades / len(trades) + loserate = nb_loss_trades / len(trades) + expectancy = (winrate * average_win) - (loserate * average_loss) + + return expectancy + + +def calculate_expectancy_ratio(trades: pd.DataFrame) -> float: + """ + Calculate expectancy ratio + :param trades: DataFrame containing trades (requires columns close_date and profit_abs) + :return: expectancy ratio + """ + + expectancy_ratio = float('inf') + + if len(trades) > 0: + winning_trades = trades.loc[trades['profit_abs'] > 0] + losing_trades = trades.loc[trades['profit_abs'] < 0] + profit_sum = winning_trades['profit_abs'].sum() + loss_sum = abs(losing_trades['profit_abs'].sum()) + nb_win_trades = len(winning_trades) + nb_loss_trades = len(losing_trades) + + average_win = (profit_sum / nb_win_trades) if nb_win_trades > 0 else 0 + average_loss = (loss_sum / nb_loss_trades) if nb_loss_trades > 0 else 0 + + if (average_loss > 0): + risk_reward_ratio = average_win / average_loss + winrate = nb_win_trades / len(trades) + expectancy = ((1 + risk_reward_ratio) * winrate) - 1 return expectancy diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index 1fd1f7a34..15d1030f2 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -233,7 +233,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Calmar', f"{strat_results['calmar']:.2f}" if 'calmar' in strat_results else 'N/A'), ('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor' in strat_results else 'N/A'), - ('Expectancy', f"{strat_results['expectancy']:.2f}" if 'expectancy' + ('Expectancy Ratio', f"{strat_results['expectancy_ratio']:.2f}" if 'expectancy_ratio' in strat_results else 'N/A'), ('Trades per day', strat_results['trades_per_day']), ('Avg. daily profit %', diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index bb1106d38..854469975 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -7,8 +7,9 @@ from pandas import DataFrame, concat, to_datetime from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, IntOrInf from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum, - calculate_expectancy, calculate_market_change, - calculate_max_drawdown, calculate_sharpe, calculate_sortino) + calculate_expectancy, calculate_expectancy_ratio, + calculate_market_change, calculate_max_drawdown, + calculate_sharpe, calculate_sortino) from freqtrade.misc import decimals_per_coin, round_coin_value @@ -414,7 +415,7 @@ def generate_strategy_stats(pairlist: List[str], 'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(), 'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(), 'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']), - 'expectancy': calculate_expectancy(results), + 'expectancy_ratio': calculate_expectancy_ratio(results), 'sortino': calculate_sortino(results, min_date, max_date, start_balance), 'sharpe': calculate_sharpe(results, min_date, max_date, start_balance), 'calmar': calculate_calmar(results, min_date, max_date, start_balance), diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b1e8520a5..440de5460 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -18,7 +18,8 @@ from freqtrade import __version__ from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT, Config from freqtrade.data.history import load_data -from freqtrade.data.metrics import calculate_max_drawdown +from freqtrade.data.metrics import (calculate_expectancy, calculate_expectancy_ratio, + calculate_max_drawdown) from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, SignalDirection, State, TradingMode) from freqtrade.exceptions import ExchangeError, PricingError diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 5e377f851..76a33b386 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -13,10 +13,10 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelis load_backtest_metadata, 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_underwater, combine_dataframes_with_mean, - create_cum_profit) + calculate_expectancy, calculate_expectancy_ratio, + calculate_market_change, calculate_max_drawdown, + calculate_sharpe, calculate_sortino, calculate_underwater, + combine_dataframes_with_mean, create_cum_profit) from freqtrade.exceptions import OperationalException from freqtrade.util import dt_utc from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades @@ -339,16 +339,16 @@ def test_calculate_csum(testdatadir): csum_min, csum_max = calculate_csum(DataFrame()) -def test_calculate_expectancy(testdatadir): +def test_calculate_expectancy_ratio(testdatadir): filename = testdatadir / "backtest_results/backtest-result.json" bt_data = load_backtest_data(filename) - expectancy = calculate_expectancy(DataFrame()) - assert expectancy == 0.0 + expectancy_ratio = calculate_expectancy_ratio(DataFrame()) + assert expectancy_ratio == 0.0 - expectancy = calculate_expectancy(bt_data) - assert isinstance(expectancy, float) - assert pytest.approx(expectancy) == 0.07151374226574791 + expectancy_ratio = calculate_expectancy_ratio(bt_data) + assert isinstance(expectancy_ratio, float) + assert pytest.approx(expectancy_ratio) == 0.07151374226574791 def test_calculate_sortino(testdatadir):