investing-algorithm-framework 7.19.14__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 (260) hide show
  1. investing_algorithm_framework/__init__.py +197 -0
  2. investing_algorithm_framework/app/__init__.py +47 -0
  3. investing_algorithm_framework/app/algorithm/__init__.py +7 -0
  4. investing_algorithm_framework/app/algorithm/algorithm.py +239 -0
  5. investing_algorithm_framework/app/algorithm/algorithm_factory.py +114 -0
  6. investing_algorithm_framework/app/analysis/__init__.py +15 -0
  7. investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
  8. investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
  9. investing_algorithm_framework/app/analysis/permutation.py +116 -0
  10. investing_algorithm_framework/app/analysis/ranking.py +297 -0
  11. investing_algorithm_framework/app/app.py +2204 -0
  12. investing_algorithm_framework/app/app_hook.py +28 -0
  13. investing_algorithm_framework/app/context.py +1667 -0
  14. investing_algorithm_framework/app/eventloop.py +590 -0
  15. investing_algorithm_framework/app/reporting/__init__.py +27 -0
  16. investing_algorithm_framework/app/reporting/ascii.py +921 -0
  17. investing_algorithm_framework/app/reporting/backtest_report.py +349 -0
  18. investing_algorithm_framework/app/reporting/charts/__init__.py +19 -0
  19. investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
  20. investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
  21. investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +74 -0
  22. investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
  23. investing_algorithm_framework/app/reporting/charts/monthly_returns_heatmap.py +70 -0
  24. investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
  25. investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +79 -0
  26. investing_algorithm_framework/app/reporting/charts/yearly_returns_barchart.py +55 -0
  27. investing_algorithm_framework/app/reporting/generate.py +185 -0
  28. investing_algorithm_framework/app/reporting/tables/__init__.py +11 -0
  29. investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +217 -0
  30. investing_algorithm_framework/app/reporting/tables/stop_loss_table.py +0 -0
  31. investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +80 -0
  32. investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +147 -0
  33. investing_algorithm_framework/app/reporting/tables/trades_table.py +75 -0
  34. investing_algorithm_framework/app/reporting/tables/utils.py +29 -0
  35. investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +154 -0
  36. investing_algorithm_framework/app/stateless/__init__.py +35 -0
  37. investing_algorithm_framework/app/stateless/action_handlers/__init__.py +84 -0
  38. investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +8 -0
  39. investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +15 -0
  40. investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +40 -0
  41. investing_algorithm_framework/app/stateless/exception_handler.py +40 -0
  42. investing_algorithm_framework/app/strategy.py +675 -0
  43. investing_algorithm_framework/app/task.py +41 -0
  44. investing_algorithm_framework/app/web/__init__.py +5 -0
  45. investing_algorithm_framework/app/web/controllers/__init__.py +13 -0
  46. investing_algorithm_framework/app/web/controllers/orders.py +20 -0
  47. investing_algorithm_framework/app/web/controllers/portfolio.py +20 -0
  48. investing_algorithm_framework/app/web/controllers/positions.py +18 -0
  49. investing_algorithm_framework/app/web/create_app.py +20 -0
  50. investing_algorithm_framework/app/web/error_handler.py +59 -0
  51. investing_algorithm_framework/app/web/responses.py +20 -0
  52. investing_algorithm_framework/app/web/run_strategies.py +4 -0
  53. investing_algorithm_framework/app/web/schemas/__init__.py +12 -0
  54. investing_algorithm_framework/app/web/schemas/order.py +12 -0
  55. investing_algorithm_framework/app/web/schemas/portfolio.py +22 -0
  56. investing_algorithm_framework/app/web/schemas/position.py +15 -0
  57. investing_algorithm_framework/app/web/setup_cors.py +6 -0
  58. investing_algorithm_framework/cli/__init__.py +0 -0
  59. investing_algorithm_framework/cli/cli.py +207 -0
  60. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +499 -0
  61. investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
  62. investing_algorithm_framework/cli/initialize_app.py +603 -0
  63. investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
  64. investing_algorithm_framework/cli/templates/app.py.template +18 -0
  65. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
  66. investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
  67. investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
  68. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  69. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  70. investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
  71. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
  72. investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
  73. investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
  74. investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
  75. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
  76. investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
  77. investing_algorithm_framework/cli/templates/env.example.template +2 -0
  78. investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
  79. investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
  80. investing_algorithm_framework/cli/templates/readme.md.template +135 -0
  81. investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
  82. investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
  83. investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
  84. investing_algorithm_framework/create_app.py +54 -0
  85. investing_algorithm_framework/dependency_container.py +155 -0
  86. investing_algorithm_framework/domain/__init__.py +148 -0
  87. investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
  88. investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
  89. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
  90. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
  91. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
  92. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  93. investing_algorithm_framework/domain/backtesting/backtest_run.py +435 -0
  94. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  95. investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
  96. investing_algorithm_framework/domain/config.py +111 -0
  97. investing_algorithm_framework/domain/constants.py +83 -0
  98. investing_algorithm_framework/domain/data_provider.py +334 -0
  99. investing_algorithm_framework/domain/data_structures.py +42 -0
  100. investing_algorithm_framework/domain/decimal_parsing.py +40 -0
  101. investing_algorithm_framework/domain/exceptions.py +112 -0
  102. investing_algorithm_framework/domain/models/__init__.py +43 -0
  103. investing_algorithm_framework/domain/models/app_mode.py +34 -0
  104. investing_algorithm_framework/domain/models/base_model.py +25 -0
  105. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  106. investing_algorithm_framework/domain/models/data/data_source.py +214 -0
  107. investing_algorithm_framework/domain/models/data/data_type.py +46 -0
  108. investing_algorithm_framework/domain/models/event.py +35 -0
  109. investing_algorithm_framework/domain/models/market/__init__.py +5 -0
  110. investing_algorithm_framework/domain/models/market/market_credential.py +88 -0
  111. investing_algorithm_framework/domain/models/order/__init__.py +6 -0
  112. investing_algorithm_framework/domain/models/order/order.py +384 -0
  113. investing_algorithm_framework/domain/models/order/order_side.py +36 -0
  114. investing_algorithm_framework/domain/models/order/order_status.py +37 -0
  115. investing_algorithm_framework/domain/models/order/order_type.py +30 -0
  116. investing_algorithm_framework/domain/models/portfolio/__init__.py +9 -0
  117. investing_algorithm_framework/domain/models/portfolio/portfolio.py +169 -0
  118. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +93 -0
  119. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +208 -0
  120. investing_algorithm_framework/domain/models/position/__init__.py +4 -0
  121. investing_algorithm_framework/domain/models/position/position.py +68 -0
  122. investing_algorithm_framework/domain/models/position/position_snapshot.py +47 -0
  123. investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
  124. investing_algorithm_framework/domain/models/strategy_profile.py +33 -0
  125. investing_algorithm_framework/domain/models/time_frame.py +153 -0
  126. investing_algorithm_framework/domain/models/time_interval.py +124 -0
  127. investing_algorithm_framework/domain/models/time_unit.py +149 -0
  128. investing_algorithm_framework/domain/models/tracing/__init__.py +0 -0
  129. investing_algorithm_framework/domain/models/tracing/trace.py +23 -0
  130. investing_algorithm_framework/domain/models/trade/__init__.py +13 -0
  131. investing_algorithm_framework/domain/models/trade/trade.py +388 -0
  132. investing_algorithm_framework/domain/models/trade/trade_risk_type.py +34 -0
  133. investing_algorithm_framework/domain/models/trade/trade_status.py +40 -0
  134. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +267 -0
  135. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +303 -0
  136. investing_algorithm_framework/domain/order_executor.py +112 -0
  137. investing_algorithm_framework/domain/portfolio_provider.py +118 -0
  138. investing_algorithm_framework/domain/positions/__init__.py +4 -0
  139. investing_algorithm_framework/domain/positions/position_size.py +41 -0
  140. investing_algorithm_framework/domain/services/__init__.py +11 -0
  141. investing_algorithm_framework/domain/services/market_credential_service.py +37 -0
  142. investing_algorithm_framework/domain/services/portfolios/__init__.py +5 -0
  143. investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +9 -0
  144. investing_algorithm_framework/domain/services/rounding_service.py +27 -0
  145. investing_algorithm_framework/domain/services/state_handler.py +38 -0
  146. investing_algorithm_framework/domain/stateless_actions.py +7 -0
  147. investing_algorithm_framework/domain/strategy.py +44 -0
  148. investing_algorithm_framework/domain/utils/__init__.py +27 -0
  149. investing_algorithm_framework/domain/utils/csv.py +104 -0
  150. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  151. investing_algorithm_framework/domain/utils/dates.py +57 -0
  152. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  153. investing_algorithm_framework/domain/utils/polars.py +53 -0
  154. investing_algorithm_framework/domain/utils/random.py +41 -0
  155. investing_algorithm_framework/domain/utils/signatures.py +17 -0
  156. investing_algorithm_framework/domain/utils/stoppable_thread.py +26 -0
  157. investing_algorithm_framework/domain/utils/synchronized.py +12 -0
  158. investing_algorithm_framework/download_data.py +108 -0
  159. investing_algorithm_framework/infrastructure/__init__.py +50 -0
  160. investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
  161. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1143 -0
  162. investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
  163. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  164. investing_algorithm_framework/infrastructure/database/__init__.py +10 -0
  165. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +120 -0
  166. investing_algorithm_framework/infrastructure/models/__init__.py +16 -0
  167. investing_algorithm_framework/infrastructure/models/decimal_parser.py +14 -0
  168. investing_algorithm_framework/infrastructure/models/model_extension.py +6 -0
  169. investing_algorithm_framework/infrastructure/models/order/__init__.py +4 -0
  170. investing_algorithm_framework/infrastructure/models/order/order.py +124 -0
  171. investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
  172. investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
  173. investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +4 -0
  174. investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +37 -0
  175. investing_algorithm_framework/infrastructure/models/portfolio/sql_portfolio.py +114 -0
  176. investing_algorithm_framework/infrastructure/models/position/__init__.py +4 -0
  177. investing_algorithm_framework/infrastructure/models/position/position.py +63 -0
  178. investing_algorithm_framework/infrastructure/models/position/position_snapshot.py +23 -0
  179. investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
  180. investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
  181. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +40 -0
  182. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +41 -0
  183. investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
  184. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  185. investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
  186. investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
  187. investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
  188. investing_algorithm_framework/infrastructure/repositories/__init__.py +21 -0
  189. investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
  190. investing_algorithm_framework/infrastructure/repositories/order_repository.py +96 -0
  191. investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +30 -0
  192. investing_algorithm_framework/infrastructure/repositories/portfolio_snapshot_repository.py +56 -0
  193. investing_algorithm_framework/infrastructure/repositories/position_repository.py +66 -0
  194. investing_algorithm_framework/infrastructure/repositories/position_snapshot_repository.py +21 -0
  195. investing_algorithm_framework/infrastructure/repositories/repository.py +299 -0
  196. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
  197. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +23 -0
  198. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +23 -0
  199. investing_algorithm_framework/infrastructure/services/__init__.py +7 -0
  200. investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
  201. investing_algorithm_framework/infrastructure/services/aws/state_handler.py +113 -0
  202. investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
  203. investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
  204. investing_algorithm_framework/services/__init__.py +132 -0
  205. investing_algorithm_framework/services/backtesting/__init__.py +5 -0
  206. investing_algorithm_framework/services/backtesting/backtest_service.py +651 -0
  207. investing_algorithm_framework/services/configuration_service.py +96 -0
  208. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  209. investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
  210. investing_algorithm_framework/services/market_credential_service.py +40 -0
  211. investing_algorithm_framework/services/metrics/__init__.py +114 -0
  212. investing_algorithm_framework/services/metrics/alpha.py +0 -0
  213. investing_algorithm_framework/services/metrics/beta.py +0 -0
  214. investing_algorithm_framework/services/metrics/cagr.py +60 -0
  215. investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
  216. investing_algorithm_framework/services/metrics/drawdown.py +181 -0
  217. investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
  218. investing_algorithm_framework/services/metrics/exposure.py +210 -0
  219. investing_algorithm_framework/services/metrics/generate.py +358 -0
  220. investing_algorithm_framework/services/metrics/mean_daily_return.py +83 -0
  221. investing_algorithm_framework/services/metrics/price_efficiency.py +57 -0
  222. investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
  223. investing_algorithm_framework/services/metrics/recovery.py +113 -0
  224. investing_algorithm_framework/services/metrics/returns.py +452 -0
  225. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  226. investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
  227. investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
  228. investing_algorithm_framework/services/metrics/standard_deviation.py +157 -0
  229. investing_algorithm_framework/services/metrics/trades.py +500 -0
  230. investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
  231. investing_algorithm_framework/services/metrics/ulcer.py +0 -0
  232. investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
  233. investing_algorithm_framework/services/metrics/volatility.py +97 -0
  234. investing_algorithm_framework/services/metrics/win_rate.py +177 -0
  235. investing_algorithm_framework/services/order_service/__init__.py +9 -0
  236. investing_algorithm_framework/services/order_service/order_backtest_service.py +178 -0
  237. investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
  238. investing_algorithm_framework/services/order_service/order_service.py +826 -0
  239. investing_algorithm_framework/services/portfolios/__init__.py +16 -0
  240. investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +54 -0
  241. investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +75 -0
  242. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
  243. investing_algorithm_framework/services/portfolios/portfolio_service.py +188 -0
  244. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +136 -0
  245. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +182 -0
  246. investing_algorithm_framework/services/positions/__init__.py +7 -0
  247. investing_algorithm_framework/services/positions/position_service.py +210 -0
  248. investing_algorithm_framework/services/positions/position_snapshot_service.py +18 -0
  249. investing_algorithm_framework/services/repository_service.py +40 -0
  250. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  251. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +132 -0
  252. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +66 -0
  253. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +41 -0
  254. investing_algorithm_framework/services/trade_service/__init__.py +3 -0
  255. investing_algorithm_framework/services/trade_service/trade_service.py +1083 -0
  256. investing_algorithm_framework-7.19.14.dist-info/LICENSE +201 -0
  257. investing_algorithm_framework-7.19.14.dist-info/METADATA +459 -0
  258. investing_algorithm_framework-7.19.14.dist-info/RECORD +260 -0
  259. investing_algorithm_framework-7.19.14.dist-info/WHEEL +4 -0
  260. investing_algorithm_framework-7.19.14.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,121 @@
