Merge pull request #8943 from stash86/bt-metrics

merge to use  expectancy and expectancy ratio from data/metrics
This commit is contained in:
Matthias
2023-07-23 08:37:08 +02:00
committed by GitHub
8 changed files with 50 additions and 55 deletions

View File

@@ -305,7 +305,7 @@ A backtesting result will look like that:
| Sharpe | 2.97 | | Sharpe | 2.97 |
| Calmar | 6.29 | | Calmar | 6.29 |
| Profit factor | 1.11 | | Profit factor | 1.11 |
| Expectancy | -0.15 | | Expectancy (Ratio) | -0.15 (-0.05) |
| Avg. stake amount | 0.001 BTC | | Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 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 | | Sharpe | 2.97 |
| Calmar | 6.29 | | Calmar | 6.29 |
| Profit factor | 1.11 | | Profit factor | 1.11 |
| Expectancy | -0.15 | | Expectancy (Ratio) | -0.15 (-0.05) |
| Avg. stake amount | 0.001 BTC | | Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC | | Total trade volume | 0.429 BTC |
| | | | | |

View File

@@ -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 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 Calculate expectancy
:param trades: DataFrame containing trades (requires columns close_date and profit_abs) :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() if len(trades) > 0:
loss_sum = abs(trades.loc[trades['profit_abs'] < 0, 'profit_abs'].sum()) winning_trades = trades.loc[trades['profit_abs'] > 0]
nb_win_trades = len(trades.loc[trades['profit_abs'] > 0]) losing_trades = trades.loc[trades['profit_abs'] < 0]
nb_loss_trades = len(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) if nb_win_trades > 0 else 0
average_win = profit_sum / nb_win_trades average_loss = (loss_sum / nb_loss_trades) if nb_loss_trades > 0 else 0
average_loss = loss_sum / nb_loss_trades winrate = (nb_win_trades / len(trades))
risk_reward_ratio = average_win / average_loss loserate = (nb_loss_trades / len(trades))
winrate = nb_win_trades / len(trades)
expectancy = ((1 + risk_reward_ratio) * winrate) - 1
elif nb_win_trades == 0:
expectancy = 0
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, def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: datetime,

View File

@@ -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'), ('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' ('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor'
in strat_results else 'N/A'), in strat_results else 'N/A'),
('Expectancy', f"{strat_results['expectancy']:.2f}" if 'expectancy' ('Expectancy (Ratio)', (
in strat_results else 'N/A'), 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']), ('Trades per day', strat_results['trades_per_day']),
('Avg. daily profit %', ('Avg. daily profit %',
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"), f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),

View File

@@ -389,6 +389,7 @@ def generate_strategy_stats(pairlist: List[str],
losing_profit = results.loc[results['profit_abs'] < 0, 'profit_abs'].sum() losing_profit = results.loc[results['profit_abs'] < 0, 'profit_abs'].sum()
profit_factor = winning_profit / abs(losing_profit) if losing_profit else 0.0 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 backtest_days = (max_date - min_date).days or 1
strat_stats = { strat_stats = {
'trades': results.to_dict(orient='records'), '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_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
'profit_total_short_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']), '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), 'sortino': calculate_sortino(results, min_date, max_date, start_balance),
'sharpe': calculate_sharpe(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), 'calmar': calculate_calmar(results, min_date, max_date, start_balance),

View File

@@ -18,7 +18,7 @@ from freqtrade import __version__
from freqtrade.configuration.timerange import TimeRange from freqtrade.configuration.timerange import TimeRange
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT, Config from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT, Config
from freqtrade.data.history import load_data 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, from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, SignalDirection,
State, TradingMode) State, TradingMode)
from freqtrade.exceptions import ExchangeError, PricingError 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') 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 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), trades_df = DataFrame([{'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
'profit_abs': trade.close_profit_abs} 'profit_abs': trade.close_profit_abs}
for trade in trades if not trade.is_open and trade.close_date]) 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_abs = 0.0
max_drawdown = 0.0 max_drawdown = 0.0
if len(trades_df) > 0: if len(trades_df) > 0:
@@ -625,23 +619,6 @@ class RPC:
return est_stake, est_bot_stake 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: def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
""" Returns current account balance per crypto """ """ Returns current account balance per crypto """
currencies: List[Dict] = [] currencies: List[Dict] = []

View File

@@ -343,12 +343,24 @@ def test_calculate_expectancy(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json" filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
expectancy = calculate_expectancy(DataFrame()) expectancy, expectancy_ratio = calculate_expectancy(DataFrame())
assert expectancy == 0.0 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 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): def test_calculate_sortino(testdatadir):

View File

@@ -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_date'] == ''
assert res['latest_trade_timestamp'] == 0 assert res['latest_trade_timestamp'] == 0
assert res['expectancy'] == 0 assert res['expectancy'] == 0
assert res['expectancy_ratio'] == float('inf') assert res['expectancy_ratio'] == 100
# Create some test data # Create some test data
create_mock_trades_usdt(fee) create_mock_trades_usdt(fee)

View File

@@ -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_sum': 1.5, 'profit_closed_ratio': 7.391275897987988e-07,
'profit_closed_percent': 0.0, 'winning_trades': 2, 'losing_trades': 0, 'profit_closed_percent': 0.0, 'winning_trades': 2, 'losing_trades': 0,
'profit_factor': None, 'winrate': 1.0, 'expectancy': 0.0003695635, 'profit_factor': None, 'winrate': 1.0, 'expectancy': 0.0003695635,
'expectancy_ratio': None, 'trading_volume': 91.074, 'expectancy_ratio': 100, 'trading_volume': 91.074,
} }
), ),
( (