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') + + 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) - 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)) - 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) + 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 + 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