investing-algorithm-framework 6.9.1__py3-none-any.whl → 7.19.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of investing-algorithm-framework might be problematic. Click here for more details.

Files changed (192) hide show
  1. investing_algorithm_framework/__init__.py +147 -44
  2. investing_algorithm_framework/app/__init__.py +23 -6
  3. investing_algorithm_framework/app/algorithm/algorithm.py +5 -41
  4. investing_algorithm_framework/app/algorithm/algorithm_factory.py +17 -10
  5. investing_algorithm_framework/app/analysis/__init__.py +15 -0
  6. investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
  7. investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
  8. investing_algorithm_framework/app/analysis/permutation.py +116 -0
  9. investing_algorithm_framework/app/analysis/ranking.py +297 -0
  10. investing_algorithm_framework/app/app.py +1322 -707
  11. investing_algorithm_framework/app/context.py +196 -88
  12. investing_algorithm_framework/app/eventloop.py +590 -0
  13. investing_algorithm_framework/app/reporting/__init__.py +16 -5
  14. investing_algorithm_framework/app/reporting/ascii.py +57 -202
  15. investing_algorithm_framework/app/reporting/backtest_report.py +284 -170
  16. investing_algorithm_framework/app/reporting/charts/__init__.py +10 -2
  17. investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
  18. investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
  19. investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +11 -26
  20. investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
  21. investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
  22. investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +1 -1
  23. investing_algorithm_framework/app/reporting/generate.py +100 -114
  24. investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +40 -32
  25. investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +34 -27
  26. investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +23 -19
  27. investing_algorithm_framework/app/reporting/tables/trades_table.py +1 -1
  28. investing_algorithm_framework/app/reporting/tables/utils.py +1 -0
  29. investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +10 -16
  30. investing_algorithm_framework/app/strategy.py +315 -175
  31. investing_algorithm_framework/app/task.py +5 -3
  32. investing_algorithm_framework/cli/cli.py +30 -12
  33. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +131 -34
  34. investing_algorithm_framework/cli/initialize_app.py +20 -1
  35. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +18 -6
  36. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  37. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  38. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -2
  39. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +1 -1
  40. investing_algorithm_framework/create_app.py +3 -5
  41. investing_algorithm_framework/dependency_container.py +25 -39
  42. investing_algorithm_framework/domain/__init__.py +45 -38
  43. investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
  44. investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
  45. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
  46. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
  47. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
  48. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  49. investing_algorithm_framework/domain/backtesting/backtest_run.py +605 -0
  50. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  51. investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
  52. investing_algorithm_framework/domain/config.py +27 -0
  53. investing_algorithm_framework/domain/constants.py +6 -34
  54. investing_algorithm_framework/domain/data_provider.py +200 -56
  55. investing_algorithm_framework/domain/exceptions.py +34 -1
  56. investing_algorithm_framework/domain/models/__init__.py +10 -19
  57. investing_algorithm_framework/domain/models/base_model.py +0 -6
  58. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  59. investing_algorithm_framework/domain/models/data/data_source.py +214 -0
  60. investing_algorithm_framework/domain/models/{market_data_type.py → data/data_type.py} +7 -7
  61. investing_algorithm_framework/domain/models/market/market_credential.py +6 -0
  62. investing_algorithm_framework/domain/models/order/order.py +34 -13
  63. investing_algorithm_framework/domain/models/order/order_status.py +1 -1
  64. investing_algorithm_framework/domain/models/order/order_type.py +1 -1
  65. investing_algorithm_framework/domain/models/portfolio/portfolio.py +14 -1
  66. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +5 -1
  67. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +51 -11
  68. investing_algorithm_framework/domain/models/position/__init__.py +2 -1
  69. investing_algorithm_framework/domain/models/position/position.py +9 -0
  70. investing_algorithm_framework/domain/models/position/position_size.py +41 -0
  71. investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
  72. investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
  73. investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
  74. investing_algorithm_framework/domain/models/snapshot_interval.py +0 -1
  75. investing_algorithm_framework/domain/models/strategy_profile.py +19 -151
  76. investing_algorithm_framework/domain/models/time_frame.py +7 -0
  77. investing_algorithm_framework/domain/models/time_interval.py +33 -0
  78. investing_algorithm_framework/domain/models/time_unit.py +63 -1
  79. investing_algorithm_framework/domain/models/trade/__init__.py +0 -2
  80. investing_algorithm_framework/domain/models/trade/trade.py +56 -32
  81. investing_algorithm_framework/domain/models/trade/trade_status.py +8 -2
  82. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +106 -41
  83. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +161 -99
  84. investing_algorithm_framework/domain/order_executor.py +19 -0
  85. investing_algorithm_framework/domain/portfolio_provider.py +20 -1
  86. investing_algorithm_framework/domain/services/__init__.py +0 -13
  87. investing_algorithm_framework/domain/strategy.py +1 -29
  88. investing_algorithm_framework/domain/utils/__init__.py +5 -1
  89. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  90. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  91. investing_algorithm_framework/domain/utils/polars.py +17 -14
  92. investing_algorithm_framework/download_data.py +40 -10
  93. investing_algorithm_framework/infrastructure/__init__.py +13 -25
  94. investing_algorithm_framework/infrastructure/data_providers/__init__.py +7 -4
  95. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +811 -546
  96. investing_algorithm_framework/infrastructure/data_providers/csv.py +433 -122
  97. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  98. investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
  99. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +81 -0
  100. investing_algorithm_framework/infrastructure/models/__init__.py +0 -13
  101. investing_algorithm_framework/infrastructure/models/order/order.py +9 -3
  102. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +27 -8
  103. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +21 -7
  104. investing_algorithm_framework/infrastructure/order_executors/__init__.py +2 -0
  105. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  106. investing_algorithm_framework/infrastructure/repositories/repository.py +16 -2
  107. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +2 -2
  108. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +6 -0
  109. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +6 -0
  110. investing_algorithm_framework/infrastructure/services/__init__.py +0 -4
  111. investing_algorithm_framework/services/__init__.py +105 -8
  112. investing_algorithm_framework/services/backtesting/backtest_service.py +536 -476
  113. investing_algorithm_framework/services/configuration_service.py +14 -4
  114. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  115. investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
  116. investing_algorithm_framework/{app/reporting → services}/metrics/__init__.py +48 -17
  117. investing_algorithm_framework/{app/reporting → services}/metrics/drawdown.py +10 -10
  118. investing_algorithm_framework/{app/reporting → services}/metrics/equity_curve.py +2 -2
  119. investing_algorithm_framework/{app/reporting → services}/metrics/exposure.py +60 -2
  120. investing_algorithm_framework/services/metrics/generate.py +358 -0
  121. investing_algorithm_framework/{app/reporting → services}/metrics/profit_factor.py +36 -0
  122. investing_algorithm_framework/{app/reporting → services}/metrics/recovery.py +2 -2
  123. investing_algorithm_framework/{app/reporting → services}/metrics/returns.py +146 -147
  124. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  125. investing_algorithm_framework/{app/reporting/metrics/sharp_ratio.py → services/metrics/sharpe_ratio.py} +6 -10
  126. investing_algorithm_framework/{app/reporting → services}/metrics/sortino_ratio.py +3 -7
  127. investing_algorithm_framework/services/metrics/trades.py +500 -0
  128. investing_algorithm_framework/services/metrics/volatility.py +97 -0
  129. investing_algorithm_framework/{app/reporting → services}/metrics/win_rate.py +70 -3
  130. investing_algorithm_framework/services/order_service/order_backtest_service.py +21 -31
  131. investing_algorithm_framework/services/order_service/order_service.py +9 -71
  132. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +0 -2
  133. investing_algorithm_framework/services/portfolios/portfolio_service.py +3 -13
  134. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +62 -96
  135. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +0 -3
  136. investing_algorithm_framework/services/repository_service.py +5 -2
  137. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  138. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +113 -0
  139. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
  140. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
  141. investing_algorithm_framework/services/trade_service/__init__.py +7 -1
  142. investing_algorithm_framework/services/trade_service/trade_service.py +51 -29
  143. investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
  144. investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
  145. investing_algorithm_framework-7.19.15.dist-info/METADATA +537 -0
  146. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/RECORD +159 -148
  147. investing_algorithm_framework/app/reporting/evaluation.py +0 -243
  148. investing_algorithm_framework/app/reporting/metrics/risk_free_rate.py +0 -8
  149. investing_algorithm_framework/app/reporting/metrics/volatility.py +0 -69
  150. investing_algorithm_framework/cli/templates/requirements_azure_function.txt.template +0 -3
  151. investing_algorithm_framework/domain/models/backtesting/__init__.py +0 -9
  152. investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +0 -47
  153. investing_algorithm_framework/domain/models/backtesting/backtest_position.py +0 -120
  154. investing_algorithm_framework/domain/models/backtesting/backtest_reports_evaluation.py +0 -0
  155. investing_algorithm_framework/domain/models/backtesting/backtest_results.py +0 -440
  156. investing_algorithm_framework/domain/models/data_source.py +0 -21
  157. investing_algorithm_framework/domain/models/date_range.py +0 -64
  158. investing_algorithm_framework/domain/models/trade/trade_risk_type.py +0 -34
  159. investing_algorithm_framework/domain/models/trading_data_types.py +0 -48
  160. investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
  161. investing_algorithm_framework/domain/services/market_data_sources.py +0 -543
  162. investing_algorithm_framework/domain/services/market_service.py +0 -153
  163. investing_algorithm_framework/domain/services/observable.py +0 -51
  164. investing_algorithm_framework/domain/services/observer.py +0 -19
  165. investing_algorithm_framework/infrastructure/models/market_data_sources/__init__.py +0 -16
  166. investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +0 -746
  167. investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py +0 -270
  168. investing_algorithm_framework/infrastructure/models/market_data_sources/pandas.py +0 -312
  169. investing_algorithm_framework/infrastructure/services/market_service/__init__.py +0 -5
  170. investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +0 -471
  171. investing_algorithm_framework/infrastructure/services/performance_service/__init__.py +0 -7
  172. investing_algorithm_framework/infrastructure/services/performance_service/backtest_performance_service.py +0 -2
  173. investing_algorithm_framework/infrastructure/services/performance_service/performance_service.py +0 -322
  174. investing_algorithm_framework/services/market_data_source_service/__init__.py +0 -10
  175. investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py +0 -269
  176. investing_algorithm_framework/services/market_data_source_service/data_provider_service.py +0 -350
  177. investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +0 -377
  178. investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -296
  179. investing_algorithm_framework-6.9.1.dist-info/METADATA +0 -440
  180. /investing_algorithm_framework/{app/reporting → services}/metrics/alpha.py +0 -0
  181. /investing_algorithm_framework/{app/reporting → services}/metrics/beta.py +0 -0
  182. /investing_algorithm_framework/{app/reporting → services}/metrics/cagr.py +0 -0
  183. /investing_algorithm_framework/{app/reporting → services}/metrics/calmar_ratio.py +0 -0
  184. /investing_algorithm_framework/{app/reporting → services}/metrics/mean_daily_return.py +0 -0
  185. /investing_algorithm_framework/{app/reporting → services}/metrics/price_efficiency.py +0 -0
  186. /investing_algorithm_framework/{app/reporting → services}/metrics/standard_deviation.py +0 -0
  187. /investing_algorithm_framework/{app/reporting → services}/metrics/treynor_ratio.py +0 -0
  188. /investing_algorithm_framework/{app/reporting → services}/metrics/ulcer.py +0 -0
  189. /investing_algorithm_framework/{app/reporting → services}/metrics/value_at_risk.py +0 -0
  190. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/LICENSE +0 -0
  191. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/WHEEL +0 -0
  192. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/entry_points.txt +0 -0
