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,812 @@
|
|
1
|
+
import pandas as pd
|
2
|
+
import datetime
|
3
|
+
from typing import Union, List, Dict, Optional, Tuple
|
4
|
+
import os
|
5
|
+
from mainsequence.logconf import logger
|
6
|
+
|
7
|
+
|
8
|
+
from mainsequence.client import (LocalTimeSerie, UniqueIdentifierRangeMap,
|
9
|
+
LocalTimeSeriesDoesNotExist,
|
10
|
+
DynamicTableDoesNotExist, DynamicTableDataSource, TDAG_CONSTANTS as CONSTANTS, DynamicTableMetaData,
|
11
|
+
UpdateStatistics, DoesNotExist)
|
12
|
+
|
13
|
+
from mainsequence.client.models_tdag import LocalTimeSerieUpdateDetails
|
14
|
+
import mainsequence.client as ms_client
|
15
|
+
import json
|
16
|
+
import threading
|
17
|
+
from concurrent.futures import Future
|
18
|
+
from .. import future_registry
|
19
|
+
from mainsequence.instrumentation import tracer, tracer_instrumentator
|
20
|
+
import inspect
|
21
|
+
import hashlib
|
22
|
+
|
23
|
+
def get_data_node_source_code(DataNodeClass: "DataNode") -> str:
|
24
|
+
"""
|
25
|
+
Gets the source code of a DataNode class.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
DataNodeClass: The class to get the source code for.
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
The source code as a string.
|
32
|
+
"""
|
33
|
+
global logger
|
34
|
+
try:
|
35
|
+
# First try the standard approach.
|
36
|
+
source = inspect.getsource(DataNodeClass)
|
37
|
+
if source.strip():
|
38
|
+
return source
|
39
|
+
except Exception:
|
40
|
+
logger.warning \
|
41
|
+
("Your TimeSeries is not in a python module this will likely bring exceptions when running in a pipeline")
|
42
|
+
from IPython import get_ipython
|
43
|
+
# Fallback: Scan IPython's input history.
|
44
|
+
ip = get_ipython() # Get the current IPython instance.
|
45
|
+
if ip is not None:
|
46
|
+
# Retrieve the full history as a single string.
|
47
|
+
history = "\n".join(code for _, _, code in ip.history_manager.get_range())
|
48
|
+
marker = f"class {DataNodeClass.__name__}"
|
49
|
+
idx = history.find(marker)
|
50
|
+
if idx != -1:
|
51
|
+
return history[idx:]
|
52
|
+
return "Source code unavailable."
|
53
|
+
|
54
|
+
def get_data_node_source_code_git_hash(DataNodeClass: "DataNode") -> str:
|
55
|
+
"""
|
56
|
+
Hashes the source code of a DataNode class using SHA-1 (Git style).
|
57
|
+
|
58
|
+
Args:
|
59
|
+
DataNodeClass: The class to hash.
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
The Git-style hash of the source code.
|
63
|
+
"""
|
64
|
+
data_node_class_source_code = get_data_node_source_code(DataNodeClass)
|
65
|
+
# Prepare the content for Git-style hashing
|
66
|
+
# Git hashing format: "blob <size_of_content>\0<content>"
|
67
|
+
content = f"blob {len(data_node_class_source_code)}\0{data_node_class_source_code}"
|
68
|
+
# Compute the SHA-1 hash (Git hash)
|
69
|
+
hash_object = hashlib.sha1(content.encode('utf-8'))
|
70
|
+
git_hash = hash_object.hexdigest()
|
71
|
+
return git_hash
|
72
|
+
|
73
|
+
|
74
|
+
class APIPersistManager:
|
75
|
+
"""
|
76
|
+
Manages persistence for time series data accessed via an API.
|
77
|
+
It handles asynchronous fetching of metadata to avoid blocking operations.
|
78
|
+
"""
|
79
|
+
|
80
|
+
def __init__(self, data_source_id: int, storage_hash: str):
|
81
|
+
"""
|
82
|
+
Initializes the APIPersistManager.
|
83
|
+
|
84
|
+
Args:
|
85
|
+
data_source_id: The ID of the data source.
|
86
|
+
update_hash: The local hash identifier for the time series.
|
87
|
+
"""
|
88
|
+
self.data_source_id: int = data_source_id
|
89
|
+
self.storage_hash: str = storage_hash
|
90
|
+
|
91
|
+
logger.debug(f"Initializing Time Serie {self.storage_hash} as APIDataNode")
|
92
|
+
|
93
|
+
# Create a Future to hold the local metadata when ready.
|
94
|
+
self._metadata_future = Future()
|
95
|
+
# Register the future globally.
|
96
|
+
future_registry.add_future(self._metadata_future)
|
97
|
+
# Launch the REST request in a separate, non-daemon thread.
|
98
|
+
thread = threading.Thread(target=self._init_metadata,
|
99
|
+
name=f"ApiMetaDataThread-{self.storage_hash}",
|
100
|
+
daemon=False)
|
101
|
+
thread.start()
|
102
|
+
|
103
|
+
|
104
|
+
@property
|
105
|
+
def metadata(self) -> DynamicTableMetaData:
|
106
|
+
"""Lazily block and cache the result if needed."""
|
107
|
+
if not hasattr(self, '_metadata_cached'):
|
108
|
+
# This call blocks until the future is resolved.
|
109
|
+
self._metadata_cached = self._metadata_future.result()
|
110
|
+
return self._metadata_cached
|
111
|
+
|
112
|
+
def _init_metadata(self) -> None:
|
113
|
+
"""
|
114
|
+
Performs the REST request to fetch local metadata asynchronously.
|
115
|
+
Sets the result or exception on the future object.
|
116
|
+
"""
|
117
|
+
try:
|
118
|
+
result = DynamicTableMetaData.get_or_none(storage_hash=self.storage_hash,
|
119
|
+
data_source__id=self.data_source_id,
|
120
|
+
include_relations_detail=True
|
121
|
+
)
|
122
|
+
self._metadata_future.set_result(result)
|
123
|
+
except Exception as exc:
|
124
|
+
self._metadata_future.set_exception(exc)
|
125
|
+
finally:
|
126
|
+
# Remove the future from the global registry once done.
|
127
|
+
future_registry.remove_future(self._metadata_future)
|
128
|
+
|
129
|
+
def get_df_between_dates(self, *args, **kwargs) -> pd.DataFrame:
|
130
|
+
"""
|
131
|
+
Retrieves a DataFrame from the API between specified dates.
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
A pandas DataFrame with the requested data.
|
135
|
+
"""
|
136
|
+
filtered_data = self.metadata.get_data_between_dates_from_api(*args, **kwargs)
|
137
|
+
if filtered_data.empty:
|
138
|
+
return filtered_data
|
139
|
+
|
140
|
+
# fix types
|
141
|
+
stc = self.metadata.sourcetableconfiguration
|
142
|
+
filtered_data[stc.time_index_name] = pd.to_datetime(filtered_data[stc.time_index_name], utc=True)
|
143
|
+
column_filter = kwargs.get("columns") or stc.column_dtypes_map.keys()
|
144
|
+
for c in column_filter:
|
145
|
+
c_type=stc.column_dtypes_map[c]
|
146
|
+
if c != stc.time_index_name:
|
147
|
+
if c_type == "object":
|
148
|
+
c_type = "str"
|
149
|
+
filtered_data[c] = filtered_data[c].astype(c_type)
|
150
|
+
filtered_data = filtered_data.set_index(stc.index_names)
|
151
|
+
|
152
|
+
return filtered_data
|
153
|
+
|
154
|
+
|
155
|
+
class PersistManager:
|
156
|
+
def __init__(self,
|
157
|
+
data_source: DynamicTableDataSource,
|
158
|
+
update_hash: str,
|
159
|
+
description: Optional[str] = None,
|
160
|
+
class_name: Optional[str] = None,
|
161
|
+
metadata: Optional[Dict] = None,
|
162
|
+
local_metadata: Optional[LocalTimeSerie] = None
|
163
|
+
):
|
164
|
+
"""
|
165
|
+
Initializes the PersistManager.
|
166
|
+
|
167
|
+
Args:
|
168
|
+
data_source: The data source for the time series.
|
169
|
+
update_hash: The local hash identifier for the time series.
|
170
|
+
description: An optional description for the time series.
|
171
|
+
class_name: The name of the DataNode class.
|
172
|
+
metadata: Optional remote metadata dictionary.
|
173
|
+
local_metadata: Optional local metadata object.
|
174
|
+
"""
|
175
|
+
self.data_source: DynamicTableDataSource = data_source
|
176
|
+
self.update_hash: str = update_hash
|
177
|
+
if local_metadata is not None and metadata is None:
|
178
|
+
# query remote storage_hash
|
179
|
+
metadata = local_metadata.remote_table
|
180
|
+
self.description: Optional[str] = description
|
181
|
+
self.logger = logger
|
182
|
+
|
183
|
+
self.table_model_loaded: bool = False
|
184
|
+
self.class_name: Optional[str] = class_name
|
185
|
+
|
186
|
+
# Private members for managing lazy asynchronous retrieval.
|
187
|
+
self._local_metadata_future: Optional[Future] = None
|
188
|
+
self._local_metadata_cached: Optional[LocalTimeSerie] = None
|
189
|
+
self._local_metadata_lock = threading.Lock()
|
190
|
+
self._metadata_cached: Optional[DynamicTableMetaData] = None
|
191
|
+
|
192
|
+
if self.update_hash is not None:
|
193
|
+
self.synchronize_metadata(local_metadata=local_metadata)
|
194
|
+
|
195
|
+
def synchronize_metadata(self, local_metadata: Optional[LocalTimeSerie]) -> None:
|
196
|
+
if local_metadata is not None:
|
197
|
+
self.set_local_metadata(local_metadata)
|
198
|
+
else:
|
199
|
+
self.set_local_metadata_lazy(force_registry=True, include_relations_detail=True)
|
200
|
+
|
201
|
+
@classmethod
|
202
|
+
def get_from_data_type(cls, data_source: DynamicTableDataSource, *args, **kwargs) -> 'PersistManager':
|
203
|
+
"""
|
204
|
+
Factory method to get the correct PersistManager based on data source type.
|
205
|
+
|
206
|
+
Args:
|
207
|
+
data_source: The data source object.
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
An instance of a PersistManager subclass.
|
211
|
+
"""
|
212
|
+
data_type = data_source.related_resource_class_type
|
213
|
+
if data_type in CONSTANTS.DATA_SOURCE_TYPE_TIMESCALEDB:
|
214
|
+
return TimeScaleLocalPersistManager(data_source=data_source, *args, **kwargs)
|
215
|
+
else:
|
216
|
+
return TimeScaleLocalPersistManager(data_source=data_source, *args, **kwargs)
|
217
|
+
|
218
|
+
def set_local_metadata(self, local_metadata: LocalTimeSerie) -> None:
|
219
|
+
"""
|
220
|
+
Caches the local metadata object for lazy queries
|
221
|
+
|
222
|
+
Args:
|
223
|
+
local_metadata: The LocalTimeSerie object to cache.
|
224
|
+
"""
|
225
|
+
self._local_metadata_cached = local_metadata
|
226
|
+
|
227
|
+
@property
|
228
|
+
def local_metadata(self) -> LocalTimeSerie:
|
229
|
+
"""Lazily block and retrieve the local metadata, caching the result."""
|
230
|
+
with self._local_metadata_lock:
|
231
|
+
if self._local_metadata_cached is None:
|
232
|
+
if self._local_metadata_future is None:
|
233
|
+
# If no future is running, start one.
|
234
|
+
self.set_local_metadata_lazy(force_registry=True)
|
235
|
+
# Block until the future completes and cache its result.
|
236
|
+
local_metadata = self._local_metadata_future.result()
|
237
|
+
self.set_local_metadata(local_metadata)
|
238
|
+
return self._local_metadata_cached
|
239
|
+
|
240
|
+
# Define a callback that will launch set_local_metadata_lazy after the remote update is complete.
|
241
|
+
@property
|
242
|
+
def metadata(self) -> Optional[DynamicTableMetaData]:
|
243
|
+
"""
|
244
|
+
Lazily retrieves and returns the remote metadata.
|
245
|
+
"""
|
246
|
+
if self.local_metadata is None:
|
247
|
+
return None
|
248
|
+
if self.local_metadata.remote_table is not None:
|
249
|
+
if self.local_metadata.remote_table.sourcetableconfiguration is not None:
|
250
|
+
if self.local_metadata.remote_table.build_meta_data.get("initialize_with_default_partitions",True) == False:
|
251
|
+
if self.local_metadata.remote_table.data_source.related_resource_class_type in CONSTANTS.DATA_SOURCE_TYPE_TIMESCALEDB:
|
252
|
+
self.logger.warning("Default Partitions will not be initialized ")
|
253
|
+
|
254
|
+
return self.local_metadata.remote_table
|
255
|
+
|
256
|
+
@property
|
257
|
+
def local_build_configuration(self) -> Dict:
|
258
|
+
return self.local_metadata.build_configuration
|
259
|
+
|
260
|
+
@property
|
261
|
+
def local_build_metadata(self) -> Dict:
|
262
|
+
return self.local_metadata.build_meta_data
|
263
|
+
|
264
|
+
def set_local_metadata_lazy_callback(self, fut: Future) -> None:
|
265
|
+
"""
|
266
|
+
Callback to handle the result of an asynchronous task and trigger a metadata refresh.
|
267
|
+
"""
|
268
|
+
try:
|
269
|
+
# This will re-raise any exception that occurred in _update_task.
|
270
|
+
fut.result()
|
271
|
+
except Exception as exc:
|
272
|
+
# Optionally, handle or log the error if needed.
|
273
|
+
# For example: logger.error("Remote build update failed: %s", exc)
|
274
|
+
raise exc
|
275
|
+
# Launch the local metadata update regardless of the outcome.
|
276
|
+
self.set_local_metadata_lazy(force_registry=True)
|
277
|
+
|
278
|
+
def set_local_metadata_lazy(self, force_registry: bool = True, include_relations_detail: bool = True) -> None:
|
279
|
+
"""
|
280
|
+
Initiates a lazy, asynchronous fetch of the local metadata.
|
281
|
+
|
282
|
+
Args:
|
283
|
+
force_registry: If True, forces a refresh even if cached data exists.
|
284
|
+
include_relations_detail: If True, includes relationship details in the fetch.
|
285
|
+
"""
|
286
|
+
with self._local_metadata_lock:
|
287
|
+
if force_registry:
|
288
|
+
self._local_metadata_cached = None
|
289
|
+
# Capture the new future in a local variable.
|
290
|
+
new_future = Future()
|
291
|
+
self._local_metadata_future = new_future
|
292
|
+
# Register the new future.
|
293
|
+
future_registry.add_future(new_future)
|
294
|
+
|
295
|
+
def _get_or_none_local_metadata():
|
296
|
+
"""Perform the REST request asynchronously."""
|
297
|
+
try:
|
298
|
+
result = LocalTimeSerie.get_or_none(
|
299
|
+
update_hash=self.update_hash,
|
300
|
+
remote_table__data_source__id=self.data_source.id,
|
301
|
+
include_relations_detail=include_relations_detail
|
302
|
+
)
|
303
|
+
if result is None:
|
304
|
+
self.logger.warning(f"TimeSeries {self.update_hash} with data source {self.data_source.id} not found in backend")
|
305
|
+
new_future.set_result(result)
|
306
|
+
except Exception as exc:
|
307
|
+
new_future.set_exception(exc)
|
308
|
+
finally:
|
309
|
+
# Remove the future from the global registry once done.
|
310
|
+
future_registry.remove_future(new_future)
|
311
|
+
|
312
|
+
thread = threading.Thread(target=_get_or_none_local_metadata,
|
313
|
+
name=f"LocalMetadataThreadPM-{self.update_hash}",
|
314
|
+
daemon=False)
|
315
|
+
thread.start()
|
316
|
+
|
317
|
+
|
318
|
+
|
319
|
+
def depends_on_connect(self, new_ts: "DataNode", is_api: bool) -> None:
|
320
|
+
"""
|
321
|
+
Connects a time series as a relationship in the DB.
|
322
|
+
|
323
|
+
Args:
|
324
|
+
new_ts: The target DataNode to connect to.
|
325
|
+
is_api: True if the target is an APIDataNode
|
326
|
+
"""
|
327
|
+
if not is_api:
|
328
|
+
self.local_metadata.depends_on_connect(target_time_serie_id=new_ts.local_time_serie.id)
|
329
|
+
else:
|
330
|
+
try:
|
331
|
+
self.local_metadata.depends_on_connect_to_api_table(target_table_id=new_ts.local_persist_manager.metadata.id)
|
332
|
+
except Exception as exc:
|
333
|
+
raise exc
|
334
|
+
|
335
|
+
def display_mermaid_dependency_diagram(self) -> str:
|
336
|
+
"""
|
337
|
+
Generates and returns an HTML string for a Mermaid dependency diagram.
|
338
|
+
|
339
|
+
Returns:
|
340
|
+
An HTML string containing the Mermaid diagram and supporting Javascript.
|
341
|
+
"""
|
342
|
+
from IPython.core.display import display, HTML, Javascript
|
343
|
+
|
344
|
+
response = ms_client.TimeSerieLocalUpdate.get_mermaid_dependency_diagram(update_hash=self.update_hash,
|
345
|
+
data_source_id=self.data_source.id
|
346
|
+
)
|
347
|
+
from IPython.core.display import display, HTML, Javascript
|
348
|
+
mermaid_chart = response.get("mermaid_chart")
|
349
|
+
metadata = response.get("metadata")
|
350
|
+
# Render Mermaid.js diagram with metadata display
|
351
|
+
html_template = f"""
|
352
|
+
<div class="mermaid">
|
353
|
+
{mermaid_chart}
|
354
|
+
</div>
|
355
|
+
<div id="metadata-display" style="margin-top: 20px; font-size: 16px; color: #333;"></div>
|
356
|
+
<script>
|
357
|
+
// Initialize Mermaid.js
|
358
|
+
if (typeof mermaid !== 'undefined') {{
|
359
|
+
mermaid.initialize({{ startOnLoad: true }});
|
360
|
+
}}
|
361
|
+
|
362
|
+
// Metadata dictionary
|
363
|
+
const metadata = {metadata};
|
364
|
+
|
365
|
+
// Attach click listeners to nodes
|
366
|
+
document.addEventListener('click', function(event) {{
|
367
|
+
const target = event.target.closest('div[data-graph-id]');
|
368
|
+
if (target) {{
|
369
|
+
const nodeId = target.dataset.graphId;
|
370
|
+
const metadataDisplay = document.getElementById('metadata-display');
|
371
|
+
if (metadata[nodeId]) {{
|
372
|
+
metadataDisplay.innerHTML = "<strong>Node Metadata:</strong> " + metadata[nodeId];
|
373
|
+
}} else {{
|
374
|
+
metadataDisplay.innerHTML = "<strong>No metadata available for this node.</strong>";
|
375
|
+
}}
|
376
|
+
}}
|
377
|
+
}});
|
378
|
+
</script>
|
379
|
+
"""
|
380
|
+
|
381
|
+
return mermaid_chart
|
382
|
+
|
383
|
+
def get_mermaid_dependency_diagram(self) -> str:
|
384
|
+
"""
|
385
|
+
Displays a Mermaid.js dependency diagram in a Jupyter environment.
|
386
|
+
|
387
|
+
Returns:
|
388
|
+
The Mermaid diagram string.
|
389
|
+
"""
|
390
|
+
from IPython.display import display, HTML
|
391
|
+
|
392
|
+
mermaid_diagram = self.display_mermaid_dependency_diagram()
|
393
|
+
|
394
|
+
# Mermaid.js initialization script (only run once)
|
395
|
+
if not hasattr(display, "_mermaid_initialized"):
|
396
|
+
mermaid_initialize = """
|
397
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
398
|
+
<script>
|
399
|
+
function initializeMermaid() {
|
400
|
+
if (typeof mermaid !== 'undefined') {
|
401
|
+
console.log('Initializing Mermaid.js...');
|
402
|
+
const mermaidDivs = document.querySelectorAll('.mermaid');
|
403
|
+
mermaidDivs.forEach(mermaidDiv => {
|
404
|
+
mermaid.init(undefined, mermaidDiv);
|
405
|
+
});
|
406
|
+
} else {
|
407
|
+
console.error('Mermaid.js is not loaded.');
|
408
|
+
}
|
409
|
+
}
|
410
|
+
</script>
|
411
|
+
"""
|
412
|
+
display(HTML(mermaid_initialize))
|
413
|
+
display._mermaid_initialized = True
|
414
|
+
|
415
|
+
# HTML template for rendering the Mermaid diagram
|
416
|
+
html_template = f"""
|
417
|
+
<div class="mermaid">
|
418
|
+
{mermaid_diagram}
|
419
|
+
</div>
|
420
|
+
<script>
|
421
|
+
initializeMermaid();
|
422
|
+
</script>
|
423
|
+
"""
|
424
|
+
|
425
|
+
# Display the Mermaid diagram in the notebook
|
426
|
+
display(HTML(html_template))
|
427
|
+
|
428
|
+
# Optionally return the raw diagram code for further use
|
429
|
+
return mermaid_diagram
|
430
|
+
|
431
|
+
def get_all_dependencies_update_priority(self) -> pd.DataFrame:
|
432
|
+
"""
|
433
|
+
Retrieves a DataFrame of all dependencies with their update priority.
|
434
|
+
|
435
|
+
Returns:
|
436
|
+
A pandas DataFrame with dependency and priority information.
|
437
|
+
"""
|
438
|
+
depth_df = self.local_metadata.get_all_dependencies_update_priority()
|
439
|
+
return depth_df
|
440
|
+
|
441
|
+
def set_ogm_dependencies_linked(self) -> None:
|
442
|
+
self.local_metadata.patch(ogm_dependencies_linked=True)
|
443
|
+
|
444
|
+
@property
|
445
|
+
def update_details(self) -> Optional[LocalTimeSerieUpdateDetails]:
|
446
|
+
"""Returns the update details associated with the local time series."""
|
447
|
+
return self.local_metadata.localtimeserieupdatedetails
|
448
|
+
|
449
|
+
@property
|
450
|
+
def run_configuration(self) -> Optional[Dict]:
|
451
|
+
"""Returns the run configuration from the local metadata."""
|
452
|
+
return self.local_metadata.run_configuration
|
453
|
+
|
454
|
+
@property
|
455
|
+
def source_table_configuration(self) -> Optional[Dict]:
|
456
|
+
"""Returns the source table configuration from the remote metadata."""
|
457
|
+
if "sourcetableconfiguration" in self.metadata.keys():
|
458
|
+
return self.metadata['sourcetableconfiguration']
|
459
|
+
return None
|
460
|
+
|
461
|
+
def update_source_informmation(self, git_hash_id: str, source_code: str) -> None:
|
462
|
+
"""
|
463
|
+
Updates the source code and git hash for the remote table.
|
464
|
+
"""
|
465
|
+
self.local_metadata.remote_table = self.metadata.patch(
|
466
|
+
time_serie_source_code_git_hash=git_hash_id,
|
467
|
+
time_serie_source_code=source_code,
|
468
|
+
)
|
469
|
+
|
470
|
+
|
471
|
+
|
472
|
+
def add_tags(self, tags: List[str]) -> None:
|
473
|
+
"""Adds tags to the local time series metadata if they don't already exist."""
|
474
|
+
if any([t not in self.local_metadata.tags for t in tags]) == True:
|
475
|
+
self.local_metadata.add_tags(tags=tags)
|
476
|
+
|
477
|
+
@property
|
478
|
+
def persist_size(self) -> int:
|
479
|
+
"""Returns the size of the persisted table, or 0 if not available."""
|
480
|
+
try:
|
481
|
+
return self.metadata['table_size']
|
482
|
+
except KeyError:
|
483
|
+
return 0
|
484
|
+
|
485
|
+
def time_serie_exist(self) -> bool:
|
486
|
+
"""Checks if the remote metadata for the time series exists."""
|
487
|
+
if hasattr(self, "metadata"):
|
488
|
+
return True
|
489
|
+
return False
|
490
|
+
|
491
|
+
def patch_build_configuration(self, local_configuration: dict, remote_configuration: dict,
|
492
|
+
remote_build_metadata: dict) -> None:
|
493
|
+
"""
|
494
|
+
Asynchronously patches the build configuration for the remote and local tables.
|
495
|
+
|
496
|
+
Args:
|
497
|
+
local_configuration: The build configuration for the local time series.
|
498
|
+
remote_configuration: The build configuration for the remote table.
|
499
|
+
remote_build_metadata: The build metadata for the remote table.
|
500
|
+
"""
|
501
|
+
# This ensures that later accesses to local_metadata will block for the new value.
|
502
|
+
with self._local_metadata_lock:
|
503
|
+
self._local_metadata_future = Future()
|
504
|
+
future_registry.add_future(self._local_metadata_future)
|
505
|
+
|
506
|
+
kwargs = dict(
|
507
|
+
build_configuration=remote_configuration, )
|
508
|
+
|
509
|
+
|
510
|
+
local_metadata_kwargs = dict(update_hash=self.update_hash,
|
511
|
+
build_configuration=local_configuration,
|
512
|
+
)
|
513
|
+
|
514
|
+
patch_future = Future()
|
515
|
+
future_registry.add_future(patch_future)
|
516
|
+
|
517
|
+
# Define the inner helper function.
|
518
|
+
def _patch_build_configuration():
|
519
|
+
"""Helper function for patching build configuration asynchronously."""
|
520
|
+
try:
|
521
|
+
# Execute the patch operation; this method is expected to return a LocalTimeSerie-like instance.
|
522
|
+
result = DynamicTableMetaData.patch_build_configuration(
|
523
|
+
remote_table_patch=kwargs,
|
524
|
+
data_source_id=self.data_source.id,
|
525
|
+
build_meta_data=remote_build_metadata,
|
526
|
+
local_table_patch=local_metadata_kwargs,
|
527
|
+
)
|
528
|
+
patch_future.set_result(True) #success
|
529
|
+
except Exception as exc:
|
530
|
+
patch_future.set_exception(exc)
|
531
|
+
finally:
|
532
|
+
# Once the operation is complete (or errors out), remove the future from the global registry.
|
533
|
+
future_registry.remove_future(result)
|
534
|
+
|
535
|
+
thread = threading.Thread(
|
536
|
+
target=_patch_build_configuration,
|
537
|
+
name=f"PatchBuildConfigThread-{self.update_hash}",
|
538
|
+
daemon=False
|
539
|
+
)
|
540
|
+
thread.start()
|
541
|
+
|
542
|
+
patch_future.add_done_callback(self.set_local_metadata_lazy_callback)
|
543
|
+
|
544
|
+
|
545
|
+
def local_persist_exist_set_config(
|
546
|
+
self,
|
547
|
+
storage_hash: str,
|
548
|
+
local_configuration: dict,
|
549
|
+
remote_configuration: dict,
|
550
|
+
data_source: DynamicTableDataSource,
|
551
|
+
time_serie_source_code_git_hash: str,
|
552
|
+
time_serie_source_code: str,
|
553
|
+
build_configuration_json_schema: dict,
|
554
|
+
) -> None:
|
555
|
+
"""
|
556
|
+
Ensures local and remote persistence objects exist and sets their configurations.
|
557
|
+
This runs on DataNode initialization.
|
558
|
+
"""
|
559
|
+
remote_build_configuration = None
|
560
|
+
if hasattr(self, "remote_build_configuration"):
|
561
|
+
remote_build_configuration = self.remote_build_configuration
|
562
|
+
|
563
|
+
if remote_build_configuration is None:
|
564
|
+
logger.debug(f"remote table {storage_hash} does not exist creating")
|
565
|
+
#create remote table
|
566
|
+
|
567
|
+
try:
|
568
|
+
|
569
|
+
# table may not exist but
|
570
|
+
remote_build_metadata = remote_configuration["build_meta_data"] if "build_meta_data" in remote_configuration.keys() else {}
|
571
|
+
remote_configuration.pop("build_meta_data", None)
|
572
|
+
kwargs = dict(storage_hash=storage_hash,
|
573
|
+
time_serie_source_code_git_hash=time_serie_source_code_git_hash,
|
574
|
+
time_serie_source_code=time_serie_source_code,
|
575
|
+
build_configuration=remote_configuration,
|
576
|
+
data_source=data_source.model_dump(),
|
577
|
+
build_meta_data=remote_build_metadata,
|
578
|
+
build_configuration_json_schema=build_configuration_json_schema
|
579
|
+
)
|
580
|
+
|
581
|
+
|
582
|
+
dtd_metadata = DynamicTableMetaData.get_or_create(**kwargs)
|
583
|
+
storage_hash = dtd_metadata.storage_hash
|
584
|
+
except Exception as e:
|
585
|
+
self.logger.exception(f"{storage_hash} Could not set meta data in DB for P")
|
586
|
+
raise e
|
587
|
+
else:
|
588
|
+
self.set_local_metadata_lazy(force_registry=True, include_relations_detail=True)
|
589
|
+
storage_hash = self.metadata.storage_hash
|
590
|
+
|
591
|
+
local_table_exist = self._verify_local_ts_exists(storage_hash=storage_hash, local_configuration=local_configuration)
|
592
|
+
|
593
|
+
|
594
|
+
def _verify_local_ts_exists(self, storage_hash: str,
|
595
|
+
local_configuration: Optional[Dict] = None) -> None:
|
596
|
+
"""
|
597
|
+
Verifies that the local time series exists in the ORM, creating it if necessary.
|
598
|
+
"""
|
599
|
+
local_build_configuration = None
|
600
|
+
if self.local_metadata is not None:
|
601
|
+
local_build_configuration, local_build_metadata = self.local_build_configuration, self.local_build_metadata
|
602
|
+
if local_build_configuration is None:
|
603
|
+
|
604
|
+
logger.debug(f"local_metadata {self.update_hash} does not exist creating")
|
605
|
+
local_update = LocalTimeSerie.get_or_none(update_hash=self.update_hash,
|
606
|
+
remote_table__data_source__id=self.data_source.id)
|
607
|
+
if local_update is None:
|
608
|
+
local_build_metadata = local_configuration[
|
609
|
+
"build_meta_data"] if "build_meta_data" in local_configuration.keys() else {}
|
610
|
+
local_configuration.pop("build_meta_data", None)
|
611
|
+
metadata_kwargs = dict(
|
612
|
+
update_hash=self.update_hash,
|
613
|
+
build_configuration=local_configuration,
|
614
|
+
remote_table__hash_id=storage_hash,
|
615
|
+
data_source_id=self.data_source.id
|
616
|
+
)
|
617
|
+
|
618
|
+
local_metadata = LocalTimeSerie.get_or_create(**metadata_kwargs,)
|
619
|
+
else:
|
620
|
+
local_metadata = local_update
|
621
|
+
|
622
|
+
self.set_local_metadata(local_metadata=local_metadata)
|
623
|
+
|
624
|
+
|
625
|
+
def _verify_insertion_format(self, temp_df: pd.DataFrame) -> None:
|
626
|
+
"""
|
627
|
+
Verifies that a DataFrame is properly configured for insertion.
|
628
|
+
"""
|
629
|
+
if isinstance(temp_df.index,pd.MultiIndex)==True:
|
630
|
+
assert temp_df.index.names==["time_index", "asset_symbol"] or temp_df.index.names==["time_index", "asset_symbol", "execution_venue_symbol"]
|
631
|
+
|
632
|
+
def build_update_details(self, source_class_name: str) -> None:
|
633
|
+
"""
|
634
|
+
Asynchronously builds or updates the update details for the time series.
|
635
|
+
"""
|
636
|
+
update_kwargs=dict(source_class_name=source_class_name,
|
637
|
+
local_metadata=json.loads(self.local_metadata.model_dump_json())
|
638
|
+
)
|
639
|
+
# This ensures that later accesses to local_metadata will block for the new value.
|
640
|
+
with self._local_metadata_lock:
|
641
|
+
self._local_metadata_future = Future()
|
642
|
+
future_registry.add_future(self._local_metadata_future)
|
643
|
+
|
644
|
+
# Create a future for the remote update task and register it.
|
645
|
+
future = Future()
|
646
|
+
future_registry.add_future(future)
|
647
|
+
|
648
|
+
def _update_task():
|
649
|
+
try:
|
650
|
+
# Run the remote build/update details task.
|
651
|
+
self.local_metadata.remote_table.build_or_update_update_details(**update_kwargs)
|
652
|
+
future.set_result(True) # Signal success
|
653
|
+
except Exception as exc:
|
654
|
+
future.set_exception(exc)
|
655
|
+
finally:
|
656
|
+
# Unregister the future once the task completes.
|
657
|
+
future_registry.remove_future(future)
|
658
|
+
|
659
|
+
thread = threading.Thread(
|
660
|
+
target=_update_task,
|
661
|
+
name=f"BuildUpdateDetailsThread-{self.update_hash}",
|
662
|
+
daemon=False
|
663
|
+
)
|
664
|
+
thread.start()
|
665
|
+
|
666
|
+
# Attach the callback to the future.
|
667
|
+
future.add_done_callback(self.set_local_metadata_lazy_callback)
|
668
|
+
|
669
|
+
def patch_table(self, **kwargs) -> None:
|
670
|
+
"""Patches the remote metadata table with the given keyword arguments."""
|
671
|
+
self.metadata.patch( **kwargs)
|
672
|
+
|
673
|
+
def protect_from_deletion(self, protect_from_deletion: bool = True) -> None:
|
674
|
+
"""Sets the 'protect_from_deletion' flag on the remote metadata."""
|
675
|
+
self.metadata.patch( protect_from_deletion=protect_from_deletion)
|
676
|
+
|
677
|
+
def open_for_everyone(self, open_for_everyone: bool = True) -> None:
|
678
|
+
"""Sets the 'open_for_everyone' flag on local, remote, and source table configurations."""
|
679
|
+
if not self.local_metadata.open_for_everyone:
|
680
|
+
self.local_metadata.patch(open_for_everyone=open_for_everyone)
|
681
|
+
|
682
|
+
if not self.metadata.open_for_everyone:
|
683
|
+
self.metadata.patch(open_for_everyone=open_for_everyone)
|
684
|
+
|
685
|
+
if not self.metadata.sourcetableconfiguration.open_for_everyone:
|
686
|
+
self.metadata.sourcetableconfiguration.patch(open_for_everyone=open_for_everyone)
|
687
|
+
|
688
|
+
|
689
|
+
|
690
|
+
|
691
|
+
|
692
|
+
def get_df_between_dates(self, *args, **kwargs) -> pd.DataFrame:
|
693
|
+
"""
|
694
|
+
Retrieves a DataFrame from the data source between specified dates.
|
695
|
+
"""
|
696
|
+
filtered_data = self.data_source.get_data_by_time_index(
|
697
|
+
local_metadata=self.local_metadata,
|
698
|
+
*args, **kwargs
|
699
|
+
)
|
700
|
+
return filtered_data
|
701
|
+
|
702
|
+
def set_column_metadata(self,
|
703
|
+
columns_metadata: Optional[List[ms_client.ColumnMetaData]]
|
704
|
+
) -> None:
|
705
|
+
if self.metadata:
|
706
|
+
if self.metadata.sourcetableconfiguration != None:
|
707
|
+
if self.metadata.sourcetableconfiguration.columns_metadata is not None:
|
708
|
+
if columns_metadata is None:
|
709
|
+
self.logger.info(f"get_column_metadata method not implemented")
|
710
|
+
return
|
711
|
+
|
712
|
+
self.metadata.sourcetableconfiguration.set_or_update_columns_metadata(
|
713
|
+
columns_metadata=columns_metadata)
|
714
|
+
|
715
|
+
def set_table_metadata(self,
|
716
|
+
table_metadata: ms_client.TableMetaData,
|
717
|
+
):
|
718
|
+
"""
|
719
|
+
Creates or updates the MarketsTimeSeriesDetails metadata in the backend.
|
720
|
+
|
721
|
+
This method orchestrates the synchronization of the time series metadata,
|
722
|
+
including its description, frequency, and associated assets, based on the
|
723
|
+
configuration returned by `_get_time_series_meta_details`.
|
724
|
+
"""
|
725
|
+
if not (self.metadata):
|
726
|
+
self.logger.warning("metadata not set")
|
727
|
+
return
|
728
|
+
|
729
|
+
# 1. Get the user-defined metadata configuration for the time series.
|
730
|
+
if table_metadata is None:
|
731
|
+
return
|
732
|
+
|
733
|
+
# 2. Get or create the MarketsTimeSeriesDetails object in the backend.
|
734
|
+
source_table_id = self.metadata.patch(**table_metadata.model_dump())
|
735
|
+
|
736
|
+
def delete_table(self) -> None:
|
737
|
+
if self.data_source.related_resource.class_type == "duck_db":
|
738
|
+
from mainsequence.client.data_sources_interfaces.duckdb import DuckDBInterface
|
739
|
+
db_interface = DuckDBInterface()
|
740
|
+
db_interface.drop_table(self.metadata.storage_hash)
|
741
|
+
|
742
|
+
self.metadata.delete()
|
743
|
+
|
744
|
+
@tracer.start_as_current_span("TS: Persist Data")
|
745
|
+
def persist_updated_data(self,
|
746
|
+
temp_df: pd.DataFrame, overwrite: bool = False) -> bool:
|
747
|
+
"""
|
748
|
+
Persists the updated data to the database.
|
749
|
+
|
750
|
+
Args:
|
751
|
+
temp_df: The DataFrame with updated data.
|
752
|
+
update_tracker: The update tracker object.
|
753
|
+
overwrite: If True, overwrites existing data.
|
754
|
+
|
755
|
+
Returns:
|
756
|
+
True if data was persisted, False otherwise.
|
757
|
+
"""
|
758
|
+
persisted = False
|
759
|
+
if not temp_df.empty:
|
760
|
+
if overwrite == True:
|
761
|
+
self.logger.warning(f"Values will be overwritten")
|
762
|
+
|
763
|
+
self._local_metadata_cached = self.local_metadata.upsert_data_into_table(
|
764
|
+
data=temp_df,
|
765
|
+
data_source=self.data_source,
|
766
|
+
|
767
|
+
)
|
768
|
+
|
769
|
+
|
770
|
+
persisted = True
|
771
|
+
return persisted
|
772
|
+
|
773
|
+
def get_update_statistics_for_table(self) -> ms_client.UpdateStatistics:
|
774
|
+
"""
|
775
|
+
Gets the latest update statistics from the database.
|
776
|
+
|
777
|
+
Args:
|
778
|
+
unique_identifier_list: An optional list of unique identifiers to filter by.
|
779
|
+
|
780
|
+
Returns:
|
781
|
+
A UpdateStatistics object with the latest statistics.
|
782
|
+
"""
|
783
|
+
if isinstance(self.metadata, int):
|
784
|
+
self.set_local_metadata_lazy(force_registry=True, include_relations_detail=True)
|
785
|
+
|
786
|
+
if self.metadata.sourcetableconfiguration is None:
|
787
|
+
return ms_client.UpdateStatistics()
|
788
|
+
|
789
|
+
update_stats = self.metadata.sourcetableconfiguration.get_data_updates()
|
790
|
+
return update_stats
|
791
|
+
|
792
|
+
def is_local_relation_tree_set(self) -> bool:
|
793
|
+
return self.local_metadata.ogm_dependencies_linked
|
794
|
+
|
795
|
+
|
796
|
+
|
797
|
+
def update_git_and_code_in_backend(self,time_serie_class) -> None:
|
798
|
+
"""Updates the source code and git hash information in the backend."""
|
799
|
+
self.update_source_informmation(
|
800
|
+
git_hash_id=get_data_node_source_code_git_hash(time_serie_class),
|
801
|
+
source_code=get_data_node_source_code(time_serie_class),
|
802
|
+
)
|
803
|
+
|
804
|
+
class TimeScaleLocalPersistManager(PersistManager):
|
805
|
+
"""
|
806
|
+
Main Controler to interacti with backend
|
807
|
+
"""
|
808
|
+
def get_table_schema(self,table_name):
|
809
|
+
return self.metadata["sourcetableconfiguration"]["column_dtypes_map"]
|
810
|
+
|
811
|
+
|
812
|
+
|