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.
- investing_algorithm_framework/__init__.py +197 -0
- investing_algorithm_framework/app/__init__.py +47 -0
- investing_algorithm_framework/app/algorithm/__init__.py +7 -0
- investing_algorithm_framework/app/algorithm/algorithm.py +239 -0
- investing_algorithm_framework/app/algorithm/algorithm_factory.py +114 -0
- investing_algorithm_framework/app/analysis/__init__.py +15 -0
- investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
- investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
- investing_algorithm_framework/app/analysis/permutation.py +116 -0
- investing_algorithm_framework/app/analysis/ranking.py +297 -0
- investing_algorithm_framework/app/app.py +2204 -0
- investing_algorithm_framework/app/app_hook.py +28 -0
- investing_algorithm_framework/app/context.py +1667 -0
- investing_algorithm_framework/app/eventloop.py +590 -0
- investing_algorithm_framework/app/reporting/__init__.py +27 -0
- investing_algorithm_framework/app/reporting/ascii.py +921 -0
- investing_algorithm_framework/app/reporting/backtest_report.py +349 -0
- investing_algorithm_framework/app/reporting/charts/__init__.py +19 -0
- investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +74 -0
- investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
- investing_algorithm_framework/app/reporting/charts/monthly_returns_heatmap.py +70 -0
- investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
- investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +79 -0
- investing_algorithm_framework/app/reporting/charts/yearly_returns_barchart.py +55 -0
- investing_algorithm_framework/app/reporting/generate.py +185 -0
- investing_algorithm_framework/app/reporting/tables/__init__.py +11 -0
- investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +217 -0
- investing_algorithm_framework/app/reporting/tables/stop_loss_table.py +0 -0
- investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +80 -0
- investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +147 -0
- investing_algorithm_framework/app/reporting/tables/trades_table.py +75 -0
- investing_algorithm_framework/app/reporting/tables/utils.py +29 -0
- investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +154 -0
- investing_algorithm_framework/app/stateless/__init__.py +35 -0
- investing_algorithm_framework/app/stateless/action_handlers/__init__.py +84 -0
- investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +8 -0
- investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +15 -0
- investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +40 -0
- investing_algorithm_framework/app/stateless/exception_handler.py +40 -0
- investing_algorithm_framework/app/strategy.py +675 -0
- investing_algorithm_framework/app/task.py +41 -0
- investing_algorithm_framework/app/web/__init__.py +5 -0
- investing_algorithm_framework/app/web/controllers/__init__.py +13 -0
- investing_algorithm_framework/app/web/controllers/orders.py +20 -0
- investing_algorithm_framework/app/web/controllers/portfolio.py +20 -0
- investing_algorithm_framework/app/web/controllers/positions.py +18 -0
- investing_algorithm_framework/app/web/create_app.py +20 -0
- investing_algorithm_framework/app/web/error_handler.py +59 -0
- investing_algorithm_framework/app/web/responses.py +20 -0
- investing_algorithm_framework/app/web/run_strategies.py +4 -0
- investing_algorithm_framework/app/web/schemas/__init__.py +12 -0
- investing_algorithm_framework/app/web/schemas/order.py +12 -0
- investing_algorithm_framework/app/web/schemas/portfolio.py +22 -0
- investing_algorithm_framework/app/web/schemas/position.py +15 -0
- investing_algorithm_framework/app/web/setup_cors.py +6 -0
- investing_algorithm_framework/cli/__init__.py +0 -0
- investing_algorithm_framework/cli/cli.py +207 -0
- investing_algorithm_framework/cli/deploy_to_aws_lambda.py +499 -0
- investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
- investing_algorithm_framework/cli/initialize_app.py +603 -0
- investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
- investing_algorithm_framework/cli/templates/app.py.template +18 -0
- investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
- investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
- investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
- investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
- investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
- investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
- investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
- investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
- investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
- investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
- investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
- investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
- investing_algorithm_framework/cli/templates/env.example.template +2 -0
- investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
- investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
- investing_algorithm_framework/cli/templates/readme.md.template +135 -0
- investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
- investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
- investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
- investing_algorithm_framework/create_app.py +54 -0
- investing_algorithm_framework/dependency_container.py +155 -0
- investing_algorithm_framework/domain/__init__.py +148 -0
- investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
- investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
- investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
- investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
- investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
- investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
- investing_algorithm_framework/domain/backtesting/backtest_run.py +435 -0
- investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
- investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
- investing_algorithm_framework/domain/config.py +111 -0
- investing_algorithm_framework/domain/constants.py +83 -0
- investing_algorithm_framework/domain/data_provider.py +334 -0
- investing_algorithm_framework/domain/data_structures.py +42 -0
- investing_algorithm_framework/domain/decimal_parsing.py +40 -0
- investing_algorithm_framework/domain/exceptions.py +112 -0
- investing_algorithm_framework/domain/models/__init__.py +43 -0
- investing_algorithm_framework/domain/models/app_mode.py +34 -0
- investing_algorithm_framework/domain/models/base_model.py +25 -0
- investing_algorithm_framework/domain/models/data/__init__.py +7 -0
- investing_algorithm_framework/domain/models/data/data_source.py +214 -0
- investing_algorithm_framework/domain/models/data/data_type.py +46 -0
- investing_algorithm_framework/domain/models/event.py +35 -0
- investing_algorithm_framework/domain/models/market/__init__.py +5 -0
- investing_algorithm_framework/domain/models/market/market_credential.py +88 -0
- investing_algorithm_framework/domain/models/order/__init__.py +6 -0
- investing_algorithm_framework/domain/models/order/order.py +384 -0
- investing_algorithm_framework/domain/models/order/order_side.py +36 -0
- investing_algorithm_framework/domain/models/order/order_status.py +37 -0
- investing_algorithm_framework/domain/models/order/order_type.py +30 -0
- investing_algorithm_framework/domain/models/portfolio/__init__.py +9 -0
- investing_algorithm_framework/domain/models/portfolio/portfolio.py +169 -0
- investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +93 -0
- investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +208 -0
- investing_algorithm_framework/domain/models/position/__init__.py +4 -0
- investing_algorithm_framework/domain/models/position/position.py +68 -0
- investing_algorithm_framework/domain/models/position/position_snapshot.py +47 -0
- investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
- investing_algorithm_framework/domain/models/strategy_profile.py +33 -0
- investing_algorithm_framework/domain/models/time_frame.py +153 -0
- investing_algorithm_framework/domain/models/time_interval.py +124 -0
- investing_algorithm_framework/domain/models/time_unit.py +149 -0
- investing_algorithm_framework/domain/models/tracing/__init__.py +0 -0
- investing_algorithm_framework/domain/models/tracing/trace.py +23 -0
- investing_algorithm_framework/domain/models/trade/__init__.py +13 -0
- investing_algorithm_framework/domain/models/trade/trade.py +388 -0
- investing_algorithm_framework/domain/models/trade/trade_risk_type.py +34 -0
- investing_algorithm_framework/domain/models/trade/trade_status.py +40 -0
- investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +267 -0
- investing_algorithm_framework/domain/models/trade/trade_take_profit.py +303 -0
- investing_algorithm_framework/domain/order_executor.py +112 -0
- investing_algorithm_framework/domain/portfolio_provider.py +118 -0
- investing_algorithm_framework/domain/positions/__init__.py +4 -0
- investing_algorithm_framework/domain/positions/position_size.py +41 -0
- investing_algorithm_framework/domain/services/__init__.py +11 -0
- investing_algorithm_framework/domain/services/market_credential_service.py +37 -0
- investing_algorithm_framework/domain/services/portfolios/__init__.py +5 -0
- investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +9 -0
- investing_algorithm_framework/domain/services/rounding_service.py +27 -0
- investing_algorithm_framework/domain/services/state_handler.py +38 -0
- investing_algorithm_framework/domain/stateless_actions.py +7 -0
- investing_algorithm_framework/domain/strategy.py +44 -0
- investing_algorithm_framework/domain/utils/__init__.py +27 -0
- investing_algorithm_framework/domain/utils/csv.py +104 -0
- investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
- investing_algorithm_framework/domain/utils/dates.py +57 -0
- investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
- investing_algorithm_framework/domain/utils/polars.py +53 -0
- investing_algorithm_framework/domain/utils/random.py +41 -0
- investing_algorithm_framework/domain/utils/signatures.py +17 -0
- investing_algorithm_framework/domain/utils/stoppable_thread.py +26 -0
- investing_algorithm_framework/domain/utils/synchronized.py +12 -0
- investing_algorithm_framework/download_data.py +108 -0
- investing_algorithm_framework/infrastructure/__init__.py +50 -0
- investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
- investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1143 -0
- investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
- investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
- investing_algorithm_framework/infrastructure/database/__init__.py +10 -0
- investing_algorithm_framework/infrastructure/database/sql_alchemy.py +120 -0
- investing_algorithm_framework/infrastructure/models/__init__.py +16 -0
- investing_algorithm_framework/infrastructure/models/decimal_parser.py +14 -0
- investing_algorithm_framework/infrastructure/models/model_extension.py +6 -0
- investing_algorithm_framework/infrastructure/models/order/__init__.py +4 -0
- investing_algorithm_framework/infrastructure/models/order/order.py +124 -0
- investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
- investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
- investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +4 -0
- investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +37 -0
- investing_algorithm_framework/infrastructure/models/portfolio/sql_portfolio.py +114 -0
- investing_algorithm_framework/infrastructure/models/position/__init__.py +4 -0
- investing_algorithm_framework/infrastructure/models/position/position.py +63 -0
- investing_algorithm_framework/infrastructure/models/position/position_snapshot.py +23 -0
- investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
- investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
- investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +40 -0
- investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +41 -0
- investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
- investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
- investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
- investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
- investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
- investing_algorithm_framework/infrastructure/repositories/__init__.py +21 -0
- investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
- investing_algorithm_framework/infrastructure/repositories/order_repository.py +96 -0
- investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +30 -0
- investing_algorithm_framework/infrastructure/repositories/portfolio_snapshot_repository.py +56 -0
- investing_algorithm_framework/infrastructure/repositories/position_repository.py +66 -0
- investing_algorithm_framework/infrastructure/repositories/position_snapshot_repository.py +21 -0
- investing_algorithm_framework/infrastructure/repositories/repository.py +299 -0
- investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
- investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +23 -0
- investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +23 -0
- investing_algorithm_framework/infrastructure/services/__init__.py +7 -0
- investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
- investing_algorithm_framework/infrastructure/services/aws/state_handler.py +113 -0
- investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
- investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
- investing_algorithm_framework/services/__init__.py +132 -0
- investing_algorithm_framework/services/backtesting/__init__.py +5 -0
- investing_algorithm_framework/services/backtesting/backtest_service.py +651 -0
- investing_algorithm_framework/services/configuration_service.py +96 -0
- investing_algorithm_framework/services/data_providers/__init__.py +5 -0
- investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
- investing_algorithm_framework/services/market_credential_service.py +40 -0
- investing_algorithm_framework/services/metrics/__init__.py +114 -0
- investing_algorithm_framework/services/metrics/alpha.py +0 -0
- investing_algorithm_framework/services/metrics/beta.py +0 -0
- investing_algorithm_framework/services/metrics/cagr.py +60 -0
- investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
- investing_algorithm_framework/services/metrics/drawdown.py +181 -0
- investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
- investing_algorithm_framework/services/metrics/exposure.py +210 -0
- investing_algorithm_framework/services/metrics/generate.py +358 -0
- investing_algorithm_framework/services/metrics/mean_daily_return.py +83 -0
- investing_algorithm_framework/services/metrics/price_efficiency.py +57 -0
- investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
- investing_algorithm_framework/services/metrics/recovery.py +113 -0
- investing_algorithm_framework/services/metrics/returns.py +452 -0
- investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
- investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
- investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
- investing_algorithm_framework/services/metrics/standard_deviation.py +157 -0
- investing_algorithm_framework/services/metrics/trades.py +500 -0
- investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
- investing_algorithm_framework/services/metrics/ulcer.py +0 -0
- investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
- investing_algorithm_framework/services/metrics/volatility.py +97 -0
- investing_algorithm_framework/services/metrics/win_rate.py +177 -0
- investing_algorithm_framework/services/order_service/__init__.py +9 -0
- investing_algorithm_framework/services/order_service/order_backtest_service.py +178 -0
- investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
- investing_algorithm_framework/services/order_service/order_service.py +826 -0
- investing_algorithm_framework/services/portfolios/__init__.py +16 -0
- investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +54 -0
- investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +75 -0
- investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
- investing_algorithm_framework/services/portfolios/portfolio_service.py +188 -0
- investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +136 -0
- investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +182 -0
- investing_algorithm_framework/services/positions/__init__.py +7 -0
- investing_algorithm_framework/services/positions/position_service.py +210 -0
- investing_algorithm_framework/services/positions/position_snapshot_service.py +18 -0
- investing_algorithm_framework/services/repository_service.py +40 -0
- investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
- investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +132 -0
- investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +66 -0
- investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +41 -0
- investing_algorithm_framework/services/trade_service/__init__.py +3 -0
- investing_algorithm_framework/services/trade_service/trade_service.py +1083 -0
- investing_algorithm_framework-7.19.14.dist-info/LICENSE +201 -0
- investing_algorithm_framework-7.19.14.dist-info/METADATA +459 -0
- investing_algorithm_framework-7.19.14.dist-info/RECORD +260 -0
- investing_algorithm_framework-7.19.14.dist-info/WHEEL +4 -0
- 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
|
+
)
|