@@ -1,14 +1,84 @@
1
- import json
1
+ import logging
2
2
  import os
3
- import sys
3
+ from pathlib import Path
4
4
  import webbrowser
5
5
  from dataclasses import dataclass
6
- from IPython.display import display, HTML
6
+ from dataclasses import field
7
+ from typing import List, Union
8
+
9
+ import pandas as pd
7
10
  from IPython import get_ipython
11
+ from IPython.display import display, HTML
12
+ from jinja2 import Environment, FileSystemLoader
13
+
14
+ from investing_algorithm_framework.domain import TimeFrame, Backtest, \
15
+ OperationalException, BacktestDateRange
16
+ from .charts import get_equity_curve_with_drawdown_chart, \
17
+ get_rolling_sharpe_ratio_chart, get_monthly_returns_heatmap_chart, \
18
+ get_yearly_returns_bar_chart, get_ohlcv_data_completeness_chart
19
+ from .tables import create_html_time_metrics_table, \
20
+ create_html_trade_metrics_table, create_html_key_metrics_table, \
21
+ create_html_trades_table
22
+
23
+ logger = logging.getLogger("investing_algorithm_framework")
24
+
25
+
26
+ def get_symbol_from_file_name(file_name: str) -> str:
27
+ """
28
+ Extract the symbol from the file name.
29
+
30
+ Args:
31
+ file_name (str): The file name from which to extract the symbol.
32
+
33
+ Returns:
34
+ str: The extracted symbol.
35
+ """
36
+ # Assuming the file name format is "symbol_timeframe.csv"
37
+ return file_name.split('_')[0].upper()
38
+
39
+ def get_market_from_file_name(file_name: str) -> str:
40
+ """
41
+ Extract the market from the file name.
42
+
43
+ Args:
44
+ file_name (str): The file name from which to extract the market.
45
+
46
+ Returns:
47
+ str: The extracted market.
48
+ """
49
+ # Assuming the file name format is "symbol_market_timeframe.csv"
50
+ parts = file_name.split('_')
51
+ if len(parts) < 2:
52
+ raise ValueError("File name does not contain a valid market.")
53
+ return parts[1].upper()
54
+
55
+
56
+ def get_time_frame_from_file_name(file_name: str) -> TimeFrame:
57
+ """
58
+ Extract the time frame from the file name.
8
59
 
9
- from investing_algorithm_framework.domain import OperationalException, \
10
- BacktestResult
11
- from .generate import add_html_report, add_metrics
60
+ Args:
61
+ file_name (str): The file name from which to extract the time frame.
62
+
63
+ Returns:
64
+ TimeFrame: The extracted time frame.
65
+ """
66
+ parts = file_name.split('_')
67
+
68
+ if len(parts) < 3:
69
+ raise ValueError(
70
+ "File name does not contain a valid time frame."
71
+ )
72
+ time_frame_str = parts[3]
73
+
74
+ try:
75
+ return TimeFrame.from_string(time_frame_str)
76
+ except ValueError:
77
+ raise ValueError(
78
+ f"Could not extract time frame from file name: {file_path}. "
79
+ f"Expected format 'OHLCV_<SYMBOL>_<MARKET>_<TIME_FRAME>_<START_DATE>_<END_DATE>.csv', "
80
+ f"got '{time_frame_str}'."
81
+ )
12
82
 
