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,40 @@
|
|
1
|
+
from mainsequence.virtualfundbuilder.portfolio_interface import PortfolioInterface
|
2
|
+
|
3
|
+
from mainsequence.virtualfundbuilder.models import PortfolioConfiguration
|
4
|
+
from mainsequence.virtualfundbuilder.utils import get_vfb_logger
|
5
|
+
|
6
|
+
from pydantic import BaseModel
|
7
|
+
from mainsequence.client.models_tdag import Artifact
|
8
|
+
from mainsequence.virtualfundbuilder.resource_factory.app_factory import BaseApp, register_app
|
9
|
+
|
10
|
+
logger = get_vfb_logger()
|
11
|
+
|
12
|
+
class PortfolioRunParameters(BaseModel):
|
13
|
+
add_portfolio_to_markets_backend: bool = True
|
14
|
+
update_tree: bool = True
|
15
|
+
|
16
|
+
class RunPortfolioConfiguration(BaseModel):
|
17
|
+
portfolio_configuration: PortfolioConfiguration
|
18
|
+
portfolio_run_parameters: PortfolioRunParameters
|
19
|
+
|
20
|
+
@register_app()
|
21
|
+
class RunPortfolio(BaseApp):
|
22
|
+
configuration_class = RunPortfolioConfiguration
|
23
|
+
|
24
|
+
def __init__(self, configuration: RunPortfolioConfiguration):
|
25
|
+
self.configuration = configuration
|
26
|
+
|
27
|
+
def run(self) -> None:
|
28
|
+
portfolio = PortfolioInterface(portfolio_config_template=self.configuration.portfolio_configuration.model_dump())
|
29
|
+
res = portfolio.run(**self.configuration.portfolio_run_parameters.model_dump())
|
30
|
+
logger.info(f"Portfolio Run successful with results {res.head()}")
|
31
|
+
self.add_output(output=portfolio.target_portfolio)
|
32
|
+
|
33
|
+
|
34
|
+
if __name__ == "__main__":
|
35
|
+
portfolio_configuration = PortfolioInterface.load_from_configuration("market_cap").portfolio_config
|
36
|
+
run_portfolio_configuration = RunPortfolioConfiguration(
|
37
|
+
portfolio_configuration=portfolio_configuration,
|
38
|
+
portfolio_run_parameters=PortfolioRunParameters(),
|
39
|
+
)
|
40
|
+
RunPortfolio(run_portfolio_configuration).run()
|
@@ -0,0 +1,147 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8" />
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
6
|
+
<title>{% block title %}{% endblock %}</title>
|
7
|
+
|
8
|
+
<!-- Bootstrap CSS -->
|
9
|
+
<link
|
10
|
+
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
11
|
+
rel="stylesheet"
|
12
|
+
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
|
13
|
+
crossorigin="anonymous"
|
14
|
+
/>
|
15
|
+
|
16
|
+
<style>
|
17
|
+
@page {
|
18
|
+
size: A4;
|
19
|
+
margin: 20mm;
|
20
|
+
}
|
21
|
+
|
22
|
+
@media print {
|
23
|
+
/* 1) Hide the Download PDF button */
|
24
|
+
.btn-download {
|
25
|
+
display: none !important;
|
26
|
+
}
|
27
|
+
.header-section img {
|
28
|
+
height: 40px; /* Or whatever height you need */
|
29
|
+
width: auto; /* So the aspect ratio stays correct */
|
30
|
+
max-width: 100%; /* Prevents it from overflowing */
|
31
|
+
margin-bottom: 200px;
|
32
|
+
}
|
33
|
+
|
34
|
+
/* 2) Hide the entire sidebar */
|
35
|
+
.sidebar {
|
36
|
+
display: none !important;
|
37
|
+
}
|
38
|
+
|
39
|
+
/* 3) Remove horizontal separators (header/footer) */
|
40
|
+
.header-section {
|
41
|
+
border-bottom: none !important;
|
42
|
+
}
|
43
|
+
.footer-section {
|
44
|
+
border-top: none !important;
|
45
|
+
}
|
46
|
+
|
47
|
+
/* 4) Expand the main content section to full width when sidebar is hidden */
|
48
|
+
.col-md-9 {
|
49
|
+
width: 100% !important;
|
50
|
+
flex: 0 0 100% !important;
|
51
|
+
max-width: 100% !important;
|
52
|
+
}
|
53
|
+
|
54
|
+
/* (Optional) Lighten or remove background color for printing */
|
55
|
+
body, .header-section, .content-section, .footer-section {
|
56
|
+
background-color: #fff !important;
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
/* Your base (screen + print) styles remain here */
|
61
|
+
body {
|
62
|
+
font-family: "Helvetica Neue", Arial, sans-serif;
|
63
|
+
font-size: 0.95rem;
|
64
|
+
background-color: #fafafa;
|
65
|
+
color: #333;
|
66
|
+
margin: 0;
|
67
|
+
padding: 0;
|
68
|
+
}
|
69
|
+
.header-section {
|
70
|
+
border-bottom: 1px solid #dee2e6;
|
71
|
+
padding: 20px 0;
|
72
|
+
background: linear-gradient(to bottom, #d3d9e8, rgb(211, 217, 232));
|
73
|
+
color: #000; /* Keep text black */
|
74
|
+
}
|
75
|
+
.sidebar {
|
76
|
+
border-right: 1px solid #dee2e6;
|
77
|
+
background-color: #ffffff;
|
78
|
+
padding: 20px;
|
79
|
+
}
|
80
|
+
.content-section {
|
81
|
+
padding: 20px;
|
82
|
+
background-color: #ffffff;
|
83
|
+
}
|
84
|
+
.footer-section {
|
85
|
+
border-top: 1px solid #dee2e6;
|
86
|
+
padding: 15px 0;
|
87
|
+
background-color: #f8f9fa;
|
88
|
+
font-size: 0.85rem;
|
89
|
+
}
|
90
|
+
/* ... etc. ... */
|
91
|
+
</style>
|
92
|
+
</head>
|
93
|
+
<body>
|
94
|
+
|
95
|
+
<!-- Header Section -->
|
96
|
+
<header class="header-section">
|
97
|
+
<!-- Row #1: Centered LOGO only. No text that says "Company Logo" -->
|
98
|
+
<div class="container text-center">
|
99
|
+
<img src="{{ logo_location }}" alt="Main Sequence GmbH Logo" height="40" class="my-2" />
|
100
|
+
</div>
|
101
|
+
|
102
|
+
<!-- Row #2: Left side for block header_logo, Right side for date/info + PDF button -->
|
103
|
+
<div class="container mt-2">
|
104
|
+
<div class="row align-items-center">
|
105
|
+
<div class="col-md-6">
|
106
|
+
<!-- Typically, you'd put "Main Sequence | Research Division" here, or nothing. -->
|
107
|
+
{% block header_logo %}{% endblock %}
|
108
|
+
</div>
|
109
|
+
<div class="col-md-6 text-md-end mt-3 mt-md-0">
|
110
|
+
{% block header_info %}{% endblock %}
|
111
|
+
</div>
|
112
|
+
</div>
|
113
|
+
</div>
|
114
|
+
</header>
|
115
|
+
|
116
|
+
<!-- Main Container: 2-col layout -->
|
117
|
+
<div class="container my-4">
|
118
|
+
<div class="row">
|
119
|
+
<!-- Left Column: Sidebar -->
|
120
|
+
<div class="col-md-3 sidebar">
|
121
|
+
<div class="sticky-top">
|
122
|
+
{% block sidebar %}{% endblock %}
|
123
|
+
</div>
|
124
|
+
</div>
|
125
|
+
|
126
|
+
<!-- Right Column: Main Content -->
|
127
|
+
<div class="col-md-9 content-section">
|
128
|
+
{% block content %}{% endblock %}
|
129
|
+
</div>
|
130
|
+
</div>
|
131
|
+
</div>
|
132
|
+
|
133
|
+
<!-- Footer Section -->
|
134
|
+
<footer class="footer-section">
|
135
|
+
<div class="container">
|
136
|
+
{% block footer %}{% endblock %}
|
137
|
+
</div>
|
138
|
+
</footer>
|
139
|
+
|
140
|
+
<!-- Bootstrap JS -->
|
141
|
+
<script
|
142
|
+
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
143
|
+
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
144
|
+
crossorigin="anonymous"
|
145
|
+
></script>
|
146
|
+
</body>
|
147
|
+
</html>
|
@@ -0,0 +1,77 @@
|
|
1
|
+
{% extends "base.html" %}
|
2
|
+
|
3
|
+
{% block title %}
|
4
|
+
Research Report
|
5
|
+
{% endblock %}
|
6
|
+
|
7
|
+
{% block header_logo %}
|
8
|
+
<!-- Example: Add a logo or just text as placeholder -->
|
9
|
+
<div class="d-flex align-items-center">
|
10
|
+
|
11
|
+
<div>
|
12
|
+
<h2 class="fw-bold mb-0">Main Sequence</h2>
|
13
|
+
<p class="text-muted mb-0">AI Research Division</p>
|
14
|
+
</div>
|
15
|
+
</div>
|
16
|
+
{% endblock %}
|
17
|
+
|
18
|
+
{% block header_info %}
|
19
|
+
<div class="small">
|
20
|
+
<p class="mb-0">Date: {{ current_date }}</p>
|
21
|
+
<p class="mb-0">Report ID: {{ report_id }}</p>
|
22
|
+
</div>
|
23
|
+
{% endblock %}
|
24
|
+
|
25
|
+
{% block sidebar %}
|
26
|
+
<h5 class="fw-bold mb-3">Report Details</h5>
|
27
|
+
<div class="mb-3">
|
28
|
+
<p class="small text-muted mb-1">Authors</p>
|
29
|
+
<p class="fw-bold">{{ authors }}</p>
|
30
|
+
</div>
|
31
|
+
<div class="mb-3">
|
32
|
+
<p class="small text-muted mb-1">Sector</p>
|
33
|
+
<p>{{ sector }}</p>
|
34
|
+
</div>
|
35
|
+
<div class="mb-3">
|
36
|
+
<p class="small text-muted mb-1">Region</p>
|
37
|
+
<p>{{ region }}</p>
|
38
|
+
</div>
|
39
|
+
<div>
|
40
|
+
<p class="small text-muted mb-1">Related Topics</p>
|
41
|
+
<ul class="list-unstyled">
|
42
|
+
{% for topic in topics %}
|
43
|
+
<li>- {{ topic }}</li>
|
44
|
+
{% endfor %}
|
45
|
+
</ul>
|
46
|
+
</div>
|
47
|
+
{% endblock %}
|
48
|
+
|
49
|
+
{% block content %}
|
50
|
+
<!-- Main Report Title and Summary -->
|
51
|
+
<h1 class="report-title">{{ report_title }}</h1>
|
52
|
+
<div class="mb-4">
|
53
|
+
<p class="lead">{{ summary }}</p>
|
54
|
+
</div>
|
55
|
+
|
56
|
+
<!-- Body of Report -->
|
57
|
+
<div class="content-body">
|
58
|
+
{% block report_content %}
|
59
|
+
<!-- Example placeholder content -->
|
60
|
+
{{ report_content|safe }}
|
61
|
+
|
62
|
+
{% endblock %}
|
63
|
+
</div>
|
64
|
+
{% endblock %}
|
65
|
+
|
66
|
+
{% block footer %}
|
67
|
+
<div class="row">
|
68
|
+
<div class="col-md-6">
|
69
|
+
<p class="small text-muted mb-0">
|
70
|
+
© {{ current_year }} Main Sequence GmbH. All Rights Reserved.
|
71
|
+
</p>
|
72
|
+
</div>
|
73
|
+
<div class="col-md-6 text-md-end">
|
74
|
+
<p class="small text-muted mb-0">For institutional use only</p>
|
75
|
+
</div>
|
76
|
+
</div>
|
77
|
+
{% endblock %}
|
@@ -0,0 +1,61 @@
|
|
1
|
+
from mainsequence.client.models_tdag import Artifact
|
2
|
+
from datetime import datetime, timedelta, tzinfo
|
3
|
+
from typing import Optional, List, Union
|
4
|
+
|
5
|
+
import pandas as pd
|
6
|
+
|
7
|
+
from mainsequence.tdag.data_nodes import DataNode
|
8
|
+
from mainsequence.client import Asset, AssetCategory
|
9
|
+
|
10
|
+
from mainsequence.virtualfundbuilder.resource_factory.signal_factory import WeightsBase, register_signal_class
|
11
|
+
from mainsequence.virtualfundbuilder.utils import TIMEDELTA
|
12
|
+
import numpy as np
|
13
|
+
import pandas.api.types as ptypes
|
14
|
+
|
15
|
+
|
16
|
+
@register_signal_class(register_in_agent=False)
|
17
|
+
class ExternalWeights(WeightsBase, DataNode):
|
18
|
+
def __init__(
|
19
|
+
self,
|
20
|
+
artifact_name: str,
|
21
|
+
bucket_name: str,
|
22
|
+
*args, **kwargs
|
23
|
+
):
|
24
|
+
self.artifact_name = artifact_name
|
25
|
+
self.bucket_name = bucket_name
|
26
|
+
super().__init__(*args, **kwargs)
|
27
|
+
|
28
|
+
|
29
|
+
def maximum_forward_fill(self):
|
30
|
+
return timedelta(days=1) - TIMEDELTA
|
31
|
+
|
32
|
+
def get_explanation(self):
|
33
|
+
explanation = (
|
34
|
+
"### External Weights Source\n\n"
|
35
|
+
f"This strategy represents weights from an artifact: {self.bucket_name}/{self.artifact_name}\n\n\n"
|
36
|
+
)
|
37
|
+
return explanation
|
38
|
+
|
39
|
+
def get_asset_list(self) -> Union[None, list]:
|
40
|
+
asset_category = AssetCategory.get(unique_identifier=self.assets_configuration.assets_category_unique_id)
|
41
|
+
asset_list = Asset.filter(id__in=asset_category.assets)
|
42
|
+
return asset_list
|
43
|
+
|
44
|
+
def update(self, update_statistics: "UpdateStatistics"):
|
45
|
+
source_artifact = Artifact.get(bucket__name=self.bucket_name, name=self.artifact_name)
|
46
|
+
weights_source = pd.read_csv(source_artifact.content)
|
47
|
+
|
48
|
+
weights_source["time_index"] = pd.to_datetime(
|
49
|
+
weights_source["time_index"], utc=True
|
50
|
+
)
|
51
|
+
|
52
|
+
# convert figis in source data
|
53
|
+
for asset in update_statistics.asset_list:
|
54
|
+
weights_source.loc[weights_source["figi"] == asset.figi, "unique_identifier"] = asset.unique_identifier
|
55
|
+
|
56
|
+
weights = weights_source[["time_index", "unique_identifier", "weight"]]
|
57
|
+
weights.rename(columns={"weight": "signal_weight"}, inplace=True)
|
58
|
+
weights.set_index(["time_index", "unique_identifier"], inplace=True)
|
59
|
+
|
60
|
+
weights = update_statistics.filter_df_by_latest_value(weights)
|
61
|
+
return weights
|
@@ -0,0 +1,149 @@
|
|
1
|
+
import copy
|
2
|
+
from datetime import datetime
|
3
|
+
from typing import Union
|
4
|
+
|
5
|
+
import numpy as np
|
6
|
+
import pandas as pd
|
7
|
+
import pytz
|
8
|
+
from mainsequence.virtualfundbuilder import TIMEDELTA
|
9
|
+
from mainsequence.virtualfundbuilder.contrib.prices.data_nodes import get_interpolated_prices_timeseries
|
10
|
+
from mainsequence.virtualfundbuilder.resource_factory.signal_factory import WeightsBase, register_signal_class
|
11
|
+
from mainsequence.tdag.data_nodes import DataNode
|
12
|
+
|
13
|
+
import pandas_market_calendars as mcal
|
14
|
+
|
15
|
+
@register_signal_class(register_in_agent=True)
|
16
|
+
class IntradayTrend(WeightsBase, DataNode):
|
17
|
+
|
18
|
+
def __init__(
|
19
|
+
self,
|
20
|
+
calendar: str,
|
21
|
+
source_frequency: str = "1d",
|
22
|
+
*args, **kwargs
|
23
|
+
):
|
24
|
+
"""
|
25
|
+
Signal Weights
|
26
|
+
|
27
|
+
Arguments
|
28
|
+
source_frequency (str): Frequency of market cap source
|
29
|
+
"""
|
30
|
+
super().__init__(*args, **kwargs)
|
31
|
+
|
32
|
+
self.source_frequency = source_frequency
|
33
|
+
self.calendar = calendar
|
34
|
+
self.bars_ts, self.asset_symbols = get_interpolated_prices_timeseries(copy.deepcopy(self.assets_configuration))
|
35
|
+
|
36
|
+
|
37
|
+
def update(self, latest_value: Union[datetime, None], params_for_tree_run=None, *args, **kwargs) -> pd.DataFrame:
|
38
|
+
"""
|
39
|
+
Updates the weights considering rebalance periods and execution frequency.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
latest_value Union[datetime, None]: The timestamp of the latest available data.
|
43
|
+
"""
|
44
|
+
asset_symbols = [a for assets in self.asset_symbols.values() for a in assets]
|
45
|
+
exchange_per_symbol = {v: k for k, values in self.asset_symbols.items() for v in values}
|
46
|
+
|
47
|
+
max_assets_time = [
|
48
|
+
ts.get_last_observation(asset_symbols=self.asset_symbols[exchange])
|
49
|
+
for exchange, ts in self.bars_ts.related_time_series.items()
|
50
|
+
]
|
51
|
+
|
52
|
+
top_date_limit = min([i.index.get_level_values("time_index").min() for i in max_assets_time]) # get the minimum available time
|
53
|
+
|
54
|
+
if latest_value is None:
|
55
|
+
latest_value = datetime(year=2018, month=1, day=1).replace(tzinfo=pytz.utc)
|
56
|
+
else:
|
57
|
+
# only when there are prices enough for the upsample
|
58
|
+
upper_range = latest_value + pd.Timedelta(self.source_frequency)
|
59
|
+
if top_date_limit < upper_range:
|
60
|
+
return pd.DataFrame()
|
61
|
+
|
62
|
+
# get last few days for past intraday returns
|
63
|
+
prices_start_date = latest_value - pd.Timedelta(days=1)
|
64
|
+
prices = self.bars_ts.pandas_df_concat_on_rows_by_key_between_dates(
|
65
|
+
start_date=prices_start_date,
|
66
|
+
end_date=top_date_limit,
|
67
|
+
great_or_equal=True,
|
68
|
+
less_or_equal=True,
|
69
|
+
asset_symbols=asset_symbols,
|
70
|
+
)
|
71
|
+
|
72
|
+
# align signal weight updates with market schedule for each day
|
73
|
+
calendar_dates = mcal.get_calendar(self.calendar)
|
74
|
+
market_schedule = calendar_dates.schedule(start_date=prices_start_date, end_date=top_date_limit)
|
75
|
+
signal_weights_index = pd.DatetimeIndex(
|
76
|
+
market_schedule.apply(
|
77
|
+
lambda d: pd.Series(pd.date_range(start=d["market_open"], end=d["market_close"], freq=self.source_frequency)),
|
78
|
+
axis=1
|
79
|
+
).values.flatten()
|
80
|
+
)
|
81
|
+
signal_prices = prices[prices.index.get_level_values("time_index").isin(signal_weights_index)]
|
82
|
+
signal_returns = signal_prices.groupby(["asset_symbol"])["close"].pct_change()
|
83
|
+
open_close_daily = prices.groupby([prices.index.get_level_values("time_index").date, prices.index.get_level_values("asset_symbol")]).agg(
|
84
|
+
open=("open", "first"),
|
85
|
+
close=("close", "last")
|
86
|
+
)
|
87
|
+
open_close_daily = open_close_daily.unstack("asset_symbol")
|
88
|
+
|
89
|
+
signal_returns = signal_returns.unstack("asset_symbol").reset_index("execution_venue_symbol", drop=True)
|
90
|
+
intraday_returns = open_close_daily["close"] / open_close_daily["open"] - 1
|
91
|
+
daily_returns = open_close_daily["open"] / open_close_daily["close"].shift(1) - 1
|
92
|
+
|
93
|
+
# Determine daily trade direction:
|
94
|
+
# Long (1) if previous intraday return was positive
|
95
|
+
# Short (-1) if previous intraday return was negative
|
96
|
+
trade_direction = intraday_returns.shift(1).map(lambda r: 1 if r>0 else -1)
|
97
|
+
|
98
|
+
# prepare signal weights
|
99
|
+
signal_weights = pd.DataFrame(index=signal_weights_index)
|
100
|
+
signal_weights["date"] = signal_weights.index.date
|
101
|
+
signal_weights = signal_weights[signal_weights["date"].isin(trade_direction.index)]
|
102
|
+
trade_direction = trade_direction.loc[signal_weights["date"]]
|
103
|
+
trade_direction.index = signal_weights.index
|
104
|
+
signal_weights = pd.concat([signal_weights, trade_direction, signal_returns], axis=1, keys=["date", "trade_direction", "signal_returns"])
|
105
|
+
|
106
|
+
# add daily returns from last day
|
107
|
+
daily_returns.index = daily_returns.index.map(signal_weights["date"].groupby("date").apply(lambda x: x.index[0]))
|
108
|
+
for asset_name in asset_symbols:
|
109
|
+
signal_weights.loc[daily_returns.index, ('signal_returns', asset_name)] = daily_returns[asset_name]
|
110
|
+
|
111
|
+
def create_weights(daily_data):
|
112
|
+
# start weights each day, grow by 1/trades_per_day and sell at the end
|
113
|
+
weights = pd.DataFrame(np.nan, index=daily_data.index, columns=asset_symbols)
|
114
|
+
weights.iloc[0] = 0
|
115
|
+
for asset_name in asset_symbols:
|
116
|
+
trade_size = 1 / len(daily_data)
|
117
|
+
daily_trade_direction = daily_data[("trade_direction", asset_name)].iloc[0]
|
118
|
+
|
119
|
+
if daily_trade_direction == 0:
|
120
|
+
return weights[asset_name].fillna(0)
|
121
|
+
|
122
|
+
# Create the indicator based on trade direction
|
123
|
+
if daily_trade_direction > 0:
|
124
|
+
indicator = daily_data[("signal_returns", asset_name)] > 0
|
125
|
+
else:
|
126
|
+
indicator = daily_data[("signal_returns", asset_name)] < 0
|
127
|
+
|
128
|
+
weights.loc[indicator, asset_name] = np.cumsum(indicator) * trade_size * daily_trade_direction
|
129
|
+
|
130
|
+
weights.iloc[-1] = 0 # sell at the end of day
|
131
|
+
return weights
|
132
|
+
|
133
|
+
# calculate signal weights based on intra-day returns in source_frequency interval
|
134
|
+
signal_weights = signal_weights.groupby(signal_weights.index.date).apply(create_weights).reset_index(drop=True, level=0).ffill().dropna()
|
135
|
+
|
136
|
+
# prepare for storage
|
137
|
+
signal_weights.index.name = "time_index"
|
138
|
+
signal_weights.columns = pd.MultiIndex.from_arrays([[exchange_per_symbol[a] for a in asset_symbols], signal_weights.columns], names=('execution_venue_symbol', 'asset_symbol'))
|
139
|
+
|
140
|
+
if len(signal_weights) == 0:
|
141
|
+
return pd.DataFrame()
|
142
|
+
|
143
|
+
signal_weights = signal_weights[signal_weights.index > latest_value]
|
144
|
+
signal_weights = signal_weights.stack().stack().to_frame(name='signal_weight').astype(float)
|
145
|
+
return signal_weights
|
146
|
+
|
147
|
+
|
148
|
+
def maximum_forward_fill(self):
|
149
|
+
return pd.Timedelta(self.source_frequency) - TIMEDELTA
|