From c048e7229a3c98d1d99d1f0d27bae7d666cd95b9 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sat, 22 Jul 2023 08:36:51 +0900 Subject: [PATCH 01/18] modify expectancy and expectancy ratio --- freqtrade/data/metrics.py | 53 ++++++++++++++----- .../optimize/optimize_reports/bt_output.py | 2 +- .../optimize_reports/optimize_reports.py | 7 +-- freqtrade/rpc/rpc.py | 3 +- tests/data/test_btanalysis.py | 20 +++---- 5 files changed, 57 insertions(+), 28 deletions(-) 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): From 4812bcc28b04f0202b36f7340c0870fa6c7259ed Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sat, 22 Jul 2023 09:13:24 +0900 Subject: [PATCH 02/18] flake8 fiz --- freqtrade/data/metrics.py | 4 ++-- freqtrade/optimize/optimize_reports/bt_output.py | 2 +- .../optimize/optimize_reports/optimize_reports.py | 5 ++--- freqtrade/rpc/rpc.py | 11 +++++++---- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index fe0d03522..294f66a66 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -244,9 +244,9 @@ def calculate_expectancy_ratio(trades: pd.DataFrame) -> float: if (average_loss > 0): risk_reward_ratio = average_win / average_loss winrate = nb_win_trades / len(trades) - expectancy = ((1 + risk_reward_ratio) * winrate) - 1 + expectancy_ratio = ((1 + risk_reward_ratio) * winrate) - 1 - return expectancy + return expectancy_ratio def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: datetime, diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index 15d1030f2..e833c5ce4 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -234,7 +234,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor' in strat_results else 'N/A'), ('Expectancy Ratio', f"{strat_results['expectancy_ratio']:.2f}" if 'expectancy_ratio' - in strat_results else 'N/A'), + in strat_results else 'N/A'), ('Trades per day', strat_results['trades_per_day']), ('Avg. daily profit %', f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"), diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index 854469975..e555077a8 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -7,9 +7,8 @@ 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_expectancy_ratio, - calculate_market_change, calculate_max_drawdown, - calculate_sharpe, calculate_sortino) + calculate_expectancy_ratio, calculate_market_change, + calculate_max_drawdown, calculate_sharpe, calculate_sortino) from freqtrade.misc import decimals_per_coin, round_coin_value diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 440de5460..e9adec793 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -530,10 +530,13 @@ class RPC: winrate = (winning_trades / closed_trade_count) if closed_trade_count > 0 else 0 loserate = (1 - winrate) - expectancy, expectancy_ratio = self.__calc_expectancy(mean_winning_profit, - mean_losing_profit, - winrate, - loserate) + # expectancy, expectancy_ratio = self.__calc_expectancy(mean_winning_profit, + # mean_losing_profit, + # winrate, + # loserate) + + expectancy = calculate_expectancy(trades) + expectancy_ratio = calculate_expectancy_ratio(trades) trades_df = DataFrame([{'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT), 'profit_abs': trade.close_profit_abs} From dcc3ef1309f4be6123bdd73016e9c97c361407d8 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sat, 22 Jul 2023 09:18:22 +0900 Subject: [PATCH 03/18] flake8 fix --- freqtrade/rpc/rpc.py | 7 ++++--- tests/data/test_btanalysis.py | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e9adec793..7d7dc4e77 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -535,12 +535,13 @@ class RPC: # winrate, # loserate) - expectancy = calculate_expectancy(trades) - expectancy_ratio = calculate_expectancy_ratio(trades) - trades_df = DataFrame([{'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT), 'profit_abs': trade.close_profit_abs} for trade in trades if not trade.is_open and trade.close_date]) + + expectancy = calculate_expectancy(trades_df) + expectancy_ratio = calculate_expectancy_ratio(trades_df) + max_drawdown_abs = 0.0 max_drawdown = 0.0 if len(trades_df) > 0: diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 76a33b386..83b0182d5 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_expectancy_ratio, - calculate_market_change, calculate_max_drawdown, - calculate_sharpe, calculate_sortino, calculate_underwater, - combine_dataframes_with_mean, create_cum_profit) + 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 From 11f24aff97e55ecf3b89b19870e3f40c661d46c2 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sat, 22 Jul 2023 09:19:36 +0900 Subject: [PATCH 04/18] flake8 --- freqtrade/rpc/rpc.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 7d7dc4e77..aaf91e456 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -524,11 +524,7 @@ class RPC: profit_factor = winning_profit / abs(losing_profit) if losing_profit else float('inf') - mean_winning_profit = (winning_profit / winning_trades) if winning_trades > 0 else 0 - mean_losing_profit = (abs(losing_profit) / losing_trades) if losing_trades > 0 else 0 - winrate = (winning_trades / closed_trade_count) if closed_trade_count > 0 else 0 - loserate = (1 - winrate) # expectancy, expectancy_ratio = self.__calc_expectancy(mean_winning_profit, # mean_losing_profit, @@ -541,7 +537,7 @@ class RPC: expectancy = calculate_expectancy(trades_df) expectancy_ratio = calculate_expectancy_ratio(trades_df) - + max_drawdown_abs = 0.0 max_drawdown = 0.0 if len(trades_df) > 0: From 8621dc96e7fae04ecd0b6d01c474cc0106a23164 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sat, 22 Jul 2023 09:44:24 +0900 Subject: [PATCH 05/18] fix tests --- freqtrade/data/metrics.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 294f66a66..cea88b88f 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -200,11 +200,6 @@ def calculate_expectancy(trades: pd.DataFrame) -> float: :param trades: DataFrame containing trades (requires columns close_date and profit_abs) :return: expectancy """ - if len(trades) == 0: - return 0 - - expectancy = 0 - winning_trades = trades.loc[trades['profit_abs'] > 0] losing_trades = trades.loc[trades['profit_abs'] < 0] profit_sum = winning_trades['profit_abs'].sum() @@ -212,8 +207,8 @@ def calculate_expectancy(trades: pd.DataFrame) -> float: nb_win_trades = len(winning_trades) nb_loss_trades = len(losing_trades) - average_win = profit_sum / nb_win_trades - average_loss = loss_sum / nb_loss_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 winrate = nb_win_trades / len(trades) loserate = nb_loss_trades / len(trades) expectancy = (winrate * average_win) - (loserate * average_loss) From cfd8b068e7d8d82da0244d09c0fea86175685faf Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sat, 22 Jul 2023 11:25:53 +0900 Subject: [PATCH 06/18] add test for expectancy --- freqtrade/data/metrics.py | 4 ++-- tests/data/test_btanalysis.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index cea88b88f..ea28b17ce 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -209,8 +209,8 @@ def calculate_expectancy(trades: pd.DataFrame) -> float: 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 - winrate = nb_win_trades / len(trades) - loserate = nb_loss_trades / len(trades) + winrate = (nb_win_trades / len(trades)) if len(trades) > 0 else 0 + loserate = (nb_loss_trades / len(trades)) if len(trades) > 0 else 0 expectancy = (winrate * average_win) - (loserate * average_loss) return expectancy diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 83b0182d5..cbf3bcf10 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -338,6 +338,16 @@ def test_calculate_csum(testdatadir): with pytest.raises(ValueError, match='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 = calculate_expectancy(DataFrame()) + assert expectancy == 0.0 + + expectancy = calculate_expectancy(bt_data) + assert isinstance(expectancy, float) + assert pytest.approx(expectancy) == 0.07151374226574791 def test_calculate_expectancy_ratio(testdatadir): filename = testdatadir / "backtest_results/backtest-result.json" From b0639ab319675a56a612d3e03fe6fa9bb20f2c9d Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sat, 22 Jul 2023 11:29:08 +0900 Subject: [PATCH 07/18] flake8 --- tests/data/test_btanalysis.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index cbf3bcf10..cb78e6a9f 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_ratio, 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 @@ -338,6 +338,7 @@ def test_calculate_csum(testdatadir): with pytest.raises(ValueError, match='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) @@ -349,6 +350,7 @@ def test_calculate_expectancy(testdatadir): assert isinstance(expectancy, float) assert pytest.approx(expectancy) == 0.07151374226574791 + def test_calculate_expectancy_ratio(testdatadir): filename = testdatadir / "backtest_results/backtest-result.json" bt_data = load_backtest_data(filename) From ee3b69ea63d854e8a685027f33f983924ec8d8ce Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sat, 22 Jul 2023 11:37:22 +0900 Subject: [PATCH 08/18] fix test --- freqtrade/data/metrics.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index ea28b17ce..271446339 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -200,6 +200,10 @@ def calculate_expectancy(trades: pd.DataFrame) -> float: :param trades: DataFrame containing trades (requires columns close_date and profit_abs) :return: expectancy """ + + if len(trades) == 0: + return 0 + winning_trades = trades.loc[trades['profit_abs'] > 0] losing_trades = trades.loc[trades['profit_abs'] < 0] profit_sum = winning_trades['profit_abs'].sum() From 3dd33cde00a9fdf5330595fa104b02771e0dbffd Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sat, 22 Jul 2023 11:39:10 +0900 Subject: [PATCH 09/18] fix test --- tests/data/test_btanalysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index cb78e6a9f..4cd11a684 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -348,7 +348,7 @@ def test_calculate_expectancy(testdatadir): expectancy = calculate_expectancy(bt_data) assert isinstance(expectancy, float) - assert pytest.approx(expectancy) == 0.07151374226574791 + assert pytest.approx(expectancy) == 5.820687070932315e-06 def test_calculate_expectancy_ratio(testdatadir): From 3552fa431b01b35d7b8a7e2b18c793c61d43cf54 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sat, 22 Jul 2023 11:40:52 +0900 Subject: [PATCH 10/18] fix test --- tests/data/test_btanalysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 4cd11a684..8594ad4ec 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -356,7 +356,7 @@ def test_calculate_expectancy_ratio(testdatadir): bt_data = load_backtest_data(filename) expectancy_ratio = calculate_expectancy_ratio(DataFrame()) - assert expectancy_ratio == 0.0 + assert expectancy_ratio == float('inf') expectancy_ratio = calculate_expectancy_ratio(bt_data) assert isinstance(expectancy_ratio, float) From c6ee8fcf5444e64eaa19d60968df46014538a165 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sat, 22 Jul 2023 12:20:35 +0900 Subject: [PATCH 11/18] remove unused check --- freqtrade/data/metrics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 271446339..67660bfdf 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -213,8 +213,8 @@ def calculate_expectancy(trades: pd.DataFrame) -> float: 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 - winrate = (nb_win_trades / len(trades)) if len(trades) > 0 else 0 - loserate = (nb_loss_trades / len(trades)) if len(trades) > 0 else 0 + winrate = (nb_win_trades / len(trades)) + loserate = (nb_loss_trades / len(trades)) expectancy = (winrate * average_win) - (loserate * average_loss) return expectancy From 40d7d05e4e2ed6b271d2ace0be8ee677a71b634f Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sat, 22 Jul 2023 17:29:43 +0900 Subject: [PATCH 12/18] merge 2 expectancy functions --- docs/backtesting.md | 4 +-- freqtrade/data/metrics.py | 33 +++++++++++-------- .../optimize/optimize_reports/bt_output.py | 4 +-- .../optimize_reports/optimize_reports.py | 6 ++-- freqtrade/rpc/rpc.py | 27 ++------------- tests/data/test_btanalysis.py | 25 +++++--------- 6 files changed, 37 insertions(+), 62 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 166c2b28b..abaf00a53 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -305,7 +305,7 @@ A backtesting result will look like that: | Sharpe | 2.97 | | Calmar | 6.29 | | Profit factor | 1.11 | -| Expectancy | -0.15 | +| Expectancy (Ratio) | -0.15 (-0.05) | | Avg. stake amount | 0.001 BTC | | Total trade volume | 0.429 BTC | | | | @@ -409,7 +409,7 @@ It contains some useful key metrics about performance of your strategy on backte | Sharpe | 2.97 | | Calmar | 6.29 | | Profit factor | 1.11 | -| Expectancy | -0.15 | +| Expectancy (Ratio) | -0.15 (-0.05) | | Avg. stake amount | 0.001 BTC | | Total trade volume | 0.429 BTC | | | | diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 67660bfdf..631ed71dd 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -201,23 +201,28 @@ def calculate_expectancy(trades: pd.DataFrame) -> float: :return: expectancy """ - if len(trades) == 0: - return 0 + expectancy = 0 + expectancy_ratio = float('inf') - 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 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 - winrate = (nb_win_trades / len(trades)) - loserate = (nb_loss_trades / len(trades)) - expectancy = (winrate * average_win) - (loserate * average_loss) + 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 + winrate = (nb_win_trades / len(trades)) + loserate = (nb_loss_trades / len(trades)) - return expectancy + expectancy = (winrate * average_win) - (loserate * average_loss) + if (average_loss > 0): + risk_reward_ratio = average_win / average_loss + expectancy_ratio = ((1 + risk_reward_ratio) * winrate) - 1 + + return expectancy, expectancy_ratio def calculate_expectancy_ratio(trades: pd.DataFrame) -> float: diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index e833c5ce4..85e91dc2a 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -233,8 +233,8 @@ 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 Ratio', f"{strat_results['expectancy_ratio']:.2f}" if 'expectancy_ratio' - in strat_results else 'N/A'), + ('Expectancy (Ratio)', f"{strat_results['expectancy']:.2f} " + f"({strat_results['expectancy_ratio']:.2f})"), ('Trades per day', strat_results['trades_per_day']), ('Avg. daily profit %', f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"), diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index e555077a8..729f61cc8 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -7,7 +7,7 @@ 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_ratio, calculate_market_change, + calculate_expectancy, calculate_market_change, calculate_max_drawdown, calculate_sharpe, calculate_sortino) from freqtrade.misc import decimals_per_coin, round_coin_value @@ -389,6 +389,7 @@ def generate_strategy_stats(pairlist: List[str], losing_profit = results.loc[results['profit_abs'] < 0, 'profit_abs'].sum() profit_factor = winning_profit / abs(losing_profit) if losing_profit else 0.0 + expectancy, expectancy_ratio = calculate_expectancy(results) backtest_days = (max_date - min_date).days or 1 strat_stats = { 'trades': results.to_dict(orient='records'), @@ -414,7 +415,8 @@ 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_ratio': calculate_expectancy_ratio(results), + 'expectancy': expectancy, + 'expectancy_ratio': expectancy_ratio, '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 aaf91e456..a1b6317b6 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -18,8 +18,7 @@ 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_expectancy, calculate_expectancy_ratio, - calculate_max_drawdown) +from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, SignalDirection, State, TradingMode) from freqtrade.exceptions import ExchangeError, PricingError @@ -526,17 +525,11 @@ class RPC: winrate = (winning_trades / closed_trade_count) if closed_trade_count > 0 else 0 - # expectancy, expectancy_ratio = self.__calc_expectancy(mean_winning_profit, - # mean_losing_profit, - # winrate, - # loserate) - trades_df = DataFrame([{'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT), 'profit_abs': trade.close_profit_abs} for trade in trades if not trade.is_open and trade.close_date]) - expectancy = calculate_expectancy(trades_df) - expectancy_ratio = calculate_expectancy_ratio(trades_df) + expectancy, expectancy_ratio = calculate_expectancy(trades_df) max_drawdown_abs = 0.0 max_drawdown = 0.0 @@ -626,22 +619,6 @@ class RPC: return est_stake, est_bot_stake - def __calc_expectancy( - self, mean_winning_profit: float, mean_losing_profit: float, - winrate: float, loserate: float) -> Tuple[float, float]: - - expectancy = ( - (winrate * mean_winning_profit) - - (loserate * mean_losing_profit) - ) - - expectancy_ratio = float('inf') - if mean_losing_profit > 0: - expectancy_ratio = ( - ((1 + (mean_winning_profit / mean_losing_profit)) * winrate) - 1 - ) - - return expectancy, expectancy_ratio def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: """ Returns current account balance per crypto """ diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 8594ad4ec..6a100904c 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_expectancy_ratio, - calculate_market_change, calculate_max_drawdown, - calculate_sharpe, calculate_sortino, calculate_underwater, - combine_dataframes_with_mean, create_cum_profit) + calculate_expectancy, 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 @@ -343,23 +343,14 @@ def test_calculate_expectancy(testdatadir): filename = testdatadir / "backtest_results/backtest-result.json" bt_data = load_backtest_data(filename) - expectancy = calculate_expectancy(DataFrame()) + expectancy, expectancy_ratio = calculate_expectancy(DataFrame()) assert expectancy == 0.0 - - expectancy = calculate_expectancy(bt_data) - assert isinstance(expectancy, float) - assert pytest.approx(expectancy) == 5.820687070932315e-06 - - -def test_calculate_expectancy_ratio(testdatadir): - filename = testdatadir / "backtest_results/backtest-result.json" - bt_data = load_backtest_data(filename) - - expectancy_ratio = calculate_expectancy_ratio(DataFrame()) assert expectancy_ratio == float('inf') - expectancy_ratio = calculate_expectancy_ratio(bt_data) + 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 From e5f01ab2e8b105bcecdba89d2e0efff12f20c63b Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sat, 22 Jul 2023 17:45:58 +0900 Subject: [PATCH 13/18] pre-commit fix --- freqtrade/data/metrics.py | 30 +----------------------------- freqtrade/rpc/rpc.py | 1 - 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 631ed71dd..2cc95fa79 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -194,7 +194,7 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1 -def calculate_expectancy(trades: pd.DataFrame) -> float: +def calculate_expectancy(trades: pd.DataFrame) -> Tuple[float, float]: """ Calculate expectancy :param trades: DataFrame containing trades (requires columns close_date and profit_abs) @@ -225,34 +225,6 @@ def calculate_expectancy(trades: pd.DataFrame) -> float: return expectancy, expectancy_ratio -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_ratio = ((1 + risk_reward_ratio) * winrate) - 1 - - return expectancy_ratio - - def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: datetime, starting_balance: float) -> float: """ diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index a1b6317b6..32d6dfef3 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -619,7 +619,6 @@ class RPC: return est_stake, est_bot_stake - def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: """ Returns current account balance per crypto """ currencies: List[Dict] = [] From 955a63725a4e0210afc9faf17d16af23bc13aded Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jul 2023 19:43:20 +0200 Subject: [PATCH 14/18] Improve resiliance when showing older backtest results --- freqtrade/data/metrics.py | 2 +- freqtrade/optimize/optimize_reports/bt_output.py | 5 +++-- tests/data/test_btanalysis.py | 9 +++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 2cc95fa79..d4746c168 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -198,7 +198,7 @@ def calculate_expectancy(trades: pd.DataFrame) -> Tuple[float, float]: """ Calculate expectancy :param trades: DataFrame containing trades (requires columns close_date and profit_abs) - :return: expectancy + :return: expectancy, expectancy_ratio """ expectancy = 0 diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index 85e91dc2a..07f23a5fa 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -233,8 +233,9 @@ 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 (Ratio)', f"{strat_results['expectancy']:.2f} " - f"({strat_results['expectancy_ratio']:.2f})"), + ('Expectancy (Ratio)', ( + f"{strat_results['expectancy']:.2f} ({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 %', f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"), diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 6a100904c..f11e5d772 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -353,6 +353,15 @@ def test_calculate_expectancy(testdatadir): 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" From 787e94924dd7459d2cbaa92e2a389a18fec3b767 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Jul 2023 07:20:59 +0200 Subject: [PATCH 15/18] Update default expectancy ratio to 100 --- freqtrade/data/metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index d4746c168..c22dcccef 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -202,7 +202,7 @@ def calculate_expectancy(trades: pd.DataFrame) -> Tuple[float, float]: """ expectancy = 0 - expectancy_ratio = float('inf') + expectancy_ratio = 100 if len(trades) > 0: winning_trades = trades.loc[trades['profit_abs'] > 0] From 0eddc6b7add6d3b5b02d9559f0650e70b6980884 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sun, 23 Jul 2023 14:27:45 +0900 Subject: [PATCH 16/18] update expectancy test --- tests/data/test_btanalysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index f11e5d772..c1b007e77 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -345,7 +345,7 @@ def test_calculate_expectancy(testdatadir): expectancy, expectancy_ratio = calculate_expectancy(DataFrame()) assert expectancy == 0.0 - assert expectancy_ratio == float('inf') + assert expectancy_ratio == 100 expectancy, expectancy_ratio = calculate_expectancy(bt_data) assert isinstance(expectancy, float) From 4c23771d398ace1424b21dae5567e29a5b1f1736 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sun, 23 Jul 2023 14:42:48 +0900 Subject: [PATCH 17/18] fix expectancy test --- tests/rpc/test_rpc_apiserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index e0b87b9cc..fd9508060 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -846,7 +846,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): 'profit_closed_percent_sum': 1.5, 'profit_closed_ratio': 7.391275897987988e-07, 'profit_closed_percent': 0.0, 'winning_trades': 2, 'losing_trades': 0, 'profit_factor': None, 'winrate': 1.0, 'expectancy': 0.0003695635, - 'expectancy_ratio': None, 'trading_volume': 91.074, + 'expectancy_ratio': 100, 'trading_volume': 91.074, } ), ( From 8f04225282b16bbe906dcfc3f975272edefad00c Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sun, 23 Jul 2023 15:00:08 +0900 Subject: [PATCH 18/18] another test fix --- tests/rpc/test_rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 65db6770a..3bc725f3a 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -403,7 +403,7 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None: assert res['latest_trade_date'] == '' assert res['latest_trade_timestamp'] == 0 assert res['expectancy'] == 0 - assert res['expectancy_ratio'] == float('inf') + assert res['expectancy_ratio'] == 100 # Create some test data create_mock_trades_usdt(fee)