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.
- investing_algorithm_framework/__init__.py +168 -45
- investing_algorithm_framework/app/__init__.py +32 -1
- 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 +1933 -589
- investing_algorithm_framework/app/app_hook.py +28 -0
- investing_algorithm_framework/app/context.py +1725 -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/action_handlers/__init__.py +4 -2
- investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +1 -1
- investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +1 -1
- investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +14 -7
- investing_algorithm_framework/app/strategy.py +664 -84
- investing_algorithm_framework/app/task.py +5 -3
- investing_algorithm_framework/app/web/__init__.py +2 -1
- investing_algorithm_framework/app/web/create_app.py +4 -2
- investing_algorithm_framework/cli/__init__.py +0 -0
- investing_algorithm_framework/cli/cli.py +226 -0
- investing_algorithm_framework/cli/deploy_to_aws_lambda.py +501 -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 +40 -6
- investing_algorithm_framework/dependency_container.py +72 -56
- investing_algorithm_framework/domain/__init__.py +71 -47
- 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 +605 -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 +59 -91
- investing_algorithm_framework/domain/constants.py +13 -38
- investing_algorithm_framework/domain/data_provider.py +334 -0
- investing_algorithm_framework/domain/data_structures.py +3 -2
- investing_algorithm_framework/domain/exceptions.py +51 -1
- investing_algorithm_framework/domain/models/__init__.py +17 -12
- 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/market_credential.py +55 -1
- investing_algorithm_framework/domain/models/order/order.py +77 -83
- investing_algorithm_framework/domain/models/order/order_status.py +2 -2
- investing_algorithm_framework/domain/models/order/order_type.py +1 -3
- investing_algorithm_framework/domain/models/portfolio/portfolio.py +81 -3
- investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +26 -3
- investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +108 -11
- investing_algorithm_framework/domain/models/position/__init__.py +2 -1
- investing_algorithm_framework/domain/models/position/position.py +12 -0
- investing_algorithm_framework/domain/models/position/position_size.py +41 -0
- investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
- investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
- investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
- investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
- investing_algorithm_framework/domain/models/strategy_profile.py +19 -151
- investing_algorithm_framework/domain/models/time_frame.py +37 -0
- investing_algorithm_framework/domain/models/time_interval.py +33 -0
- investing_algorithm_framework/domain/models/time_unit.py +66 -2
- investing_algorithm_framework/domain/models/trade/__init__.py +8 -1
- investing_algorithm_framework/domain/models/trade/trade.py +295 -171
- investing_algorithm_framework/domain/models/trade/trade_status.py +9 -2
- investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +332 -0
- investing_algorithm_framework/domain/models/trade/trade_take_profit.py +365 -0
- investing_algorithm_framework/domain/order_executor.py +112 -0
- investing_algorithm_framework/domain/portfolio_provider.py +118 -0
- investing_algorithm_framework/domain/services/__init__.py +2 -9
- investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +0 -6
- investing_algorithm_framework/domain/services/state_handler.py +38 -0
- investing_algorithm_framework/domain/strategy.py +1 -29
- investing_algorithm_framework/domain/utils/__init__.py +12 -7
- 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 +29 -0
- investing_algorithm_framework/download_data.py +108 -0
- investing_algorithm_framework/infrastructure/__init__.py +31 -18
- 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 +6 -2
- investing_algorithm_framework/infrastructure/database/sql_alchemy.py +86 -12
- investing_algorithm_framework/infrastructure/models/__init__.py +6 -11
- investing_algorithm_framework/infrastructure/models/order/__init__.py +2 -1
- investing_algorithm_framework/infrastructure/models/order/order.py +35 -49
- 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 +1 -1
- investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +8 -0
- investing_algorithm_framework/infrastructure/models/portfolio/{portfolio.py → sql_portfolio.py} +17 -5
- 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 +59 -0
- investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +55 -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 +8 -0
- investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
- investing_algorithm_framework/infrastructure/repositories/order_repository.py +5 -0
- investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +1 -1
- investing_algorithm_framework/infrastructure/repositories/position_repository.py +11 -0
- investing_algorithm_framework/infrastructure/repositories/repository.py +81 -27
- investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
- investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +29 -0
- investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +29 -0
- investing_algorithm_framework/infrastructure/services/__init__.py +4 -4
- 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 +113 -16
- investing_algorithm_framework/services/backtesting/__init__.py +0 -7
- investing_algorithm_framework/services/backtesting/backtest_service.py +566 -359
- investing_algorithm_framework/services/configuration_service.py +77 -11
- 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 +16 -1
- 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/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 +3 -1
- investing_algorithm_framework/services/order_service/order_backtest_service.py +76 -89
- investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
- investing_algorithm_framework/services/order_service/order_service.py +407 -326
- investing_algorithm_framework/services/portfolios/__init__.py +3 -1
- investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +37 -3
- investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +22 -8
- investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
- investing_algorithm_framework/services/portfolios/portfolio_service.py +96 -28
- investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +97 -28
- investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +116 -313
- investing_algorithm_framework/services/positions/__init__.py +7 -0
- investing_algorithm_framework/services/positions/position_service.py +210 -0
- investing_algorithm_framework/services/repository_service.py +8 -2
- investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
- investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +113 -0
- investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
- investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
- investing_algorithm_framework/services/trade_service/__init__.py +7 -1
- investing_algorithm_framework/services/trade_service/trade_service.py +1013 -315
- investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
- investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
- investing_algorithm_framework-7.19.15.dist-info/METADATA +537 -0
- investing_algorithm_framework-7.19.15.dist-info/RECORD +263 -0
- investing_algorithm_framework-7.19.15.dist-info/entry_points.txt +3 -0
- investing_algorithm_framework/app/algorithm.py +0 -1105
- investing_algorithm_framework/domain/graphs.py +0 -382
- investing_algorithm_framework/domain/metrics/__init__.py +0 -6
- investing_algorithm_framework/domain/models/backtesting/__init__.py +0 -11
- investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +0 -43
- investing_algorithm_framework/domain/models/backtesting/backtest_position.py +0 -120
- investing_algorithm_framework/domain/models/backtesting/backtest_report.py +0 -580
- investing_algorithm_framework/domain/models/backtesting/backtest_reports_evaluation.py +0 -243
- investing_algorithm_framework/domain/models/trading_data_types.py +0 -47
- investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
- investing_algorithm_framework/domain/services/market_data_sources.py +0 -344
- investing_algorithm_framework/domain/services/market_service.py +0 -153
- investing_algorithm_framework/domain/singleton.py +0 -9
- investing_algorithm_framework/domain/utils/backtesting.py +0 -472
- investing_algorithm_framework/infrastructure/models/market_data_sources/__init__.py +0 -12
- investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +0 -559
- investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py +0 -254
- investing_algorithm_framework/infrastructure/models/market_data_sources/us_treasury_yield.py +0 -47
- investing_algorithm_framework/infrastructure/services/market_service/__init__.py +0 -5
- investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +0 -455
- investing_algorithm_framework/infrastructure/services/performance_service/__init__.py +0 -7
- investing_algorithm_framework/infrastructure/services/performance_service/backtest_performance_service.py +0 -2
- investing_algorithm_framework/infrastructure/services/performance_service/performance_service.py +0 -350
- investing_algorithm_framework/services/backtesting/backtest_report_writer_service.py +0 -53
- investing_algorithm_framework/services/backtesting/graphs.py +0 -61
- investing_algorithm_framework/services/market_data_source_service/__init__.py +0 -8
- investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py +0 -150
- investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +0 -189
- investing_algorithm_framework/services/position_service.py +0 -31
- investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -264
- investing_algorithm_framework-3.7.0.dist-info/METADATA +0 -339
- investing_algorithm_framework-3.7.0.dist-info/RECORD +0 -147
- /investing_algorithm_framework/{domain → services}/metrics/price_efficiency.py +0 -0
- /investing_algorithm_framework/services/{position_snapshot_service.py → positions/position_snapshot_service.py} +0 -0
- {investing_algorithm_framework-3.7.0.dist-info → investing_algorithm_framework-7.19.15.dist-info}/LICENSE +0 -0
- {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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
123
|
-
self.DEFAULT_NOT_FOUND_MESSAGE
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 .
|
|
2
|
-
from .
|
|
1
|
+
from .azure import AzureBlobStorageStateHandler
|
|
2
|
+
from .aws import AWSS3StorageStateHandler
|
|
3
3
|
|
|
4
4
|
__all__ = [
|
|
5
|
-
"
|
|
6
|
-
"
|
|
5
|
+
"AzureBlobStorageStateHandler",
|
|
6
|
+
"AWSS3StorageStateHandler"
|
|
7
7
|
]
|
|
@@ -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,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
|