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,499 @@
1
+ import json
2
+ import os
3
+ import re
4
+ import subprocess
5
+ import time
6
+ import zipfile
7
+
8
+ import boto3
9
+ import click
10
+
11
+ from investing_algorithm_framework.domain import AWS_S3_STATE_BUCKET_NAME
12
+
13
+
14
+ def sanitize_bucket_name(name: str) -> str:
15
+ """
16
+ Sanitize the bucket name to conform to AWS S3 bucket naming rules.
17
+ AWS S3 bucket names must be globally unique, lowercase,
18
+ and can only contain lowercase letters, numbers, and hyphens.
19
+ They must start and end with a lowercase letter or number.
20
+
21
+ An exception is raised if the name is invalid.
22
+
23
+ Args:
24
+ name: str, the name to sanitize.
25
+ Returns:
26
+ str: The sanitized bucket name.
27
+ """
28
+ # Lowercase, replace invalid chars with hyphen, and strip multiple hyphens
29
+ name = name.lower()
30
+ name = re.sub(r'[^a-z0-9-]', '-', name)
31
+ name = re.sub(r'-+', '-', name).strip('-')
32
+
33
+ # Check if the name exceeds the length limit
34
+ if len(name) < 3 or len(name) > 63:
35
+ raise ValueError(
36
+ "Bucket name must be between 3 and 63 characters long."
37
+ )
38
+
39
+ return name # Enforce length limit
40
+
41
+
42
+ def zip_code(source_dir, zip_file, ignore_dirs=None):
43
+ """
44
+ Recursively zips the contents of source_dir into zip_file,
45
+ preserving directory structure — suitable for AWS Lambda deployment.
46
+
47
+ Args:
48
+ source_dir: str, the directory containing the Lambda function code.
49
+ zip_file: str, the path where the zip file will be created.
50
+ ignore_dirs: list, directories to ignore when zipping the code.
51
+
52
+ Returns:
53
+ None
54
+ """
55
+ click.echo(f"Zipping code from {source_dir} to {zip_file}")
56
+
57
+ # Function should recursively zip all files and directories
58
+ with zipfile.ZipFile(zip_file, 'w', zipfile.ZIP_DEFLATED) as zf:
59
+ for root, _, files in os.walk(source_dir):
60
+
61
+ # Skip ignored directories
62
+ if ignore_dirs is not None:
63
+ relative_root = os.path.relpath(root, source_dir)
64
+ if any(
65
+ relative_root.startswith(ignore) for ignore in ignore_dirs
66
+ ):
67
+ click.echo(f"Ignoring directory: {relative_root}")
68
+ continue
69
+
70
+ for file in files:
71
+ click.echo(f"Adding {file} to zip")
72
+ file_path = os.path.join(root, file)
73
+ # Preserve the directory structure in the zip file
74
+ arcname = os.path.relpath(file_path, source_dir)
75
+ zf.write(file_path, arcname)
76
+
77
+
78
+ def create_iam_role(role_name):
79
+ """
80
+ Function to create an IAM role for AWS Lambda execution and access
81
+ to the S3 bucket for state handling.
82
+ This role allows Lambda functions to assume the
83
+ role and execute with basic permissions and access to CloudWatch logs.
84
+ Next to that the role will be able to access the S3 bucket
85
+ for state handling.
86
+
87
+ Args:
88
+ role_name: str, the name of the IAM role to create.
89
+
90
+ Returns:
91
+ str: The ARN of the created or existing IAM role.
92
+ """
93
+ iam = boto3.client("iam")
94
+
95
+ assume_policy = {
96
+ "Version": "2012-10-17",
97
+ "Statement": [{
98
+ "Effect": "Allow",
99
+ "Principal": {"Service": "lambda.amazonaws.com"},
100
+ "Action": "sts:AssumeRole"
101
+ }]
102
+ }
103
+
104
+ try:
105
+ role = iam.create_role(
106
+ RoleName=role_name,
107
+ AssumeRolePolicyDocument=json.dumps(assume_policy)
108
+ )
109
+ click.echo(f"Created IAM Role: {role_name}")
110
+ iam.attach_role_policy(
111
+ RoleName=role_name,
112
+ PolicyArn=("arn:aws:iam::aws:policy/"
113
+ "service-role/AWSLambdaBasicExecutionRole")
114
+ )
115
+ iam.attach_role_policy(
116
+ RoleName=role_name,
117
+ PolicyArn="arn:aws:iam::aws:policy/AmazonS3FullAccess"
118
+ )
119
+ time.sleep(10) # IAM roles may take a few seconds to propagate
120
+ return role["Role"]["Arn"]
121
+ except iam.exceptions.EntityAlreadyExistsException:
122
+ click.echo(f"IAM Role {role_name} already exists.")
123
+ return iam.get_role(RoleName=role_name)['Role']['Arn']
124
+
125
+
126
+ def deploy_lambda(
127
+ function_name,
128
+ region,
129
+ image_uri,
130
+ role_arn,
131
+ memory_size,
132
+ runtime="python3.10",
133
+ env_vars=None,
134
+ ):
135
+ """
136
+ Function to deploy a trading bot created with the framework to AWS Lambda.
137
+
138
+ Args:
139
+ function_name: str, the name of the Lambda function to
140
+ create or update.
141
+ region: str, the AWS region where the Lambda
142
+ function will be deployed.
143
+ image_uri: str, the URI of the Docker image in ECR.
144
+ role_arn: str, the ARN of the IAM role that Lambda will assume.
145
+ runtime: str, the runtime environment for the
146
+ Lambda function (default is "python3.10").
147
+ env_vars: dict, optional environment variables
148
+ to set for the Lambda function.
149
+ memory_size: int, the amount of memory allocated
150
+ to the Lambda function.
151
+
152
+ Returns:
153
+ None
154
+ """
155
+ lambda_client = boto3.client('lambda', region_name=region)
156
+
157
+ try:
158
+ lambda_client.get_function(FunctionName=function_name)
159
+ click.echo(f"Function {function_name} already exists. Updating...")
160
+
161
+ lambda_client.update_function_code(
162
+ FunctionName=function_name,
163
+ ImageUri=image_uri
164
+ )
165
+ wait_for_lambda_update(lambda_client, function_name, timeout=120)
166
+ lambda_client.update_function_configuration(
167
+ FunctionName=function_name,
168
+ Environment={'Variables': env_vars or {}}
169
+ )
170
+ except lambda_client.exceptions.ResourceNotFoundException:
171
+ click.echo(f"Creating new function: {function_name}")
172
+
173
+ try:
174
+ click.echo(
175
+ "Creating new container-based "
176
+ f"Lambda function: {function_name}"
177
+ )
178
+ lambda_client.create_function(
179
+ FunctionName=function_name,
180
+ Role=role_arn,
181
+ PackageType="Image",
182
+ Code={"ImageUri": image_uri},
183
+ Timeout=900,
184
+ MemorySize=memory_size,
185
+ Environment={"Variables": env_vars or {}}
186
+ )
187
+ except Exception as e:
188
+ raise click.ClickException(
189
+ f"Failed to create Lambda function: {e}"
190
+ )
191
+
192
+ click.echo(f"Lambda function '{function_name}' deployed successfully.")
193
+
194
+
195
+ def s3_bucket_exists(bucket_name, region):
196
+ """
197
+ Function to check if an S3 bucket exists.
198
+
199
+ Args:
200
+ bucket_name: str, the name of the S3 bucket to check.
201
+ region: str, the AWS region where the bucket is located.
202
+
203
+ Returns:
204
+ bool: True if the bucket exists, False otherwise.
205
+ """
206
+ s3 = boto3.client('s3', region_name=region)
207
+ try:
208
+ s3.head_bucket(Bucket=bucket_name)
209
+ return True
210
+ except s3.exceptions.ClientError as e:
211
+ if e.response['Error']['Code'] == '404':
212
+ return False
213
+ raise
214
+
215
+
216
+ def create_ecr_repository(repository_name, region):
217
+ """
218
+ Function to create an ECR repository for storing Docker images.
219
+ It checks if the repository already exists and creates it if not.
220
+
221
+ Args:
222
+ repository_name: str, the name of the ECR repository to create.
223
+ region: str, the AWS region where the repository will be created.
224
+
225
+ Returns:
226
+ None
227
+ """
228
+
229
+ ecr = boto3.client('ecr', region_name=region)
230
+ try:
231
+ response = ecr.create_repository(repositoryName=repository_name)
232
+ click.echo(
233
+ "Created ECR repository: "
234
+ f"{response['repository']['repositoryUri']}"
235
+ )
236
+ except ecr.exceptions.RepositoryAlreadyExistsException:
237
+ click.echo(f"ECR repository {repository_name} already exists.")
238
+
239
+
240
+ def build_and_push_docker_image(
241
+ repository_name, region, dockerfile_path='Dockerfile', tag='latest'
242
+ ):
243
+ """
244
+ Function to build a Docker image and push it to an ECR repository.
245
+
246
+ Args:
247
+ repository_name: str, the name of the ECR repository.
248
+ region: str, the AWS region where the repository is located.
249
+ dockerfile_path: str, path to the Dockerfile (default is 'Dockerfile').
250
+ tag: str, the tag for the Docker image (default is 'latest').
251
+
252
+ Returns:
253
+ None
254
+ """
255
+
256
+ # Retrieve the ECR repository URI
257
+ ecr = boto3.client('ecr', region_name=region)
258
+ try:
259
+ response = ecr.describe_repositories(repositoryNames=[repository_name])
260
+ repository_uri = response['repositories'][0]['repositoryUri']
261
+ except ecr.exceptions.RepositoryNotFoundException:
262
+ raise click.ClickException(
263
+ f"ECR repository {repository_name} does "
264
+ f"not exist in region {region}."
265
+ )
266
+
267
+ # Authenticate Docker to the ECR registry
268
+ auth = ecr.get_authorization_token()
269
+ proxy = auth['authorizationData'][0]['proxyEndpoint']
270
+
271
+ click.echo(f"Authenticating Docker to ECR repository {repository_name}...")
272
+ subprocess.run(
273
+ f"aws ecr get-login-password --region {region} | "
274
+ f"docker login --username AWS --password-stdin {proxy}",
275
+ shell=True, check=True
276
+ )
277
+
278
+ click.echo(f"Building Docker image {repository_name}:{tag}...")
279
+ # Build and push Docker image with the docker file path
280
+ image_full_uri = f"{repository_uri}:{tag}"
281
+ subprocess.run([
282
+ "docker", "build",
283
+ "--platform=linux/amd64",
284
+ "-t", image_full_uri,
285
+ "-f", dockerfile_path,
286
+ "."
287
+ ], check=True)
288
+ subprocess.run(f"docker push {image_full_uri}", shell=True, check=True)
289
+ return image_full_uri
290
+
291
+
292
+ def create_s3_bucket(bucket_name, region):
293
+ """
294
+ Function to create an S3 bucket for storing Lambda function code.
295
+
296
+ Args:
297
+ bucket_name: str, the name of the S3 bucket to create.
298
+ region: str, the AWS region where the bucket will be created.
299
+
300
+ Returns:
301
+ None
302
+ """
303
+ bucket_name = sanitize_bucket_name(bucket_name)
304
+ s3 = boto3.client('s3', region_name=region)
305
+ try:
306
+ s3.create_bucket(
307
+ Bucket=bucket_name,
308
+ CreateBucketConfiguration={'LocationConstraint': region}
309
+ )
310
+ click.echo(f"Created S3 bucket: {bucket_name}")
311
+ except s3.exceptions.BucketAlreadyExists:
312
+ click.echo(f"S3 bucket {bucket_name} already exists.")
313
+
314
+
315
+ def read_env_file(env_path):
316
+ """
317
+ Function to read environment variables from a .env file.
318
+
319
+ Args:
320
+ env_path: str, the path to the .env file.
321
+
322
+ Returns:
323
+ None
324
+ """
325
+ if not os.path.exists(env_path):
326
+ click.echo(f"No .env file found at {env_path}")
327
+ return {}
328
+ with open(env_path) as f:
329
+ return dict(
330
+ line.strip().split("=", 1)
331
+ for line in f if "=" in line
332
+ )
333
+
334
+
335
+ def check_lambda_permissions(required_actions=None):
336
+ """
337
+ Checks whether the current IAM user/role has the required permissions
338
+ to interact with AWS Lambda. Raises a ClickException if not.
339
+
340
+ Args:
341
+ required_actions: List of required IAM actions (strings).
342
+ Default: basic Lambda deploy permissions.
343
+
344
+ Raises:
345
+ click.ClickException: If user lacks one or more required permissions.
346
+ """
347
+ if required_actions is None:
348
+ required_actions = [
349
+ "lambda:GetFunction",
350
+ "lambda:UpdateFunctionCode",
351
+ "lambda:UpdateFunctionConfiguration",
352
+ "lambda:CreateFunction",
353
+ "ecr:CreateRepository"
354
+ ]
355
+
356
+ sts = boto3.client("sts")
357
+ iam = boto3.client("iam")
358
+
359
+ # Get caller identity ARN (could be user or role)
360
+ identity_arn = sts.get_caller_identity()["Arn"]
361
+
362
+ # Extract the principal name from the ARN
363
+ if ":user/" in identity_arn:
364
+ principal_type = "user"
365
+ principal_name = identity_arn.split(":user/")[1]
366
+ elif ":role/" in identity_arn:
367
+ principal_type = "role"
368
+ principal_name = identity_arn.split(":role/")[1]
369
+ else:
370
+ raise click.ClickException(
371
+ f"Unsupported identity type: {identity_arn}"
372
+ )
373
+
374
+ click.echo(
375
+ f"Checking permissions for IAM {principal_type}: {principal_name}"
376
+ )
377
+
378
+ # Simulate the policy and check each required action
379
+ failed_actions = []
380
+
381
+ for action in required_actions:
382
+ response = iam.simulate_principal_policy(
383
+ PolicySourceArn=identity_arn,
384
+ ActionNames=[action]
385
+ )
386
+ decision = response["EvaluationResults"][0]["EvalDecision"]
387
+
388
+ if decision != "allowed":
389
+ failed_actions.append(action)
390
+
391
+ if failed_actions:
392
+ raise click.ClickException(
393
+ f"Your IAM identity '{principal_name}' is missing "
394
+ "required permissions: "
395
+ f"{', '.join(failed_actions)}"
396
+ ". Please ensure you have the necessary permissions "
397
+ "to deploy your trading bot to Lambda functions."
398
+ )
399
+
400
+
401
+ def wait_for_lambda_update(lambda_client, function_name, timeout=60):
402
+ """
403
+ Wait until the Lambda function update completes or times out.
404
+
405
+ Args:
406
+ lambda_client: boto3 Lambda client.
407
+ function_name: str, the name of the Lambda function to check.
408
+ timeout: int, maximum time to wait for the
409
+ update (default is 60 seconds).
410
+
411
+ Returns:
412
+ None: If the update is successful.
413
+ """
414
+ start = time.time()
415
+
416
+ while time.time() - start < timeout:
417
+ response = lambda_client.get_function_configuration(
418
+ FunctionName=function_name
419
+ )
420
+ status = response.get("LastUpdateStatus")
421
+ if status == "Successful":
422
+ return
423
+ elif status == "Failed":
424
+ raise click.ClickException("Previous Lambda update failed.")
425
+
426
+ time.sleep(2)
427
+
428
+ raise click.ClickException(
429
+ "Timed out waiting for Lambda update to complete."
430
+ )
431
+
432
+
433
+ def command(
434
+ lambda_function_name,
435
+ region,
436
+ project_dir=None,
437
+ memory_size=3000
438
+ ):
439
+ """
440
+ Command-line tool for deploying a trading bot to AWS Lambda.
441
+
442
+ Args:
443
+ lambda_function_name:
444
+ region: str, the AWS region where the Lambda function will be deployed.
445
+ project_dir: str, the directory containing the Lambda function code.
446
+ If None, it defaults to the current directory.
447
+ memory_size: int, the amount of memory allocated
448
+ to the Lambda function
449
+
450
+ Returns:
451
+ None
452
+ """
453
+ if project_dir is None:
454
+ # Get the current working directory
455
+ project_dir = os.getcwd()
456
+
457
+ check_lambda_permissions()
458
+
459
+ click.echo(
460
+ "Deploying to AWS Lambda "
461
+ f"function: {lambda_function_name} in region: {region}"
462
+ )
463
+ click.echo(f"Project directory: {project_dir}")
464
+
465
+ # Create s3 bucket for state handler
466
+ bucket_name = f"{lambda_function_name}-state-handler-{region}"
467
+ bucket_name = sanitize_bucket_name(bucket_name)
468
+
469
+ if not s3_bucket_exists(bucket_name, region):
470
+ create_s3_bucket(bucket_name, region)
471
+
472
+ env_vars = read_env_file(
473
+ env_path=os.path.join(project_dir, ".env") if project_dir else ".env"
474
+ )
475
+ for key, value in env_vars.items():
476
+ click.echo(f"{key}={value}")
477
+
478
+ click.echo("Read the following environment variables from .env file:")
479
+ click.echo("Adding S3 bucket name to environment variables")
480
+ env_vars[AWS_S3_STATE_BUCKET_NAME] = bucket_name
481
+
482
+ click.echo("Building and pushing Docker image to ECR")
483
+ create_ecr_repository(lambda_function_name, region)
484
+ image_uri = build_and_push_docker_image(
485
+ lambda_function_name,
486
+ region,
487
+ dockerfile_path=os.path.join(project_dir, "Dockerfile"),
488
+ tag="latest"
489
+ )
490
+ click.echo("Creating IAM role for Lambda execution")
491
+ role_arn = create_iam_role("lambda-execution-role")
492
+ deploy_lambda(
493
+ lambda_function_name,
494
+ image_uri=image_uri,
495
+ role_arn=role_arn,
496
+ env_vars=env_vars,
497
+ region=region,
498
+ memory_size=memory_size
499
+ )