investing-algorithm-framework 3.7.0__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 (256) hide show
  1. investing_algorithm_framework/__init__.py +168 -45
  2. investing_algorithm_framework/app/__init__.py +32 -1
  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 +1933 -589
  12. investing_algorithm_framework/app/app_hook.py +28 -0
  13. investing_algorithm_framework/app/context.py +1725 -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/action_handlers/__init__.py +4 -2
  37. investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +1 -1
  38. investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +1 -1
  39. investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +14 -7
  40. investing_algorithm_framework/app/strategy.py +664 -84
  41. investing_algorithm_framework/app/task.py +5 -3
  42. investing_algorithm_framework/app/web/__init__.py +2 -1
  43. investing_algorithm_framework/app/web/create_app.py +4 -2
  44. investing_algorithm_framework/cli/__init__.py +0 -0
  45. investing_algorithm_framework/cli/cli.py +226 -0
  46. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +501 -0
  47. investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
  48. investing_algorithm_framework/cli/initialize_app.py +603 -0
  49. investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
  50. investing_algorithm_framework/cli/templates/app.py.template +18 -0
  51. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
  52. investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
  53. investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
  54. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  55. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  56. investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
  57. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
  58. investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
  59. investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
  60. investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
  61. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
  62. investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
  63. investing_algorithm_framework/cli/templates/env.example.template +2 -0
  64. investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
  65. investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
  66. investing_algorithm_framework/cli/templates/readme.md.template +135 -0
  67. investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
  68. investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
  69. investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
  70. investing_algorithm_framework/create_app.py +40 -6
  71. investing_algorithm_framework/dependency_container.py +72 -56
  72. investing_algorithm_framework/domain/__init__.py +71 -47
  73. investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
  74. investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
  75. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
  76. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
  77. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
  78. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  79. investing_algorithm_framework/domain/backtesting/backtest_run.py +605 -0
  80. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  81. investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
  82. investing_algorithm_framework/domain/config.py +59 -91
  83. investing_algorithm_framework/domain/constants.py +13 -38
  84. investing_algorithm_framework/domain/data_provider.py +334 -0
  85. investing_algorithm_framework/domain/data_structures.py +3 -2
  86. investing_algorithm_framework/domain/exceptions.py +51 -1
  87. investing_algorithm_framework/domain/models/__init__.py +17 -12
  88. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  89. investing_algorithm_framework/domain/models/data/data_source.py +214 -0
  90. investing_algorithm_framework/domain/models/data/data_type.py +46 -0
  91. investing_algorithm_framework/domain/models/event.py +35 -0
  92. investing_algorithm_framework/domain/models/market/market_credential.py +55 -1
  93. investing_algorithm_framework/domain/models/order/order.py +77 -83
  94. investing_algorithm_framework/domain/models/order/order_status.py +2 -2
  95. investing_algorithm_framework/domain/models/order/order_type.py +1 -3
  96. investing_algorithm_framework/domain/models/portfolio/portfolio.py +81 -3
  97. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +26 -3
  98. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +108 -11
  99. investing_algorithm_framework/domain/models/position/__init__.py +2 -1
  100. investing_algorithm_framework/domain/models/position/position.py +12 -0
  101. investing_algorithm_framework/domain/models/position/position_size.py +41 -0
  102. investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
  103. investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
  104. investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
  105. investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
  106. investing_algorithm_framework/domain/models/strategy_profile.py +19 -151
  107. investing_algorithm_framework/domain/models/time_frame.py +37 -0
  108. investing_algorithm_framework/domain/models/time_interval.py +33 -0
  109. investing_algorithm_framework/domain/models/time_unit.py +66 -2
  110. investing_algorithm_framework/domain/models/trade/__init__.py +8 -1
  111. investing_algorithm_framework/domain/models/trade/trade.py +295 -171
  112. investing_algorithm_framework/domain/models/trade/trade_status.py +9 -2
  113. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +332 -0
  114. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +365 -0
  115. investing_algorithm_framework/domain/order_executor.py +112 -0
  116. investing_algorithm_framework/domain/portfolio_provider.py +118 -0
  117. investing_algorithm_framework/domain/services/__init__.py +2 -9
  118. investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +0 -6
  119. investing_algorithm_framework/domain/services/state_handler.py +38 -0
  120. investing_algorithm_framework/domain/strategy.py +1 -29
  121. investing_algorithm_framework/domain/utils/__init__.py +12 -7
  122. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  123. investing_algorithm_framework/domain/utils/dates.py +57 -0
  124. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  125. investing_algorithm_framework/domain/utils/polars.py +53 -0
  126. investing_algorithm_framework/domain/utils/random.py +29 -0
  127. investing_algorithm_framework/download_data.py +108 -0
  128. investing_algorithm_framework/infrastructure/__init__.py +31 -18
  129. investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
  130. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1143 -0
  131. investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
  132. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  133. investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
  134. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +86 -12
  135. investing_algorithm_framework/infrastructure/models/__init__.py +6 -11
  136. investing_algorithm_framework/infrastructure/models/order/__init__.py +2 -1
  137. investing_algorithm_framework/infrastructure/models/order/order.py +35 -49
  138. investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
  139. investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
  140. investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +1 -1
  141. investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +8 -0
  142. investing_algorithm_framework/infrastructure/models/portfolio/{portfolio.py → sql_portfolio.py} +17 -5
  143. investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
  144. investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
  145. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +59 -0
  146. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +55 -0
  147. investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
  148. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  149. investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
  150. investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
  151. investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
  152. investing_algorithm_framework/infrastructure/repositories/__init__.py +8 -0
  153. investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
  154. investing_algorithm_framework/infrastructure/repositories/order_repository.py +5 -0
  155. investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +1 -1
  156. investing_algorithm_framework/infrastructure/repositories/position_repository.py +11 -0
  157. investing_algorithm_framework/infrastructure/repositories/repository.py +81 -27
  158. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
  159. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +29 -0
  160. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +29 -0
  161. investing_algorithm_framework/infrastructure/services/__init__.py +4 -4
  162. investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
  163. investing_algorithm_framework/infrastructure/services/aws/state_handler.py +113 -0
  164. investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
  165. investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
  166. investing_algorithm_framework/services/__init__.py +113 -16
  167. investing_algorithm_framework/services/backtesting/__init__.py +0 -7
  168. investing_algorithm_framework/services/backtesting/backtest_service.py +566 -359
  169. investing_algorithm_framework/services/configuration_service.py +77 -11
  170. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  171. investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
  172. investing_algorithm_framework/services/market_credential_service.py +16 -1
  173. investing_algorithm_framework/services/metrics/__init__.py +114 -0
  174. investing_algorithm_framework/services/metrics/alpha.py +0 -0
  175. investing_algorithm_framework/services/metrics/beta.py +0 -0
  176. investing_algorithm_framework/services/metrics/cagr.py +60 -0
  177. investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
  178. investing_algorithm_framework/services/metrics/drawdown.py +181 -0
  179. investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
  180. investing_algorithm_framework/services/metrics/exposure.py +210 -0
  181. investing_algorithm_framework/services/metrics/generate.py +358 -0
  182. investing_algorithm_framework/services/metrics/mean_daily_return.py +83 -0
  183. investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
  184. investing_algorithm_framework/services/metrics/recovery.py +113 -0
  185. investing_algorithm_framework/services/metrics/returns.py +452 -0
  186. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  187. investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
  188. investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
  189. investing_algorithm_framework/services/metrics/standard_deviation.py +157 -0
  190. investing_algorithm_framework/services/metrics/trades.py +500 -0
  191. investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
  192. investing_algorithm_framework/services/metrics/ulcer.py +0 -0
  193. investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
  194. investing_algorithm_framework/services/metrics/volatility.py +97 -0
  195. investing_algorithm_framework/services/metrics/win_rate.py +177 -0
  196. investing_algorithm_framework/services/order_service/__init__.py +3 -1
  197. investing_algorithm_framework/services/order_service/order_backtest_service.py +76 -89
  198. investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
  199. investing_algorithm_framework/services/order_service/order_service.py +407 -326
  200. investing_algorithm_framework/services/portfolios/__init__.py +3 -1
  201. investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +37 -3
  202. investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +22 -8
  203. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
  204. investing_algorithm_framework/services/portfolios/portfolio_service.py +96 -28
  205. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +97 -28
  206. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +116 -313
  207. investing_algorithm_framework/services/positions/__init__.py +7 -0
  208. investing_algorithm_framework/services/positions/position_service.py +210 -0
  209. investing_algorithm_framework/services/repository_service.py +8 -2
  210. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  211. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +113 -0
  212. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
  213. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
  214. investing_algorithm_framework/services/trade_service/__init__.py +7 -1
  215. investing_algorithm_framework/services/trade_service/trade_service.py +1013 -315
  216. investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
  217. investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
  218. investing_algorithm_framework-7.19.15.dist-info/METADATA +537 -0
  219. investing_algorithm_framework-7.19.15.dist-info/RECORD +263 -0
  220. investing_algorithm_framework-7.19.15.dist-info/entry_points.txt +3 -0
  221. investing_algorithm_framework/app/algorithm.py +0 -1105
  222. investing_algorithm_framework/domain/graphs.py +0 -382
  223. investing_algorithm_framework/domain/metrics/__init__.py +0 -6
  224. investing_algorithm_framework/domain/models/backtesting/__init__.py +0 -11
  225. investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +0 -43
  226. investing_algorithm_framework/domain/models/backtesting/backtest_position.py +0 -120
  227. investing_algorithm_framework/domain/models/backtesting/backtest_report.py +0 -580
  228. investing_algorithm_framework/domain/models/backtesting/backtest_reports_evaluation.py +0 -243
  229. investing_algorithm_framework/domain/models/trading_data_types.py +0 -47
  230. investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
  231. investing_algorithm_framework/domain/services/market_data_sources.py +0 -344
  232. investing_algorithm_framework/domain/services/market_service.py +0 -153
  233. investing_algorithm_framework/domain/singleton.py +0 -9
  234. investing_algorithm_framework/domain/utils/backtesting.py +0 -472
  235. investing_algorithm_framework/infrastructure/models/market_data_sources/__init__.py +0 -12
  236. investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +0 -559
  237. investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py +0 -254
  238. investing_algorithm_framework/infrastructure/models/market_data_sources/us_treasury_yield.py +0 -47
  239. investing_algorithm_framework/infrastructure/services/market_service/__init__.py +0 -5
  240. investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +0 -455
  241. investing_algorithm_framework/infrastructure/services/performance_service/__init__.py +0 -7
  242. investing_algorithm_framework/infrastructure/services/performance_service/backtest_performance_service.py +0 -2
  243. investing_algorithm_framework/infrastructure/services/performance_service/performance_service.py +0 -350
  244. investing_algorithm_framework/services/backtesting/backtest_report_writer_service.py +0 -53
  245. investing_algorithm_framework/services/backtesting/graphs.py +0 -61
  246. investing_algorithm_framework/services/market_data_source_service/__init__.py +0 -8
  247. investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py +0 -150
  248. investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +0 -189
  249. investing_algorithm_framework/services/position_service.py +0 -31
  250. investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -264
  251. investing_algorithm_framework-3.7.0.dist-info/METADATA +0 -339
  252. investing_algorithm_framework-3.7.0.dist-info/RECORD +0 -147
  253. /investing_algorithm_framework/{domain → services}/metrics/price_efficiency.py +0 -0
  254. /investing_algorithm_framework/services/{position_snapshot_service.py → positions/position_snapshot_service.py} +0 -0
  255. {investing_algorithm_framework-3.7.0.dist-info → investing_algorithm_framework-7.19.15.dist-info}/LICENSE +0 -0
  256. {investing_algorithm_framework-3.7.0.dist-info → investing_algorithm_framework-7.19.15.dist-info}/WHEEL +0 -0
