mainsequence 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. mainsequence/__init__.py +0 -0
  2. mainsequence/__main__.py +9 -0
  3. mainsequence/cli/__init__.py +1 -0
  4. mainsequence/cli/api.py +157 -0
  5. mainsequence/cli/cli.py +442 -0
  6. mainsequence/cli/config.py +78 -0
  7. mainsequence/cli/ssh_utils.py +126 -0
  8. mainsequence/client/__init__.py +17 -0
  9. mainsequence/client/base.py +431 -0
  10. mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  11. mainsequence/client/data_sources_interfaces/duckdb.py +1468 -0
  12. mainsequence/client/data_sources_interfaces/timescale.py +479 -0
  13. mainsequence/client/models_helpers.py +113 -0
  14. mainsequence/client/models_report_studio.py +412 -0
  15. mainsequence/client/models_tdag.py +2276 -0
  16. mainsequence/client/models_vam.py +1983 -0
  17. mainsequence/client/utils.py +387 -0
  18. mainsequence/dashboards/__init__.py +0 -0
  19. mainsequence/dashboards/streamlit/__init__.py +0 -0
  20. mainsequence/dashboards/streamlit/assets/config.toml +12 -0
  21. mainsequence/dashboards/streamlit/assets/favicon.png +0 -0
  22. mainsequence/dashboards/streamlit/assets/logo.png +0 -0
  23. mainsequence/dashboards/streamlit/core/__init__.py +0 -0
  24. mainsequence/dashboards/streamlit/core/theme.py +212 -0
  25. mainsequence/dashboards/streamlit/pages/__init__.py +0 -0
  26. mainsequence/dashboards/streamlit/scaffold.py +220 -0
  27. mainsequence/instrumentation/__init__.py +7 -0
  28. mainsequence/instrumentation/utils.py +101 -0
  29. mainsequence/instruments/__init__.py +1 -0
  30. mainsequence/instruments/data_interface/__init__.py +10 -0
  31. mainsequence/instruments/data_interface/data_interface.py +361 -0
  32. mainsequence/instruments/instruments/__init__.py +3 -0
  33. mainsequence/instruments/instruments/base_instrument.py +85 -0
  34. mainsequence/instruments/instruments/bond.py +447 -0
  35. mainsequence/instruments/instruments/european_option.py +74 -0
  36. mainsequence/instruments/instruments/interest_rate_swap.py +217 -0
  37. mainsequence/instruments/instruments/json_codec.py +585 -0
  38. mainsequence/instruments/instruments/knockout_fx_option.py +146 -0
  39. mainsequence/instruments/instruments/position.py +475 -0
  40. mainsequence/instruments/instruments/ql_fields.py +239 -0
  41. mainsequence/instruments/instruments/vanilla_fx_option.py +107 -0
  42. mainsequence/instruments/pricing_models/__init__.py +0 -0
  43. mainsequence/instruments/pricing_models/black_scholes.py +49 -0
  44. mainsequence/instruments/pricing_models/bond_pricer.py +182 -0
  45. mainsequence/instruments/pricing_models/fx_option_pricer.py +90 -0
  46. mainsequence/instruments/pricing_models/indices.py +350 -0
  47. mainsequence/instruments/pricing_models/knockout_fx_pricer.py +209 -0
  48. mainsequence/instruments/pricing_models/swap_pricer.py +502 -0
  49. mainsequence/instruments/settings.py +175 -0
  50. mainsequence/instruments/utils.py +29 -0
  51. mainsequence/logconf.py +284 -0
  52. mainsequence/reportbuilder/__init__.py +0 -0
  53. mainsequence/reportbuilder/__main__.py +0 -0
  54. mainsequence/reportbuilder/examples/ms_template_report.py +706 -0
  55. mainsequence/reportbuilder/model.py +713 -0
  56. mainsequence/reportbuilder/slide_templates.py +532 -0
  57. mainsequence/tdag/__init__.py +8 -0
  58. mainsequence/tdag/__main__.py +0 -0
  59. mainsequence/tdag/config.py +129 -0
  60. mainsequence/tdag/data_nodes/__init__.py +12 -0
  61. mainsequence/tdag/data_nodes/build_operations.py +751 -0
  62. mainsequence/tdag/data_nodes/data_nodes.py +1292 -0
  63. mainsequence/tdag/data_nodes/persist_managers.py +812 -0
  64. mainsequence/tdag/data_nodes/run_operations.py +543 -0
  65. mainsequence/tdag/data_nodes/utils.py +24 -0
  66. mainsequence/tdag/future_registry.py +25 -0
  67. mainsequence/tdag/utils.py +40 -0
  68. mainsequence/virtualfundbuilder/__init__.py +45 -0
  69. mainsequence/virtualfundbuilder/__main__.py +235 -0
  70. mainsequence/virtualfundbuilder/agent_interface.py +77 -0
  71. mainsequence/virtualfundbuilder/config_handling.py +86 -0
  72. mainsequence/virtualfundbuilder/contrib/__init__.py +0 -0
  73. mainsequence/virtualfundbuilder/contrib/apps/__init__.py +8 -0
  74. mainsequence/virtualfundbuilder/contrib/apps/etf_replicator_app.py +164 -0
  75. mainsequence/virtualfundbuilder/contrib/apps/generate_report.py +292 -0
  76. mainsequence/virtualfundbuilder/contrib/apps/load_external_portfolio.py +107 -0
  77. mainsequence/virtualfundbuilder/contrib/apps/news_app.py +437 -0
  78. mainsequence/virtualfundbuilder/contrib/apps/portfolio_report_app.py +91 -0
  79. mainsequence/virtualfundbuilder/contrib/apps/portfolio_table.py +95 -0
  80. mainsequence/virtualfundbuilder/contrib/apps/run_named_portfolio.py +45 -0
  81. mainsequence/virtualfundbuilder/contrib/apps/run_portfolio.py +40 -0
  82. mainsequence/virtualfundbuilder/contrib/apps/templates/base.html +147 -0
  83. mainsequence/virtualfundbuilder/contrib/apps/templates/report.html +77 -0
  84. mainsequence/virtualfundbuilder/contrib/data_nodes/__init__.py +5 -0
  85. mainsequence/virtualfundbuilder/contrib/data_nodes/external_weights.py +61 -0
  86. mainsequence/virtualfundbuilder/contrib/data_nodes/intraday_trend.py +149 -0
  87. mainsequence/virtualfundbuilder/contrib/data_nodes/market_cap.py +310 -0
  88. mainsequence/virtualfundbuilder/contrib/data_nodes/mock_signal.py +78 -0
  89. mainsequence/virtualfundbuilder/contrib/data_nodes/portfolio_replicator.py +269 -0
  90. mainsequence/virtualfundbuilder/contrib/prices/__init__.py +1 -0
  91. mainsequence/virtualfundbuilder/contrib/prices/data_nodes.py +810 -0
  92. mainsequence/virtualfundbuilder/contrib/prices/utils.py +11 -0
  93. mainsequence/virtualfundbuilder/contrib/rebalance_strategies/__init__.py +1 -0
  94. mainsequence/virtualfundbuilder/contrib/rebalance_strategies/rebalance_strategies.py +313 -0
  95. mainsequence/virtualfundbuilder/data_nodes.py +637 -0
  96. mainsequence/virtualfundbuilder/enums.py +23 -0
  97. mainsequence/virtualfundbuilder/models.py +282 -0
  98. mainsequence/virtualfundbuilder/notebook_handling.py +42 -0
  99. mainsequence/virtualfundbuilder/portfolio_interface.py +272 -0
  100. mainsequence/virtualfundbuilder/resource_factory/__init__.py +0 -0
  101. mainsequence/virtualfundbuilder/resource_factory/app_factory.py +170 -0
  102. mainsequence/virtualfundbuilder/resource_factory/base_factory.py +238 -0
  103. mainsequence/virtualfundbuilder/resource_factory/rebalance_factory.py +101 -0
  104. mainsequence/virtualfundbuilder/resource_factory/signal_factory.py +183 -0
  105. mainsequence/virtualfundbuilder/utils.py +381 -0
  106. mainsequence-2.0.0.dist-info/METADATA +105 -0
  107. mainsequence-2.0.0.dist-info/RECORD +110 -0
  108. mainsequence-2.0.0.dist-info/WHEEL +5 -0
  109. mainsequence-2.0.0.dist-info/licenses/LICENSE +40 -0
  110. mainsequence-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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,5 @@
1
+ from .intraday_trend import *
2
+ from .market_cap import *
3
+ from .mock_signal import *
4
+ from .portfolio_replicator import *
5
+ from .external_weights import *
@@ -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