investing-algorithm-framework 1.5__py3-none-any.whl → 7.25.6__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.
Files changed (276) hide show
  1. investing_algorithm_framework/__init__.py +192 -16
  2. investing_algorithm_framework/analysis/__init__.py +16 -0
  3. investing_algorithm_framework/analysis/backtest_data_ranges.py +202 -0
  4. investing_algorithm_framework/analysis/data.py +170 -0
  5. investing_algorithm_framework/analysis/markdown.py +91 -0
  6. investing_algorithm_framework/analysis/ranking.py +298 -0
  7. investing_algorithm_framework/app/__init__.py +29 -4
  8. investing_algorithm_framework/app/algorithm/__init__.py +7 -0
  9. investing_algorithm_framework/app/algorithm/algorithm.py +193 -0
  10. investing_algorithm_framework/app/algorithm/algorithm_factory.py +118 -0
  11. investing_algorithm_framework/app/app.py +2220 -379
  12. investing_algorithm_framework/app/app_hook.py +28 -0
  13. investing_algorithm_framework/app/context.py +1724 -0
  14. investing_algorithm_framework/app/eventloop.py +620 -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/time_metrics_table.py +80 -0
  31. investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +147 -0
  32. investing_algorithm_framework/app/reporting/tables/trades_table.py +75 -0
  33. investing_algorithm_framework/app/reporting/tables/utils.py +29 -0
  34. investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +154 -0
  35. investing_algorithm_framework/app/stateless/action_handlers/__init__.py +6 -3
  36. investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +1 -1
  37. investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +2 -1
  38. investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +14 -7
  39. investing_algorithm_framework/app/strategy.py +867 -60
  40. investing_algorithm_framework/app/task.py +5 -3
  41. investing_algorithm_framework/app/web/__init__.py +2 -1
  42. investing_algorithm_framework/app/web/controllers/__init__.py +2 -2
  43. investing_algorithm_framework/app/web/controllers/orders.py +3 -2
  44. investing_algorithm_framework/app/web/controllers/positions.py +2 -2
  45. investing_algorithm_framework/app/web/create_app.py +4 -2
  46. investing_algorithm_framework/app/web/schemas/position.py +1 -0
  47. investing_algorithm_framework/cli/__init__.py +0 -0
  48. investing_algorithm_framework/cli/cli.py +231 -0
  49. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +501 -0
  50. investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
  51. investing_algorithm_framework/cli/initialize_app.py +603 -0
  52. investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
  53. investing_algorithm_framework/cli/templates/app.py.template +18 -0
  54. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
  55. investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
  56. investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
  57. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  58. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  59. investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
  60. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
  61. investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
  62. investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
  63. investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
  64. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
  65. investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
  66. investing_algorithm_framework/cli/templates/env.example.template +2 -0
  67. investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
  68. investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
  69. investing_algorithm_framework/cli/templates/readme.md.template +135 -0
  70. investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
  71. investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
  72. investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
  73. investing_algorithm_framework/cli/validate_backtest_checkpoints.py +197 -0
  74. investing_algorithm_framework/create_app.py +40 -7
  75. investing_algorithm_framework/dependency_container.py +100 -47
  76. investing_algorithm_framework/domain/__init__.py +97 -30
  77. investing_algorithm_framework/domain/algorithm_id.py +69 -0
  78. investing_algorithm_framework/domain/backtesting/__init__.py +25 -0
  79. investing_algorithm_framework/domain/backtesting/backtest.py +548 -0
  80. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +113 -0
  81. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +241 -0
  82. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +470 -0
  83. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  84. investing_algorithm_framework/domain/backtesting/backtest_run.py +663 -0
  85. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  86. investing_algorithm_framework/domain/backtesting/backtest_utils.py +198 -0
  87. investing_algorithm_framework/domain/backtesting/combine_backtests.py +392 -0
  88. investing_algorithm_framework/domain/config.py +59 -136
  89. investing_algorithm_framework/domain/constants.py +18 -37
  90. investing_algorithm_framework/domain/data_provider.py +334 -0
  91. investing_algorithm_framework/domain/data_structures.py +42 -0
  92. investing_algorithm_framework/domain/exceptions.py +51 -1
  93. investing_algorithm_framework/domain/models/__init__.py +26 -19
  94. investing_algorithm_framework/domain/models/app_mode.py +34 -0
  95. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  96. investing_algorithm_framework/domain/models/data/data_source.py +222 -0
  97. investing_algorithm_framework/domain/models/data/data_type.py +46 -0
  98. investing_algorithm_framework/domain/models/event.py +35 -0
  99. investing_algorithm_framework/domain/models/market/__init__.py +5 -0
  100. investing_algorithm_framework/domain/models/market/market_credential.py +88 -0
  101. investing_algorithm_framework/domain/models/order/__init__.py +3 -4
  102. investing_algorithm_framework/domain/models/order/order.py +198 -65
  103. investing_algorithm_framework/domain/models/order/order_status.py +2 -2
  104. investing_algorithm_framework/domain/models/order/order_type.py +1 -3
  105. investing_algorithm_framework/domain/models/portfolio/__init__.py +6 -2
  106. investing_algorithm_framework/domain/models/portfolio/portfolio.py +98 -3
  107. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +37 -43
  108. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +108 -11
  109. investing_algorithm_framework/domain/models/position/__init__.py +2 -1
  110. investing_algorithm_framework/domain/models/position/position.py +20 -0
  111. investing_algorithm_framework/domain/models/position/position_size.py +41 -0
  112. investing_algorithm_framework/domain/models/position/position_snapshot.py +0 -2
  113. investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
  114. investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
  115. investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
  116. investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
  117. investing_algorithm_framework/domain/models/strategy_profile.py +19 -141
  118. investing_algorithm_framework/domain/models/time_frame.py +94 -98
  119. investing_algorithm_framework/domain/models/time_interval.py +33 -0
  120. investing_algorithm_framework/domain/models/time_unit.py +66 -2
  121. investing_algorithm_framework/domain/models/tracing/__init__.py +0 -0
  122. investing_algorithm_framework/domain/models/tracing/trace.py +23 -0
  123. investing_algorithm_framework/domain/models/trade/__init__.py +11 -0
  124. investing_algorithm_framework/domain/models/trade/trade.py +389 -0
  125. investing_algorithm_framework/domain/models/trade/trade_status.py +40 -0
  126. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +332 -0
  127. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +365 -0
  128. investing_algorithm_framework/domain/order_executor.py +112 -0
  129. investing_algorithm_framework/domain/portfolio_provider.py +118 -0
  130. investing_algorithm_framework/domain/services/__init__.py +11 -0
  131. investing_algorithm_framework/domain/services/market_credential_service.py +37 -0
  132. investing_algorithm_framework/domain/services/portfolios/__init__.py +5 -0
  133. investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +9 -0
  134. investing_algorithm_framework/domain/services/rounding_service.py +27 -0
  135. investing_algorithm_framework/domain/services/state_handler.py +38 -0
  136. investing_algorithm_framework/domain/strategy.py +1 -29
  137. investing_algorithm_framework/domain/utils/__init__.py +15 -5
  138. investing_algorithm_framework/domain/utils/csv.py +22 -0
  139. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  140. investing_algorithm_framework/domain/utils/dates.py +57 -0
  141. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  142. investing_algorithm_framework/domain/utils/polars.py +53 -0
  143. investing_algorithm_framework/domain/utils/random.py +29 -0
  144. investing_algorithm_framework/download_data.py +244 -0
  145. investing_algorithm_framework/infrastructure/__init__.py +37 -11
  146. investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
  147. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1152 -0
  148. investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
  149. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  150. investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
  151. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +86 -12
  152. investing_algorithm_framework/infrastructure/models/__init__.py +7 -3
  153. investing_algorithm_framework/infrastructure/models/order/__init__.py +2 -2
  154. investing_algorithm_framework/infrastructure/models/order/order.py +53 -53
  155. investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
  156. investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
  157. investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +1 -1
  158. investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +8 -2
  159. investing_algorithm_framework/infrastructure/models/portfolio/{portfolio.py → sql_portfolio.py} +17 -6
  160. investing_algorithm_framework/infrastructure/models/position/position_snapshot.py +3 -1
  161. investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
  162. investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
  163. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +59 -0
  164. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +55 -0
  165. investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
  166. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  167. investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
  168. investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
  169. investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
  170. investing_algorithm_framework/infrastructure/repositories/__init__.py +10 -4
  171. investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
  172. investing_algorithm_framework/infrastructure/repositories/order_repository.py +16 -5
  173. investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +2 -2
  174. investing_algorithm_framework/infrastructure/repositories/position_repository.py +11 -0
  175. investing_algorithm_framework/infrastructure/repositories/repository.py +84 -30
  176. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
  177. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +29 -0
  178. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +29 -0
  179. investing_algorithm_framework/infrastructure/services/__init__.py +9 -4
  180. investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
  181. investing_algorithm_framework/infrastructure/services/aws/state_handler.py +193 -0
  182. investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
  183. investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
  184. investing_algorithm_framework/infrastructure/services/backtesting/__init__.py +9 -0
  185. investing_algorithm_framework/infrastructure/services/backtesting/backtest_service.py +2596 -0
  186. investing_algorithm_framework/infrastructure/services/backtesting/event_backtest_service.py +285 -0
  187. investing_algorithm_framework/infrastructure/services/backtesting/vector_backtest_service.py +468 -0
  188. investing_algorithm_framework/services/__init__.py +123 -15
  189. investing_algorithm_framework/services/configuration_service.py +77 -11
  190. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  191. investing_algorithm_framework/services/data_providers/data_provider_service.py +1058 -0
  192. investing_algorithm_framework/services/market_credential_service.py +40 -0
  193. investing_algorithm_framework/services/metrics/__init__.py +119 -0
  194. investing_algorithm_framework/services/metrics/alpha.py +0 -0
  195. investing_algorithm_framework/services/metrics/beta.py +0 -0
  196. investing_algorithm_framework/services/metrics/cagr.py +60 -0
  197. investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
  198. investing_algorithm_framework/services/metrics/drawdown.py +218 -0
  199. investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
  200. investing_algorithm_framework/services/metrics/exposure.py +210 -0
  201. investing_algorithm_framework/services/metrics/generate.py +358 -0
  202. investing_algorithm_framework/services/metrics/mean_daily_return.py +84 -0
  203. investing_algorithm_framework/services/metrics/price_efficiency.py +57 -0
  204. investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
  205. investing_algorithm_framework/services/metrics/recovery.py +113 -0
  206. investing_algorithm_framework/services/metrics/returns.py +452 -0
  207. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  208. investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
  209. investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
  210. investing_algorithm_framework/services/metrics/standard_deviation.py +156 -0
  211. investing_algorithm_framework/services/metrics/trades.py +473 -0
  212. investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
  213. investing_algorithm_framework/services/metrics/ulcer.py +0 -0
  214. investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
  215. investing_algorithm_framework/services/metrics/volatility.py +118 -0
  216. investing_algorithm_framework/services/metrics/win_rate.py +177 -0
  217. investing_algorithm_framework/services/order_service/__init__.py +9 -0
  218. investing_algorithm_framework/services/order_service/order_backtest_service.py +178 -0
  219. investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
  220. investing_algorithm_framework/services/order_service/order_service.py +826 -0
  221. investing_algorithm_framework/services/portfolios/__init__.py +16 -0
  222. investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +54 -0
  223. investing_algorithm_framework/services/{portfolio_configuration_service.py → portfolios/portfolio_configuration_service.py} +27 -12
  224. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
  225. investing_algorithm_framework/services/portfolios/portfolio_service.py +188 -0
  226. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +136 -0
  227. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +182 -0
  228. investing_algorithm_framework/services/positions/__init__.py +7 -0
  229. investing_algorithm_framework/services/positions/position_service.py +210 -0
  230. investing_algorithm_framework/services/repository_service.py +8 -2
  231. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  232. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +117 -0
  233. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
  234. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
  235. investing_algorithm_framework/services/trade_service/__init__.py +9 -0
  236. investing_algorithm_framework/services/trade_service/trade_service.py +1099 -0
  237. investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
  238. investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
  239. investing_algorithm_framework-7.25.6.dist-info/METADATA +535 -0
  240. investing_algorithm_framework-7.25.6.dist-info/RECORD +268 -0
  241. {investing_algorithm_framework-1.5.dist-info → investing_algorithm_framework-7.25.6.dist-info}/WHEEL +1 -2
  242. investing_algorithm_framework-7.25.6.dist-info/entry_points.txt +3 -0
  243. investing_algorithm_framework/app/algorithm.py +0 -630
  244. investing_algorithm_framework/domain/models/backtest_profile.py +0 -414
  245. investing_algorithm_framework/domain/models/market_data/__init__.py +0 -11
  246. investing_algorithm_framework/domain/models/market_data/asset_price.py +0 -50
  247. investing_algorithm_framework/domain/models/market_data/ohlcv.py +0 -105
  248. investing_algorithm_framework/domain/models/market_data/order_book.py +0 -63
  249. investing_algorithm_framework/domain/models/market_data/ticker.py +0 -92
  250. investing_algorithm_framework/domain/models/order/order_fee.py +0 -45
  251. investing_algorithm_framework/domain/models/trade.py +0 -78
  252. investing_algorithm_framework/domain/models/trading_data_types.py +0 -47
  253. investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
  254. investing_algorithm_framework/domain/singleton.py +0 -9
  255. investing_algorithm_framework/domain/utils/backtesting.py +0 -82
  256. investing_algorithm_framework/infrastructure/models/order/order_fee.py +0 -21
  257. investing_algorithm_framework/infrastructure/repositories/order_fee_repository.py +0 -15
  258. investing_algorithm_framework/infrastructure/services/market_backtest_service.py +0 -360
  259. investing_algorithm_framework/infrastructure/services/market_service.py +0 -410
  260. investing_algorithm_framework/infrastructure/services/performance_service.py +0 -192
  261. investing_algorithm_framework/services/backtest_service.py +0 -268
  262. investing_algorithm_framework/services/market_data_service.py +0 -77
  263. investing_algorithm_framework/services/order_backtest_service.py +0 -122
  264. investing_algorithm_framework/services/order_service.py +0 -752
  265. investing_algorithm_framework/services/portfolio_service.py +0 -164
  266. investing_algorithm_framework/services/portfolio_snapshot_service.py +0 -68
  267. investing_algorithm_framework/services/position_cost_service.py +0 -5
  268. investing_algorithm_framework/services/position_service.py +0 -63
  269. investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -225
  270. investing_algorithm_framework-1.5.dist-info/AUTHORS.md +0 -8
  271. investing_algorithm_framework-1.5.dist-info/METADATA +0 -230
  272. investing_algorithm_framework-1.5.dist-info/RECORD +0 -119
  273. investing_algorithm_framework-1.5.dist-info/top_level.txt +0 -1
  274. /investing_algorithm_framework/{infrastructure/services/performance_backtest_service.py → app/reporting/tables/stop_loss_table.py} +0 -0
  275. /investing_algorithm_framework/services/{position_snapshot_service.py → positions/position_snapshot_service.py} +0 -0
  276. {investing_algorithm_framework-1.5.dist-info → investing_algorithm_framework-7.25.6.dist-info}/LICENSE +0 -0
