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,11 @@
1
+ def transform_frequency_to_seconds(frequency_id:str)->int:
2
+ if frequency_id in ["1min", "1m"]:
3
+ return 60
4
+ elif frequency_id in ["5min", "5m"]:
5
+ return 60 * 5
6
+ elif frequency_id in ["15min", "15m"]:
7
+ return 60 * 15
8
+ elif frequency_id in ["1days", "1d"]:
9
+ return 60 * 60 * 24
10
+ else:
11
+ raise NotImplementedError
@@ -0,0 +1 @@
1
+ from .rebalance_strategies import *
@@ -0,0 +1,313 @@
1
+ import datetime
2
+
3
+ import pandas as pd
4
+
5
+ from mainsequence.virtualfundbuilder.enums import RebalanceFrequencyStrategyName, PriceTypeNames
6
+ from mainsequence.virtualfundbuilder.resource_factory.rebalance_factory import RebalanceStrategyBase, register_rebalance_class
7
+
8
+
9
+ @register_rebalance_class(register_in_agent=True)
10
+ class VolumeParticipation(RebalanceStrategyBase):
11
+ """
12
+ This rebalance strategy implies volume participation with no market impact.
13
+ i.e. that the execution price will be vwap and it will never execute more than max_percent_volume_in_bar
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ rebalance_start: str = "9:00",
19
+ rebalance_end: str = "23:00",
20
+ rebalance_frequency_strategy: RebalanceFrequencyStrategyName = RebalanceFrequencyStrategyName.DAILY,
21
+ max_percent_volume_in_bar: float = 0.01,
22
+ total_notional: float = 50000000,
23
+ *args, **kwargs
24
+ ):
25
+ """
26
+ Initializes the VolumeParticipation strategy.
27
+
28
+ Attributes:
29
+ rebalance_start (str): Start time for rebalancing, in "hh:mm" format.
30
+ rebalance_end (str): End time for rebalancing, in "hh:mm" format.
31
+ rebalance_frequency_strategy (RebalanceFrequencyStrategyName): Rebalance frequency.
32
+ max_percent_volume_in_bar (float): Maximum percentage of volume to trade in a bar.
33
+ total_notional (int): Initial notional invested in the strategy.
34
+ """
35
+ self.rebalance_start = rebalance_start
36
+ self.rebalance_end = rebalance_end
37
+ self.rebalance_frequency_strategy = rebalance_frequency_strategy
38
+ self.max_percent_volume_in_bar = max_percent_volume_in_bar
39
+ self.total_notional = total_notional
40
+
41
+ super().__init__(*args, **kwargs)
42
+
43
+ def apply_rebalance_logic(
44
+ self,
45
+ last_rebalance_weights: pd.DataFrame,
46
+ start_date: datetime.datetime,
47
+ end_date: datetime.datetime,
48
+ signal_weights: pd.DataFrame,
49
+ prices_df: pd.DataFrame,
50
+ price_type: str,
51
+ ) -> pd.DataFrame:
52
+ raise NotImplementedError
53
+ asset_list = list(signal_weights.columns)
54
+ start_time, end_time = pd.Timestamp(self.rebalance_start).time(), pd.Timestamp(self.rebalance_end).time()
55
+
56
+ rebalance_dates = self.calculate_rebalance_dates(
57
+ start=start_date,
58
+ end=end_date,
59
+ rebalance_frequency_strategy=self.rebalance_frequency_strategy,
60
+ calendar=self.calendar
61
+ )
62
+
63
+ signal_weights["day"] = signal_weights.index.floor("D")
64
+
65
+ volume_df = prices_df.reset_index().pivot(index="time_index", columns="asset_symbol", values="volume").fillna(0)
66
+ prices_df = prices_df.reset_index().pivot(index="time_index", columns="asset_symbol", values=price_type).fillna(
67
+ 0)
68
+
69
+ rebalance_days = np.intersect1d(signal_weights["day"], rebalance_dates)
70
+
71
+ rebalance_weights = signal_weights[signal_weights["day"].isin(rebalance_days)]
72
+ rebalance_weights = rebalance_weights[rebalance_weights.index.time >= start_time]
73
+ rebalance_weights = rebalance_weights[rebalance_weights.index.time <= end_time]
74
+
75
+ rebalance_weights = rebalance_weights.set_index("day", append=True)
76
+
77
+ # calculcate the maximal participation volumes at each rebalancing steps
78
+ max_participation_volume = (volume_df * prices_df) * self.max_percent_volume_in_bar
79
+ max_participation_volume["day"] = max_participation_volume.index.floor("D")
80
+ max_participation_volume = max_participation_volume.set_index("day", append=True)
81
+ max_participation_volume = max_participation_volume[
82
+ max_participation_volume.index.isin(rebalance_weights.index)]
83
+
84
+ rebalance_weights = pd.concat(
85
+ objs=[rebalance_weights, max_participation_volume],
86
+ axis=1,
87
+ keys=["weights", "max_dollar_volume"]
88
+ )
89
+
90
+ if last_rebalance_weights is not None:
91
+ # get last rebalance weights
92
+ past_rebalance_weight = last_rebalance_weights["weights_current"].reset_index(level="time_index", drop=True)
93
+ else:
94
+ past_rebalance_weight = pd.Series(0, index=asset_list)
95
+
96
+ # recursively group by day
97
+ past_day_rebalance_weight = past_rebalance_weight
98
+ new_rebalance_weights = []
99
+ for day, day_df in tqdm(rebalance_weights.groupby("day"), desc="building volume participation"):
100
+ if (day_df.weights.max() - day_df.weights.min()).sum() != 0.0:
101
+ logger.warning("Signal weight in time period changes, using weights at rebalancing start")
102
+ day_df.loc[:, "weights"] = day_df["weights"].iloc[0].to_numpy()
103
+
104
+ # calculate changes in backtesting weights
105
+ weights_diff = day_df.weights - past_day_rebalance_weight
106
+
107
+ target_dollar_volume = np.abs(weights_diff) * self.total_notional
108
+ cumulative_dollar_volume = day_df.max_dollar_volume.fillna(0).cumsum()
109
+ weighted_volume_multiplier = (cumulative_dollar_volume / target_dollar_volume).replace([np.inf], np.nan)
110
+ weighted_volume_multiplier = weighted_volume_multiplier.fillna(0).map(lambda x: min(1.0, x))
111
+
112
+ new_rebalance_weights_day = past_day_rebalance_weight + weights_diff * weighted_volume_multiplier
113
+ new_rebalance_weights.append(new_rebalance_weights_day)
114
+ past_day_rebalance_weight = new_rebalance_weights_day.iloc[-1]
115
+
116
+ if len(new_rebalance_weights) == 0:
117
+ logger.info("No new rebalancing weights found - returning empty DataFrame")
118
+ return pd.DataFrame()
119
+
120
+ rebalance_weights = pd.concat(new_rebalance_weights, axis=0).reset_index("day", drop=True)
121
+ rebalance_weights_index = rebalance_weights.index
122
+
123
+ shifted_rebalance_weights = rebalance_weights.shift(1)
124
+ shifted_rebalance_weights.iloc[0] = past_rebalance_weight
125
+ rebalance_weights = pd.concat(
126
+ objs=[
127
+ rebalance_weights, shifted_rebalance_weights,
128
+ prices_df, prices_df.shift(1),
129
+ volume_df, volume_df.shift(1),
130
+ ],
131
+ keys=["weights_current", "weights_before", "price_current", "price_before", "volume_current",
132
+ "volume_before"],
133
+ axis=1
134
+ )
135
+
136
+ # filter out values with no weights and set NaNs to 0 weight
137
+ rebalance_weights = rebalance_weights.loc[rebalance_weights_index].fillna(0)
138
+
139
+ logger.info(f"{len(rebalance_weights)} new rebalancing weights calculated")
140
+ return rebalance_weights
141
+
142
+ @register_rebalance_class(register_in_agent=True)
143
+ class TimeWeighted(RebalanceStrategyBase):
144
+ def __init__(
145
+ self,
146
+ rebalance_start: str = "9:00",
147
+ rebalance_end: str = "23:00",
148
+ rebalance_frequency_strategy: RebalanceFrequencyStrategyName = RebalanceFrequencyStrategyName.DAILY,
149
+ *args, **kwargs
150
+ ):
151
+ """
152
+ Initialize the time weighted rebalance strategy.
153
+
154
+ Attributes:
155
+ rebalance_start (str): Start time for rebalancing, in "hh:mm" format.
156
+ rebalance_end (str): End time for rebalancing, in "hh:mm" format.
157
+ rebalance_frequency_strategy (RebalanceFrequencyStrategyName): Rebalance frequency.
158
+ max_percent_volume_in_bar (float): Maximum percentage of volume to trade in a bar.
159
+ total_notional (int): Initial notional invested in the strategy.
160
+ """
161
+ self.rebalance_start = rebalance_start
162
+ self.rebalance_end = rebalance_end
163
+ self.rebalance_frequency_strategy = rebalance_frequency_strategy
164
+ super().__init__(*args, **kwargs)
165
+
166
+ def apply_rebalance_logic(
167
+ self,
168
+ last_rebalance_weights: pd.DataFrame,
169
+ start_date: datetime.datetime,
170
+ end_date: datetime.datetime,
171
+ signal_weights: pd.DataFrame,
172
+ prices_df: pd.DataFrame,
173
+ price_type: PriceTypeNames,
174
+ ) -> pd.DataFrame:
175
+ """
176
+ Rebalance weights are set at start_time of rebalancing
177
+
178
+ Parameters
179
+ ----------
180
+ signal_weights
181
+ rebalance_dates
182
+
183
+ Returns
184
+ -------
185
+ """
186
+ raise NotImplementedError
187
+ # TODO change last_rebalance_weights, as the calculations are wrong if there was a past rebalance during the day (= during the current rebalance window)
188
+ asset_list = list(signal_weights.columns)
189
+ start_time, end_time = pd.Timestamp(self.rebalance_start).time(), pd.Timestamp(self.rebalance_end).time()
190
+
191
+ get_time_seconds = lambda x: x.hour * 3600 + x.minute * 60 + x.second
192
+
193
+ rebalance_dates = self.calculate_rebalance_dates(
194
+ start=start_date,
195
+ end=end_date,
196
+ rebalance_frequency_strategy=self.rebalance_frequency_strategy,
197
+ calendar=self.calendar
198
+ )
199
+
200
+ # get rebalance dates from signal weights
201
+ signal_weights["day"] = signal_weights.index.floor("D")
202
+ rebalance_days = np.intersect1d(signal_weights["day"], rebalance_dates)
203
+ rebalance_weights = signal_weights[signal_weights["day"].isin(rebalance_days)]
204
+
205
+ rebalance_weights = rebalance_weights[rebalance_weights.index.time >= start_time]
206
+ rebalance_weights = rebalance_weights[rebalance_weights.index.time <= end_time]
207
+
208
+ # get past rebalance dates to calculate rebalance steps
209
+ past_rebalance_weights = rebalance_weights.groupby("day").first().shift()
210
+
211
+ # if past backtesting weights exist, get last one; otherwise start at 0
212
+ if last_rebalance_weights is not None:
213
+ # get last rebalance weights
214
+ past_rebalance_weight = last_rebalance_weights["weights_current"].reset_index(level="time_index", drop=True)
215
+ else:
216
+ past_rebalance_weight = pd.Series(0, index=asset_list)
217
+
218
+ past_rebalance_weights.index += datetime.timedelta(seconds=get_time_seconds(start_time))
219
+ past_rebalance_weights = past_rebalance_weights.reindex(rebalance_weights.index).ffill()
220
+
221
+ # time-based multiplicator during rebalancing
222
+ time_weight = (rebalance_weights.index - rebalance_weights["day"]).dt.total_seconds()
223
+ time_weight = (time_weight - get_time_seconds(start_time)) / (
224
+ get_time_seconds(end_time) - get_time_seconds(start_time))
225
+
226
+ rebalance_weights = rebalance_weights.drop(columns=["day"])
227
+
228
+ # calculate new backtesting weights as past_weights + time_weight * diff_weights
229
+ diff_weights = rebalance_weights - past_rebalance_weights
230
+ rebalance_weights = past_rebalance_weights + diff_weights.multiply(time_weight, axis=0)
231
+
232
+ # when columns are not available weights and prices need to be set to 0
233
+ prices_df = prices_df.reset_index().pivot(index="time_index", columns="asset_symbol",
234
+ values=price_type).ffill().fillna(0)
235
+ valid_columns = rebalance_weights.columns[rebalance_weights.columns.isin(prices_df.columns)]
236
+ if len(valid_columns) != rebalance_weights.shape[1]:
237
+ rebalance_weights = rebalance_weights[valid_columns].copy()
238
+ rebalance_weights = rebalance_weights.divide(rebalance_weights.sum(axis=1), axis=0)
239
+
240
+ # make backtesting weights nan when there is no price
241
+ nan_mask = prices_df.loc[rebalance_weights.index].isna()
242
+ rebalance_weights[nan_mask] = np.nan
243
+
244
+ if len(rebalance_weights) == 0:
245
+ logger.info("No new rebalancing weights found - returning empty DataFrame")
246
+ return pd.DataFrame()
247
+
248
+ # create backtesting weights before and after rebalancing
249
+ shifted_rebalance_weights = rebalance_weights.shift(1)
250
+ shifted_rebalance_weights.iloc[0] = past_rebalance_weight
251
+ rebalance_weights = pd.concat(
252
+ objs=[shifted_rebalance_weights, rebalance_weights, prices_df, prices_df.shift(1)],
253
+ keys=["weights_before", "weights_current", "price_current", "price_before"],
254
+ axis=1
255
+ )
256
+
257
+ logger.info(f"{len(rebalance_weights)} new rebalancing weights calculated")
258
+ return rebalance_weights
259
+
260
+ @register_rebalance_class(register_in_agent=True)
261
+ class ImmediateSignal(RebalanceStrategyBase):
262
+ """
263
+ This rebalance strategy 'immediately' rebalances the weights. This is equivalent to just using the signal weights.
264
+ """
265
+
266
+ def __init__(self, *args, **kwargs):
267
+ """
268
+ Initialize the immediate rebalance strategy.
269
+ """
270
+ super().__init__(*args, **kwargs)
271
+
272
+ def get_explanation(self):
273
+ explanation=f"""<p> This rebalance strategy 'immediately' rebalances the weights. This is equivalent to just using the signal weights. </p>"""
274
+ return explanation
275
+
276
+ def apply_rebalance_logic(
277
+ self,
278
+ last_rebalance_weights: pd.DataFrame,
279
+ signal_weights: pd.DataFrame,
280
+ prices_df,
281
+ price_type: PriceTypeNames,
282
+ *args, **kwargs
283
+ ) -> pd.DataFrame:
284
+
285
+ volume_df = prices_df.reset_index().pivot(index="time_index", columns=["unique_identifier"], values="volume")
286
+ prices_df = prices_df.reset_index().pivot(index="time_index", columns=["unique_identifier"], values=price_type.value)
287
+
288
+ if last_rebalance_weights is not None:
289
+ #because this function provides backtest weights we can concatenate last observation
290
+ volume_df = pd.concat([last_rebalance_weights.unstack()["volume_current"], volume_df], axis=0)
291
+ prices_df = pd.concat([last_rebalance_weights.unstack()["price_current"], prices_df], axis=0)
292
+ signal_weights = pd.concat([last_rebalance_weights.unstack()["weights_current"], signal_weights], axis=0)
293
+ rebalance_weights = pd.concat(
294
+ objs=[
295
+ signal_weights,
296
+ signal_weights.shift(1),
297
+ prices_df,
298
+ prices_df.shift(1),
299
+ volume_df,
300
+ volume_df.shift(1)
301
+ ],
302
+ keys=["weights_current", "weights_before", "price_current", "price_before", "volume_current",
303
+ "volume_before"],
304
+ axis=1
305
+ )
306
+
307
+ if last_rebalance_weights is not None:
308
+ # align last rebalance weights
309
+ rebalance_weights = rebalance_weights[
310
+ rebalance_weights.index > last_rebalance_weights.index.get_level_values("time_index")[0]]
311
+
312
+
313
+ return rebalance_weights