13
83
 
14
84
  @dataclass
@@ -29,207 +99,251 @@ class BacktestReport:
29
99
  strategy_path (str): The path to the strategy directory used in the
30
100
  backtest.
31
101
  """
102
+ backtest: Backtest = None
32
103
  html_report: str = None
33
104
  html_report_path: str = None
34
- metrics = None
35
- results: BacktestResult = None
36
- risk_free_rate: float = None
37
105
 
38
- def __post_init__(self):
106
+ def show(
107
+ self,
108
+ backtest_date_range: BacktestDateRange,
109
+ browser: bool = False
110
+ ):
39
111
  """
40
- Post-initialization method to create the metrics and HTML report
112
+ Display the HTML report in a Jupyter notebook cell.
41
113
  """
42
- if self.metrics is None:
43
- add_metrics(self, self.risk_free_rate)
44
114
 
45
- if self.html_report is None:
46
- add_html_report(self)
115
+ if not self.html_report:
116
+ # If the HTML report is not created, create it
117
+ self._create_html_report(backtest_date_range)
47
118
 
48
- def show(self):
49
- """
50
- Display the HTML report in a Jupyter notebook cell.
51
- """
52
- if self.html_report:
119
+ # Save the html report to a tmp location
120
+ path = "/tmp/backtest_report.html"
121
+ with open(path, "w") as html_file:
122
+ html_file.write(self.html_report)
53
123
 
54
- def in_jupyter_notebook():
55
- try:
56
- shell = get_ipython().__class__.__name__
57
- return shell == 'ZMQInteractiveShell'
58
- except (NameError, ImportError):
59
- return False
124
+ if browser:
125
+ webbrowser.open(f"file://{path}")
60
126
 
61
- if in_jupyter_notebook():
62
- display(HTML(self.html_report))
63
- else:
64
- webbrowser.open(f"file://{self.html_report_path}")
127
+ def in_jupyter_notebook():
128
+ try:
129
+ shell = get_ipython().__class__.__name__
130
+ return shell == 'ZMQInteractiveShell'
131
+ except (NameError, ImportError):
132
+ return False
133
+
134
+ if in_jupyter_notebook():
135
+ display(HTML(self.html_report))
65
136
  else:
66
- raise OperationalException("No HTML report available to display.")
137
+ webbrowser.open(f"file://{path}")
67
138
 
68
- @staticmethod
69
- def open(file_path) -> "BacktestReport":
139
+ def _create_html_report(self, backtest_date_range: BacktestDateRange):
70
140
  """