@@ -9,6 +9,7 @@ class SQLPositionRepository(Repository):
9
9
  DEFAULT_NOT_FOUND_MESSAGE = "Position not found"
10
10
 
11
11
  def _apply_query_params(self, db, query, query_params):
12
+ id_query_param = self.get_query_param("id", query_params)
12
13
  amount_query_param = self.get_query_param("amount", query_params)
13
14
  symbol_query_param = self.get_query_param("symbol", query_params)
14
15
  portfolio_query_param = self.get_query_param("portfolio", query_params)
@@ -20,6 +21,10 @@ class SQLPositionRepository(Repository):
20
21
  amount_lte_query_param = self.get_query_param(
21
22
  "amount_lte", query_params
22
23
  )
24
+ order_id_query_param = self.get_query_param("order_id", query_params)
25
+
26
+ if id_query_param:
27
+ query = query.filter_by(id=id_query_param)
23
28
 
24
29
  if amount_query_param:
25
30
  query = query.filter(
@@ -51,5 +56,11 @@ class SQLPositionRepository(Repository):
51
56
  query = query.filter(
52
57
  cast(SQLPosition.amount, Numeric) <= amount_lte_query_param
53
58
  )
59
+ # Filter by order_id, orders is a one-to-many relationship
60
+ # with 3 position
61
+ if order_id_query_param:
62
+ query = query.filter(
63
+ SQLPosition.orders.any(id=order_id_query_param)
64
+ )
54
65
 
55
66
  return query
@@ -1,38 +1,55 @@
1
1
  import logging
2
- from abc import ABC
2
+ from abc import ABC, abstractmethod
3
3
  from typing import Callable
4
+ from dateutil.parser import parse
4
5
 
5
6
  from sqlalchemy.exc import SQLAlchemyError
6
7
  from werkzeug.datastructures import MultiDict
7
8
 
8
- from investing_algorithm_framework.domain import ApiException, \
9
+ from investing_algorithm_framework.domain import OperationalException, \
9
10
  DEFAULT_PAGE_VALUE, DEFAULT_PER_PAGE_VALUE
10
11
  from investing_algorithm_framework.infrastructure.database import Session
11
12
 
12
13
  logger = logging.getLogger("investing_algorithm_framework")
13
14
 
14
15
 
16
+ def convert_datetime_fields(data, datetime_fields):
17
+ for field in datetime_fields:
18
+ if field in data and isinstance(data[field], str):
19
+ try:
20
+ data[field] = parse(data[field])
21
+ except Exception:
22
+ pass # Ignore if not a valid datetime string
23
+ return data
24
+
25
+
15
26
  class Repository(ABC):
16
27
  base_class: Callable
17
28
  DEFAULT_NOT_FOUND_MESSAGE = "The requested resource was not found"
18
29
  DEFAULT_PER_PAGE = DEFAULT_PER_PAGE_VALUE
19
30
  DEFAULT_PAGE = DEFAULT_PAGE_VALUE
20
31
 
21
- def create(self, data):
32
+ def create(self, data, save=True):
33
+ created_object = self.base_class(**data)
34
+ if save:
35
+ with Session() as db:
36
+ try:
37
+ db.add(created_object)
38
+ db.commit()
39
+ return self.get(created_object.id)
40
+ except SQLAlchemyError as e:
41
+ logger.error(e)
42
+ db.rollback()
43
+ raise OperationalException("Error creating object")
22
44
 
23
- with Session() as db:
24
- try:
25
- created_object = self.base_class(**data)
26
- db.add(created_object)
27
- db.commit()
28
- return self.get(created_object.id)
29
- except SQLAlchemyError as e:
30
- logger.error(e)
31
- db.rollback()
32
- raise ApiException("Error creating object")
45
+ return created_object
33
46
 
34
47
  def update(self, object_id, data):
35
-
48
+ # List all datetime fields for your model
49
+ datetime_fields = [
50
+ "created_at", "updated_at", "closed_at", "opened_at"
51
+ ]
52
+ data = convert_datetime_fields(data, datetime_fields)
36
53
  with Session() as db:
37
54
  try:
38
55
  update_object = self.get(object_id)
@@ -43,7 +60,7 @@ class Repository(ABC):
43
60
  except SQLAlchemyError as e:
44
61
  logger.error(e)
45
62
  db.rollback()
46
- raise ApiException("Error updating object")
63
+ raise OperationalException("Error updating object")
47
64
 
48
65
  def update_all(self, query_params, data):
49
66
 
@@ -63,7 +80,7 @@ class Repository(ABC):
63
80
  except SQLAlchemyError as e:
64
81
  logger.error(e)
65
82
  db.rollback()
66
- raise ApiException("Error updating object")
83
+ raise OperationalException("Error updating object")
67
84
 
68
85
  def delete(self, object_id):
69
86
 
@@ -71,17 +88,18 @@ class Repository(ABC):
71
88
  try:
72
89
  delete_object = self.get(object_id)
73
90
  db.delete(delete_object)
91
+ db.commit()
74
92
  return delete_object
75
93
  except SQLAlchemyError as e:
76
94
  logger.error(e)
77
95
  db.rollback()
78
- raise ApiException("Error deleting object")
96
+ raise OperationalException("Error deleting object")
79
97
 
80
98
  def delete_all(self, query_params):
81
99
 
82
100
  with Session() as db:
83
101
  if query_params is None:
84
- raise ApiException("No parameters are required")
102
+ raise OperationalException("No parameters are required")
85
103
 
86
104
  try:
87
105
  query_set = db.query(self.base_class)
@@ -96,7 +114,7 @@ class Repository(ABC):
96
114
  except SQLAlchemyError as e:
97
115
  logger.error(e)
98
116
  db.rollback()
99
- raise ApiException("Error deleting all objects")
117
+ raise OperationalException("Error deleting all objects")
100
118
 
101
119
  def get_all(self, query_params=None):
102
120
  query_params = MultiDict(query_params)
@@ -110,7 +128,7 @@ class Repository(ABC):
110
128
  return query_set.all()
111
129
  except SQLAlchemyError as e:
112
130
  logger.error(e)
113
- raise ApiException("Error getting all objects")
131
+ raise OperationalException("Error getting all objects")
114
132
 
115
133
  def get(self, object_id):
116
134
 
@@ -119,14 +137,15 @@ class Repository(ABC):
119
137
  .first()
120
138
 
121
139
  if not match:
122
- raise ApiException(
123
- self.DEFAULT_NOT_FOUND_MESSAGE, status_code=404
140
+ raise OperationalException(
141
+ self.DEFAULT_NOT_FOUND_MESSAGE
124
142
  )
125
143
 
126
144
  return match
127
145
 
146
+ @abstractmethod
128
147
  def _apply_query_params(self, db, query, query_params):
129
- return query
148
+ raise NotImplementedError()
130
149
 
131
150
  def apply_query_params(self, db, query, query_params):
132
151
 
@@ -137,7 +156,6 @@ class Repository(ABC):
137
156
  return query
138
157
 
139
158
  def exists(self, query_params):
140
-
141
159
  with Session() as db:
142
160
  try:
143
161
  query = db.query(self.base_class)
@@ -145,10 +163,13 @@ class Repository(ABC):
145
163
  return query.first() is not None
146
164
  except SQLAlchemyError as e:
147
165
  logger.error(e)
148
- raise ApiException("Error checking if object exists")
166
+ raise OperationalException("Error checking if object exists")
149
167
 
150
168
  def find(self, query_params):
151
169
 
170
+ if query_params is None or len(query_params) == 0:
171
+ raise OperationalException("Find requires query parameters")
172
+
152
173
  with Session() as db:
153
174
  try:
154
175
  query = db.query(self.base_class)
@@ -156,12 +177,12 @@ class Repository(ABC):
156
177
  result = query.first()
157
178
 
158
179
  if result is None:
159
- raise ApiException(self.DEFAULT_NOT_FOUND_MESSAGE)
180
+ raise OperationalException(self.DEFAULT_NOT_FOUND_MESSAGE)
160
181
 
161
182
  return result
162
183
  except SQLAlchemyError as e:
163
184
  logger.error(e)
164
- raise ApiException(self.DEFAULT_NOT_FOUND_MESSAGE)
185
+ raise OperationalException(self.DEFAULT_NOT_FOUND_MESSAGE)
165
186
 
166
187
  def count(self, query_params=None):
167
188
 
@@ -172,7 +193,7 @@ class Repository(ABC):
172
193
  return query.count()
173
194
  except SQLAlchemyError as e:
174
195
  logger.error(e)
175
- raise ApiException("Error counting objects")
196
+ raise OperationalException("Error counting objects")
176
197
 
177
198
  def normalize_query_param(self, value):
178
199
  """
@@ -193,7 +214,7 @@ class Repository(ABC):
193
214
  if not throw_exception:
194
215
  return False
195
216
 
196
- raise ApiException(f"{key} is not specified")
217
+ raise OperationalException(f"{key} is not specified")
197
218
  else:
198
219
  return True
199
220
 
@@ -213,7 +234,7 @@ class Repository(ABC):
213
234
  def get_query_param(self, key, params, default=None, many=False):
214
235
  boolean_array = ["true", "false"]
215
236
 
216
- if params is None:
237
+ if params is None or key not in params:
217
238
  return default
218
239
 
219
240
  params = self.normalize_query(params)
@@ -243,3 +264,36 @@ class Repository(ABC):
243
264
  return new_selection[0]
244
265
 
245
266
  return new_selection
267
+
268
+ def save(self, object_to_save):
269
+ """
270
+ Save an object to the database with SQLAlchemy.
271
+
272
+ Args:
273
+ object_to_save: instance of the object to save.
274
+
275
+ Returns:
276
+ Object: The saved object.
277
+ """
278
+ with Session() as db:
279
+ try:
280
+ db.add(object_to_save)
281
+ db.commit()
282
+ return self.get(object_to_save.id)
283
+ except SQLAlchemyError as e:
284
+ logger.error(e)
285
+ db.rollback()
286
+ raise OperationalException("Error saving object")
287
+
288
+ def save_objects(self, objects):
289
+
290
+ with Session() as db:
291
+ try:
292
+ for object in objects:
293
+ db.add(object)
294
+ db.commit()
295
+ return objects
296
+ except SQLAlchemyError as e:
297
+ logger.error(e)
298
+ db.rollback()
299
+ raise OperationalException("Error saving objects")
@@ -0,0 +1,71 @@
1
+ import logging
2
+ from sqlalchemy.exc import SQLAlchemyError
3
+
4
+ from investing_algorithm_framework.domain import TradeStatus, ApiException
5
+ from investing_algorithm_framework.infrastructure.models import SQLPosition, \
6
+ SQLPortfolio, SQLTrade, SQLOrder
7
+ from investing_algorithm_framework.infrastructure.database import Session
8
+
9
+ from .repository import Repository
10
+
11
+ logger = logging.getLogger("investing_algorithm_framework")
12
+
13
+
14
+ class SQLTradeRepository(Repository):
15
+ base_class = SQLTrade
16
+ DEFAULT_NOT_FOUND_MESSAGE = "The requested trade was not found"
17
+
18
+ def _apply_query_params(self, db, query, query_params):
19
+ portfolio_query_param = self.get_query_param(
20
+ "portfolio_id", query_params
21
+ )
22
+ status_query_param = self.get_query_param("status", query_params)
23
+ target_symbol = self.get_query_param(
24
+ "target_symbol", query_params
25
+ )
26
+ trading_symbol = self.get_query_param("trading_symbol", query_params)
27
+ order_id_query_param = self.get_query_param("order_id", query_params)
28
+
29
+ if order_id_query_param:
30
+ query = query.filter(SQLTrade.orders.any(id=order_id_query_param))
31
+
32
+ if portfolio_query_param is not None:
33
+ portfolio = db.query(SQLPortfolio).filter_by(
34
+ id=portfolio_query_param
35
+ ).first()
36
+
37
+ if portfolio is None:
38
+ raise ApiException("Portfolio not found")
39
+
40
+ # Query trades belonging to the portfolio
41
+ query = db.query(SQLTrade).join(SQLOrder, SQLTrade.orders) \
42
+ .join(SQLPosition, SQLOrder.position_id == SQLPosition.id) \
43
+ .filter(SQLPosition.portfolio_id == portfolio.id)
44
+
45
+ if status_query_param:
46
+ status = TradeStatus.from_value(status_query_param)
47
+ # Explicitly filter on SQLTrade.status
48
+ query = query.filter(SQLTrade.status == status.value)
49
+
50
+ if target_symbol:
51
+ # Explicitly filter on SQLTrade.target_symbol
52
+ query = query.filter(SQLTrade.target_symbol == target_symbol)
53
+
54
+ if trading_symbol:
55
+ # Explicitly filter on SQLTrade.trading_symbol
56
+ query = query.filter(SQLTrade.trading_symbol == trading_symbol)
57
+
58
+ return query
59
+
60
+ def add_order_to_trade(self, trade, order):
61
+ with Session() as db:
62
+ try:
63
+ db.add(order)
64
+ db.add(trade)
65
+ trade.orders.append(order)
66
+ db.commit()
67
+ return trade
68
+ except SQLAlchemyError as e:
69
+ logger.error(f"Error saving trade: {e}")
70
+ db.rollback()
71
+ raise ApiException("Error saving trade")
@@ -0,0 +1,29 @@
1
+ import logging
2
+
3
+ from investing_algorithm_framework.infrastructure.models import \
4
+ SQLTradeStopLoss
5
+
6
+ from .repository import Repository
7
+
8
+ logger = logging.getLogger("investing_algorithm_framework")
9
+
10
+
11
+ class SQLTradeStopLossRepository(Repository):
12
+ base_class = SQLTradeStopLoss
13
+ DEFAULT_NOT_FOUND_MESSAGE = "The requested trade stop loss was not found"
14
+
15
+ def _apply_query_params(self, db, query, query_params):
16
+ trade_query_param = self.get_query_param("trade_id", query_params)
17
+ triggered_query_param = self.get_query_param(
18
+ "triggered", query_params
19
+ )
20
+
21
+ if trade_query_param:
22
+ query = query.filter(
23
+ SQLTradeStopLoss.trade_id == trade_query_param
24
+ )
25
+
26
+ if triggered_query_param is not None:
27
+ query = query.filter_by(triggered=triggered_query_param)
28
+
29
+ return query
@@ -0,0 +1,29 @@
1
+ import logging
2
+
3
+ from investing_algorithm_framework.infrastructure.models import \
4
+ SQLTradeTakeProfit
5
+
6
+ from .repository import Repository
7
+
8
+ logger = logging.getLogger("investing_algorithm_framework")
9
+
10
+
11
+ class SQLTradeTakeProfitRepository(Repository):
12
+ base_class = SQLTradeTakeProfit
13
+ DEFAULT_NOT_FOUND_MESSAGE = "The requested trade take profit was not found"
14
+
15
+ def _apply_query_params(self, db, query, query_params):
16
+ trade_query_param = self.get_query_param("trade_id", query_params)
17
+ triggered_query_param = self.get_query_param(
18
+ "triggered", query_params
19
+ )
20
+
21
+ if trade_query_param:
22
+ query = query.filter(
23
+ SQLTradeTakeProfit.trade_id == trade_query_param
24
+ )
25
+
26
+ if triggered_query_param is not None:
27
+ query = query.filter_by(triggered=triggered_query_param)
28
+
29
+ return query
@@ -1,5 +1,10 @@
1
- from .market_service import MarketService
2
- from .market_backtest_service import MarketBacktestService
3
- from .performance_service import PerformanceService
1
+ from .azure import AzureBlobStorageStateHandler
2
+ from .aws import AWSS3StorageStateHandler
3
+ from .backtesting import BacktestService, EventBacktestService
4
4
 
5
- __all__ = ["MarketService", "MarketBacktestService", "PerformanceService"]
5
+ __all__ = [
6
+ "AzureBlobStorageStateHandler",
7
+ "AWSS3StorageStateHandler",
8
+ "BacktestService",
9
+ "EventBacktestService",
10
+ ]
@@ -0,0 +1,6 @@
1
+ from .state_handler import AWSS3StorageStateHandler
2
+
3
+
4
+ __all__ = [
5
+ "AWSS3StorageStateHandler",
6
+ ]
@@ -0,0 +1,193 @@
1
+ import os
2
+ import logging
3
+ import boto3
4
+ import stat
5
+ from botocore.exceptions import NoCredentialsError, PartialCredentialsError
6
+ from investing_algorithm_framework.domain import OperationalException, \
7
+ StateHandler
8
+
9
+ logger = logging.getLogger("investing_algorithm_framework")
10
+
11
+
12
+ def _fix_permissions(target_directory: str):
13
+ """
14
+ Fix permissions on downloaded files to make them writable.
15
+
16
+ Args:
17
+ target_directory (str): Directory to fix permissions for
18
+ """
19
+ try:
20
+ # Fix the target directory itself
21
+ os.chmod(target_directory, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
22
+
23
+ # Recursively fix all subdirectories and files
24
+ for root, dirs, files in os.walk(target_directory):
25
+ # Fix current directory permissions
26
+ os.chmod(root, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
27
+
28
+ # Fix all subdirectories
29
+ for dir_name in dirs:
30
+ dir_path = os.path.join(root, dir_name)
31
+ os.chmod(dir_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
32
+
33
+ # Fix all files - make them readable and writable
34
+ for file_name in files:
35
+ file_path = os.path.join(root, file_name)
36
+ os.chmod(
37
+ file_path,
38
+ stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
39
+ )
40
+
41
+ logger.info(f"Update permissions for {target_directory}")
42
+ except Exception as e:
43
+ logger.warning(f"Error fixing permissions: {e}")
44
+
45
+
46
+ class AWSS3StorageStateHandler(StateHandler):
47
+ """
48
+ A state handler for AWS S3 storage.
49
+
50
+ This class provides methods to save and load state to and from
51
+ AWS S3 storage.
52
+
53
+ Attributes:
54
+ bucket_name (str): The name of the AWS S3 bucket.
55
+ """
56
+
57
+ def __init__(self, bucket_name: str = None):
58
+ self.bucket_name = bucket_name
59
+ self.s3_client = None
60
+
61
+ def initialize(self):
62
+ self.bucket_name = self.bucket_name or os.getenv("AWS_S3_BUCKET_NAME")
63
+
64
+ if not self.bucket_name:
65
+ raise OperationalException(
66
+ "AWS S3 state handler requires a bucket_name para or the "
67
+ "AWS_S3_BUCKET_NAME environment variable needs to be set "
68
+ "in the environment."
69
+ )
70
+
71
+ self.s3_client = boto3.client("s3")
72
+
73
+ def save(self, source_directory: str):
74
+ """
75
+ Save the state to AWS S3.
76
+
77
+ Args:
78
+ source_directory (str): Directory to save the state
79
+
80
+ Returns:
81
+ None
82
+ """
83
+ logger.info("Saving state to AWS S3 ...")
84
+
85
+ try:
86
+ # Walk through the directory
87
+ for root, _, files in os.walk(source_directory):
88
+ for file_name in files:
89
+ # Get the full path of the file
90
+ file_path = os.path.join(root, file_name)
91
+
92
+ # Construct the S3 object key (relative path in the bucket)
93
+ s3_key = os.path.relpath(file_path, source_directory)
94
+ # Convert to forward slashes for S3 compatibility
95
+ s3_key = s3_key.replace(os.sep, "/")
96
+
97
+ self.s3_client.upload_file(
98
+ file_path,
99
+ self.bucket_name,
100
+ s3_key,
101
+ ExtraArgs={'ACL': 'private'}
102
+ )
103
+
104
+ except (NoCredentialsError, PartialCredentialsError) as ex:
105
+ logger.error(f"Error saving state to AWS S3: {ex}")
106
+ raise OperationalException(
107
+ "AWS credentials are missing or incomplete."
108
+ )
109
+ except Exception as ex:
110
+ logger.error(f"Error saving state to AWS S3: {ex}")
111
+ raise ex
112
+
113
+ def load(self, target_directory: str):
114
+ """
115
+ Load the state from AWS S3.
116
+ """
117
+ logger.info("Loading state from AWS S3 ...")
118
+
119
+ try:
120
+ if not os.path.exists(target_directory):
121
+ os.makedirs(
122
+ target_directory,
123
+ mode=stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO
124
+ )
125
+
126
+ os.chmod(
127
+ target_directory, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO
128
+ )
129
+
130
+ response = self.s3_client.list_objects_v2(Bucket=self.bucket_name)
131
+
132
+ if "Contents" in response:
133
+ for obj in response["Contents"]:
134
+ s3_key = obj["Key"]
135
+ # Convert S3 forward slashes to OS-specific separators
136
+ file_path = os.path.join(
137
+ target_directory, s3_key.replace("/", os.sep)
138
+ )
139
+
140
+ os.makedirs(
141
+ os.path.dirname(file_path),
142
+ exist_ok=True,
143
+ mode=stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO
144
+ )
145
+
146
+ self.s3_client.download_file(
147
+ self.bucket_name, s3_key, file_path
148
+ )
149
+
150
+ if os.path.isfile(file_path):
151
+ os.chmod(
152
+ file_path,
153
+ stat.S_IRUSR |
154
+ stat.S_IWUSR |
155
+ stat.S_IRGRP |
156
+ stat.S_IROTH
157
+ )
158
+ else:
159
+ os.chmod(
160
+ file_path,
161
+ stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO
162
+ )
163
+
164
+ # Final recursive fix
165
+ _fix_permissions(target_directory)
166
+
167
+ # Add write permission to database file
168
+ db_file = os.path.join(
169
+ target_directory, "databases", "prod-database.sqlite3"
170
+ )
171
+ if os.path.exists(db_file):
172
+ os.chmod(
173
+ db_file,
174
+ stat.S_IRUSR |
175
+ stat.S_IWUSR |
176
+ stat.S_IRGRP |
177
+ stat.S_IWGRP |
178
+ stat.S_IROTH |
179
+ stat.S_IWOTH
180
+ )
181
+ logger.info(
182
+ f"Database file permissions "
183
+ f"after fix: {oct(os.stat(db_file).st_mode)}"
184
+ )
185
+
186
+ except (NoCredentialsError, PartialCredentialsError) as ex:
187
+ logger.error(f"Error loading state from AWS S3: {ex}")
188
+ raise OperationalException(
189
+ "AWS credentials are missing or incomplete."
190
+ )
191
+ except Exception as ex:
192
+ logger.error(f"Error loading state from AWS S3: {ex}")
193
+ raise ex
@@ -0,0 +1,5 @@
1
+ from .state_handler import AzureBlobStorageStateHandler
2
+
3
+ __all__ = [
4
+ "AzureBlobStorageStateHandler"
5
+ ]