mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-05-03 23:36:51 +00:00
Merge pull request #8943 from stash86/bt-metrics
merge to use expectancy and expectancy ratio from data/metrics
This commit is contained in:
@@ -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 |
|
||||||
| | |
|
| | |
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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%}"),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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] = []
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|||||||
Reference in New Issue
Block a user