71
- Open the backtest report from a file.
141
+ Create an HTML report from the backtest metrics and results.
72
142
 
73
- Args:
74
- file_path (str): The file path from which the report will
75
- be loaded.
143
+ This method generates various charts and tables from the backtest
144
+ metrics and results, and renders them into an HTML template using
145
+ Jinja2.
146
+
147
+ Raises:
148
+ OperationalException: If no backtests are available to
149
+ create a report.
76
150
 
77
151
  Returns:
78
- BacktestReport: An instance of BacktestReport loaded from the file.
152
+ None
79
153
  """
80
- if not os.path.exists(file_path):
154
+ # Get the first backtest
155
+ if not self.backtest:
81
156
  raise OperationalException(
82
- "Backtest report file or folder does not exist"
157
+ "No backtest available to create a report."
83
158
  )
84
159
 
85
- if os.path.isdir(file_path):
86
- report_file_path = os.path.join(file_path, "report.json")
87
- results = None
88
- html_report = None
89
-
90
- # Open the results file
91
- if os.path.isfile(report_file_path):
92
- with open(report_file_path, 'r') as json_file:
93
- data = json.load(json_file)
94
160
 
95
- results = BacktestResult.from_dict(data)
96
-
97
- # Open the html file
98
- html_file_path = os.path.join(file_path, "report.html")
99
- if os.path.isfile(html_file_path):
100
- with open(html_file_path, 'r') as html_file:
101
- html_content = html_file.read()
102
- html_report = html_content
161
+ metrics = self.backtest.get_backtest_metrics(backtest_date_range)
162
+ run = self.backtest.get_backtest_run(backtest_date_range)
163
+ # Create plots
164
+ equity_with_drawdown_fig = get_equity_curve_with_drawdown_chart(
165
+ metrics.equity_curve, metrics.drawdown_series
166
+ )
167
+ equity_with_drawdown_plot_html = equity_with_drawdown_fig.to_html(
168
+ full_html=False, include_plotlyjs='cdn',
169
+ config={'responsive': True}, default_width="90%"
170
+ )
171
+ rolling_sharpe_ratio_fig = get_rolling_sharpe_ratio_chart(
172
+ metrics.rolling_sharpe_ratio
173
+ )
174
+ rolling_sharpe_ratio_plot_html = rolling_sharpe_ratio_fig.to_html(
175
+ full_html=False, include_plotlyjs='cdn',
176
+ config={'responsive': True}, default_width="90%"
177
+ )
178
+ monthly_returns_heatmap_fig = get_monthly_returns_heatmap_chart(
179
+ metrics.monthly_returns
180
+ )
181
+ monthly_returns_heatmap_html = monthly_returns_heatmap_fig.to_html(
182
+ full_html=False, include_plotlyjs='cdn',
183
+ config={'responsive': True}
184
+ )
185
+ yearly_returns_histogram_fig = get_yearly_returns_bar_chart(
186
+ metrics.yearly_returns
187
+ )
188
+ yearly_returns_histogram_html = yearly_returns_histogram_fig.to_html(
189
+ full_html=False, include_plotlyjs='cdn',
190
+ config={'responsive': True}
191
+ )
192
+
193
+ # Create OHLCV data completeness charts
194
+ data_files = []
195
+ ohlcv_data_completeness_charts_html = ""
196
+
197
+ for file in data_files:
198
+ try:
199
+ if file.endswith('.csv'):
200
+ df = pd.read_csv(file, parse_dates=['Datetime'])
201
+ file_name = os.path.basename(file)
202
+ symbol = get_symbol_from_file_name(file_name)
203
+ market = get_market_from_file_name(file_name)
204
+ time_frame = get_time_frame_from_file_name(file_name)
205
+ title = f"OHLCV Data Completeness for {market} - {symbol} - {time_frame.value}"
206
+ ohlcv_data_completeness_chart_html = \
207
+ get_ohlcv_data_completeness_chart(
208
+ df,
209
+ timeframe=time_frame.value,
210
+ windowsize=200,
211
+ title=title
212
+ )
213
+
214
+ ohlcv_data_completeness_charts_html += (
215
+ '<div class="ohlcv-data-completeness-chart">'
216
+ f'{ohlcv_data_completeness_chart_html}'
217
+ '</div>'
218
+ )
103
219
 
104
- if not results and not html_report:
105
- raise OperationalException(
106
- "No report.json or report.html found in the directory"
220
+ except Exception as e:
221
+ logger.warning(
222
+ "Error creating OHLCV data completeness " +
223
+ f"chart for {file}: {e}"
107
224
  )
225
+ continue
226
+
227
+ # Create HTML tables
228
+ key_metrics_table_html = create_html_key_metrics_table(
229
+ metrics, run
230
+ )
231
+ trades_metrics_table_html = create_html_trade_metrics_table(
232
+ metrics, run
233
+ )
234
+ time_metrics_table_html = create_html_time_metrics_table(
235
+ metrics, run
236
+ )
237
+ trades_table_html = create_html_trades_table(run)
238
+
239
+ # Jinja2 environment setup
240
+ template_dir = os.path.join(os.path.dirname(__file__), 'templates')
241
+ env = Environment(loader=FileSystemLoader(template_dir))
242
+ template = env.get_template('report_template.html.j2')
243
+
244
+ # Render template with variables
245
+ html_rendered = template.render(
246
+ report=run,
247
+ equity_with_drawdown_plot_html=equity_with_drawdown_plot_html,
248
+ rolling_sharpe_ratio_plot_html=rolling_sharpe_ratio_plot_html,
249
+ monthly_returns_heatmap_html=monthly_returns_heatmap_html,
250
+ yearly_returns_histogram_html=yearly_returns_histogram_html,
251
+ key_metrics_table_html=key_metrics_table_html,
252
+ trades_metrics_table_html=trades_metrics_table_html,
253
+ time_metrics_table_html=time_metrics_table_html,
254
+ trades_table_html=trades_table_html,
255
+ data_completeness_charts_html=ohlcv_data_completeness_charts_html
256
+ )
257
+ self.html_report = html_rendered
108
258
 
109
- return BacktestReport(
110
- html_report=html_report,
111
- results=results,
112
- html_report_path=html_file_path
113
- )
114
- else:
115
- return BacktestReport.open(file_path)
116
-
117
- def save(
118
- self,
119
- path,
120
- save_strategy=True,
121
- algorithm=None,
122
- strategy_directory_path=None
123
- ):
259
+ @staticmethod
260
+ def _load_backtest(backtest_path: str) -> Backtest:
124
261
  """
