Merge pull request #7810 from stash86/bt-metrics
Add more calculations for backtest metrics
This commit is contained in:
commit
973cfd0182
@ -300,7 +300,11 @@ A backtesting result will look like that:
|
|||||||
| Absolute profit | 0.00762792 BTC |
|
| Absolute profit | 0.00762792 BTC |
|
||||||
| Total profit % | 76.2% |
|
| Total profit % | 76.2% |
|
||||||
| CAGR % | 460.87% |
|
| CAGR % | 460.87% |
|
||||||
|
| Sortino | 1.88 |
|
||||||
|
| Sharpe | 2.97 |
|
||||||
|
| Calmar | 6.29 |
|
||||||
| Profit factor | 1.11 |
|
| Profit factor | 1.11 |
|
||||||
|
| Expectancy | -0.15 |
|
||||||
| Avg. stake amount | 0.001 BTC |
|
| Avg. stake amount | 0.001 BTC |
|
||||||
| Total trade volume | 0.429 BTC |
|
| Total trade volume | 0.429 BTC |
|
||||||
| | |
|
| | |
|
||||||
@ -400,7 +404,11 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
| Absolute profit | 0.00762792 BTC |
|
| Absolute profit | 0.00762792 BTC |
|
||||||
| Total profit % | 76.2% |
|
| Total profit % | 76.2% |
|
||||||
| CAGR % | 460.87% |
|
| CAGR % | 460.87% |
|
||||||
|
| Sortino | 1.88 |
|
||||||
|
| Sharpe | 2.97 |
|
||||||
|
| Calmar | 6.29 |
|
||||||
| Profit factor | 1.11 |
|
| Profit factor | 1.11 |
|
||||||
|
| Expectancy | -0.15 |
|
||||||
| Avg. stake amount | 0.001 BTC |
|
| Avg. stake amount | 0.001 BTC |
|
||||||
| Total trade volume | 0.429 BTC |
|
| Total trade volume | 0.429 BTC |
|
||||||
| | |
|
| | |
|
||||||
@ -447,6 +455,9 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
- `Absolute profit`: Profit made in stake currency.
|
- `Absolute profit`: Profit made in stake currency.
|
||||||
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`.
|
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`.
|
||||||
- `CAGR %`: Compound annual growth rate.
|
- `CAGR %`: Compound annual growth rate.
|
||||||
|
- `Sortino`: Annualized Sortino ratio.
|
||||||
|
- `Sharpe`: Annualized Sharpe ratio.
|
||||||
|
- `Calmar`: Annualized Calmar ratio.
|
||||||
- `Profit factor`: profit / loss.
|
- `Profit factor`: profit / loss.
|
||||||
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
|
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
|
||||||
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
|
from datetime import datetime
|
||||||
from typing import Dict, Tuple
|
from typing import Dict, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -190,3 +192,119 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo
|
|||||||
:return: CAGR
|
:return: CAGR
|
||||||
"""
|
"""
|
||||||
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:
|
||||||
|
"""
|
||||||
|
Calculate expectancy
|
||||||
|
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
|
||||||
|
:return: expectancy
|
||||||
|
"""
|
||||||
|
if len(trades) == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
expectancy = 1
|
||||||
|
|
||||||
|
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 (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
|
||||||
|
|
||||||
|
return expectancy
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
|
||||||
|
starting_balance: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate sortino
|
||||||
|
:param trades: DataFrame containing trades (requires columns profit_abs)
|
||||||
|
:return: sortino
|
||||||
|
"""
|
||||||
|
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
total_profit = trades['profit_abs'] / starting_balance
|
||||||
|
days_period = max(1, (max_date - min_date).days)
|
||||||
|
|
||||||
|
expected_returns_mean = total_profit.sum() / days_period
|
||||||
|
|
||||||
|
down_stdev = np.std(trades.loc[trades['profit_abs'] < 0, 'profit_abs'] / starting_balance)
|
||||||
|
|
||||||
|
if down_stdev != 0:
|
||||||
|
sortino_ratio = expected_returns_mean / down_stdev * np.sqrt(365)
|
||||||
|
else:
|
||||||
|
# Define high (negative) sortino ratio to be clear that this is NOT optimal.
|
||||||
|
sortino_ratio = -100
|
||||||
|
|
||||||
|
# print(expected_returns_mean, down_stdev, sortino_ratio)
|
||||||
|
return sortino_ratio
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_sharpe(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
|
||||||
|
starting_balance: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate sharpe
|
||||||
|
:param trades: DataFrame containing trades (requires column profit_abs)
|
||||||
|
:return: sharpe
|
||||||
|
"""
|
||||||
|
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
total_profit = trades['profit_abs'] / starting_balance
|
||||||
|
days_period = max(1, (max_date - min_date).days)
|
||||||
|
|
||||||
|
expected_returns_mean = total_profit.sum() / days_period
|
||||||
|
up_stdev = np.std(total_profit)
|
||||||
|
|
||||||
|
if up_stdev != 0:
|
||||||
|
sharp_ratio = expected_returns_mean / up_stdev * np.sqrt(365)
|
||||||
|
else:
|
||||||
|
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
|
||||||
|
sharp_ratio = -100
|
||||||
|
|
||||||
|
# print(expected_returns_mean, up_stdev, sharp_ratio)
|
||||||
|
return sharp_ratio
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_calmar(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
|
||||||
|
starting_balance: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate calmar
|
||||||
|
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
|
||||||
|
:return: calmar
|
||||||
|
"""
|
||||||
|
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
total_profit = trades['profit_abs'].sum() / starting_balance
|
||||||
|
days_period = max(1, (max_date - min_date).days)
|
||||||
|
|
||||||
|
# adding slippage of 0.1% per trade
|
||||||
|
# total_profit = total_profit - 0.0005
|
||||||
|
expected_returns_mean = total_profit / days_period * 100
|
||||||
|
|
||||||
|
# calculate max drawdown
|
||||||
|
try:
|
||||||
|
_, _, _, _, _, max_drawdown = calculate_max_drawdown(
|
||||||
|
trades, value_col="profit_abs", starting_balance=starting_balance
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
max_drawdown = 0
|
||||||
|
|
||||||
|
if max_drawdown != 0:
|
||||||
|
calmar_ratio = expected_returns_mean / max_drawdown * math.sqrt(365)
|
||||||
|
else:
|
||||||
|
# Define high (negative) calmar ratio to be clear that this is NOT optimal.
|
||||||
|
calmar_ratio = -100
|
||||||
|
|
||||||
|
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
||||||
|
return calmar_ratio
|
||||||
|
@ -9,8 +9,9 @@ from tabulate import tabulate
|
|||||||
|
|
||||||
from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT,
|
from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT,
|
||||||
Config)
|
Config)
|
||||||
from freqtrade.data.metrics import (calculate_cagr, calculate_csum, calculate_market_change,
|
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
|
||||||
calculate_max_drawdown)
|
calculate_expectancy, calculate_market_change,
|
||||||
|
calculate_max_drawdown, calculate_sharpe, calculate_sortino)
|
||||||
from freqtrade.misc import decimals_per_coin, file_dump_joblib, file_dump_json, round_coin_value
|
from freqtrade.misc import decimals_per_coin, file_dump_joblib, file_dump_json, round_coin_value
|
||||||
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
||||||
|
|
||||||
@ -448,6 +449,10 @@ 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),
|
||||||
|
'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),
|
||||||
'profit_factor': profit_factor,
|
'profit_factor': profit_factor,
|
||||||
'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
|
'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
'backtest_start_ts': int(min_date.timestamp() * 1000),
|
'backtest_start_ts': int(min_date.timestamp() * 1000),
|
||||||
@ -785,8 +790,13 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Total profit %', f"{strat_results['profit_total']:.2%}"),
|
('Total profit %', f"{strat_results['profit_total']:.2%}"),
|
||||||
('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
|
('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
|
||||||
|
('Sortino', f"{strat_results['sortino']:.2f}" if 'sortino' in strat_results else 'N/A'),
|
||||||
|
('Sharpe', f"{strat_results['sharpe']:.2f}" if 'sharpe' 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'
|
||||||
|
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%}"),
|
||||||
|
@ -12,9 +12,11 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelis
|
|||||||
get_latest_hyperopt_file, load_backtest_data,
|
get_latest_hyperopt_file, load_backtest_data,
|
||||||
load_backtest_metadata, load_trades, load_trades_from_db)
|
load_backtest_metadata, load_trades, load_trades_from_db)
|
||||||
from freqtrade.data.history import load_data, load_pair_history
|
from freqtrade.data.history import load_data, load_pair_history
|
||||||
from freqtrade.data.metrics import (calculate_cagr, calculate_csum, calculate_market_change,
|
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
|
||||||
calculate_max_drawdown, calculate_underwater,
|
calculate_expectancy, calculate_market_change,
|
||||||
combine_dataframes_with_mean, create_cum_profit)
|
calculate_max_drawdown, calculate_sharpe, calculate_sortino,
|
||||||
|
calculate_underwater, combine_dataframes_with_mean,
|
||||||
|
create_cum_profit)
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades
|
from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades
|
||||||
from tests.conftest_trades import MOCK_TRADE_COUNT
|
from tests.conftest_trades import MOCK_TRADE_COUNT
|
||||||
@ -336,6 +338,69 @@ def test_calculate_csum(testdatadir):
|
|||||||
csum_min, csum_max = calculate_csum(DataFrame())
|
csum_min, csum_max = calculate_csum(DataFrame())
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_expectancy(testdatadir):
|
||||||
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
|
bt_data = load_backtest_data(filename)
|
||||||
|
|
||||||
|
expectancy = calculate_expectancy(DataFrame())
|
||||||
|
assert expectancy == 0.0
|
||||||
|
|
||||||
|
expectancy = calculate_expectancy(bt_data)
|
||||||
|
assert isinstance(expectancy, float)
|
||||||
|
assert pytest.approx(expectancy) == 0.07151374226574791
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_sortino(testdatadir):
|
||||||
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
|
bt_data = load_backtest_data(filename)
|
||||||
|
|
||||||
|
sortino = calculate_sortino(DataFrame(), None, None, 0)
|
||||||
|
assert sortino == 0.0
|
||||||
|
|
||||||
|
sortino = calculate_sortino(
|
||||||
|
bt_data,
|
||||||
|
bt_data['open_date'].min(),
|
||||||
|
bt_data['close_date'].max(),
|
||||||
|
0.01,
|
||||||
|
)
|
||||||
|
assert isinstance(sortino, float)
|
||||||
|
assert pytest.approx(sortino) == 35.17722
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_sharpe(testdatadir):
|
||||||
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
|
bt_data = load_backtest_data(filename)
|
||||||
|
|
||||||
|
sharpe = calculate_sharpe(DataFrame(), None, None, 0)
|
||||||
|
assert sharpe == 0.0
|
||||||
|
|
||||||
|
sharpe = calculate_sharpe(
|
||||||
|
bt_data,
|
||||||
|
bt_data['open_date'].min(),
|
||||||
|
bt_data['close_date'].max(),
|
||||||
|
0.01,
|
||||||
|
)
|
||||||
|
assert isinstance(sharpe, float)
|
||||||
|
assert pytest.approx(sharpe) == 44.5078669
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_calmar(testdatadir):
|
||||||
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
|
bt_data = load_backtest_data(filename)
|
||||||
|
|
||||||
|
calmar = calculate_calmar(DataFrame(), None, None, 0)
|
||||||
|
assert calmar == 0.0
|
||||||
|
|
||||||
|
calmar = calculate_calmar(
|
||||||
|
bt_data,
|
||||||
|
bt_data['open_date'].min(),
|
||||||
|
bt_data['close_date'].max(),
|
||||||
|
0.01,
|
||||||
|
)
|
||||||
|
assert isinstance(calmar, float)
|
||||||
|
assert pytest.approx(calmar) == 559.040508
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('start,end,days, expected', [
|
@pytest.mark.parametrize('start,end,days, expected', [
|
||||||
(64900, 176000, 3 * 365, 0.3945),
|
(64900, 176000, 3 * 365, 0.3945),
|
||||||
(64900, 176000, 365, 1.7119),
|
(64900, 176000, 365, 1.7119),
|
||||||
|
Loading…
Reference in New Issue
Block a user