mainsequence 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. mainsequence/__init__.py +0 -0
  2. mainsequence/__main__.py +9 -0
  3. mainsequence/cli/__init__.py +1 -0
  4. mainsequence/cli/api.py +157 -0
  5. mainsequence/cli/cli.py +442 -0
  6. mainsequence/cli/config.py +78 -0
  7. mainsequence/cli/ssh_utils.py +126 -0
  8. mainsequence/client/__init__.py +17 -0
  9. mainsequence/client/base.py +431 -0
  10. mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  11. mainsequence/client/data_sources_interfaces/duckdb.py +1468 -0
  12. mainsequence/client/data_sources_interfaces/timescale.py +479 -0
  13. mainsequence/client/models_helpers.py +113 -0
  14. mainsequence/client/models_report_studio.py +412 -0
  15. mainsequence/client/models_tdag.py +2276 -0
  16. mainsequence/client/models_vam.py +1983 -0
  17. mainsequence/client/utils.py +387 -0
  18. mainsequence/dashboards/__init__.py +0 -0
  19. mainsequence/dashboards/streamlit/__init__.py +0 -0
  20. mainsequence/dashboards/streamlit/assets/config.toml +12 -0
  21. mainsequence/dashboards/streamlit/assets/favicon.png +0 -0
  22. mainsequence/dashboards/streamlit/assets/logo.png +0 -0
  23. mainsequence/dashboards/streamlit/core/__init__.py +0 -0
  24. mainsequence/dashboards/streamlit/core/theme.py +212 -0
  25. mainsequence/dashboards/streamlit/pages/__init__.py +0 -0
  26. mainsequence/dashboards/streamlit/scaffold.py +220 -0
  27. mainsequence/instrumentation/__init__.py +7 -0
  28. mainsequence/instrumentation/utils.py +101 -0
  29. mainsequence/instruments/__init__.py +1 -0
  30. mainsequence/instruments/data_interface/__init__.py +10 -0
  31. mainsequence/instruments/data_interface/data_interface.py +361 -0
  32. mainsequence/instruments/instruments/__init__.py +3 -0
  33. mainsequence/instruments/instruments/base_instrument.py +85 -0
  34. mainsequence/instruments/instruments/bond.py +447 -0
  35. mainsequence/instruments/instruments/european_option.py +74 -0
  36. mainsequence/instruments/instruments/interest_rate_swap.py +217 -0
  37. mainsequence/instruments/instruments/json_codec.py +585 -0
  38. mainsequence/instruments/instruments/knockout_fx_option.py +146 -0
  39. mainsequence/instruments/instruments/position.py +475 -0
  40. mainsequence/instruments/instruments/ql_fields.py +239 -0
  41. mainsequence/instruments/instruments/vanilla_fx_option.py +107 -0
  42. mainsequence/instruments/pricing_models/__init__.py +0 -0
  43. mainsequence/instruments/pricing_models/black_scholes.py +49 -0
  44. mainsequence/instruments/pricing_models/bond_pricer.py +182 -0
  45. mainsequence/instruments/pricing_models/fx_option_pricer.py +90 -0
  46. mainsequence/instruments/pricing_models/indices.py +350 -0
  47. mainsequence/instruments/pricing_models/knockout_fx_pricer.py +209 -0
  48. mainsequence/instruments/pricing_models/swap_pricer.py +502 -0
  49. mainsequence/instruments/settings.py +175 -0
  50. mainsequence/instruments/utils.py +29 -0
  51. mainsequence/logconf.py +284 -0
  52. mainsequence/reportbuilder/__init__.py +0 -0
  53. mainsequence/reportbuilder/__main__.py +0 -0
  54. mainsequence/reportbuilder/examples/ms_template_report.py +706 -0
  55. mainsequence/reportbuilder/model.py +713 -0
  56. mainsequence/reportbuilder/slide_templates.py +532 -0
  57. mainsequence/tdag/__init__.py +8 -0
  58. mainsequence/tdag/__main__.py +0 -0
  59. mainsequence/tdag/config.py +129 -0
  60. mainsequence/tdag/data_nodes/__init__.py +12 -0
  61. mainsequence/tdag/data_nodes/build_operations.py +751 -0
  62. mainsequence/tdag/data_nodes/data_nodes.py +1292 -0
  63. mainsequence/tdag/data_nodes/persist_managers.py +812 -0
  64. mainsequence/tdag/data_nodes/run_operations.py +543 -0
  65. mainsequence/tdag/data_nodes/utils.py +24 -0
  66. mainsequence/tdag/future_registry.py +25 -0
  67. mainsequence/tdag/utils.py +40 -0
  68. mainsequence/virtualfundbuilder/__init__.py +45 -0
  69. mainsequence/virtualfundbuilder/__main__.py +235 -0
  70. mainsequence/virtualfundbuilder/agent_interface.py +77 -0
  71. mainsequence/virtualfundbuilder/config_handling.py +86 -0
  72. mainsequence/virtualfundbuilder/contrib/__init__.py +0 -0
  73. mainsequence/virtualfundbuilder/contrib/apps/__init__.py +8 -0
  74. mainsequence/virtualfundbuilder/contrib/apps/etf_replicator_app.py +164 -0
  75. mainsequence/virtualfundbuilder/contrib/apps/generate_report.py +292 -0
  76. mainsequence/virtualfundbuilder/contrib/apps/load_external_portfolio.py +107 -0
  77. mainsequence/virtualfundbuilder/contrib/apps/news_app.py +437 -0
  78. mainsequence/virtualfundbuilder/contrib/apps/portfolio_report_app.py +91 -0
  79. mainsequence/virtualfundbuilder/contrib/apps/portfolio_table.py +95 -0
  80. mainsequence/virtualfundbuilder/contrib/apps/run_named_portfolio.py +45 -0
  81. mainsequence/virtualfundbuilder/contrib/apps/run_portfolio.py +40 -0
  82. mainsequence/virtualfundbuilder/contrib/apps/templates/base.html +147 -0
  83. mainsequence/virtualfundbuilder/contrib/apps/templates/report.html +77 -0
  84. mainsequence/virtualfundbuilder/contrib/data_nodes/__init__.py +5 -0
  85. mainsequence/virtualfundbuilder/contrib/data_nodes/external_weights.py +61 -0
  86. mainsequence/virtualfundbuilder/contrib/data_nodes/intraday_trend.py +149 -0
  87. mainsequence/virtualfundbuilder/contrib/data_nodes/market_cap.py +310 -0
  88. mainsequence/virtualfundbuilder/contrib/data_nodes/mock_signal.py +78 -0
  89. mainsequence/virtualfundbuilder/contrib/data_nodes/portfolio_replicator.py +269 -0
  90. mainsequence/virtualfundbuilder/contrib/prices/__init__.py +1 -0
  91. mainsequence/virtualfundbuilder/contrib/prices/data_nodes.py +810 -0
  92. mainsequence/virtualfundbuilder/contrib/prices/utils.py +11 -0
  93. mainsequence/virtualfundbuilder/contrib/rebalance_strategies/__init__.py +1 -0
  94. mainsequence/virtualfundbuilder/contrib/rebalance_strategies/rebalance_strategies.py +313 -0
  95. mainsequence/virtualfundbuilder/data_nodes.py +637 -0
  96. mainsequence/virtualfundbuilder/enums.py +23 -0
  97. mainsequence/virtualfundbuilder/models.py +282 -0
  98. mainsequence/virtualfundbuilder/notebook_handling.py +42 -0
  99. mainsequence/virtualfundbuilder/portfolio_interface.py +272 -0
  100. mainsequence/virtualfundbuilder/resource_factory/__init__.py +0 -0
  101. mainsequence/virtualfundbuilder/resource_factory/app_factory.py +170 -0
  102. mainsequence/virtualfundbuilder/resource_factory/base_factory.py +238 -0
  103. mainsequence/virtualfundbuilder/resource_factory/rebalance_factory.py +101 -0
  104. mainsequence/virtualfundbuilder/resource_factory/signal_factory.py +183 -0
  105. mainsequence/virtualfundbuilder/utils.py +381 -0
  106. mainsequence-2.0.0.dist-info/METADATA +105 -0
  107. mainsequence-2.0.0.dist-info/RECORD +110 -0
  108. mainsequence-2.0.0.dist-info/WHEEL +5 -0
  109. mainsequence-2.0.0.dist-info/licenses/LICENSE +40 -0
  110. mainsequence-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,282 @@
