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,292 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
import base64
|
4
|
+
import os
|
5
|
+
from datetime import datetime
|
6
|
+
from io import BytesIO
|
7
|
+
from typing import List
|
8
|
+
|
9
|
+
from jinja2 import Environment, FileSystemLoader
|
10
|
+
import numpy as np
|
11
|
+
import pandas as pd
|
12
|
+
import plotly.express as px
|
13
|
+
import plotly.graph_objs as go
|
14
|
+
|
15
|
+
from mainsequence.tdag import APIDataNode
|
16
|
+
from mainsequence.virtualfundbuilder.utils import get_vfb_logger
|
17
|
+
|
18
|
+
from pydantic import BaseModel
|
19
|
+
from jinja2 import Template
|
20
|
+
|
21
|
+
from mainsequence.client import DoesNotExist, AssetCategory
|
22
|
+
from mainsequence.client.models_tdag import Artifact
|
23
|
+
from mainsequence.virtualfundbuilder.resource_factory.app_factory import register_app, BaseApp
|
24
|
+
|
25
|
+
logger = get_vfb_logger()
|
26
|
+
|
27
|
+
def example_data(assets):
|
28
|
+
"""
|
29
|
+
Fetch real data from the 'api_ts.get_df_between_dates()' call, then:
|
30
|
+
1) Build a time-series chart of 'Revenue' vs. time for each asset (ticker).
|
31
|
+
2) Build a correlation heatmap of 'Revenue' vs. 'EPS' for the latest time period.
|
32
|
+
3) Return both figures as Base64-encoded PNGs.
|
33
|
+
"""
|
34
|
+
|
35
|
+
# ------------------------------------------------------------------------------
|
36
|
+
# 1) GET THE REAL DATA
|
37
|
+
market_time_serie_unique_identifier = "polygon_historical_fundamentals"
|
38
|
+
try:
|
39
|
+
from mainsequence.client import MarketsTimeSeriesDetails
|
40
|
+
hbs = MarketsTimeSeriesDetails.get(unique_identifier=market_time_serie_unique_identifier)
|
41
|
+
except DoesNotExist as e:
|
42
|
+
logger.exception(f"HistoricalBarsSource does not exist for {market_time_serie_unique_identifier}")
|
43
|
+
raise e
|
44
|
+
|
45
|
+
api_ts = APIDataNode(
|
46
|
+
data_source_id=hbs.related_local_time_serie.data_source_id,
|
47
|
+
update_hash=hbs.related_local_time_serie.update_hash
|
48
|
+
)
|
49
|
+
|
50
|
+
# This returns a DataFrame, indexed by (time_index, unique_identifier)
|
51
|
+
# with columns like: 'is_revenues', 'is_basic_earnings_per_share', etc.
|
52
|
+
data = api_ts.get_df_between_dates()
|
53
|
+
|
54
|
+
# Move the multi-index to columns for easier manipulation
|
55
|
+
df = data.reset_index()
|
56
|
+
# Example: 'unique_identifier' might be "AAPL_ms_share_class_123xyz"
|
57
|
+
# We'll extract the first underscore-delimited part as the 'asset_id'.
|
58
|
+
df["asset_id"] = df["unique_identifier"].str.split("_").str[0]
|
59
|
+
|
60
|
+
df = df[df["unique_identifier"].isin([a.unique_identifier for a in assets])]
|
61
|
+
|
62
|
+
# Rename columns to something more readable in the charts
|
63
|
+
df["Revenue"] = df["is_revenues"]
|
64
|
+
df["EPS"] = df["is_basic_earnings_per_share"]
|
65
|
+
|
66
|
+
# OPTIONAL: If you want to drop rows that have no revenue or EPS data
|
67
|
+
# df.dropna(subset=["Revenue", "EPS"], how="all", inplace=True)
|
68
|
+
|
69
|
+
# ------------------------------------------------------------------------------
|
70
|
+
# 2) TIME-SERIES LINE CHART: Revenue over time, color by ticker
|
71
|
+
import plotly.express as px
|
72
|
+
fig_line = px.line(
|
73
|
+
df,
|
74
|
+
x="time_index",
|
75
|
+
y="Revenue",
|
76
|
+
color="asset_id",
|
77
|
+
title="Revenue Over Time by Asset"
|
78
|
+
)
|
79
|
+
fig_line.update_layout(xaxis_title="Date", yaxis_title="Revenue")
|
80
|
+
|
81
|
+
# ------------------------------------------------------------------------------
|
82
|
+
# 3) CORRELATION HEATMAP
|
83
|
+
latest_date = df.groupby("unique_identifier")["quarter"].max().min() # latest date where all values are present
|
84
|
+
df_latest = df[df["quarter"] == latest_date].copy()
|
85
|
+
|
86
|
+
# Pivot so each row is an asset and columns are the fundamental metrics
|
87
|
+
# (Here, "Revenue" and "EPS").
|
88
|
+
df_pivot = df_latest.pivot_table(
|
89
|
+
index="asset_id",
|
90
|
+
values=["Revenue", "EPS"],
|
91
|
+
aggfunc="mean" # or 'first' if each (asset, time_index) is unique
|
92
|
+
)
|
93
|
+
|
94
|
+
corr_matrix = df_pivot.corr()
|
95
|
+
import plotly.graph_objs as go
|
96
|
+
fig_heatmap = go.Figure(
|
97
|
+
data=go.Heatmap(
|
98
|
+
z=corr_matrix.values,
|
99
|
+
x=corr_matrix.columns,
|
100
|
+
y=corr_matrix.index,
|
101
|
+
colorscale="Blues"
|
102
|
+
)
|
103
|
+
)
|
104
|
+
fig_heatmap.update_layout(
|
105
|
+
title=f"Correlation of Fundamentals on {latest_date}"
|
106
|
+
)
|
107
|
+
|
108
|
+
# ------------------------------------------------------------------------------
|
109
|
+
# 4) CONVERT PLOTS TO BASE64 STRINGS
|
110
|
+
import base64
|
111
|
+
from io import BytesIO
|
112
|
+
|
113
|
+
def fig_to_base64(fig):
|
114
|
+
"""Render a Plotly figure to a PNG and return a base64 string."""
|
115
|
+
buf = BytesIO()
|
116
|
+
fig.write_image(buf, format="png")
|
117
|
+
buf.seek(0)
|
118
|
+
return base64.b64encode(buf.read()).decode("utf-8")
|
119
|
+
|
120
|
+
chart1_base64 = fig_to_base64(fig_line)
|
121
|
+
chart2_base64 = fig_to_base64(fig_heatmap)
|
122
|
+
|
123
|
+
return chart1_base64, chart2_base64
|
124
|
+
|
125
|
+
class ExampleReportConfig(BaseModel):
|
126
|
+
"""Pydantic model defining the parameters for report generation."""
|
127
|
+
report_id: str = "MC-2025"
|
128
|
+
report_title: str = "Global Strategy Views: Diversify to Amplify"
|
129
|
+
bucket_name: str = "Reports"
|
130
|
+
authors: str = "Main Sequence AI"
|
131
|
+
sector: str = "US Equities"
|
132
|
+
region: str = "USA"
|
133
|
+
topics: List[str] = ["Diversification", "Equities", "Fundamentals"]
|
134
|
+
asset_category_unique_identifier: str = "magnificent_7"
|
135
|
+
summary: str = (
|
136
|
+
"We are entering a more benign phase of the economic cycle characterized by "
|
137
|
+
"sustained economic growth and declining policy interest rates. Historically, "
|
138
|
+
"such an environment supports equities but also highlights the increasing "
|
139
|
+
"importance of broad diversification across regions and sectors."
|
140
|
+
)
|
141
|
+
|
142
|
+
@register_app()
|
143
|
+
class ExampleReportApp(BaseApp):
|
144
|
+
"""
|
145
|
+
Minimal example of a 'ReportApp' that can:
|
146
|
+
1) Generate dummy data and create charts (line + heatmap).
|
147
|
+
2) Embed those charts into an HTML template.
|
148
|
+
3) Optionally export the HTML to PDF using WeasyPrint.
|
149
|
+
"""
|
150
|
+
configuration_class = ExampleReportConfig
|
151
|
+
def __init__(self, *args, **kwargs):
|
152
|
+
super().__init__(*args, **kwargs)
|
153
|
+
|
154
|
+
category = AssetCategory.get(unique_identifier=self.configuration.asset_category_unique_identifier)
|
155
|
+
self.assets = category.get_assets()
|
156
|
+
self.category_name = category.display_name
|
157
|
+
|
158
|
+
def _fig_to_base64(self, fig) -> str:
|
159
|
+
"""
|
160
|
+
Render a Plotly figure to PNG and return a Base64 string.
|
161
|
+
"""
|
162
|
+
buf = BytesIO()
|
163
|
+
fig.write_image(buf, format="png")
|
164
|
+
buf.seek(0)
|
165
|
+
return base64.b64encode(buf.read()).decode("utf-8")
|
166
|
+
|
167
|
+
def run(self):
|
168
|
+
"""
|
169
|
+
Generates an HTML report (and optional PDF) in a minimal, self-contained way.
|
170
|
+
"""
|
171
|
+
print(f"Running tool with configuration {self.configuration}")
|
172
|
+
|
173
|
+
# 1) Retrieve the chart images:
|
174
|
+
chart1_base64, chart2_base64 = example_data(self.assets)
|
175
|
+
|
176
|
+
# Build context from config
|
177
|
+
template_context = {
|
178
|
+
"current_date": datetime.now().strftime('%Y-%m-%d'),
|
179
|
+
"current_year": datetime.now().year,
|
180
|
+
"logo_location": f"https://main-sequence.app/static/media/logos/MS_logo_long_white.png",
|
181
|
+
# Pulling fields from our pydantic config:
|
182
|
+
"report_id": self.configuration.report_id,
|
183
|
+
"authors": self.configuration.authors,
|
184
|
+
"sector": self.configuration.sector,
|
185
|
+
"region": self.configuration.region,
|
186
|
+
"topics": self.configuration.topics,
|
187
|
+
"report_title": self.configuration.report_title,
|
188
|
+
"summary": self.configuration.summary,
|
189
|
+
"report_content": f"""
|
190
|
+
<h2>Overview</h2>
|
191
|
+
<p>
|
192
|
+
Longer-term interest rates are expected to remain elevated, driven by rising government deficits
|
193
|
+
and persistent term premiums. However, the reduced likelihood of a near-term recession presents
|
194
|
+
opportunities for positive equity returns, notably in sectors like technology and select value-oriented
|
195
|
+
areas such as financials.
|
196
|
+
</p>
|
197
|
+
<p>
|
198
|
+
This evolving landscape emphasizes the necessity of expanding our investment horizon beyond traditional
|
199
|
+
focuses—such as large-cap US technology—to include regional markets, mid-cap companies, and "Ex-Tech Compounders."
|
200
|
+
Such diversification aims to enhance risk-adjusted returns as global growth trajectories become more aligned.
|
201
|
+
</p>
|
202
|
+
<h2>Key Takeaways</h2>
|
203
|
+
<ul>
|
204
|
+
<li>
|
205
|
+
<strong>Diversification enhances return potential:</strong> Capturing alpha in the upcoming cycle
|
206
|
+
will likely depend on a diversified approach across multiple regions and investment factors.
|
207
|
+
</li>
|
208
|
+
<li>
|
209
|
+
<strong>Technology remains essential:</strong> Rising demand for physical infrastructure, such as
|
210
|
+
data centers and AI-supportive hardware, will benefit traditional industrial sectors, creating
|
211
|
+
new investment opportunities.
|
212
|
+
</li>
|
213
|
+
<li>
|
214
|
+
<strong>Divergent interest rate dynamics:</strong> Central banks have started easing policies, but
|
215
|
+
persistent high bond yields imply limitations on further equity valuation expansions.
|
216
|
+
</li>
|
217
|
+
</ul>
|
218
|
+
|
219
|
+
<!-- Page break before next section if printing to PDF -->
|
220
|
+
<div style="page-break-after: always;"></div>
|
221
|
+
|
222
|
+
<h2>Fundamental Trends and Correlation Analysis</h2>
|
223
|
+
<p>
|
224
|
+
The following charts illustrate recent fundamental trends among selected US equities, focusing specifically
|
225
|
+
on revenue performance over recent reporting periods. This analysis leverages data obtained via the internal
|
226
|
+
"pylong" API, clearly highlighting the evolving top-line dynamics across multiple companies.
|
227
|
+
</p>
|
228
|
+
<p style="text-align:center;">
|
229
|
+
<img alt="Revenue Over Time"
|
230
|
+
src="data:image/png;base64,{chart1_base64}"
|
231
|
+
style="max-width:600px; width:100%;">
|
232
|
+
</p>
|
233
|
+
|
234
|
+
<p>
|
235
|
+
Further, the correlation heatmap below illustrates the relationships between key fundamental indicators—such
|
236
|
+
as Revenue and Earnings Per Share (EPS)—across companies at the latest reporting date. This visualization
|
237
|
+
provides strategic insights into how closely fundamental metrics move together, enabling more informed
|
238
|
+
portfolio allocation decisions.
|
239
|
+
</p>
|
240
|
+
<p style="text-align:center;">
|
241
|
+
<img alt="Correlation Heatmap"
|
242
|
+
src="data:image/png;base64,{chart2_base64}"
|
243
|
+
style="max-width:600px; width:100%;">
|
244
|
+
</p>
|
245
|
+
|
246
|
+
<p>
|
247
|
+
As the macroeconomic environment evolves, shifts in these fundamental correlations may offer additional
|
248
|
+
opportunities for strategic repositioning and optimized sector exposures.
|
249
|
+
</p>
|
250
|
+
"""
|
251
|
+
}
|
252
|
+
|
253
|
+
"""
|
254
|
+
Renders a static HTML report from Jinja2 templates, embedding two charts as Base64 images,
|
255
|
+
and (optionally) saves it as PDF using WeasyPrint.
|
256
|
+
"""
|
257
|
+
# 2) Setup the Jinja2 environment: (point to the templates directory)
|
258
|
+
template_dir = os.path.join(os.path.dirname(__file__), 'templates')
|
259
|
+
env = Environment(loader=FileSystemLoader(template_dir), autoescape=False)
|
260
|
+
|
261
|
+
# 3) Load the derived template (which should reference placeholders in Jinja syntax).
|
262
|
+
template = env.get_template('report.html')
|
263
|
+
|
264
|
+
# 5) Render the HTML:
|
265
|
+
rendered_html = template.render(template_context)
|
266
|
+
|
267
|
+
# 6) Write the rendered HTML to a file
|
268
|
+
output_html_path = os.path.join(os.path.dirname(__file__), 'output_report.html')
|
269
|
+
with open(output_html_path, 'w', encoding='utf-8') as f:
|
270
|
+
f.write(rendered_html)
|
271
|
+
|
272
|
+
print(f"HTML report generated: {output_html_path}")
|
273
|
+
|
274
|
+
print("Generated HTML:", output_html_path)
|
275
|
+
|
276
|
+
# from weasyprint import HTML
|
277
|
+
# pdf_path = "/tmp/report.pdf"
|
278
|
+
# HTML(string=rendered_html).write_pdf(pdf_path)
|
279
|
+
# print(f"PDF generated: {pdf_path}")
|
280
|
+
# pdf_artifact = Artifact.upload_file(filepath=pdf_path, name="Report PDF", created_by_resource_name=self.__class__.__name__, bucket_name="Reports")
|
281
|
+
html_artifact = Artifact.upload_file(
|
282
|
+
filepath=output_html_path,
|
283
|
+
name=self.configuration.report_id,
|
284
|
+
created_by_resource_name=self.__class__.__name__,
|
285
|
+
bucket_name=self.configuration.bucket_name
|
286
|
+
)
|
287
|
+
|
288
|
+
if __name__ == "__main__":
|
289
|
+
# Example usage:
|
290
|
+
config = ExampleReportConfig() # Or override fields as needed
|
291
|
+
app = ExampleReportApp(config)
|
292
|
+
html_artifact = app.run() # Creates output_report.html and weasy_output_report.pdf
|
@@ -0,0 +1,107 @@
|
|
1
|
+
import pandas as pd
|
2
|
+
|
3
|
+
from mainsequence.client import Asset, AssetCategory, CONSTANTS
|
4
|
+
from mainsequence.virtualfundbuilder.contrib.prices.data_nodes import ExternalPrices
|
5
|
+
from mainsequence.virtualfundbuilder.portfolio_interface import PortfolioInterface
|
6
|
+
from mainsequence.virtualfundbuilder.utils import get_vfb_logger
|
7
|
+
|
8
|
+
from pydantic import BaseModel
|
9
|
+
from mainsequence.client.models_tdag import Artifact
|
10
|
+
from mainsequence.virtualfundbuilder.resource_factory.app_factory import BaseApp, register_app
|
11
|
+
|
12
|
+
logger = get_vfb_logger()
|
13
|
+
|
14
|
+
class LoadExternalPortfolioConfig(BaseModel):
|
15
|
+
bucket_name: str
|
16
|
+
artifact_name: str
|
17
|
+
portfolio_name: str
|
18
|
+
created_asset_category_name: str
|
19
|
+
|
20
|
+
@register_app()
|
21
|
+
class LoadExternalPortfolio(BaseApp):
|
22
|
+
configuration_class = LoadExternalPortfolioConfig
|
23
|
+
|
24
|
+
def run(self):
|
25
|
+
|
26
|
+
# get the data and store it on local storage
|
27
|
+
source_artifact = Artifact.get(bucket__name=self.configuration.bucket_name, name=self.configuration.artifact_name)
|
28
|
+
weights_source = pd.read_csv(source_artifact.content)
|
29
|
+
|
30
|
+
# validate data
|
31
|
+
expected_cols = [
|
32
|
+
"time_index",
|
33
|
+
"figi",
|
34
|
+
"weight",
|
35
|
+
"price"
|
36
|
+
]
|
37
|
+
if set(weights_source.columns) != set(expected_cols):
|
38
|
+
raise ValueError(
|
39
|
+
f"Invalid CSV format: expected columns {expected_cols!r} "
|
40
|
+
f"but got {list(weights_source.columns)!r}"
|
41
|
+
)
|
42
|
+
|
43
|
+
weights_source["time_index"] = pd.to_datetime(
|
44
|
+
weights_source["time_index"], utc=True
|
45
|
+
)
|
46
|
+
|
47
|
+
# create assets from figi in the backend
|
48
|
+
assets = []
|
49
|
+
for figi in weights_source["figi"].unique():
|
50
|
+
asset = Asset.get_or_none(figi=figi)
|
51
|
+
if asset is None:
|
52
|
+
try:
|
53
|
+
asset = Asset.register_figi_as_asset_in_main_sequence_venue(
|
54
|
+
figi=figi,
|
55
|
+
execution_venue__symbol=CONSTANTS.MAIN_SEQUENCE_EV,
|
56
|
+
)
|
57
|
+
except Exception as e:
|
58
|
+
print(f"Could not register asset with figi {figi}, error {e}")
|
59
|
+
continue
|
60
|
+
assets.append(asset)
|
61
|
+
|
62
|
+
# create asset category
|
63
|
+
portfolio_category = AssetCategory.get_or_create(
|
64
|
+
display_name=self.configuration.created_asset_category_name,
|
65
|
+
source="external",
|
66
|
+
description=f"This category contains the assets for the external portfolio {self.configuration.portfolio_name}",
|
67
|
+
unique_identifier=self.configuration.created_asset_category_name.replace(" ", "_").lower(),
|
68
|
+
)
|
69
|
+
portfolio_category.append_assets([a.id for a in assets])
|
70
|
+
|
71
|
+
# insert prices
|
72
|
+
external_prices_source = ExternalPrices(
|
73
|
+
bucket_name=self.configuration.bucket_name,
|
74
|
+
artifact_name=self.configuration.artifact_name,
|
75
|
+
asset_category_unique_id=portfolio_category.unique_identifier
|
76
|
+
).run(debug_mode=True, force_update=True)
|
77
|
+
|
78
|
+
# adapt portfolio configuration
|
79
|
+
portfolio = PortfolioInterface.load_from_configuration("external_portfolio_template")
|
80
|
+
current_template_dict = portfolio.portfolio_config_template
|
81
|
+
|
82
|
+
sw_config = current_template_dict['portfolio_build_configuration']['backtesting_weights_configuration']['signal_weights_configuration']
|
83
|
+
sw_config['bucket_name'] = self.configuration.bucket_name
|
84
|
+
sw_config['artifact_name'] = self.configuration.artifact_name
|
85
|
+
sw_config['assets_category_unique_id'] = portfolio_category.unique_identifier
|
86
|
+
sw_config['signal_assets_configuration']['assets_category_unique_id'] = portfolio_category.unique_identifier
|
87
|
+
current_template_dict['portfolio_markets_configuration']['portfolio_name'] = self.configuration.portfolio_name
|
88
|
+
|
89
|
+
portfolio = PortfolioInterface(current_template_dict)
|
90
|
+
|
91
|
+
# Switch out the prices source to get our external prices
|
92
|
+
portfolio._initialize_nodes()
|
93
|
+
portfolio.portfolio_strategy_data_node.bars_ts = external_prices_source
|
94
|
+
|
95
|
+
# Run the portfolio
|
96
|
+
res = portfolio.run(add_portfolio_to_markets_backend=True)
|
97
|
+
logger.info(f"Portfolio integrated successfully with results {res.head()}")
|
98
|
+
|
99
|
+
if __name__ == "__main__":
|
100
|
+
app_config = LoadExternalPortfolioConfig(
|
101
|
+
bucket_name="Sample Data",
|
102
|
+
artifact_name="portfolio_weights_mag7.csv",
|
103
|
+
portfolio_name="Mag 7 External Portfolio",
|
104
|
+
created_asset_category_name="external_magnificent_7",
|
105
|
+
)
|
106
|
+
|
107
|
+
LoadExternalPortfolio(app_config).run()
|