125
- Save the backtest report. When saving the report, the metrics
126
- will be saved as a JSON, the backtest results will be saved as a
127
- JSON, and the HTML report will be saved as an HTML file.
262
+ Load the backtest from a give path
263
+ and return an instance of Backtest.
128
264
 
129
265
  Args:
130
- path (str): The file path where the report will be saved.
131
- save_strategy (bool): If True, the strategy code will be copied
132
- to the report directory. Defaults to True.
133
- algorithm (Algorithm, optional): The algorithm used for the
134
- backtest. If provided, the strategy code will be copied to
135
- the report directory.
136
- strategy_directory_path (str, optional): The path to the
137
- strategy directory. If provided, the strategy code will be
138
- copied to the report directory.
266
+ backtest_path (str): The path to the backtest directory.
139
267
 
140
268
  Returns:
141
- None
269
+ Backtest: An instance of Backtest loaded from the
270
+ backtest directory.
142
271
  """
272
+ return Backtest.open(backtest_path)
143
273
 
144
- if not os.path.exists(path):
145
- os.makedirs(path)
146
-
147
- print("Saving backtest report to", path)
148
- print(f"save_strategy: {save_strategy}")
149
- if save_strategy:
150
- # If strategy_directory_path is set, copy the strategy code to
151
- # the report directory
152
- if strategy_directory_path is not None:
153
- print("Saving strategy code from directory")
154
- # check if the strategy directory exists
155
- if not os.path.exists(strategy_directory_path) and \
156
- not os.path.isdir(strategy_directory_path):
157
- raise OperationalException(
158
- "Strategy directory does not exist"
159
- )
274
+ @staticmethod
275
+ def _is_backtest(backtest_path: Union[str, Path]) -> bool:
276
+ """
277
+ Check if the given path is a valid backtest report directory.
160
278
 
