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.
- mainsequence/__init__.py +0 -0
- mainsequence/__main__.py +9 -0
- mainsequence/cli/__init__.py +1 -0
- mainsequence/cli/api.py +157 -0
- mainsequence/cli/cli.py +442 -0
- mainsequence/cli/config.py +78 -0
- mainsequence/cli/ssh_utils.py +126 -0
- mainsequence/client/__init__.py +17 -0
- mainsequence/client/base.py +431 -0
- mainsequence/client/data_sources_interfaces/__init__.py +0 -0
- mainsequence/client/data_sources_interfaces/duckdb.py +1468 -0
- mainsequence/client/data_sources_interfaces/timescale.py +479 -0
- mainsequence/client/models_helpers.py +113 -0
- mainsequence/client/models_report_studio.py +412 -0
- mainsequence/client/models_tdag.py +2276 -0
- mainsequence/client/models_vam.py +1983 -0
- mainsequence/client/utils.py +387 -0
- mainsequence/dashboards/__init__.py +0 -0
- mainsequence/dashboards/streamlit/__init__.py +0 -0
- mainsequence/dashboards/streamlit/assets/config.toml +12 -0
- mainsequence/dashboards/streamlit/assets/favicon.png +0 -0
- mainsequence/dashboards/streamlit/assets/logo.png +0 -0
- mainsequence/dashboards/streamlit/core/__init__.py +0 -0
- mainsequence/dashboards/streamlit/core/theme.py +212 -0
- mainsequence/dashboards/streamlit/pages/__init__.py +0 -0
- mainsequence/dashboards/streamlit/scaffold.py +220 -0
- mainsequence/instrumentation/__init__.py +7 -0
- mainsequence/instrumentation/utils.py +101 -0
- mainsequence/instruments/__init__.py +1 -0
- mainsequence/instruments/data_interface/__init__.py +10 -0
- mainsequence/instruments/data_interface/data_interface.py +361 -0
- mainsequence/instruments/instruments/__init__.py +3 -0
- mainsequence/instruments/instruments/base_instrument.py +85 -0
- mainsequence/instruments/instruments/bond.py +447 -0
- mainsequence/instruments/instruments/european_option.py +74 -0
- mainsequence/instruments/instruments/interest_rate_swap.py +217 -0
- mainsequence/instruments/instruments/json_codec.py +585 -0
- mainsequence/instruments/instruments/knockout_fx_option.py +146 -0
- mainsequence/instruments/instruments/position.py +475 -0
- mainsequence/instruments/instruments/ql_fields.py +239 -0
- mainsequence/instruments/instruments/vanilla_fx_option.py +107 -0
- mainsequence/instruments/pricing_models/__init__.py +0 -0
- mainsequence/instruments/pricing_models/black_scholes.py +49 -0
- mainsequence/instruments/pricing_models/bond_pricer.py +182 -0
- mainsequence/instruments/pricing_models/fx_option_pricer.py +90 -0
- mainsequence/instruments/pricing_models/indices.py +350 -0
- mainsequence/instruments/pricing_models/knockout_fx_pricer.py +209 -0
- mainsequence/instruments/pricing_models/swap_pricer.py +502 -0
- mainsequence/instruments/settings.py +175 -0
- mainsequence/instruments/utils.py +29 -0
- mainsequence/logconf.py +284 -0
- mainsequence/reportbuilder/__init__.py +0 -0
- mainsequence/reportbuilder/__main__.py +0 -0
- mainsequence/reportbuilder/examples/ms_template_report.py +706 -0
- mainsequence/reportbuilder/model.py +713 -0
- mainsequence/reportbuilder/slide_templates.py +532 -0
- mainsequence/tdag/__init__.py +8 -0
- mainsequence/tdag/__main__.py +0 -0
- mainsequence/tdag/config.py +129 -0
- mainsequence/tdag/data_nodes/__init__.py +12 -0
- mainsequence/tdag/data_nodes/build_operations.py +751 -0
- mainsequence/tdag/data_nodes/data_nodes.py +1292 -0
- mainsequence/tdag/data_nodes/persist_managers.py +812 -0
- mainsequence/tdag/data_nodes/run_operations.py +543 -0
- mainsequence/tdag/data_nodes/utils.py +24 -0
- mainsequence/tdag/future_registry.py +25 -0
- mainsequence/tdag/utils.py +40 -0
- mainsequence/virtualfundbuilder/__init__.py +45 -0
- mainsequence/virtualfundbuilder/__main__.py +235 -0
- mainsequence/virtualfundbuilder/agent_interface.py +77 -0
- mainsequence/virtualfundbuilder/config_handling.py +86 -0
- mainsequence/virtualfundbuilder/contrib/__init__.py +0 -0
- mainsequence/virtualfundbuilder/contrib/apps/__init__.py +8 -0
- mainsequence/virtualfundbuilder/contrib/apps/etf_replicator_app.py +164 -0
- mainsequence/virtualfundbuilder/contrib/apps/generate_report.py +292 -0
- mainsequence/virtualfundbuilder/contrib/apps/load_external_portfolio.py +107 -0
- mainsequence/virtualfundbuilder/contrib/apps/news_app.py +437 -0
- mainsequence/virtualfundbuilder/contrib/apps/portfolio_report_app.py +91 -0
- mainsequence/virtualfundbuilder/contrib/apps/portfolio_table.py +95 -0
- mainsequence/virtualfundbuilder/contrib/apps/run_named_portfolio.py +45 -0
- mainsequence/virtualfundbuilder/contrib/apps/run_portfolio.py +40 -0
- mainsequence/virtualfundbuilder/contrib/apps/templates/base.html +147 -0
- mainsequence/virtualfundbuilder/contrib/apps/templates/report.html +77 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/__init__.py +5 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/external_weights.py +61 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/intraday_trend.py +149 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/market_cap.py +310 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/mock_signal.py +78 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/portfolio_replicator.py +269 -0
- mainsequence/virtualfundbuilder/contrib/prices/__init__.py +1 -0
- mainsequence/virtualfundbuilder/contrib/prices/data_nodes.py +810 -0
- mainsequence/virtualfundbuilder/contrib/prices/utils.py +11 -0
- mainsequence/virtualfundbuilder/contrib/rebalance_strategies/__init__.py +1 -0
- mainsequence/virtualfundbuilder/contrib/rebalance_strategies/rebalance_strategies.py +313 -0
- mainsequence/virtualfundbuilder/data_nodes.py +637 -0
- mainsequence/virtualfundbuilder/enums.py +23 -0
- mainsequence/virtualfundbuilder/models.py +282 -0
- mainsequence/virtualfundbuilder/notebook_handling.py +42 -0
- mainsequence/virtualfundbuilder/portfolio_interface.py +272 -0
- mainsequence/virtualfundbuilder/resource_factory/__init__.py +0 -0
- mainsequence/virtualfundbuilder/resource_factory/app_factory.py +170 -0
- mainsequence/virtualfundbuilder/resource_factory/base_factory.py +238 -0
- mainsequence/virtualfundbuilder/resource_factory/rebalance_factory.py +101 -0
- mainsequence/virtualfundbuilder/resource_factory/signal_factory.py +183 -0
- mainsequence/virtualfundbuilder/utils.py +381 -0
- mainsequence-2.0.0.dist-info/METADATA +105 -0
- mainsequence-2.0.0.dist-info/RECORD +110 -0
- mainsequence-2.0.0.dist-info/WHEEL +5 -0
- mainsequence-2.0.0.dist-info/licenses/LICENSE +40 -0
- 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}'.")
|
File without changes
|