From 514590a397e3d5091025ee0d5442ed5047509109 Mon Sep 17 00:00:00 2001 From: PyPI Poetry Publish Bot Date: Sun, 31 Aug 2025 23:05:06 +0000 Subject: [PATCH 01/11] Change version to v7.4.0 --- __init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index a6345f48..7b5e7c2f 100644 --- a/__init__.py +++ b/__init__.py @@ -1 +1 @@ -__version__='v7.3.0' +__version__='v7.4.0' diff --git a/pyproject.toml b/pyproject.toml index 52dad5d1..ddd6c972 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "investing-algorithm-framework" -version = "v7.3.0" +version = "v7.4.0" description = "A framework for creating trading bots" authors = ["MDUYN"] readme = "README.md" From 40b3a70d1ececc102fc239391baabf8e24cd4603 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Mon, 1 Sep 2025 16:37:43 +0200 Subject: [PATCH 02/11] Fix data provider retrieval for permutation tests --- investing_algorithm_framework/app/app.py | 3 ++- .../data_providers/data_provider_service.py | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py index 6b6f0d47..e432d906 100644 --- a/investing_algorithm_framework/app/app.py +++ b/investing_algorithm_framework/app/app.py @@ -1325,9 +1325,10 @@ def run_permutation_test( for data_source in data_sources: if DataType.OHLCV.equals(data_source.data_type): + data_provider = data_provider_service.get(data_source) data = data_provider_service.get_data( data_source=data_source, - start_date=backtest_date_range.start_date, + start_date=data_provider._start_date_data_source, end_date=backtest_date_range.end_date ) original_data_combinations.append((data_source, data)) diff --git a/investing_algorithm_framework/services/data_providers/data_provider_service.py b/investing_algorithm_framework/services/data_providers/data_provider_service.py index 9cbb9b49..b5509e35 100644 --- a/investing_algorithm_framework/services/data_providers/data_provider_service.py +++ b/investing_algorithm_framework/services/data_providers/data_provider_service.py @@ -322,6 +322,19 @@ def __init__( self.configuration_service = configuration_service self.market_credential_service = market_credential_service + def get(self, data_source: DataSource) -> Optional[DataProvider]: + """ + Get a registered data provider by its data source. + + Args: + data_source (DataSource): The data source to get the data provider for. + + Returns: + Optional[DataProvider]: The registered data provider for + the data source, or None if not found. + """ + return self.data_provider_index.get(data_source) + def get_data( self, data_source: DataSource, @@ -742,6 +755,15 @@ def get_data_files(self): return data_files + def get_all_registered_data_providers(self) -> List[DataProvider]: + """ + Function to get all registered data providers. + + Returns: + List[DataProvider]: A list of all registered data providers. + """ + return self.data_provider_index.get_all() + def reset(self): """ Function to reset all the data providers and the data provider From c3f0a87eb2af5552714abbbd2ac4ddf91ad50da1 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Mon, 1 Sep 2025 16:40:05 +0200 Subject: [PATCH 03/11] Fix flake8 warnings --- .../services/data_providers/data_provider_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/investing_algorithm_framework/services/data_providers/data_provider_service.py b/investing_algorithm_framework/services/data_providers/data_provider_service.py index b5509e35..37dddffd 100644 --- a/investing_algorithm_framework/services/data_providers/data_provider_service.py +++ b/investing_algorithm_framework/services/data_providers/data_provider_service.py @@ -327,7 +327,8 @@ def get(self, data_source: DataSource) -> Optional[DataProvider]: Get a registered data provider by its data source. Args: - data_source (DataSource): The data source to get the data provider for. + data_source (DataSource): The data source to get the + data provider for. Returns: Optional[DataProvider]: The registered data provider for From 66a5d2e0ebe953e666dd8959ea240f42cb62cb74 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Tue, 2 Sep 2025 12:55:38 +0200 Subject: [PATCH 04/11] Fix data provider initialization for permutation tests --- investing_algorithm_framework/app/app.py | 27 ++++++++++--------- .../app/reporting/charts/__init__.py | 2 ++ .../data_providers/data_provider_service.py | 22 +++++++++++++++ 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py index e432d906..9d6d5899 100644 --- a/investing_algorithm_framework/app/app.py +++ b/investing_algorithm_framework/app/app.py @@ -1357,19 +1357,20 @@ def run_permutation_test( self._data_providers = [] for combi in permutated_datasets: - dp_identifier = combi[0].data_provider_identifier + data_source = combi[0] + data_provider = PandasOHLCVDataProvider( + dataframe=combi[1], + symbol=data_source.symbol, + market=data_source.market, + window_size=data_source.window_size, + time_frame=data_source.time_frame, + data_provider_identifier=data_source.data_provider_identifier, + pandas=data_source.pandas, + ) # Add pandas ohlcv data provider to the data provider service - self.add_data_provider( - PandasOHLCVDataProvider( - dataframe=combi[1], - symbol=combi[0].symbol, - market=combi[0].market, - window_size=combi[0].window_size, - time_frame=combi[0].time_frame, - data_provider_identifier=dp_identifier, - pandas=combi[0].pandas, - ), - priority=1 + data_provider_service.register_data_provider( + data_source=data_source, + data_provider=data_provider ) # Run the backtest with the permuted strategy @@ -1379,7 +1380,7 @@ def run_permutation_test( strategy=strategy, snapshot_interval=SnapshotInterval.DAILY, risk_free_rate=risk_free_rate, - show_data_initialization_progress=False + skip_data_sources_initialization=True ) # Add the results of the permuted backtest to the main backtest diff --git a/investing_algorithm_framework/app/reporting/charts/__init__.py b/investing_algorithm_framework/app/reporting/charts/__init__.py index 68bd7f7c..240b188a 100644 --- a/investing_algorithm_framework/app/reporting/charts/__init__.py +++ b/investing_algorithm_framework/app/reporting/charts/__init__.py @@ -4,6 +4,7 @@ from .yearly_returns_barchart import get_yearly_returns_bar_chart from .ohlcv_data_completeness import get_ohlcv_data_completeness_chart from .entry_exist_signals import get_entry_and_exit_signals +from .line_chart import create_line_scatter __all__ = [ "get_equity_curve_with_drawdown_chart", @@ -12,4 +13,5 @@ "get_yearly_returns_bar_chart", "get_ohlcv_data_completeness_chart", "get_entry_and_exit_signals", + "create_line_scatter", ] diff --git a/investing_algorithm_framework/services/data_providers/data_provider_service.py b/investing_algorithm_framework/services/data_providers/data_provider_service.py index 37dddffd..781a1735 100644 --- a/investing_algorithm_framework/services/data_providers/data_provider_service.py +++ b/investing_algorithm_framework/services/data_providers/data_provider_service.py @@ -592,6 +592,28 @@ def _throw_no_data_provider_exception(self, params): f"No data provider found for the given parameters: {params}" ) + def register_data_provider( + self, data_source: DataSource, data_provider: DataProvider + ) -> DataProvider: + """ + Function to directly register a data provider for a given data source. + + This method will not check if the data provider supports the + data source. It will directly register the data provider in the index. + + Args: + data_source (DataSource): The data source to register the + data provider for. + data_provider (DataProvider): The data provider to register. + + Returns: + DataProvider: The registered data provider. + """ + data_provider = data_provider.copy(data_source) + self.data_provider_index.data_providers_lookup[data_source] = \ + data_provider + return data_provider + def add_data_provider( self, data_provider: DataProvider, priority: int = 3 ): From 4d05d5da836ee4e4da97f93f9463870a6d6c9591 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Tue, 2 Sep 2025 12:56:27 +0200 Subject: [PATCH 05/11] Fix flake8 warnings --- investing_algorithm_framework/app/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py index 9d6d5899..6097b88e 100644 --- a/investing_algorithm_framework/app/app.py +++ b/investing_algorithm_framework/app/app.py @@ -1364,7 +1364,8 @@ def run_permutation_test( market=data_source.market, window_size=data_source.window_size, time_frame=data_source.time_frame, - data_provider_identifier=data_source.data_provider_identifier, + data_provider_identifier=data_source + .data_provider_identifier, pandas=data_source.pandas, ) # Add pandas ohlcv data provider to the data provider service From 247e1f492a9b62898a6e93b320e4a4a27363527c Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Tue, 2 Sep 2025 13:43:17 +0200 Subject: [PATCH 06/11] Fix permutation test dataset reference --- investing_algorithm_framework/app/app.py | 12 +++++-- .../domain/backtesting/backtest.py | 33 +++++++++++++++++++ .../backtesting/backtest_permutation_test.py | 3 ++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py index 6097b88e..280e7c73 100644 --- a/investing_algorithm_framework/app/app.py +++ b/investing_algorithm_framework/app/app.py @@ -1332,7 +1332,12 @@ def run_permutation_test( end_date=backtest_date_range.end_date ) original_data_combinations.append((data_source, data)) - original_datasets_ordered_by_symbol[data_source.symbol] = data + original_datasets_ordered_by_symbol[data_source.symbol] = \ + data_provider_service.get_data( + data_source=data_source, + start_date=data_provider._start_date_data_source, + end_date=backtest_date_range.end_date + ) for _ in tqdm( range(number_of_permutations), @@ -1394,7 +1399,10 @@ def run_permutation_test( real_metrics=backtest_metrics, permutated_metrics=permuted_metrics, ohlcv_permutated_datasets=permuted_datasets_ordered_by_symbol, - ohlcv_original_datasets=original_datasets_ordered_by_symbol + ohlcv_original_datasets=original_datasets_ordered_by_symbol, + backtest_start_date=backtest_date_range.start_date, + backtest_end_date=backtest_date_range.end_date, + backtest_date_range_name=backtest_date_range.name ) return permutation_test_metrics diff --git a/investing_algorithm_framework/domain/backtesting/backtest.py b/investing_algorithm_framework/domain/backtesting/backtest.py index 267736a1..61dd3b10 100644 --- a/investing_algorithm_framework/domain/backtesting/backtest.py +++ b/investing_algorithm_framework/domain/backtesting/backtest.py @@ -91,6 +91,27 @@ def get_backtest_run( return run return None + def get_backtest_permutation_test( + self, date_range: BacktestDateRange + ) -> Union[BacktestPermutationTest, None]: + """ + Retrieve a specific BacktestPermutationTest based on + the provided date range. + + Args: + date_range (BacktestDateRange): The date range to search for. + + Returns: + Union[BacktestPermutationTest, None]: The + matching BacktestPermutationTest if found, + otherwise None. + """ + for perm_test in self.backtest_permutation_tests: + if (perm_test.backtest_start_date == date_range.start_date and + perm_test.backtest_end_date == date_range.end_date): + return perm_test + return None + def get_all_backtest_metrics(self) -> List[BacktestMetrics]: """ Retrieve all BacktestMetrics from the backtest runs. @@ -382,3 +403,15 @@ def get_backtest_date_ranges(self): ) for run in self.backtest_runs ] + + def add_permutation_test( + self, permutation_test: BacktestPermutationTest + ) -> None: + """ + Add a permutation test to the backtest. + + Args: + permutation_test (BacktestPermutationTest): The permutation test + to add. + """ + self.backtest_permutation_tests.append(permutation_test) diff --git a/investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py b/investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py index 679ea683..3a7d4d7a 100644 --- a/investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +++ b/investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py @@ -42,6 +42,9 @@ class BacktestPermutationTest: field(default_factory=dict) ohlcv_original_datasets: Dict[str, pd.DataFrame] = \ field(default_factory=dict) + backtest_start_date: pd.Timestamp = None + backtest_end_date: pd.Timestamp = None + backtest_date_range_name: str = None def compute_p_values( self, metrics: List[str] = None, one_sided: bool = True From ac45f0d5dfe7a59cd6757002e9d61fdfe2b4fc44 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Tue, 2 Sep 2025 13:44:52 +0200 Subject: [PATCH 07/11] Add line chart functionality --- .../app/reporting/charts/line_chart.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 investing_algorithm_framework/app/reporting/charts/line_chart.py diff --git a/investing_algorithm_framework/app/reporting/charts/line_chart.py b/investing_algorithm_framework/app/reporting/charts/line_chart.py new file mode 100644 index 00000000..30de6cc8 --- /dev/null +++ b/investing_algorithm_framework/app/reporting/charts/line_chart.py @@ -0,0 +1,11 @@ +import plotly.graph_objects as go + + +def create_line_scatter(x, y, name, colour = 'blue'): + return go.Scatter( + x=x, + y=y, + mode='lines', + name=name, + line=dict(color=colour) + ) From eaf5e8f966ea0f78cca9e0f9205379c29ec7f0f1 Mon Sep 17 00:00:00 2001 From: PyPI Poetry Publish Bot Date: Tue, 2 Sep 2025 11:53:57 +0000 Subject: [PATCH 08/11] Change version to v7.4.1 --- __init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 7b5e7c2f..5f55dde1 100644 --- a/__init__.py +++ b/__init__.py @@ -1 +1 @@ -__version__='v7.4.0' +__version__='v7.4.1' diff --git a/pyproject.toml b/pyproject.toml index ddd6c972..ee5e9a4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "investing-algorithm-framework" -version = "v7.4.0" +version = "v7.4.1" description = "A framework for creating trading bots" authors = ["MDUYN"] readme = "README.md" From 8d4726f8c148ea2f65d6af275dbfa617a7fa8721 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Mon, 8 Sep 2025 14:24:35 +0200 Subject: [PATCH 09/11] Add combine backtests function --- investing_algorithm_framework/__init__.py | 5 +- .../domain/__init__.py | 5 +- .../domain/backtesting/__init__.py | 4 +- .../domain/backtesting/backtest_metrics.py | 5 + .../backtesting/backtest_summary_metrics.py | 23 +- .../domain/backtesting/combine_backtests.py | 210 ++++++++++++++++++ 6 files changed, 246 insertions(+), 6 deletions(-) create mode 100644 investing_algorithm_framework/domain/backtesting/combine_backtests.py diff --git a/investing_algorithm_framework/__init__.py b/investing_algorithm_framework/__init__.py index 04310b17..aecee89d 100644 --- a/investing_algorithm_framework/__init__.py +++ b/investing_algorithm_framework/__init__.py @@ -8,7 +8,7 @@ get_monthly_returns_heatmap_chart, create_weights, \ get_yearly_returns_bar_chart, get_entry_and_exit_signals, \ get_ohlcv_data_completeness_chart -from .domain import ApiException, \ +from .domain import ApiException, combine_backtests, \ OrderType, OperationalException, OrderStatus, OrderSide, \ TimeUnit, TimeInterval, Order, Portfolio, Backtest, \ Position, TimeFrame, INDEX_DATETIME, MarketCredential, \ @@ -163,5 +163,6 @@ "get_entry_and_exit_signals", "get_growth", "get_growth_percentage", - "BacktestEvaluationFocus" + "BacktestEvaluationFocus", + "combine_backtests", ] diff --git a/investing_algorithm_framework/domain/__init__.py b/investing_algorithm_framework/domain/__init__.py index 020da792..364c0c38 100644 --- a/investing_algorithm_framework/domain/__init__.py +++ b/investing_algorithm_framework/domain/__init__.py @@ -34,7 +34,7 @@ csv_to_list, StoppableThread, load_csv_into_dict, tqdm, \ is_timezone_aware, sync_timezones, get_timezone from .backtesting import BacktestRun, BacktestSummaryMetrics, \ - BacktestDateRange, Backtest, BacktestMetrics, \ + BacktestDateRange, Backtest, BacktestMetrics, combine_backtests, \ BacktestPermutationTest, BacktestEvaluationFocus __all__ = [ @@ -140,5 +140,6 @@ "is_jupyter_notebook", "tqdm", "DEFAULT_DATETIME_FORMAT", - "BacktestEvaluationFocus" + "BacktestEvaluationFocus", + 'combine_backtests' ] diff --git a/investing_algorithm_framework/domain/backtesting/__init__.py b/investing_algorithm_framework/domain/backtesting/__init__.py index 4b1e6d34..aee3b907 100644 --- a/investing_algorithm_framework/domain/backtesting/__init__.py +++ b/investing_algorithm_framework/domain/backtesting/__init__.py @@ -5,6 +5,7 @@ from .backtest import Backtest from .backtest_permutation_test import BacktestPermutationTest from .backtest_evaluation_focuss import BacktestEvaluationFocus +from .combine_backtests import combine_backtests __all__ = [ "Backtest", @@ -13,5 +14,6 @@ "BacktestMetrics", "BacktestRun", "BacktestPermutationTest", - "BacktestEvaluationFocus" + "BacktestEvaluationFocus", + "combine_backtests" ] diff --git a/investing_algorithm_framework/domain/backtesting/backtest_metrics.py b/investing_algorithm_framework/domain/backtesting/backtest_metrics.py index 9f27f970..0986abbb 100644 --- a/investing_algorithm_framework/domain/backtesting/backtest_metrics.py +++ b/investing_algorithm_framework/domain/backtesting/backtest_metrics.py @@ -165,6 +165,11 @@ class BacktestMetrics: best_year: Tuple[float, date] = None worst_month: Tuple[float, datetime] = None worst_year: Tuple[float, date] = None + total_number_of_days: int = None + + def __post_init__(self): + self.total_number_of_days = (self.backtest_end_date - + self.backtest_start_date).days def to_dict(self) -> dict: """ diff --git a/investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py b/investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py index 824424f6..9b105b82 100644 --- a/investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +++ b/investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py @@ -20,9 +20,18 @@ class BacktestSummaryMetrics: total_net_gain (float): Total net gain from the backtest. total_net_gain_percentage (float): Total net gain percentage from the backtest. + average_total_net_gain (float): Average total net gain across + multiple backtests. + average_total_net_gain_percentage (float): Average total net gain + percentage across multiple backtests. gross_loss (float): Total gross loss from all trades. + average_gross_loss (float): Average gross loss across + multiple backtests. growth (float): Total growth from the backtest. growth_percentage (float): Total growth percentage from the backtest. + average_growth (float): Average growth across multiple backtests. + average_growth_percentage (float): Average growth percentage across + multiple backtests. trades_average_return (float): Average return per trade. cagr (float): Compound annual growth rate of the backtest. sharpe_ratio (float): Sharpe ratio, risk-adjusted return. @@ -41,9 +50,14 @@ class BacktestSummaryMetrics: """ total_net_gain: float = None total_net_gain_percentage: float = None + average_total_net_gain: float = None + average_total_net_gain_percentage: float = None gross_loss: float = None + average_gross_loss: float = None growth: float = None growth_percentage: float = None + average_growth: float = None + average_growth_percentage: float = None trades_average_return: float = None cagr: float = None sharpe_ratio: float = None @@ -67,9 +81,15 @@ def to_dict(self) -> dict: return { "total_net_gain": self.total_net_gain, "total_net_gain_percentage": self.total_net_gain_percentage, + "average_total_net_gain": self.average_total_net_gain, + "average_total_net_gain_percentage": + self.average_total_net_gain_percentage, "gross_loss": self.gross_loss, + "average_gross_loss": self.average_gross_loss, "growth": self.growth, "growth_percentage": self.growth_percentage, + "average_growth": self.average_growth, + "average_growth_percentage": self.average_growth_percentage, "trades_average_return": self.trades_average_return, "cagr": self.cagr, "sharpe_ratio": self.sharpe_ratio, @@ -120,7 +140,8 @@ def safe_mean(a, b): self.cumulative_exposure = other.cumulative_exposure else: self.cumulative_exposure = safe_mean( - self.cumulative_exposure, other.cumulative_exposure + self.cumulative_exposure, + other.cumulative_exposure ) if self.exposure_ratio is None: diff --git a/investing_algorithm_framework/domain/backtesting/combine_backtests.py b/investing_algorithm_framework/domain/backtesting/combine_backtests.py new file mode 100644 index 00000000..8c183c37 --- /dev/null +++ b/investing_algorithm_framework/domain/backtesting/combine_backtests.py @@ -0,0 +1,210 @@ +from typing import List + +from investing_algorithm_framework.domain.backtesting import Backtest, BacktestDateRange +from investing_algorithm_framework.domain.backtesting import \ + BacktestSummaryMetrics + + +def safe_weighted_mean(values, weights): + """ + Calculate the weighted mean of a list of values, + ignoring None values and weights <= 0. + + Args: + values (List[float | None]): List of values to average. + weights (List[float | None]): Corresponding weights for the values. + + Returns: + float | None: The weighted mean, or None if no valid values. + """ + vals = [(v, w) for v, w in zip(values, weights) if + v is not None and w is not None and w > 0] + if not vals: + return None + total_weight = sum(w for _, w in vals) + return sum( + v * w for v, w in vals + ) / total_weight if total_weight > 0 else None + + +def combine_backtests( + backtests: List[Backtest], + backtest_date_range: BacktestDateRange = None +) -> Backtest: + """ + Combine multiple backtests into a single backtest by aggregating + their results. + + Args: + backtests (List[Backtest]): List of Backtest instances to combine. + backtest_date_range (BacktestDateRange, optional): The date range + for the combined backtest. + + Returns: + Backtest: A new Backtest instance representing the combined results. + """ + backtest_metrics = [] + backtest_runs = [] + + for backtest in backtests: + backtest_metric = None + backtest_run = None + + if backtest_date_range is not None: + backtest_metric = \ + backtest.get_backtest_metrics(backtest_date_range) + backtest_run = \ + backtest.get_backtest_run(backtest_date_range) + else: + backtest_run = backtest.backtest_runs[0] \ + if len(backtest.backtest_runs) > 0 else None + + if backtest_run is not None: + backtest_metric = backtest_run.backtest_metrics + + if backtest_metric is not None: + backtest_metrics.append(backtest_metric) + backtest_runs.append(backtest_run) + + total_net_gain = sum( + b.total_net_gain for b in backtest_metrics + if b.total_net_gain is not None + ) + average_total_net_gain = safe_weighted_mean( + [b.total_net_gain for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + average_total_net_gain_percentage = safe_weighted_mean( + [b.total_net_gain_percentage for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + total_net_gain_percentage = sum( + b.total_net_gain_percentage for b in backtest_metrics + if b.total_net_gain_percentage is not None + ) + gross_loss = sum( + b.gross_loss for b in backtest_metrics + if b.gross_loss is not None + ) + average_gross_loss = safe_weighted_mean( + [b.gross_loss for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + growth = sum( + b.growth for b in backtest_metrics + if b.growth is not None + ) + growth_percentage = sum( + b.growth_percentage for b in backtest_metrics + if b.growth_percentage is not None + ) + average_growth = safe_weighted_mean( + [b.growth for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + average_growth_percentage = safe_weighted_mean( + [b.growth_percentage for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + trades_average_return = safe_weighted_mean( + [b.trades_average_return for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + cagr = safe_weighted_mean( + [b.cagr for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + sharp_ratio = safe_weighted_mean( + [b.sharpe_ratio for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + sortino_ratio = safe_weighted_mean( + [b.sortino_ratio for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + calmar_ratio = safe_weighted_mean( + [b.calmar_ratio for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + profit_factor = safe_weighted_mean( + [b.profit_factor for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + annual_volatility = safe_weighted_mean( + [b.annual_volatility for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + max_drawdown = max( + (b.max_drawdown for b in backtest_metrics + if b.max_drawdown is not None), default=None + ) + max_drawdown_duration = max( + (b.max_drawdown_duration for b in backtest_metrics + if b.max_drawdown_duration is not None), default=None + ) + trades_per_year = safe_weighted_mean( + [b.trades_per_year for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + win_rate = safe_weighted_mean( + [b.win_rate for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + win_loss_ratio = safe_weighted_mean( + [b.win_loss_ratio for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + number_of_trades = sum( + b.number_of_trades for b in backtest_metrics + if b.number_of_trades is not None + ) + cumulative_exposure = safe_weighted_mean( + [b.cumulative_exposure for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + exposure_ratio = safe_weighted_mean( + [b.exposure_ratio for b in backtest_metrics], + [b.total_number_of_days for b in backtest_metrics] + ) + summary = BacktestSummaryMetrics( + total_net_gain=total_net_gain, + total_net_gain_percentage=total_net_gain_percentage, + average_total_net_gain=average_total_net_gain, + average_total_net_gain_percentage=average_total_net_gain_percentage, + gross_loss=gross_loss, + average_gross_loss=average_gross_loss, + growth=growth, + growth_percentage=growth_percentage, + average_growth=average_growth, + average_growth_percentage=average_growth_percentage, + trades_average_return=trades_average_return, + cagr=cagr, + sharpe_ratio=sharp_ratio, + sortino_ratio=sortino_ratio, + calmar_ratio=calmar_ratio, + profit_factor=profit_factor, + annual_volatility=annual_volatility, + max_drawdown=max_drawdown, + max_drawdown_duration=max_drawdown_duration, + trades_per_year=trades_per_year, + win_rate=win_rate, + win_loss_ratio=win_loss_ratio, + number_of_trades=number_of_trades, + cumulative_exposure=cumulative_exposure, + exposure_ratio=exposure_ratio + ) + + metadata = None + + # Get first non-empty metadata + for backtest in backtests: + if backtest.metadata: + metadata = backtest.metadata + break + + backtest = Backtest( + backtest_summary=summary, + metadata=metadata, + backtest_runs=backtest_runs + ) + return backtest From 20161d8c57f326599af6bb3e46824a89ce9e4252 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Mon, 8 Sep 2025 14:27:12 +0200 Subject: [PATCH 10/11] Add combine backtests function --- .../domain/backtesting/combine_backtests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/investing_algorithm_framework/domain/backtesting/combine_backtests.py b/investing_algorithm_framework/domain/backtesting/combine_backtests.py index 8c183c37..123dfc87 100644 --- a/investing_algorithm_framework/domain/backtesting/combine_backtests.py +++ b/investing_algorithm_framework/domain/backtesting/combine_backtests.py @@ -1,6 +1,7 @@ from typing import List -from investing_algorithm_framework.domain.backtesting import Backtest, BacktestDateRange +from investing_algorithm_framework.domain.backtesting import Backtest, \ + BacktestDateRange from investing_algorithm_framework.domain.backtesting import \ BacktestSummaryMetrics From 2d1aa9fa5386207a9650df9ecb5d7ecfa0897f78 Mon Sep 17 00:00:00 2001 From: PyPI Poetry Publish Bot Date: Mon, 8 Sep 2025 12:47:43 +0000 Subject: [PATCH 11/11] Change version to v7.5.0 --- __init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 5f55dde1..56e35c15 100644 --- a/__init__.py +++ b/__init__.py @@ -1 +1 @@ -__version__='v7.4.1' +__version__='v7.5.0' diff --git a/pyproject.toml b/pyproject.toml index ee5e9a4c..21c7b58c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "investing-algorithm-framework" -version = "v7.4.1" +version = "v7.5.0" description = "A framework for creating trading bots" authors = ["MDUYN"] readme = "README.md"