161
- # Copy the strategy directory to the report directory
162
- strategy_directory_name = os.path.basename(
163
- strategy_directory_path
164
- )
165
- output_strategy_directory = os.path.join(
166
- path, strategy_directory_name
167
- )
168
- if not os.path.exists(output_strategy_directory):
169
- os.makedirs(output_strategy_directory)
170
-
171
- strategy_files = os.listdir(strategy_directory_path)
172
- for file in strategy_files:
173
- source_file = os.path.join(strategy_directory_path, file)
174
- destination_file = os.path.join(
175
- output_strategy_directory, file
176
- )
279
+ Args:
280
+ backtest_path (Union[str, Path]): The path to check.
177
281
 
178
- if os.path.isfile(source_file):
179
- # Copy the file to the output directory
180
- with open(source_file, "rb") as src:
181
- with open(destination_file, "wb") as dst:
182
- dst.write(src.read())
183
-
184
- elif algorithm is not None:
185
- print("saving strategy code from algorithm")
186
- strategy = algorithm.strategies[0]
187
- mod = sys.modules[strategy.__module__]
188
- strategy_directory_path = os.path.dirname(mod.__file__)
189
- strategy_directory_name = os.path.basename(
190
- strategy_directory_path
191
- )
192
- strategy_files = os.listdir(strategy_directory_path)
193
- output_strategy_directory = os.path.join(
194
- path, strategy_directory_name
195
- )
282
+ Returns:
283
+ bool: True if the path is a valid backtest report directory,
284
+ False otherwise.
285
+ """
286
+ return (
287
+ os.path.exists(backtest_path) and
288
+ os.path.isdir(backtest_path) and
289
+ os.path.isfile(os.path.join(backtest_path, "results.json"))
290
+ and os.path.isfile(os.path.join(backtest_path, "metrics.json"))
291
+ )
196
292
 
197
- if not os.path.exists(output_strategy_directory):
198
- os.makedirs(output_strategy_directory)
293
+ @staticmethod
294
+ def open(
295
+ backtests: List[Backtest] = [],
296
+ directory_path=None
297
+ ) -> "BacktestReport":
298
+ """
299
+ Open the backtest report from a file.
199
300
 
