diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 1b36c60ad..f501d0e49 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -287,12 +287,17 @@ Return a summary of your profit/loss and performance. > **Best Performing:** `PAY/BTC: 50.23%` > **Trading volume:** `0.5 BTC` > **Profit factor:** `1.04` +> **Win / Loss:** `102 / 36` +> **Winrate:** `73.91%` +> **Expectancy (Ratio):** `4.87 (1.66)` > **Max Drawdown:** `9.23% (0.01255 BTC)` The relative profit of `1.2%` is the average profit per trade. The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`. Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits. Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy. +Expectancy corresponds to the average return per currency unit at risk, i.e. the winrate and the risk-reward ratio (the average gain of winning trades compared to the average loss of losing trades). +Expectancy Ratio is expected profit or loss of a subsequent trade based on the performance of all past trades. Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`. Bot started date will refer to the date the bot was first started. For older bots, this will default to the first trade's open date. diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 2c2d40a3d..04769b119 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -136,6 +136,9 @@ class Profit(BaseModel): winning_trades: int losing_trades: int profit_factor: float + winrate: float + expectancy: float + expectancy_ratio: float max_drawdown: float max_drawdown_abs: float trading_volume: Optional[float] diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c3759e03a..53e3f97e5 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -494,6 +494,8 @@ class RPC: profit_all_coin.append(profit_abs) profit_all_ratio.append(profit_ratio) + closed_trade_count = len([t for t in trades if not t.is_open]) + best_pair = Trade.get_best_pair(start_date) trading_volume = Trade.get_trading_volume(start_date) @@ -521,6 +523,17 @@ 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]) @@ -562,7 +575,7 @@ class RPC: 'profit_all_percent': round(profit_all_ratio_fromstart * 100, 2), 'profit_all_fiat': profit_all_fiat, 'trade_count': len(trades), - 'closed_trade_count': len([t for t in trades if not t.is_open]), + 'closed_trade_count': closed_trade_count, 'first_trade_date': first_date.strftime(DATETIME_PRINT_FORMAT) if first_date else '', 'first_trade_humanized': dt_humanize(first_date) if first_date else '', 'first_trade_timestamp': int(first_date.timestamp() * 1000) if first_date else 0, @@ -576,6 +589,9 @@ class RPC: 'winning_trades': winning_trades, 'losing_trades': losing_trades, 'profit_factor': profit_factor, + 'winrate': winrate, + 'expectancy': expectancy, + 'expectancy_ratio': expectancy_ratio, 'max_drawdown': max_drawdown, 'max_drawdown_abs': max_drawdown_abs, 'trading_volume': trading_volume, @@ -609,6 +625,23 @@ 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/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index aad7fd8c4..aced89d7a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -849,6 +849,10 @@ class Telegram(RPCHandler): avg_duration = stats['avg_duration'] best_pair = stats['best_pair'] best_pair_profit_ratio = stats['best_pair_profit_ratio'] + winrate = stats['winrate'] + expectancy = stats['expectancy'] + expectancy_ratio = stats['expectancy_ratio'] + if stats['trade_count'] == 0: markdown_msg = f"No trades yet.\n*Bot started:* `{stats['bot_start_date']}`" else: @@ -873,7 +877,9 @@ class Telegram(RPCHandler): f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* " f"`{first_trade_date}`\n" f"*Latest Trade opened:* `{latest_trade_date}`\n" - f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`" + f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`\n" + f"*Winrate:* `{winrate:.2%}`\n" + f"*Expectancy (Ratio):* `{expectancy:.2f} ({expectancy_ratio:.2f})`" ) if stats['closed_trade_count'] > 0: markdown_msg += ( diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 5dfeeb632..65db6770a 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -402,6 +402,8 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None: assert res['first_trade_timestamp'] == 0 assert res['latest_trade_date'] == '' assert res['latest_trade_timestamp'] == 0 + assert res['expectancy'] == 0 + assert res['expectancy_ratio'] == float('inf') # Create some test data create_mock_trades_usdt(fee) @@ -413,6 +415,9 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None: assert pytest.approx(stats['profit_all_coin']) == -77.45964918 assert pytest.approx(stats['profit_all_percent_mean']) == -57.86 assert pytest.approx(stats['profit_all_fiat']) == -85.205614098 + assert pytest.approx(stats['winrate']) == 0.666666667 + assert pytest.approx(stats['expectancy']) == 0.913333333 + assert pytest.approx(stats['expectancy_ratio']) == 0.223308883 assert stats['trade_count'] == 7 assert stats['first_trade_humanized'] == '2 days ago' assert stats['latest_trade_humanized'] == '17 minutes ago' diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index f793b1f9c..851e035b1 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -829,7 +829,8 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): 'profit_closed_percent_mean': -0.75, 'profit_closed_ratio_sum': -0.015, 'profit_closed_percent_sum': -1.5, 'profit_closed_ratio': -6.739057628404269e-06, 'profit_closed_percent': -0.0, 'winning_trades': 0, 'losing_trades': 2, - 'profit_factor': 0.0, 'trading_volume': 91.074, + 'profit_factor': 0.0, 'winrate': 0.0, 'expectancy': -0.0033695635, + 'expectancy_ratio': -1.0, 'trading_volume': 91.074, } ), ( @@ -844,7 +845,8 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): 'profit_closed_percent_mean': 0.75, 'profit_closed_ratio_sum': 0.015, '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, 'trading_volume': 91.074, + 'profit_factor': None, 'winrate': 1.0, 'expectancy': 0.0003695635, + 'expectancy_ratio': None, 'trading_volume': 91.074, } ), ( @@ -859,7 +861,9 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): 'profit_closed_percent_mean': 0.25, 'profit_closed_ratio_sum': 0.005, 'profit_closed_percent_sum': 0.5, 'profit_closed_ratio': -5.429078808526421e-06, 'profit_closed_percent': -0.0, 'winning_trades': 1, 'losing_trades': 1, - 'profit_factor': 0.02775724835771106, 'trading_volume': 91.074, + 'profit_factor': 0.02775724835771106, 'winrate': 0.5, + 'expectancy': -0.0027145635000000003, 'expectancy_ratio': -0.48612137582114445, + 'trading_volume': 91.074, } ) ]) @@ -916,6 +920,9 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected) 'winning_trades': expected['winning_trades'], 'losing_trades': expected['losing_trades'], 'profit_factor': expected['profit_factor'], + 'winrate': expected['winrate'], + 'expectancy': expected['expectancy'], + 'expectancy_ratio': expected['expectancy_ratio'], 'max_drawdown': ANY, 'max_drawdown_abs': ANY, 'trading_volume': expected['trading_volume'], diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 51879f5ad..72d70bfb9 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -799,6 +799,8 @@ async def test_telegram_profit_handle( assert '*Best Performing:* `ETH/USDT: 9.45%`' in msg_mock.call_args_list[-1][0][0] assert '*Max Drawdown:*' in msg_mock.call_args_list[-1][0][0] assert '*Profit factor:*' in msg_mock.call_args_list[-1][0][0] + assert '*Winrate:*' in msg_mock.call_args_list[-1][0][0] + assert '*Expectancy (Ratio):*' in msg_mock.call_args_list[-1][0][0] assert '*Trading volume:* `126 USDT`' in msg_mock.call_args_list[-1][0][0]