From f410b1b14d71ea170a03b6e95576f70a8f7f6385 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Mon, 28 Nov 2022 08:56:49 +0900 Subject: [PATCH 01/26] 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 02/26] 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 03/26] 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 04/26] 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 05/26] 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 d60b38dad2badc94d30038ac5e03f24fb8a2c238 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Dec 2022 09:04:53 +0000 Subject: [PATCH 06/26] Bump tables from 3.7.0 to 3.8.0 Bumps [tables](https://github.com/PyTables/PyTables) from 3.7.0 to 3.8.0. - [Release notes](https://github.com/PyTables/PyTables/releases) - [Changelog](https://github.com/PyTables/PyTables/blob/master/RELEASE_NOTES.rst) - [Commits](https://github.com/PyTables/PyTables/compare/v3.7.0...v3.8.0) --- updated-dependencies: - dependency-name: tables dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fa689de14..4e2ea6caf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ technical==1.3.0 tabulate==0.9.0 pycoingecko==3.1.0 jinja2==3.1.2 -tables==3.7.0 +tables==3.8.0 blosc==1.11.1 joblib==1.2.0 pyarrow==10.0.1; platform_machine != 'armv7l' From 4d112def172cb9a34520f2876b825c486d7ccbef Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Dec 2022 07:10:11 +0100 Subject: [PATCH 07/26] Remove binance AD from docs page fixes #7921 --- docs/overrides/main.html | 24 ------------------------ docs/stylesheets/ft.extra.css | 15 --------------- 2 files changed, 39 deletions(-) diff --git a/docs/overrides/main.html b/docs/overrides/main.html index dfc5264be..cba627ead 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -11,9 +11,6 @@ {% endif %}