200
- for file in strategy_files:
201
- source_file = os.path.join(strategy_directory_path, file)
202
- destination_file = os.path.join(
203
- output_strategy_directory, file
204
- )
301
+ Args:
302
+ backtests (List[Backtest], optional): A list of Backtest instances
303
+ to include in the report. If provided, it will use these
304
+ backtests as part of the report.
305
+ directory_path (str, optional): The directory path from
306
+ which the reports will be loaded.
307
+
308
+ Returns:
309
+ BacktestReport: An instance of BacktestReport loaded from the file.
205
310
 
206
- if os.path.isfile(source_file):
207
- # Copy the file to the output directory
208
- with open(source_file, "rb") as src:
209
- with open(destination_file, "wb") as dst:
210
- dst.write(src.read())
311
+ Raises:
312
+ OperationalException: If the backtest path is not a valid backtest
313
+ report directory.
314
+ """
315
+ loaded_backtests = []
211
316
 
212
- else:
213
- raise OperationalException(
214
- "No strategy directory or algorithm provided to save the "
215
- "strategy code."
317
+ if directory_path is not None:
318
+ # Check if the directory is a valid backtest report directory
319
+ if BacktestReport._is_backtest(directory_path):
320
+ backtests.append(
321
+ Backtest.open(directory_path)
216
322
  )
323
+ else:
324
+ # Loop over all subdirectories
325
+ for root, dirs, _ in os.walk(directory_path):
326
+ for dir_name in dirs:
327
+ subdir_path = os.path.join(root, dir_name)
328
+ if BacktestReport._is_backtest(subdir_path):
329
+ loaded_backtests.append(
330
+ Backtest.open(directory_path=subdir_path)
331
+ )
332
+
333
+ if len(backtests) > 0:
334
+
335
+ for backtest in backtests:
336
+ if not isinstance(backtest, Backtest):
337
+ raise OperationalException(
338
+ "The provided backtest is not a valid Backtest instance."
339
+ )
217
340
 
218
- # Save the results as a JSON file
219
- if self.results is not None:
220
- results_file_path = os.path.join(path, "report.json")
221
- with open(results_file_path, 'w') as json_file:
222
- json.dump(self.results.to_dict(), json_file, indent=4)
223
-
224
- # Save the HTML report
225
- if self.html_report is not None:
226
- html_file_path = os.path.join(path, "report.html")
227
- with open(html_file_path, 'w') as html_file:
228
- html_file.write(self.html_report)
229
- self.html_report_path = html_file_path
341
+ # Add the backtests to the backtests list
342
+ loaded_backtests.extend(backtests)
230
343
 
231
- if self.results is not None:
232
- data = self.results.to_dict()
344
+ if len(loaded_backtests) == 0:
345
+ raise OperationalException(
346
+ f"The directory {directory_path} is not a valid backtest report."
347
+ )
233
348
 
234
- with open(os.path.join(path, "report.json"), 'w') as json_file:
235
- json.dump(data, json_file, indent=4)
349
+ return BacktestReport(backtest=loaded_backtests)
@@ -1,11 +1,19 @@
1
1
  from .equity_curve_drawdown import get_equity_curve_with_drawdown_chart
2
- from .rolling_sharp_ratio import get_rolling_sharp_ratio_chart
2
+ from .equity_curve import get_equity_curve_chart
3
+ from .rolling_sharp_ratio import get_rolling_sharpe_ratio_chart
3
4
  from .monthly_returns_heatmap import get_monthly_returns_heatmap_chart
4
5
  from .yearly_returns_barchart import get_yearly_returns_bar_chart
6
+ from .ohlcv_data_completeness import get_ohlcv_data_completeness_chart
7
+ from .entry_exist_signals import get_entry_and_exit_signals
8
+ from .line_chart import create_line_scatter
5
9
 
6
10
  __all__ = [
7
11
  "get_equity_curve_with_drawdown_chart",
8
- "get_rolling_sharp_ratio_chart",
12
+ "get_rolling_sharpe_ratio_chart",
9
13
  "get_monthly_returns_heatmap_chart",
10
14
  "get_yearly_returns_bar_chart",
15
+ "get_ohlcv_data_completeness_chart",
16
+ "get_entry_and_exit_signals",
17
+ "create_line_scatter",
18
+ "get_equity_curve_chart"
11
19
  ]
@@ -0,0 +1,66 @@
1
+ import pandas as pd
2
+ import plotly.graph_objects as go
3
+
4
+
5
+ def get_entry_and_exit_signals(
6
+ entry_signals: pd.Series,
7
+ exit_signals: pd.Series,
8
+ price_data: pd.DataFrame
9
+ ):
10
+ """
11
+ Plots the price chart with entry and exit signals.
12
+
13
+ Args:
14
+ entry_signals (pd.Series): Series containing buy
15
+ signals with datetime index. Entry signals should
16
+ be boolean values.
17
+ exit_signals (pd.Series): Series containing exit signals
18
+ with datetime index. Exit signals should be boolean values.
19
+ price_data (pd.DataFrame): DataFrame containing price
20
+ data with datetime index and 'Close'
21
+
22
+ Returns:
23
+ go.Figure: Plotly figure with price chart and signals.
24
+ """
25
+ # Create the base candlestick or line chart
26
+ fig = go.Figure()
27
+
28
+ fig.add_trace(go.Scatter(
29
+ x=price_data.index,
30
+ y=price_data["Close"],
31
+ mode="lines",
32
+ name="Close Price",
33
+ line=dict(color="blue")
34
+ ))
35
+
36
+ # Entry points
37
+ entry_points = price_data[entry_signals]
38
+ fig.add_trace(go.Scatter(
39
+ x=entry_points.index,
40
+ y=entry_points["Close"],
41
+ mode="markers",
42
+ name="Entry",
43
+ marker=dict(symbol="triangle-up", color="green", size=10)
44
+ ))
45
+
46
+ # Exit points
47
+ exit_points = price_data[exit_signals]
48
+ fig.add_trace(go.Scatter(
49
+ x=exit_points.index,
50
+ y=exit_points["Close"],
51
+ mode="markers",
52
+ name="Exit",
53
+ marker=dict(symbol="triangle-down", color="red", size=10)
54
+ ))
55
+
56
+ # Layout settings
57
+ fig.update_layout(
58
+ title="Entry and Exit Signals",
59
+ xaxis_title="Date",
60
+ yaxis_title="Price",
61
+ legend=dict(x=0, y=1),
62
+ height=600,
63
+ template="plotly_white"
64
+ )
65
+
66
+ return fig
@@ -0,0 +1,37 @@
1
+ import pandas as pd
2
+ from plotly import graph_objects as go
3
+
4
+
5
+ def get_equity_curve_chart(equity_curve_series):
6
+ equity_curve_df = pd.DataFrame(
7
+ equity_curve_series, columns=["value", "datetime"]
8
+ )
9
+
10
+ # Normalize equity to start at 1
11
+ equity_curve_df["value"] = (
12
+ equity_curve_df["value"] / equity_curve_df["value"].iloc[0]
13
+ )
14
+
15
+ fig = go.Figure()
16
+
17
+ # Draw equity curve
18
+ fig.add_trace(
19
+ go.Scatter(
20
+ x=equity_curve_df["datetime"],
21
+ y=equity_curve_df["value"],
22
+ mode="lines",
23
+ line=dict(color="rgba(0, 128, 0, 0.8)", width=1),
24
+ name="Equity Curve",
25
+ hovertemplate="<b>Equity</b><br>%{x}<br>Value: %{y:.2f}<extra></extra>"
26
+ )
27
+ )
28
+
29
+ fig.update_layout(
30
+ xaxis=dict(title=None),
31
+ yaxis=dict(title="Cumulative Equity (log)", type="log"),
32
+ title="Equity Curve with Drawdown",
33
+ hovermode="x unified",
34
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
35
+ )
36
+
37
+ return fig