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 138903b57..c22dcccef 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -194,32 +194,35 @@ 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) - :return: expectancy + :return: expectancy, expectancy_ratio """ - if len(trades) == 0: - return 0 - expectancy = 1 + expectancy = 0 + expectancy_ratio = 100 - 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]) + 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) - 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) 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_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 1fd1f7a34..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', f"{strat_results['expectancy']:.2f}" if 'expectancy' - in strat_results else 'N/A'), + ('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/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index bb1106d38..729f61cc8 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -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': calculate_expectancy(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 b1e8520a5..32d6dfef3 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -18,7 +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_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 @@ -523,20 +523,14 @@ 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, - 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, expectancy_ratio = calculate_expectancy(trades_df) + max_drawdown_abs = 0.0 max_drawdown = 0.0 if len(trades_df) > 0: @@ -625,23 +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 """ currencies: List[Dict] = [] diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 5e377f851..c1b007e77 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -343,12 +343,24 @@ 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 + assert expectancy_ratio == 100 - expectancy = calculate_expectancy(bt_data) + expectancy, expectancy_ratio = calculate_expectancy(bt_data) assert isinstance(expectancy, float) - assert pytest.approx(expectancy) == 0.07151374226574791 + 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): 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) 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, } ), (