1
+ from typing import Dict, List, Optional
2
+
3
+ from pydantic import (BaseModel, validator, model_validator)
4
+ import os
5
+ import pandas as pd
6
+
7
+ from mainsequence.client import AssetMixin, Asset, MARKETS_CONSTANTS
8
+ import json
9
+ from pydantic import FieldValidationInfo, field_validator, root_validator, Field
10
+
11
+ from mainsequence.virtualfundbuilder.enums import (PriceTypeNames)
12
+
13
+ from mainsequence.client.models_tdag import RunConfiguration
14
+ import mainsequence.client as msc
15
+ import yaml
16
+ from mainsequence.virtualfundbuilder.utils import get_vfb_logger
17
+ from mainsequence.tdag.utils import write_yaml
18
+ from mainsequence.tdag.utils import hash_dict
19
+ import copy
20
+ from functools import lru_cache
21
+ from datetime import datetime
22
+ from typing import Any, Dict, List, Literal, Union
23
+
24
+ from pydantic import (
25
+ BaseModel,
26
+ Field,
27
+ PositiveInt,
28
+ model_validator,
29
+ )
30
+
31
+ logger = get_vfb_logger()
32
+
33
+ class VFBConfigBaseModel(BaseModel):
34
+ class Config:
35
+ arbitrary_types_allowed = True
36
+
37
+
38
+ class MarketsTimeSeries(VFBConfigBaseModel):
39
+ """
40
+ MarketsTimeSeries based on their unique id. Used as the data sources for the prices.
41
+ Values include alpaca_1d_bars, binance_1d_bars etc.
42
+
43
+ Attributes:
44
+ unique_identifier (str): Identfier of the MarketsTimeSeries.
45
+ """
46
+ unique_identifier: str = "alpaca_1d_bars"
47
+
48
+ class PricesConfiguration(VFBConfigBaseModel):
49
+ """
50
+ Configuration for price data handling in a portfolio.
51
+
52
+ Attributes:
53
+ bar_frequency_id (str): The frequency of price bars.
54
+ upsample_frequency_id (str): Frequency to upsample intraday data to.
55
+ intraday_bar_interpolation_rule (str): Rule for interpolating missing intraday bars.
56
+ is_live (bool): Boolean flag indicating if the price feed is live.
57
+ translation_table_unique_id (str): The unique identifier of the translation table used to identify the price source.
58
+ """
59
+ bar_frequency_id: str = "1d"
60
+ upsample_frequency_id: str = "1d" # "15m"
61
+ intraday_bar_interpolation_rule: str = "ffill"
62
+ is_live: bool = False
63
+ translation_table_unique_id: str = "prices_translation_table_1d"
64
+ forward_fill_to_now: bool = False
65
+
66
+ @lru_cache(maxsize=1028) # Cache up to 1028 different combinations
67
+ def cached_asset_filter(*args,**kwargs):
68
+ tmp_assets = Asset.filter_with_asset_class( *args,**kwargs)
69
+ return tmp_assets
70
+
71
+ class AssetsConfiguration(VFBConfigBaseModel):
72
+ """
73
+ Configuration for assets included in a portfolio.
74
+
75
+ Attributes:
76
+ assets_category_unique_id (str):
77
+ Unique Identifier of assets category
78
+ price_type (PriceTypeNames): Type of price used for backtesting.
79
+ prices_configuration (PricesConfiguration): Configuration for price data handling.
80
+ """
81
+ assets_category_unique_id: str
82
+ price_type: PriceTypeNames = PriceTypeNames.CLOSE
83
+ prices_configuration: PricesConfiguration
84
+
85
+ def get_asset_list(self):
86
+ asset_category=msc.AssetCategory.get(unique_identifier=self.assets_category_unique_id)
87
+ assets=msc.Asset.filter(id__in=asset_category.assets)
88
+ return assets
89
+
90
+ class BacktestingWeightsConfig(VFBConfigBaseModel):
91
+ """
92
+ Configuration for backtesting weights.
93
+
94
+ Attributes:
95
+ rebalance_strategy_name (str): Strategy used for rebalancing.
96
+ rebalance_strategy_configuration (Dict): Placeholder dict for the rebalance strategy configuration.
97
+ signal_weights_name (str): Type of signal weights strategy.
98
+ signal_weights_configuration (Dict): Placeholder dict for the signal weights configuration.
99
+ """
100
+ rebalance_strategy_name: str = "ImmediateSignal"
101
+ rebalance_strategy_configuration: Dict
102
+ signal_weights_name: str = "MarketCap"
103
+ signal_weights_configuration: Dict
104
+
105
+ def model_dump(self, **kwargs):
106
+ signal_weights_configuration = self.signal_weights_configuration
107
+ data = super().model_dump(**kwargs)
108
+ data["signal_weights_configuration"]["signal_assets_configuration"] = signal_weights_configuration[
109
+ "signal_assets_configuration"].model_dump(**kwargs)
110
+
111
+ return data
112
+
113
+ @model_validator(mode="before")
114
+ def parse_signal_weights_configuration(cls, values):
115
+ if isinstance(values["signal_weights_configuration"]["signal_assets_configuration"], AssetsConfiguration):
116
+ return values
117
+
118
+ asset_configuration = copy.deepcopy(values["signal_weights_configuration"]["signal_assets_configuration"])
119
+ if "prices_configuration" not in asset_configuration:
120
+ logger.info("No Price Configuration in Configuration - Use Default Price Configuration")
121
+ asset_configuration["prices_configuration"] = PricesConfiguration()
122
+
123
+ values["signal_weights_configuration"]["signal_assets_configuration"] = AssetsConfiguration(
124
+ **asset_configuration
125
+ )
126
+ return values
127
+
128
+
129
+ class PortfolioExecutionConfiguration(VFBConfigBaseModel):
130
+ """
131
+ Configuration for portfolio execution.
132
+
133
+ Attributes:
134
+ commission_fee (float): Commission fee percentage.
135
+ """
136
+ commission_fee: float = 0.00018
137
+
138
+
139
+ class FrontEndDetails(VFBConfigBaseModel):
140
+ description: str # required field; must be provided and cannot be None
141
+
142
+ signal_name: Optional[str] = None
143
+ signal_description: Optional[str] = None
144
+ rebalance_strategy_name: Optional[str] = None
145
+ rebalance_strategy_description: Optional[str] = None
146
+
147
+ class PortfolioMarketsConfig(VFBConfigBaseModel):
148
+ """
149
+ Configuration for Virtual Asset Management (VAM) portfolio.
150
+
151
+ Attributes:
152
+ portfolio_name (str): Name of the portfolio.
153
+ execution_configuration (VAMExecutionConfiguration): Execution configuration for VAM.
154
+ """
155
+ portfolio_name: str = "Portfolio Strategy Title"
156
+ front_end_details: Optional[FrontEndDetails] = None
157
+
158
+ class AssetMixinOverwrite(VFBConfigBaseModel):
159
+ """
160
+ The Asset for evaluating the portfolio.
161
+
162
+ Attributes:
163
+ unique_identifier (str): The unique_identifier of the asset.
164
+ """
165
+ unique_identifier: str = MARKETS_CONSTANTS.UNIQUE_IDENTIFIER_USD
166
+
167
+ class PortfolioBuildConfiguration(VFBConfigBaseModel):
168
+ """
169
+ Main class for configuring and building a portfolio.
170
+
171
+ This class defines the configuration parameters needed for
172
+ building a portfolio, including asset configurations, backtesting
173
+ weights, and execution parameters.
174
+
175
+ Attributes:
176
+ assets_configuration (AssetsConfiguration): Configuration details for assets.
177
+ portfolio_prices_frequency (str): Frequency to upsample portoflio. Optional.
178
+ backtesting_weights_configuration (BacktestingWeightsConfig): Weights configuration used for backtesting.
179
+ execution_configuration (PortfolioExecutionConfiguration): Execution settings for the portfolio.
180
+ """
181
+ assets_configuration: AssetsConfiguration
182
+ portfolio_prices_frequency: Optional[str] = "1d"
183
+ backtesting_weights_configuration: BacktestingWeightsConfig
184
+ execution_configuration: PortfolioExecutionConfiguration
185
+
186
+ def model_dump(self, **kwargs):
187
+ serialized_asset_config = self.assets_configuration.model_dump(**kwargs)
188
+ data = super().model_dump(**kwargs)
189
+ data["assets_configuration"] = serialized_asset_config
190
+
191
+ data["backtesting_weights_configuration"] = self.backtesting_weights_configuration.model_dump(**kwargs)
192
+ return data
193
+
194
+ @root_validator(pre=True)
195
+ def parse_assets_configuration(cls, values):
196
+
197
+ if not isinstance(values["assets_configuration"], AssetsConfiguration) and values['assets_configuration'] is not None:
198
+ values["assets_configuration"] = AssetsConfiguration(
199
+ assets_category_unique_id=values['assets_configuration']['assets_category_unique_id'],
200
+ price_type=PriceTypeNames(values['assets_configuration']['price_type']),
201
+ prices_configuration=PricesConfiguration(
202
+ **values['assets_configuration']['prices_configuration'])
203
+ )
204
+
205
+ return values
206
+
207
+
208
+ class PortfolioConfiguration(VFBConfigBaseModel):
209
+ """
210
+ Configuration for a complete portfolio, including build configuration,
211
+ TDAG updates, and VAM settings.
212
+
213
+ This class aggregates different configurations required for the
214
+ management and operation of a portfolio.
215
+
216
+ Attributes:
217
+ portfolio_build_configuration (PortfolioBuildConfiguration): Configuration for building the portfolio.
218
+ portfolio_markets_configuration (PortfolioMarketsConfig): VAM execution configuration.
219
+ """
220
+ portfolio_build_configuration: PortfolioBuildConfiguration
221
+ portfolio_markets_configuration: PortfolioMarketsConfig
222
+
223
+ @staticmethod
224
+ def read_portfolio_configuration_from_yaml(yaml_path: str):
225
+ with open(yaml_path, 'r') as file:
226
+ return yaml.safe_load(file)
227
+
228
+ @staticmethod
229
+ def parse_portfolio_configuration_from_yaml(yaml_path: str, auto_complete=False):
230
+ from mainsequence.virtualfundbuilder.config_handling import configuration_sanitizer
231
+ configuration = PortfolioConfiguration.read_portfolio_configuration_from_yaml(yaml_path)
232
+ return configuration_sanitizer(configuration, auto_complete=auto_complete)
233
+
234
+ @staticmethod
235
+ def parse_portfolio_configurations(
236
+ portfolio_build_configuration: dict,
237
+ portfolio_markets_configuration: dict,
238
+ ):
239
+ # Parse the individual components
240
+ backtesting_weights_configuration = BacktestingWeightsConfig(
241
+ rebalance_strategy_name=portfolio_build_configuration['backtesting_weights_configuration']['rebalance_strategy_name'],
242
+ rebalance_strategy_configuration=portfolio_build_configuration['backtesting_weights_configuration'][
243
+ 'rebalance_strategy_configuration'],
244
+ signal_weights_name=portfolio_build_configuration['backtesting_weights_configuration'][
245
+ 'signal_weights_name'],
246
+ signal_weights_configuration=portfolio_build_configuration['backtesting_weights_configuration'][
247
+ 'signal_weights_configuration']
248
+ )
249
+
250
+ execution_configuration = PortfolioExecutionConfiguration(
251
+ commission_fee=portfolio_build_configuration['execution_configuration']['commission_fee']
252
+ )
253
+
254
+ portfolio_build_config = PortfolioBuildConfiguration(
255
+ assets_configuration=portfolio_build_configuration['assets_configuration'],
256
+ backtesting_weights_configuration=backtesting_weights_configuration,
257
+ execution_configuration=execution_configuration,
258
+ portfolio_prices_frequency=portfolio_build_configuration['portfolio_prices_frequency'],
259
+ )
260
+
261
+ portfolio_markets_configuration = PortfolioMarketsConfig(**portfolio_markets_configuration)
262
+
263
+ # Combine everything into the final PortfolioConfiguration
264
+ portfolio_config = PortfolioConfiguration(
265
+ portfolio_build_configuration=portfolio_build_config,
266
+ portfolio_markets_configuration=portfolio_markets_configuration
267
+ )
268
+
269
+ return portfolio_config
270
+
271
+ def build_yaml_configuration_file(self):
272
+ signal_type = self.portfolio_build_configuration.backtesting_weights_configuration.signal_weights_name
273
+ vfb_folder = os.path.join(os.path.expanduser("~"), "VirtualFundBuilder", "configurations")
274
+ vfb_folder = os.path.join(vfb_folder, signal_type)
275
+ if not os.path.exists(vfb_folder):
276
+ os.makedirs(vfb_folder)
277
+
278
+ config_hash = hash_dict(self.model_dump_json())
279
+ config_file_name = f"{vfb_folder}/{config_hash}.yaml"
280
+
281
+ write_yaml(dict_file=json.loads(self.model_dump_json()), path=config_file_name)
282
+ return config_file_name
@@ -0,0 +1,42 @@
1
+ import tempfile
2
+ from pathlib import Path
3
+ from nbconvert import PythonExporter
4
+ import importlib.util
5
+ import sys
6
+ import inspect
7
+ from typing import List, Type
8
+
9
+
10
+ def convert_notebook_to_python_file(notebook_path):
11
+ """
12
+ Converts a Jupyter notebook to a Python file in a temporary directory.
13
+
14
+ Args:
15
+ notebook_path (str or pathlib.Path): The path to the Jupyter notebook (.ipynb) file.
16
+
17
+ Returns:
18
+ pathlib.Path: The path to the generated Python file in the temporary directory.
19
+ """
20
+
21
+ # Ensure notebook_path is a Path object
22
+ notebook_path = Path(notebook_path)
23
+
24
+ # Create a temporary directory
25
+ temp_dir = tempfile.mkdtemp()
26
+
27
+ # Convert the notebook to a Python script
28
+ exporter = PythonExporter()
29
+ (python_code, resources) = exporter.from_filename(str(notebook_path))
30
+
31
+ # Create the output file path
32
+ python_file_path = Path(temp_dir) / (notebook_path.stem + ".py")
33
+
34
+ # Write the Python code to the file
35
+ with open(python_file_path, "w", encoding="utf-8") as f:
36
+ f.write(python_code)
37
+
38
+ print(f"Notebook converted and saved to: {python_file_path}")
39
+
40
+ return python_file_path
41
+
42
+
@@ -0,0 +1,272 @@
1
+ import copy
2
+ from mainsequence.tdag.utils import write_yaml
3
+ import os
4
+ from typing import Dict, Any, List, Union, Optional, Tuple
5
+ import yaml
6
+ import re
7
+
8
+ from .config_handling import configuration_sanitizer
9
+ from .data_nodes import PortfolioStrategy, PortfolioFromDF
10
+ from mainsequence.client import Asset, AssetFutureUSDM, MARKETS_CONSTANTS as CONSTANTS, Portfolio, PortfolioIndexAsset
11
+
12
+ from .models import PortfolioConfiguration
13
+ from .utils import get_vfb_logger
14
+
15
+
16
+ class PortfolioInterface():
17
+ """
18
+ Manages the overall strategy of investing. It initializes the tree and runs it either within the scheduler or
19
+ directly with a full tree update.
20
+ """
21
+ def __init__(self, portfolio_config_template: dict,
22
+ configuration_name: str=None,is_portfolio_from_df=False):
23
+ """
24
+ Initializes the portfolio strategy with the necessary configurations.
25
+ """
26
+ if is_portfolio_from_df == True:
27
+ return None
28
+ if configuration_name:
29
+ self.check_valid_configuration_name(configuration_name)
30
+ self.portfolio_config_template = portfolio_config_template
31
+ self.portfolio_config = configuration_sanitizer(portfolio_config_template)
32
+ self.configuration_name = configuration_name
33
+
34
+ self.portfolio_markets_config = self.portfolio_config.portfolio_markets_configuration
35
+ self.portfolio_build_configuration = self.portfolio_config.portfolio_build_configuration
36
+ self.logger = get_vfb_logger()
37
+ self._is_initialized = False
38
+
39
+ def __str__(self):
40
+ configuration_name = self.configuration_name or "-"
41
+ str_configuration = yaml.dump(self.portfolio_config_template, default_flow_style=False)
42
+ return f"Configuration Name: {configuration_name}\n{str_configuration}"
43
+
44
+ def __repr__(self):
45
+ return self.__str__()
46
+
47
+ @classmethod
48
+ def build_and_run_portfolio_from_df(cls,portfolio_node:PortfolioFromDF,
49
+ debug_mode=True,
50
+ force_update=True,
51
+ update_tree=True,
52
+ portfolio_tags: List[str] = None,
53
+ add_portfolio_to_markets_backend=False,
54
+ ):
55
+ assert isinstance(portfolio_node, PortfolioFromDF)
56
+
57
+ interface=cls(portfolio_config_template=None,is_portfolio_from_df=True)
58
+ interface._is_initialized=True
59
+ interface.portfolio_strategy_data_node=portfolio_node
60
+
61
+
62
+
63
+ interface.run( patch_build_configuration=False,
64
+ debug_mode=debug_mode,
65
+ force_update=force_update,
66
+ update_tree=update_tree,
67
+ portfolio_tags = portfolio_tags,
68
+ add_portfolio_to_markets_backend=False,)
69
+
70
+
71
+ ## manualely
72
+ target_portfolio = Portfolio.get_or_none(local_time_serie__id=portfolio_node.local_time_serie.id)
73
+ standard_kwargs = dict(portfolio_name=portfolio_node.portfolio_name,
74
+ local_time_serie_id=portfolio_node.local_time_serie.id,
75
+ signal_local_time_serie_id=None,
76
+ is_active=True,
77
+ calendar_name=portfolio_node.calendar_name,
78
+ target_portfolio_about=dict(description=portfolio_node.target_portfolio_about,
79
+ signal_name=None,
80
+ signal_description=None,
81
+ rebalance_strategy_name=None),
82
+ backtest_table_price_column_name="close")
83
+ if target_portfolio is None:
84
+ target_portfolio, index_asset = Portfolio.create_from_time_series(**standard_kwargs)
85
+ else:
86
+ # patch timeserie of portfolio to guaranteed recreation
87
+ target_portfolio.patch(**standard_kwargs)
88
+
89
+ index_asset = PortfolioIndexAsset.get(reference_portfolio__id=target_portfolio.id)
90
+
91
+
92
+ def _initialize_nodes(self,patch_build_configuration=True) -> None:
93
+ """
94
+ Initializes the portfolio strategy for backtesting and for live prediction.
95
+ Also, forces an update of the build configuration in tdag to guarantee that assets are properly rebuilt
96
+ patch_build_configuration:defaults to True as we want to patch the configuration while we test but for production can be set to False
97
+ """
98
+ patch = os.environ.get("PATCH_BUILD_CONFIGURATION", "False")
99
+ os.environ[
100
+ "PATCH_BUILD_CONFIGURATION"] = "True" if patch_build_configuration else "False" # It always needs to be true as we always want to overwrite the build
101
+ self.portfolio_strategy_data_node = PortfolioStrategy(
102
+ portfolio_build_configuration=copy.deepcopy(self.portfolio_build_configuration)
103
+ )
104
+
105
+ os.environ["PATCH_BUILD_CONFIGURATION"] = patch
106
+ self._is_initialized = True
107
+
108
+ def build_target_portfolio_in_backend(self, portfolio_tags=None) -> Tuple[Portfolio, PortfolioIndexAsset]:
109
+ """
110
+ This method creates a portfolio in VAM with configm file settings.
111
+
112
+ Returns:
113
+ """
114
+ if not self._is_initialized:
115
+ self._initialize_nodes()
116
+
117
+ portfolio_ts = self.portfolio_strategy_data_node
118
+
119
+ def build_markets_portfolio(ts, portfolio_tags):
120
+ # when is live target portfolio
121
+ signal_weights_ts = ts.signal_weights
122
+
123
+ # timeseries can be running in local lake so need to request the id
124
+ standard_kwargs = dict(local_time_serie_id=ts.local_time_serie.id,
125
+ is_active=True,
126
+ signal_local_time_serie_id=signal_weights_ts.local_time_serie.id,
127
+ )
128
+
129
+ user_kwargs = self.portfolio_markets_config.model_dump()
130
+ user_kwargs.pop("front_end_details", None)
131
+
132
+ standard_kwargs.update(user_kwargs)
133
+ standard_kwargs["calendar_name"] = self.portfolio_build_configuration.backtesting_weights_configuration.rebalance_strategy_configuration[
134
+ "calendar"]
135
+
136
+ if portfolio_tags is not None:
137
+ standard_kwargs["tags"] = portfolio_tags
138
+ # front end details
139
+ standard_kwargs["target_portfolio_about"] = {
140
+ "description": ts.get_portfolio_about_text(),
141
+ "signal_name": ts.backtesting_weights_config.signal_weights_name,
142
+ "signal_description": ts.signal_weights.get_explanation(),
143
+ "rebalance_strategy_name": ts.backtesting_weights_config.rebalance_strategy_name,
144
+ }
145
+
146
+ standard_kwargs["backtest_table_price_column_name"] = "close"
147
+
148
+ target_portfolio = Portfolio.get_or_none(local_time_serie__id=ts.local_time_serie.id)
149
+ if target_portfolio is None:
150
+ target_portfolio, index_asset = Portfolio.create_from_time_series(**standard_kwargs)
151
+ else:
152
+ # patch timeserie of portfolio to guaranteed recreation
153
+ target_portfolio.patch(**standard_kwargs)
154
+ self.logger.debug(f"Target portfolio {target_portfolio.portfolio_ticker} for local time serie {ts.local_time_serie.update_hash} already exists in Backend")
155
+ index_asset = PortfolioIndexAsset.get(reference_portfolio__id=target_portfolio.id)
156
+
157
+ return target_portfolio, index_asset
158
+
159
+ target_portfolio, index_asset = build_markets_portfolio(portfolio_ts, portfolio_tags=portfolio_tags)
160
+
161
+ self.index_asset = index_asset
162
+ self.target_portfolio = target_portfolio
163
+ return target_portfolio, index_asset
164
+
165
+ def run(
166
+ self,
167
+ patch_build_configuration=True,
168
+ debug_mode=True,
169
+ force_update=True,
170
+ update_tree=True,
171
+ portfolio_tags:List[str] = None,
172
+ add_portfolio_to_markets_backend=False,
173
+ *args, **kwargs
174
+ ):
175
+
176
+ if not self._is_initialized or patch_build_configuration == True:
177
+ self._initialize_nodes(patch_build_configuration=patch_build_configuration)
178
+
179
+ self.portfolio_strategy_data_node.run(
180
+ debug_mode=debug_mode,
181
+ update_tree=update_tree,
182
+ force_update=force_update,
183
+ **kwargs
184
+ )
185
+ if add_portfolio_to_markets_backend:
186
+ self.build_target_portfolio_in_backend(portfolio_tags=portfolio_tags)
187
+
188
+ res = self.portfolio_strategy_data_node.get_df_between_dates()
189
+ if len(res) > 0:
190
+ res = res.sort_values("time_index")
191
+ return res
192
+
193
+ @classmethod
194
+ @property
195
+ def configuration_folder_path(self):
196
+ vfb_project_path = os.getenv("VFB_PROJECT_PATH")
197
+ if not vfb_project_path:
198
+ raise ValueError(
199
+ "VFB_PROJECT_PATH environment variable is not set. "
200
+ "Please set it before using 'configuration_path'."
201
+ )
202
+ return os.path.join(vfb_project_path, "configurations")
203
+
204
+ @staticmethod
205
+ def check_valid_configuration_name(s: str) -> bool:
206
+ if not bool(re.match(r'^[A-Za-z0-9_]+$', s)):
207
+ raise ValueError(f"Name {s} not valid")
208
+
209
+ def store_configuration(self, configuration_name: Optional[str] = None):
210
+ """
211
+ Stores the current configuration as a YAML file under the configuration_name
212
+ """
213
+ if configuration_name and not self.configuration_name:
214
+ self.configuration_name = configuration_name
215
+
216
+ if not self.configuration_name:
217
+ raise ValueError(
218
+ "No configuration name was set. Provide a `configuration_name` "
219
+ "argument or load/set one before storing."
220
+ )
221
+
222
+ config_file = os.path.join(
223
+ self.configuration_folder_path,
224
+ f"{self.configuration_name}.yaml"
225
+ )
226
+
227
+ write_yaml(dict_file=self.portfolio_config_template, path=config_file)
228
+ self.logger.info(f"Configuration stored under {config_file}")
229
+ return config_file
230
+
231
+
232
+ @classmethod
233
+ def load_configuration(cls, configuration_name) -> PortfolioConfiguration:
234
+ config_file = os.path.join(cls.configuration_folder_path, f"{configuration_name}.yaml")
235
+ portfolio_config = PortfolioConfiguration.read_portfolio_configuration_from_yaml(config_file)
236
+ return PortfolioConfiguration(**portfolio_config)
237
+
238
+ @classmethod
239
+ def load_from_configuration(cls, configuration_name, config_file: Optional[str] = None):
240
+ if config_file is None:
241
+ config_file = os.path.join(cls.configuration_folder_path, f"{configuration_name}.yaml")
242
+ if not os.path.exists(config_file):
243
+ raise FileNotFoundError(f"Configuration file '{config_file}' does not exist.")
244
+
245
+ portfolio_config = PortfolioConfiguration.read_portfolio_configuration_from_yaml(config_file)
246
+ portfolio = cls(portfolio_config_template=portfolio_config, configuration_name=configuration_name)
247
+ return portfolio
248
+
249
+ @classmethod
250
+ def list_configurations(cls):
251
+ """
252
+ Lists all YAML configuration files found in the configuration_path.
253
+ """
254
+ if not os.path.exists(cls.configuration_folder_path):
255
+ return []
256
+
257
+ files = os.listdir(cls.configuration_folder_path)
258
+ yaml_files = [f for f in files if f.endswith(".yaml")]
259
+ # Strip off the '.yaml' extension to return just the base names
260
+ return [os.path.splitext(f)[0] for f in yaml_files]
261
+
262
+ def delete_stored_configuration(self):
263
+ """
264
+ Removes a saved configuration file from the configuration folder
265
+ """
266
+ if not self.configuration_name:
267
+ raise ValueError("No configuration name set. Cannot delete an unnamed configuration.")
268
+ config_file = os.path.join(self.configuration_folder_path, f"{self.configuration_name}.yaml")
269
+ if not os.path.exists(config_file):
270
+ raise FileNotFoundError(f"Configuration file '{config_file}' does not exist.")
271
+ os.remove(config_file)
272
+ self.logger.info(f"Deleted configuration file '{config_file}'.")