1
+ import pandas as pd
2
+ from typing import List, Union
3
+
4
+ from datetime import timezone
5
+ from investing_algorithm_framework.domain import BacktestDateRange, \
6
+ OperationalException
7
+
8
+
9
+ def select_backtest_date_ranges(
10
+ df: pd.DataFrame, window: Union[str, int] = '365D'
11
+ ) -> List[BacktestDateRange]:
12
+ """
13
+ Identifies the best upturn, worst downturn, and sideways periods
14
+ for the given window duration. This allows you to quickly select
15
+ interesting periods for backtesting.
16
+
17
+ Args:
18
+ df (pd.DataFrame): DataFrame with a DateTime index
19
+ and 'Close' column.
20
+ window (Union[str, int]): Duration of the window
21
+ to analyze. Can be a string like '365D' or an
22
+ integer representing days.
23
+
24
+ Returns:
25
+ List[BacktestDateRange]: List of BacktestDateRange
26
+ objects representing the best upturn, worst
27
+ downturn, and most sideways periods.
28
+ """
29
+ df = df.copy()
30
+ df = df.sort_index()
31
+
32
+ if isinstance(window, int):
33
+ window = pd.Timedelta(days=window)
34
+ elif isinstance(window, str):
35
+ window = pd.to_timedelta(window)
36
+ else:
37
+ raise OperationalException("window must be a string or integer")
38
+
39
+ # Check if the window is larger than the DataFrame
40
+ if len(df) == 0:
41
+ raise OperationalException("DataFrame is empty")
42
+
43
+ if df.index[-1] - df.index[0] < window:
44
+ raise OperationalException(
45
+ "Window duration is larger than the data duration"
46
+ )
47
+
48
+ if len(df) < 2 or df.index[-1] - df.index[0] < window:
49
+ raise OperationalException(
50
+ "DataFrame must contain at least two rows and span "
51
+ "the full window duration"
52
+ )
53
+
54
+ best_upturn = {
55
+ "name": "UpTurn", "return": float('-inf'), "start": None, "end": None
56
+ }
57
+ worst_downturn = {
58
+ "name": "DownTurn", "return": float('inf'), "start": None, "end": None
59
+ }
60
+ most_sideways = {
61
+ "name": "SideWays",
62
+ "volatility": float('inf'),
63
+ "return": None,
64
+ "start": None,
65
+ "end": None
66
+ }
67
+
68
+ for i in range(len(df)):
69
+ start_time = df.index[i]
70
+ end_time = start_time + window
71
+ window_df = df[(df.index >= start_time) & (df.index <= end_time)]
72
+
73
+ if len(window_df) < 2 or (window_df.index[-1] - start_time) < window:
74
+ continue
75
+
76
+ start_price = window_df['Close'].iloc[0]
77
+ end_price = window_df['Close'].iloc[-1]
78
+ ret = (end_price / start_price) - 1 # relative return
79
+ volatility = window_df['Close'].std()
80
+
81
+ # Ensure datetime for BacktestDateRange and with timezone utc
82
+ start_time = pd.Timestamp(start_time).to_pydatetime()
83
+ start_time = start_time.replace(tzinfo=timezone.utc)
84
+ end_time = pd.Timestamp(window_df.index[-1]).to_pydatetime()
85
+ end_time = end_time.replace(tzinfo=timezone.utc)
86
+
87
+ if ret > best_upturn["return"]:
88
+ best_upturn.update(
89
+ {"return": ret, "start": start_time, "end": end_time}
90
+ )
91
+
92
+ if ret < worst_downturn["return"]:
93
+ worst_downturn.update(
94
+ {"return": ret, "start": start_time, "end": end_time}
95
+ )
96
+
97
+ if volatility < most_sideways["volatility"]:
98
+ most_sideways.update({
99
+ "return": ret,
100
+ "volatility": volatility,
101
+ "start": start_time,
102
+ "end": end_time
103
+ })
104
+
105
+ return [
106
+ BacktestDateRange(
107
+ start_date=best_upturn['start'],
108
+ end_date=best_upturn['end'],
109
+ name=best_upturn['name']
110
+ ),
111
+ BacktestDateRange(
112
+ start_date=worst_downturn['start'],
113
+ end_date=worst_downturn['end'],
114
+ name=worst_downturn['name']
115
+ ),
116
+ BacktestDateRange(
117
+ start_date=most_sideways['start'],
118
+ end_date=most_sideways['end'],
119
+ name=most_sideways['name']
120
+ )
121
+ ]
@@ -0,0 +1,107 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import List, Union, Callable
4
+ from logging import getLogger
5
+ from random import Random
6
+
7
+ from investing_algorithm_framework.domain import Backtest
8
+
9
+
10
+ logger = getLogger("investing_algorithm_framework")
11
+
12
+
13
+ def save_backtests_to_directory(
14
+ backtests: List[Backtest],
15
+ directory_path: Union[str, Path],
16
+ dir_name_generation_function: Callable[[Backtest], str] = None,
17
+ filter_function: Callable[[Backtest], bool] = None
18
+ ) -> None:
19
+ """
20
+ Saves a list of Backtest objects to the specified directory.
21
+
22
+ Args:
23
+ backtests (List[Backtest]): List of Backtest objects to save.
24
+ directory_path (str): Path to the directory where backtests
25
+ will be saved.
26
+ dir_name_generation_function (Callable[[Backtest], str], optional):
27
+ A function that takes a Backtest object as input and returns
28
+ a string to be used as the directory name for that backtest.
29
+ If not provided, the backtest's metadata 'id' will be used.
30
+ Defaults to None.
31
+ filter_function (Callable[[Backtest], bool], optional): A function
32
+ that takes a Backtest object as input and returns True if the
33
+ backtest should be saved. Defaults to None.
34
+
35
+ Returns:
36
+ None
37
+ """
38
+
39
+ if not os.path.exists(directory_path):
40
+ os.makedirs(directory_path)
41
+
42
+ for backtest in backtests:
43
+
44
+ if filter_function is not None:
45
+ if not filter_function(backtest):
46
+ continue
47
+
48
+ if dir_name_generation_function is not None:
49
+ dir_name = dir_name_generation_function(backtest)
50
+ else:
51
+ # Check if there is an ID in the backtest metadata
52
+ dir_name = backtest.metadata.get('id', None)
53
+
54
+ if dir_name is None:
55
+ logger.warning(
56
+ "Backtest metadata does not contain an 'id' field. "
57
+ "Generating a random directory name."
58
+ )
59
+ dir_name = str(Random().randint(100000, 999999))
60
+
61
+ backtest.save(os.path.join(directory_path, dir_name))
62
+
63
+
64
+ def load_backtests_from_directory(
65
+ directory_path: Union[str, Path],
66
+ filter_function: Callable[[Backtest], bool] = None
67
+ ) -> List[Backtest]:
68
+ """
69
+ Loads Backtest objects from the specified directory.
70
+
71
+ Args:
72
+ directory_path (str): Path to the directory from which backtests
73
+ will be loaded.
74
+ filter_function (Callable[[Backtest], bool], optional): A function
75
+ that takes a Backtest object as input and returns True if the
76
+ backtest should be included in the result. Defaults to None.
77
+
78
+ Returns:
79
+ List[Backtest]: List of loaded Backtest objects.
80
+ """
81
+
82
+ backtests = []
83
+
84
+ if not os.path.exists(directory_path):
85
+ logger.warning(
86
+ f"Directory {directory_path} does not exist. "
87
+ "No backtests loaded."
88
+ )
89
+ return backtests
90
+
91
+ for file_name in os.listdir(directory_path):
92
+ file_path = os.path.join(directory_path, file_name)
93
+
94
+ try:
95
+ backtest = Backtest.open(file_path)
96
+
97
+ if filter_function is not None:
98
+ if not filter_function(backtest):
99
+ continue
100
+
101
+ backtests.append(backtest)
102
+ except Exception as e:
103
+ logger.error(
104
+ f"Failed to load backtest from {file_path}: {e}"
105
+ )
106
+
107
+ return backtests
@@ -0,0 +1,116 @@
1
+ from typing import Union
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ import polars as pl
6
+
7
+ from investing_algorithm_framework.domain import OperationalException
8
+
9
+
10
+ def create_ohlcv_permutation(
11
+ data: Union[pd.DataFrame, pl.DataFrame],
12
+ start_index: int = 0,
13
+ seed: int | None = None,
14
+ ) -> Union[pd.DataFrame, pl.DataFrame]:
15
+ """
16
+ Create a permuted OHLCV dataset by shuffling relative price moves.
17
+
18
+ Args:
19
+ data: A single OHLCV DataFrame (pandas or polars)
20
+ with columns ['Open', 'High', 'Low', 'Close', 'Volume'].
21
+ For pandas: Datetime can be either
22
+ index or a 'Datetime' column. For polars: Datetime
23
+ must be a 'Datetime' column.
24
+ start_index: Index at which the permutation should begin
25
+ (bars before remain unchanged).
26
+ seed: Random seed for reproducibility.
27
+
28
+ Returns:
29
+ DataFrame of the same type (pandas or polars) with
30
+ permuted OHLCV values, preserving the datetime
31
+ structure (index vs column) of the input.
32
+ """
33
+
34
+ if start_index < 0:
35
+ raise OperationalException("start_index must be >= 0")
36
+
37
+ if seed is None:
38
+ seed = np.random.randint(0, 1_000_000)
39
+
40
+ np.random.seed(seed)
41
+ is_polars = isinstance(data, pl.DataFrame)
42
+
43
+ # Normalize input to pandas
44
+ if is_polars:
45
+ has_datetime_col = "Datetime" in data.columns
46
+ ohlcv_pd = data.to_pandas().copy()
47
+ if has_datetime_col:
48
+ time_index = pd.to_datetime(ohlcv_pd["Datetime"])
49
+ else:
50
+ time_index = np.arange(len(ohlcv_pd))
51
+ else:
52
+ has_datetime_col = "Datetime" in data.columns
53
+ if isinstance(data.index, pd.DatetimeIndex):
54
+ time_index = data.index
55
+ elif has_datetime_col:
56
+ time_index = pd.to_datetime(data["Datetime"])
57
+ else:
58
+ time_index = np.arange(len(data))
59
+ ohlcv_pd = data.copy()
60
+
61
+ # Prepare data
62
+ n_bars = len(ohlcv_pd)
63
+ perm_index = start_index + 1
64
+ perm_n = n_bars - perm_index
65
+
66
+ log_bars = np.log(ohlcv_pd[["Open", "High", "Low", "Close"]])
67
+
68
+ # Start bar
69
+ start_bar = log_bars.iloc[start_index].to_numpy()
70
+
71
+ # Relative series
72
+ rel_open = (log_bars["Open"] - log_bars["Close"].shift()).to_numpy()
73
+ rel_high = (log_bars["High"] - log_bars["Open"]).to_numpy()
74
+ rel_low = (log_bars["Low"] - log_bars["Open"]).to_numpy()
75
+ rel_close = (log_bars["Close"] - log_bars["Open"]).to_numpy()
76
+
77
+ # Shuffle independently
78
+ idx = np.arange(perm_n)
79
+ rel_high = rel_high[perm_index:][np.random.permutation(idx)]
80
+ rel_low = rel_low[perm_index:][np.random.permutation(idx)]
81
+ rel_close = rel_close[perm_index:][np.random.permutation(idx)]
82
+ rel_open = rel_open[perm_index:][np.random.permutation(idx)]
83
+
84
+ # Build permuted OHLC
85
+ perm_bars = np.zeros((n_bars, 4))
86
+ perm_bars[:start_index] = log_bars.iloc[:start_index].to_numpy()
87
+ perm_bars[start_index] = start_bar
88
+
89
+ for i in range(perm_index, n_bars):
90
+ k = i - perm_index
91
+ perm_bars[i, 0] = perm_bars[i - 1, 3] + rel_open[k] # Open
92
+ perm_bars[i, 1] = perm_bars[i, 0] + rel_high[k] # High
93
+ perm_bars[i, 2] = perm_bars[i, 0] + rel_low[k] # Low
94
+ perm_bars[i, 3] = perm_bars[i, 0] + rel_close[k] # Close
95
+
96
+ perm_bars = np.exp(perm_bars)
97
+
98
+ # Rebuild OHLCV
99
+ perm_df = pd.DataFrame(
100
+ perm_bars,
101
+ columns=["Open", "High", "Low", "Close"],
102
+ )
103
+ perm_df["Volume"] = ohlcv_pd["Volume"].values
104
+
105
+ # Restore datetime structure
106
+ if is_polars:
107
+ if has_datetime_col:
108
+ perm_df.insert(0, "Datetime", time_index)
109
+ return pl.from_pandas(perm_df)
110
+ else:
111
+ if isinstance(data.index, pd.DatetimeIndex):
112
+ perm_df.index = time_index
113
+ perm_df.index.name = data.index.name or "Datetime"
114
+ elif has_datetime_col:
115
+ perm_df.insert(0, "Datetime", time_index)
116
+ return perm_df
@@ -0,0 +1,297 @@
1
+ import math
2
+ from typing import List
3
+ from statistics import mean
4
+
5
+ from investing_algorithm_framework.domain import BacktestEvaluationFocus, \
6
+ BacktestDateRange, Backtest, BacktestMetrics, OperationalException
7
+
8
+
9
+ def normalize(value, min_val, max_val):
10
+ """
11
+ Normalize a value to a range [0, 1].
12
+ """
13
+ if value is None or math.isnan(value) or math.isinf(value):
14
+ return 0
15
+ if min_val == max_val:
16
+ return 0
17
+ return (value - min_val) / (max_val - min_val)
18
+
19
+
20
+ def compute_score(metrics, weights, ranges):
21
+ """
22
+ Compute a weighted score for the given metrics.
23
+
24
+ Args:
25
+ metrics: The metrics to evaluate.
26
+ weights: The weights to apply to each metric.
27
+ ranges: The min/max ranges for each metric.
28
+
29
+ Returns:
30
+ float: The computed score.
31
+ """
32
+ score = 0
33
+ for key, weight in weights.items():
34
+ if not hasattr(metrics, key):
35
+ continue
36
+ value = getattr(metrics, key)
37
+ if value is None or (
38
+ isinstance(value, float) and
39
+ (math.isnan(value) or math.isinf(value))
40
+ ):
41
+ continue
42
+ if key in ranges:
43
+ value = normalize(value, ranges[key][0], ranges[key][1])
44
+ score += weight * value
45
+ return score
46
+
47
+
48
+ def create_weights(
49
+ focus: BacktestEvaluationFocus | str | None = None,
50
+ custom_weights: dict | None = None,
51
+ ) -> dict:
52
+ """
53
+ Utility to generate weights dicts for ranking backtests.
54
+
55
+ This function does not assign weights to every possible performance
56
+ metric. Instead, it focuses on a curated subset of commonly relevant
57
+ ones (profitability, win rate, trade frequency, and risk-adjusted returns).
58
+ The rationale is to avoid overfitting ranking logic to noisy or redundant
59
+ statistics (e.g., monthly return breakdowns, best/worst trade), while
60
+ keeping the weighting system simple and interpretable.
61
+ Users who need fine-grained control can pass `custom_weights` to fully
62
+ override defaults.
63
+
64
+ Args:
65
+ focus (BacktestEvaluationFocus | str | None): The focus for ranking.
66
+ custom_weights (dict): Full override for weights (all metrics).
67
+ If provided, it takes precedence over presets.
68
+
69
+ Returns:
70
+ dict: A dictionary of weights for ranking backtests.
71
+ """
72
+ if focus is None:
73
+ focus = BacktestEvaluationFocus.BALANCED
74
+
75
+ weights = focus.get_weights()
76
+
77
+ # if full custom dict is given → override everything
78
+ if custom_weights is not None:
79
+ weights = {**weights, **custom_weights}
80
+
81
+ return weights
82
+
83
+
84
+ def rank_results(
85
+ backtests: List[Backtest],
86
+ focus=None,
87
+ weights=None,
88
+ filter_fn=None,
89
+ backtest_date_range: BacktestDateRange = None
90
+ ) -> List[Backtest]:
91
+ """
92
+ Rank backtest results based on specified focus, weights, and filters.
93
+
94
+ Args:
95
+ backtests (List[Backtest]): List of backtest results to rank.
96
+ focus (str, optional): Focus for ranking. If None,
97
+ uses default weights. Options: "balanced", "profit",
98
+ "frequency", "risk_adjusted".
99
+ weights (dict, optional): Custom weights for ranking metrics.
100
+ If None, uses default weights based on focus.
101
+ filter_fn (callable | dict, optional): A filter to apply to
102
+ backtests before ranking.
103
+ - If callable: receives metrics and should return True/False.
104
+ - If dict: mapping {metric_name: condition_fn},
105
+ all conditions must pass.
106
+ backtest_date_range (BacktestDateRange, optional): If provided,
107
+ only backtests matching this date range are considered.
108
+
109
+ Returns:
110
+ List[Backtest]: Sorted list of backtests based on computed scores.
111
+ """
112
+
113
+ if weights is None:
114
+ weights = create_weights(focus=focus)
115
+
116
+ # Pair backtests with their metrics
117
+ paired = []
118
+ for backtest in backtests:
119
+ if backtest_date_range is not None:
120
+ metrics = backtest.get_backtest_metrics(backtest_date_range)
121
+ else:
122
+ metrics = backtest.backtest_summary
123
+
124
+ if metrics is not None:
125
+ paired.append((backtest, metrics))
126
+
127
+ # Apply filtering on metrics
128
+ if filter_fn is not None:
129
+ if callable(filter_fn):
130
+ paired = [
131
+ (bt, m) for bt, m in paired if filter_fn(m)
132
+ ]
133
+ elif isinstance(filter_fn, dict):
134
+ paired = [
135
+ (bt, m) for bt, m in paired
136
+ if all(
137
+ cond(getattr(m, key, None))
138
+ for key, cond in filter_fn.items()
139
+ )
140
+ ]
141
+
142
+ # Compute normalization ranges
143
+ ranges = {}
144
+ for key in weights:
145
+ values = [
146
+ getattr(m, key, None) for _, m in paired
147
+ ]
148
+ values = [
149
+ v for v in values
150
+ if isinstance(v, (int, float)) and v is not None
151
+ and not math.isnan(v) and not math.isinf(v)
152
+ ]
153
+ if values:
154
+ ranges[key] = (min(values), max(values))
155
+
156
+ # Sort Backtests by score
157
+ ranked = sorted(
158
+ paired,
159
+ key=lambda bm: compute_score(bm[1], weights, ranges),
160
+ reverse=True
161
+ )
162
+
163
+ return [bt for bt, _ in ranked]
164
+
165
+
166
+ def combine_backtest_metrics(
167
+ backtest_metrics: List[BacktestMetrics]
168
+ ) -> BacktestMetrics:
169
+ """
170
+ Combine backtest metrics from multiple backtests into a single list.
171
+
172
+ Args:
173
+ backtest_metrics (List[BacktestMetrics]): List of backtest
174
+ metrics to combine.
175
+
176
+ Returns:
177
+ BacktestMetrics: Combined list of backtest metrics.
178
+ """
179
+ if not backtest_metrics:
180
+ raise OperationalException("No BacktestMetrics provided")
181
+
182
+ # Helper to take mean safely
183
+
184
+ def safe_mean(values):
185
+ vals = [v for v in values if v is not None]
186
+ return mean(vals) if vals else 0.0
187
+
188
+ # Dates
189
+
190
+ start_date = min(m.backtest_start_date for m in backtest_metrics)
191
+ end_date = max(m.backtest_end_date for m in backtest_metrics)
192
+
193
+ # Aggregate
194
+ return BacktestMetrics(
195
+ backtest_start_date=start_date,
196
+ backtest_end_date=end_date,
197
+ equity_curve=[], # leave empty to avoid misleading curves
198
+ total_growth=safe_mean([m.total_growth for m in backtest_metrics]),
199
+ total_growth_percentage=safe_mean(
200
+ [m.total_growth_percentage for m in backtest_metrics]),
201
+ total_net_gain=safe_mean([m.total_net_gain for m in backtest_metrics]),
202
+ total_net_gain_percentage=safe_mean(
203
+ [m.total_net_gain_percentage for m in backtest_metrics]),
204
+ final_value=safe_mean([m.final_value for m in backtest_metrics]),
205
+ cagr=safe_mean([m.cagr for m in backtest_metrics]),
206
+ sharpe_ratio=safe_mean([m.sharpe_ratio for m in backtest_metrics]),
207
+ rolling_sharpe_ratio=[],
208
+ sortino_ratio=safe_mean([m.sortino_ratio for m in backtest_metrics]),
209
+ calmar_ratio=safe_mean([m.calmar_ratio for m in backtest_metrics]),
210
+ profit_factor=safe_mean([m.profit_factor for m in backtest_metrics]),
211
+ gross_profit=sum(m.gross_profit or 0 for m in backtest_metrics),
212
+ gross_loss=sum(m.gross_loss or 0 for m in backtest_metrics),
213
+ annual_volatility=safe_mean(
214
+ [m.annual_volatility for m in backtest_metrics]),
215
+ monthly_returns=[],
216
+ yearly_returns=[],
217
+ drawdown_series=[],
218
+ max_drawdown=max(m.max_drawdown for m in backtest_metrics),
219
+ max_drawdown_absolute=max(
220
+ m.max_drawdown_absolute for m in backtest_metrics),
221
+ max_daily_drawdown=max(m.max_daily_drawdown for m in backtest_metrics),
222
+ max_drawdown_duration=max(
223
+ m.max_drawdown_duration for m in backtest_metrics),
224
+ trades_per_year=safe_mean(
225
+ [m.trades_per_year for m in backtest_metrics]
226
+ ),
227
+ trade_per_day=safe_mean([m.trade_per_day for m in backtest_metrics]),
228
+ exposure_ratio=safe_mean(
229
+ [m.exposure_ratio for m in backtest_metrics]
230
+ ),
231
+ average_trade_gain=safe_mean(
232
+ [m.average_trade_gain for m in backtest_metrics]),
233
+ average_trade_gain_percentage=(
234
+ safe_mean(
235
+ [m.average_trade_gain_percentage for m in backtest_metrics]
236
+ )
237
+ ),
238
+ average_trade_loss=safe_mean(
239
+ [m.average_trade_loss for m in backtest_metrics]),
240
+ average_trade_loss_percentage=(
241
+ safe_mean(
242
+ [m.average_trade_loss_percentage for m in backtest_metrics]
243
+ )
244
+ ),
245
+ median_trade_return=safe_mean(
246
+ [m.median_trade_return for m in backtest_metrics]),
247
+ median_trade_return_percentage=(
248
+ safe_mean(
249
+ [m.median_trade_return_percentage for m in backtest_metrics]
250
+ )
251
+ ),
252
+ best_trade=max((
253
+ m.best_trade for m in backtest_metrics if m.best_trade),
254
+ key=lambda t: t.net_gain if t else float('-inf'),
255
+ default=None
256
+ ),
257
+ worst_trade=min(
258
+ (m.worst_trade for m in backtest_metrics if m.worst_trade),
259
+ key=lambda t: t.net_gain if t else float('inf'),
260
+ default=None
261
+ ),
262
+ average_trade_duration=safe_mean(
263
+ [m.average_trade_duration for m in backtest_metrics]),
264
+ number_of_trades=sum(m.number_of_trades for m in backtest_metrics),
265
+ win_rate=safe_mean([m.win_rate for m in backtest_metrics]),
266
+ win_loss_ratio=safe_mean([m.win_loss_ratio for m in backtest_metrics]),
267
+ percentage_winning_months=safe_mean(
268
+ [m.percentage_winning_months for m in backtest_metrics]),
269
+ percentage_winning_years=safe_mean(
270
+ [m.percentage_winning_years for m in backtest_metrics]),
271
+ average_monthly_return=safe_mean(
272
+ [m.average_monthly_return for m in backtest_metrics]),
273
+ average_monthly_return_losing_months=safe_mean(
274
+ [m.average_monthly_return_losing_months for m in backtest_metrics]
275
+ ),
276
+ average_monthly_return_winning_months=safe_mean(
277
+ [m.average_monthly_return_winning_months for m in backtest_metrics]
278
+ ),
279
+ best_month=max(
280
+ (m.best_month for m in backtest_metrics if m.best_month),
281
+ key=lambda x: x[0] if x else float('-inf'),
282
+ default=None
283
+ ),
284
+ best_year=max((m.best_year for m in backtest_metrics if m.best_year),
285
+ key=lambda x: x[0] if x else float('-inf'),
286
+ default=None),
287
+ worst_month=min(
288
+ (m.worst_month for m in backtest_metrics if m.worst_month),
289
+ key=lambda x: x[0] if x else float('inf'),
290
+ default=None
291
+ ),
292
+ worst_year=min(
293
+ (m.worst_year for m in backtest_metrics if m.worst_year),
294
+ key=lambda x: x[0] if x else float('inf'),
295
+ default=None
296
+ ),
297
+ )