@@ -1,38 +1,55 @@
1
1
  import logging
2
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,8 +137,8 @@ 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
@@ -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,7 +1,7 @@
1
- from .market_service import CCXTMarketService
2
- from .performance_service import PerformanceService
1
+ from .azure import AzureBlobStorageStateHandler
2
+ from .aws import AWSS3StorageStateHandler
3
3
 
4
4
  __all__ = [
5
- "PerformanceService",
6
- "CCXTMarketService"
5
+ "AzureBlobStorageStateHandler",
6
+ "AWSS3StorageStateHandler"
7
7
  ]
@@ -0,0 +1,6 @@
1
+ from .state_handler import AWSS3StorageStateHandler
2
+
3
+
4
+ __all__ = [
5
+ "AWSS3StorageStateHandler",
6
+ ]
@@ -0,0 +1,113 @@
1
+ import os
2
+ import logging
3
+ import boto3
4
+ from botocore.exceptions import NoCredentialsError, PartialCredentialsError
5
+ from investing_algorithm_framework.domain import OperationalException, \
6
+ StateHandler
7
+
8
+ logger = logging.getLogger("investing_algorithm_framework")
9
+
10
+
11
+ class AWSS3StorageStateHandler(StateHandler):
12
+ """
13
+ A state handler for AWS S3 storage.
14
+
15
+ This class provides methods to save and load state to and from
16
+ AWS S3 storage.
17
+
18
+ Attributes:
19
+ bucket_name (str): The name of the AWS S3 bucket.
20
+ """
21
+
22
+ def __init__(self, bucket_name: str = None):
23
+ self.bucket_name = bucket_name
24
+ self.s3_client = None
25
+
26
+ def initialize(self):
27
+ self.bucket_name = self.bucket_name or os.getenv("AWS_S3_BUCKET_NAME")
28
+
29
+ if not self.bucket_name:
30
+ raise OperationalException(
31
+ "AWS S3 state handler requires a bucket name or the "
32
+ "AWS_S3_BUCKET_NAME environment variable to be set."
33
+ )
34
+
35
+ self.s3_client = boto3.client("s3")
36
+
37
+ def save(self, source_directory: str):
38
+ """
39
+ Save the state to AWS S3.
40
+
41
+ Args:
42
+ source_directory (str): Directory to save the state
43
+
44
+ Returns:
45
+ None
46
+ """
47
+ logger.info("Saving state to AWS S3 ...")
48
+
49
+ try:
50
+ # Walk through the directory
51
+ for root, _, files in os.walk(source_directory):
52
+ for file_name in files:
53
+ # Get the full path of the file
54
+ file_path = os.path.join(root, file_name)
55
+
56
+ # Construct the S3 object key (relative path in the bucket)
57
+ s3_key = os.path.relpath(file_path, source_directory)\
58
+ .replace("\\", "/")
59
+
60
+ # Upload the file
61
+ self.s3_client.upload_file(
62
+ file_path, self.bucket_name, s3_key
63
+ )
64
+
65
+ except (NoCredentialsError, PartialCredentialsError) as ex:
66
+ logger.error(f"Error saving state to AWS S3: {ex}")
67
+ raise OperationalException(
68
+ "AWS credentials are missing or incomplete."
69
+ )
70
+ except Exception as ex:
71
+ logger.error(f"Error saving state to AWS S3: {ex}")
72
+ raise ex
73
+
74
+ def load(self, target_directory: str):
75
+ """
76
+ Load the state from AWS S3.
77
+
78
+ Args:
79
+ target_directory (str): Directory to load the state
80
+
81
+ Returns:
82
+ None
83
+ """
84
+ logger.info("Loading state from AWS S3 ...")
85
+
86
+ try:
87
+ # Ensure the local directory exists
88
+ if not os.path.exists(target_directory):
89
+ os.makedirs(target_directory)
90
+
91
+ # List and download objects
92
+ response = self.s3_client.list_objects_v2(Bucket=self.bucket_name)
93
+ if "Contents" in response:
94
+ for obj in response["Contents"]:
95
+ s3_key = obj["Key"]
96
+ file_path = os.path.join(target_directory, s3_key)
97
+
98
+ # Create subdirectories locally if needed
99
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
100
+
101
+ # Download object to file
102
+ self.s3_client.download_file(
103
+ self.bucket_name, s3_key, file_path
104
+ )
105
+
106
+ except (NoCredentialsError, PartialCredentialsError) as ex:
107
+ logger.error(f"Error loading state from AWS S3: {ex}")
108
+ raise OperationalException(
109
+ "AWS credentials are missing or incomplete."
110
+ )
111
+ except Exception as ex:
112
+ logger.error(f"Error loading state from AWS S3: {ex}")
113
+ raise ex
@@ -0,0 +1,5 @@
1
+ from .state_handler import AzureBlobStorageStateHandler
2
+
3
+ __all__ = [
4
+ "AzureBlobStorageStateHandler"
5
+ ]
@@ -0,0 +1,158 @@
1
+ import os
2
+ import logging
3
+
4
+ from azure.storage.blob import ContainerClient
5
+ from investing_algorithm_framework.domain import OperationalException, \
6
+ StateHandler
7
+
8
+ logger = logging.getLogger("investing_algorithm_framework")
9
+
10
+
11
+ class AzureBlobStorageStateHandler(StateHandler):
12
+ """
13
+ A state handler for Azure Blob Storage.
14
+
15
+ This class provides methods to save and load state to and from
16
+ Azure Blob Storage.
17
+
18
+ Attributes:
19
+ connection_string (str): The connection string for Azure Blob Storage.
20
+ container_name (str): The name of the Azure Blob Storage container.
21
+ """
22
+
23
+ def __init__(
24
+ self, connection_string: str = None, container_name: str = None
25
+ ):
26
+ self.connection_string = connection_string
27
+ self.container_name = container_name
28
+
29
+ def initialize(self):
30
+ """
31
+ Internal helper to initialize the state handler.
32
+ """
33
+
34
+ if self.connection_string is None:
35
+
36
+ # Check if environment variable is set
37
+ self.connection_string = \
38
+ os.getenv("AZURE_STORAGE_CONNECTION_STRING")
39
+
40
+ if self.connection_string is None:
41
+ raise OperationalException(
42
+ "Azure Blob Storage state handler requires" +
43
+ " a connection string or an environment" +
44
+ " variable AZURE_STORAGE_CONNECTION_STRING to be set."
45
+ )
46
+
47
+ if self.container_name is None:
48
+
49
+ # Check if environment variable is set
50
+ self.container_name = os.getenv("AZURE_STORAGE_CONTAINER_NAME")
51
+
52
+ if self.container_name is None:
53
+ raise OperationalException(
54
+ "Azure Blob Storage state handler requires a" +
55
+ " container name or an environment" +
56
+ " variable AZURE_STORAGE_CONTAINER_NAME to be set."
57
+ )
58
+
59
+ def save(self, source_directory: str):
60
+ """
61
+ Save the state to Azure Blob Storage.
62
+
63
+ Parameters:
64
+ source_directory (str): Directory to save the state
65
+
66
+ Returns:
67
+ None
68
+ """
69
+ logger.info("Saving state to Azure Blob Storage ...")
70
+
71
+ try:
72
+ container_client = self._create_container_client()
73
+
74
+ # Create container if it does not exist
75
+ if not container_client.exists():
76
+ container_client.create_container()
77
+
78
+ # Walk through the directory
79
+ for root, _, files in os.walk(source_directory):
80
+ for file_name in files:
81
+ # Get the full path of the file
82
+ file_path = os.path.join(root, file_name)
83
+
84
+ # Construct the blob name (relative path in the container)
85
+ blob_name = os.path.relpath(file_path, source_directory)\
86
+ .replace("\\", "/")
87
+
88
+ # Upload the file
89
+ with open(file_path, "rb") as data:
90
+ container_client.upload_blob(
91
+ name=blob_name, data=data, overwrite=True
92
+ )
93
+
94
+ except Exception as ex:
95
+ logger.error(f"Error saving state to Azure Blob Storage: {ex}")
96
+ raise ex
97
+
98
+ def load(self, target_directory: str):
99
+ """
100
+ Load the state from Azure Blob Storage.
101
+
102
+ Parameters:
103
+ target_directory (str): Directory to load the state
104
+
105
+ Returns:
106
+ None
107
+ """
108
+ logger.info("Loading state from Azure Blob Storage ...")
109
+
110
+ try:
111
+ container_client = self._create_container_client()
112
+
113
+ # Ensure the local directory exists
114
+ if not os.path.exists(target_directory):
115
+ os.makedirs(target_directory)
116
+
117
+ # List and download blobs
118
+ for blob in container_client.list_blobs():
119
+ blob_name = blob.name
120
+ blob_file_path = os.path.join(target_directory, blob_name)
121
+
122
+ # Create subdirectories locally if needed
123
+ os.makedirs(os.path.dirname(blob_file_path), exist_ok=True)
124
+
125
+ # Download blob to file
126
+ with open(blob_file_path, "wb") as file:
127
+ blob_client = container_client.get_blob_client(blob_name)
128
+ file.write(blob_client.download_blob().readall())
129
+
130
+ except Exception as ex:
131
+ logger.error(f"Error loading state from Azure Blob Storage: {ex}")
132
+ raise ex
133
+
134
+ def _create_container_client(self):
135
+ """
136
+ Internal helper to create a Container clinet.
137
+
138
+ Returns:
139
+ ContainerClient
140
+ """
141
+
142
+ # Ensure the container exists
143
+ try:
144
+ container_client = ContainerClient.from_connection_string(
145
+ conn_str=self.connection_string,
146
+ container_name=self.container_name
147
+ )
148
+ container_client.create_container(timeout=10)
149
+ except Exception as e:
150
+
151
+ if "ContainerAlreadyExists" in str(e):
152
+ pass
153
+ else:
154
+ raise OperationalException(
155
+ f"Error occurred while creating the container: {e}"
156
+ )
157
+
158
+ return container_client