From f410b1b14d71ea170a03b6e95576f70a8f7f6385 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Mon, 28 Nov 2022 08:56:49 +0900 Subject: [PATCH 1/9] Update metrics.py --- freqtrade/data/metrics.py | 129 +++++++++++++++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index c11a2df88..4d442ac6a 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -1,9 +1,9 @@ import logging from typing import Dict, Tuple - +from datetime import datetime import numpy as np import pandas as pd - +import math logger = logging.getLogger(__name__) @@ -190,3 +190,128 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo :return: CAGR """ 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) -> float: + """ + Calculate sortino + :param trades: DataFrame containing trades (requires columns profit_ratio) + :return: sortino + """ + if (len(trades) == 0) or (min_date == None) or (max_date == None) or (min_date == max_date): + return 0 + + total_profit = trades["profit_ratio"] + days_period = (max_date - min_date).days + + if days_period == 0: + return 0 + + # adding slippage of 0.1% per trade + # total_profit = total_profit - 0.0005 + expected_returns_mean = total_profit.sum() / days_period + + trades['downside_returns'] = 0 + trades.loc[total_profit < 0, 'downside_returns'] = trades['profit_ratio'] + down_stdev = np.std(trades['downside_returns']) + + 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) -> float: + """ + Calculate sharpe + :param trades: DataFrame containing trades (requires columns close_date and profit_ratio) + :return: sharpe + """ + if (len(trades) == 0) or (min_date == None) or (max_date == None) or (min_date == max_date): + return 0 + + total_profit = trades["profit_ratio"] + days_period = (max_date - min_date).days + + if days_period == 0: + return 0 + + # adding slippage of 0.1% per trade + # total_profit = total_profit - 0.0005 + 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) -> float: + """ + Calculate calmar + :param trades: DataFrame containing trades (requires columns close_date and profit_ratio) + :return: calmar + """ + if (len(trades) == 0) or (min_date == None) or (max_date == None) or (min_date == max_date): + return 0 + + total_profit = trades["profit_ratio"] + days_period = (max_date - min_date).days + + # adding slippage of 0.1% per trade + # total_profit = total_profit - 0.0005 + expected_returns_mean = total_profit.sum() / days_period * 100 + + # calculate max drawdown + try: + _, _, _, _, _, max_drawdown = calculate_max_drawdown( + trades, value_col="profit_abs" + ) + 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 From 611e35ed81dd305b36fc6a4a1a8cf1371585a3da Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Wed, 7 Dec 2022 15:47:58 +0900 Subject: [PATCH 2/9] flake8 fix --- freqtrade/data/metrics.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 4d442ac6a..02a57517b 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -219,6 +219,7 @@ def calculate_expectancy(trades: pd.DataFrame) -> float: return expectancy + def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: datetime) -> float: """ @@ -226,7 +227,7 @@ def calculate_sortino(trades: pd.DataFrame, :param trades: DataFrame containing trades (requires columns profit_ratio) :return: sortino """ - if (len(trades) == 0) or (min_date == None) or (max_date == None) or (min_date == max_date): + 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_ratio"] @@ -252,14 +253,15 @@ def calculate_sortino(trades: pd.DataFrame, # print(expected_returns_mean, down_stdev, sortino_ratio) return sortino_ratio + def calculate_sharpe(trades: pd.DataFrame, - min_date: datetime, max_date: datetime) -> float: + min_date: datetime, max_date: datetime) -> float: """ Calculate sharpe :param trades: DataFrame containing trades (requires columns close_date and profit_ratio) :return: sharpe """ - if (len(trades) == 0) or (min_date == None) or (max_date == None) or (min_date == max_date): + 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_ratio"] @@ -282,14 +284,15 @@ def calculate_sharpe(trades: pd.DataFrame, # print(expected_returns_mean, up_stdev, sharp_ratio) return sharp_ratio + def calculate_calmar(trades: pd.DataFrame, - min_date: datetime, max_date: datetime) -> float: + min_date: datetime, max_date: datetime) -> float: """ Calculate calmar :param trades: DataFrame containing trades (requires columns close_date and profit_ratio) :return: calmar """ - if (len(trades) == 0) or (min_date == None) or (max_date == None) or (min_date == max_date): + 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_ratio"] From 89c7c2fec647df3c5760ddf206ade0a3cf2b9c03 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Wed, 7 Dec 2022 18:09:57 +0900 Subject: [PATCH 3/9] isort fix --- freqtrade/data/metrics.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 02a57517b..eccb8a04d 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -1,9 +1,11 @@ import logging -from typing import Dict, Tuple +import math from datetime import datetime +from typing import Dict, Tuple + import numpy as np import pandas as pd -import math + logger = logging.getLogger(__name__) From 7a5439321c9f45fbd8103538c4049074d1dae495 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Dec 2022 21:29:37 +0100 Subject: [PATCH 4/9] Show new metrics in backtesting --- freqtrade/data/metrics.py | 12 ++++++------ freqtrade/optimize/optimize_reports.py | 12 ++++++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index eccb8a04d..00168bbfa 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -287,8 +287,8 @@ def calculate_sharpe(trades: pd.DataFrame, return sharp_ratio -def calculate_calmar(trades: pd.DataFrame, - min_date: datetime, max_date: datetime) -> float: +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_ratio) @@ -297,17 +297,17 @@ def calculate_calmar(trades: pd.DataFrame, 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_ratio"] - days_period = (max_date - min_date).days + 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.sum() / days_period * 100 + expected_returns_mean = total_profit / days_period * 100 # calculate max drawdown try: _, _, _, _, _, max_drawdown = calculate_max_drawdown( - trades, value_col="profit_abs" + trades, value_col="profit_abs", starting_balance=starting_balance ) except ValueError: max_drawdown = 0 diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 8ad37e7d8..eb635cde6 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -9,8 +9,9 @@ from tabulate import tabulate from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT, Config) -from freqtrade.data.metrics import (calculate_cagr, calculate_csum, calculate_market_change, - calculate_max_drawdown) +from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum, + 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.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_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(), 'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']), + 'expectancy': calculate_expectancy(results), + 'sortino': calculate_sortino(results, min_date, max_date), + 'sharpe': calculate_sharpe(results, min_date, max_date), + 'calmar': calculate_calmar(results, min_date, max_date, start_balance), 'profit_factor': profit_factor, 'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT), 'backtest_start_ts': int(min_date.timestamp() * 1000), @@ -785,6 +790,9 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Total profit %', f"{strat_results['profit_total']:.2%}"), ('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' in strat_results else 'N/A'), ('Trades per day', strat_results['trades_per_day']), From 6353f3ac1aff1a93d54def083bfa392d7a0f01be Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Mon, 26 Dec 2022 08:19:51 +0900 Subject: [PATCH 5/9] fix formulas and implement new metrics --- freqtrade/data/metrics.py | 28 +++++++++----------------- freqtrade/optimize/optimize_reports.py | 6 ++++-- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 00168bbfa..8401e31bb 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -222,8 +222,8 @@ def calculate_expectancy(trades: pd.DataFrame) -> float: return expectancy -def calculate_sortino(trades: pd.DataFrame, - min_date: datetime, max_date: datetime) -> float: +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_ratio) @@ -232,18 +232,13 @@ def calculate_sortino(trades: pd.DataFrame, 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_ratio"] - days_period = (max_date - min_date).days + total_profit = trades['profit_abs'] / starting_balance + days_period = max(1, (max_date - min_date).days) - if days_period == 0: - return 0 - - # adding slippage of 0.1% per trade - # total_profit = total_profit - 0.0005 expected_returns_mean = total_profit.sum() / days_period trades['downside_returns'] = 0 - trades.loc[total_profit < 0, 'downside_returns'] = trades['profit_ratio'] + trades.loc[total_profit < 0, 'downside_returns'] = (trades['profit_abs'] / starting_balance) down_stdev = np.std(trades['downside_returns']) if down_stdev != 0: @@ -256,8 +251,8 @@ def calculate_sortino(trades: pd.DataFrame, return sortino_ratio -def calculate_sharpe(trades: pd.DataFrame, - min_date: datetime, max_date: datetime) -> float: +def calculate_sharpe(trades: pd.DataFrame, min_date: datetime, max_date: datetime, + starting_balance: float) -> float: """ Calculate sharpe :param trades: DataFrame containing trades (requires columns close_date and profit_ratio) @@ -266,14 +261,9 @@ def calculate_sharpe(trades: pd.DataFrame, 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_ratio"] - days_period = (max_date - min_date).days + total_profit = trades['profit_abs'] / starting_balance + days_period = max(1, (max_date - min_date).days) - if days_period == 0: - return 0 - - # adding slippage of 0.1% per trade - # total_profit = total_profit - 0.0005 expected_returns_mean = total_profit.sum() / days_period up_stdev = np.std(total_profit) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index eb635cde6..7de8f1a47 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -450,8 +450,8 @@ def generate_strategy_stats(pairlist: List[str], 'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(), 'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']), 'expectancy': calculate_expectancy(results), - 'sortino': calculate_sortino(results, min_date, max_date), - 'sharpe': calculate_sharpe(results, min_date, max_date), + '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, 'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT), @@ -795,6 +795,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', f"{strat_results['expectancy']:.2f}" if 'expectancy' + in strat_results else 'N/A'), ('Trades per day', strat_results['trades_per_day']), ('Avg. daily profit %', f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"), From f21185d1c421d6ecb4e52304a1f5cbfb230a4218 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Dec 2022 15:38:58 +0100 Subject: [PATCH 6/9] Add tests for new metrics --- tests/data/test_btanalysis.py | 72 +++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 1cc1aa0c9..22b0e3832 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -1,3 +1,4 @@ +from datetime import datetime from pathlib import Path from unittest.mock import MagicMock @@ -12,9 +13,11 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelis get_latest_hyperopt_file, load_backtest_data, 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_csum, calculate_market_change, - calculate_max_drawdown, calculate_underwater, - combine_dataframes_with_mean, create_cum_profit) +from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum, + 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 tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades from tests.conftest_trades import MOCK_TRADE_COUNT @@ -336,6 +339,69 @@ def test_calculate_csum(testdatadir): 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) == 55.1447312 + + +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', [ (64900, 176000, 3 * 365, 0.3945), (64900, 176000, 365, 1.7119), From d5b516842c09ec2e4a6bf902f3b89e818f079952 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Dec 2022 14:44:23 +0100 Subject: [PATCH 7/9] Fix 2 docstrings --- freqtrade/data/metrics.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 8401e31bb..9d32121e8 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -226,7 +226,7 @@ def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: dateti starting_balance: float) -> float: """ Calculate sortino - :param trades: DataFrame containing trades (requires columns profit_ratio) + :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): @@ -255,7 +255,7 @@ def calculate_sharpe(trades: pd.DataFrame, min_date: datetime, max_date: datetim starting_balance: float) -> float: """ Calculate sharpe - :param trades: DataFrame containing trades (requires columns close_date and profit_ratio) + :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): @@ -281,7 +281,7 @@ def calculate_calmar(trades: pd.DataFrame, min_date: datetime, max_date: datetim starting_balance: float) -> float: """ Calculate calmar - :param trades: DataFrame containing trades (requires columns close_date and profit_ratio) + :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): From 32bbe603cbf9932250916f6f15d219d15317f517 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Dec 2022 14:59:23 +0100 Subject: [PATCH 8/9] Fix sortino std calculation --- freqtrade/data/metrics.py | 4 +--- tests/data/test_btanalysis.py | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 9d32121e8..09dd60208 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -237,9 +237,7 @@ def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: dateti expected_returns_mean = total_profit.sum() / days_period - trades['downside_returns'] = 0 - trades.loc[total_profit < 0, 'downside_returns'] = (trades['profit_abs'] / starting_balance) - down_stdev = np.std(trades['downside_returns']) + 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) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 22b0e3832..345e3c299 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -1,4 +1,3 @@ -from datetime import datetime from pathlib import Path from unittest.mock import MagicMock @@ -365,7 +364,7 @@ def test_calculate_sortino(testdatadir): 0.01, ) assert isinstance(sortino, float) - assert pytest.approx(sortino) == 55.1447312 + assert pytest.approx(sortino) == 35.17722 def test_calculate_sharpe(testdatadir): From 6434bf6745ed64ad5006b30f4a9b271f9012d7dc Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Dec 2022 15:29:55 +0100 Subject: [PATCH 9/9] Document new backtesting metrics --- docs/backtesting.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/backtesting.md b/docs/backtesting.md index bfe0f4d07..0227df3f6 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -300,7 +300,11 @@ A backtesting result will look like that: | Absolute profit | 0.00762792 BTC | | Total profit % | 76.2% | | CAGR % | 460.87% | +| Sortino | 1.88 | +| Sharpe | 2.97 | +| Calmar | 6.29 | | Profit factor | 1.11 | +| Expectancy | -0.15 | | Avg. stake amount | 0.001 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 | | Total profit % | 76.2% | | CAGR % | 460.87% | +| Sortino | 1.88 | +| Sharpe | 2.97 | +| Calmar | 6.29 | | Profit factor | 1.11 | +| Expectancy | -0.15 | | Avg. stake amount | 0.001 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. - `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. +- `Sortino`: Annualized Sortino ratio. +- `Sharpe`: Annualized Sharpe ratio. +- `Calmar`: Annualized Calmar ratio. - `Profit factor`: profit / loss. - `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.