mainsequence 2.0.4rc0__py3-none-any.whl → 3.0.2__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 (33) hide show
  1. mainsequence/cli/cli.py +4 -7
  2. mainsequence/cli/ssh_utils.py +17 -2
  3. mainsequence/client/__init__.py +3 -3
  4. mainsequence/client/base.py +3 -3
  5. mainsequence/client/data_sources_interfaces/timescale.py +20 -19
  6. mainsequence/client/exceptions.py +11 -0
  7. mainsequence/client/models_helpers.py +2 -2
  8. mainsequence/client/models_tdag.py +104 -87
  9. mainsequence/client/models_vam.py +9 -9
  10. mainsequence/dashboards/streamlit/core/theme.py +128 -109
  11. mainsequence/dashboards/streamlit/scaffold.py +3 -0
  12. mainsequence/instruments/__init__.py +1 -1
  13. mainsequence/instruments/data_interface/__init__.py +1 -1
  14. mainsequence/instruments/data_interface/data_interface.py +31 -11
  15. mainsequence/instruments/instruments/bond.py +8 -0
  16. mainsequence/instruments/pricing_models/indices.py +26 -14
  17. mainsequence/instruments/settings.py +2 -162
  18. mainsequence/tdag/config.py +2 -2
  19. mainsequence/tdag/data_nodes/build_operations.py +3 -3
  20. mainsequence/tdag/data_nodes/data_nodes.py +23 -23
  21. mainsequence/tdag/data_nodes/persist_managers.py +121 -121
  22. mainsequence/tdag/data_nodes/run_operations.py +25 -25
  23. mainsequence/virtualfundbuilder/contrib/apps/portfolio_report_app.py +1 -1
  24. mainsequence/virtualfundbuilder/contrib/prices/data_nodes.py +2 -2
  25. mainsequence/virtualfundbuilder/data_nodes.py +1 -1
  26. mainsequence/virtualfundbuilder/portfolio_interface.py +7 -7
  27. mainsequence/virtualfundbuilder/utils.py +2 -2
  28. {mainsequence-2.0.4rc0.dist-info → mainsequence-3.0.2.dist-info}/METADATA +1 -1
  29. {mainsequence-2.0.4rc0.dist-info → mainsequence-3.0.2.dist-info}/RECORD +33 -32
  30. {mainsequence-2.0.4rc0.dist-info → mainsequence-3.0.2.dist-info}/WHEEL +0 -0
  31. {mainsequence-2.0.4rc0.dist-info → mainsequence-3.0.2.dist-info}/entry_points.txt +0 -0
  32. {mainsequence-2.0.4rc0.dist-info → mainsequence-3.0.2.dist-info}/licenses/LICENSE +0 -0
  33. {mainsequence-2.0.4rc0.dist-info → mainsequence-3.0.2.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,10 @@
1
1
  from __future__ import annotations
2
- import streamlit as st
3
2
  from pathlib import Path
3
+ import streamlit as st
4
4
 
5
5
 
6
+ # --------------------- small theme helpers ---------------------
7
+
6
8
  def inject_css_for_dark_accents():
7
9
  st.markdown(
8
10
  """
@@ -22,126 +24,173 @@ def explain_theming():
22
24
  )
23
25
 
24
26
 
27
+ # --------------------- spinner frame loader (runs once on import) ---------------------
25
28
 
29
+ def _read_txt(p: Path) -> str:
30
+ return p.read_text(encoding="utf-8").strip()
26
31
 
27
- # --- Load spinner frames ONCE from two levels above, files: image_1_base64.txt ... image_5_base64.txt ---
28
32
  def _load_spinner_frames_for_this_template() -> list[str]:
29
- base_dir = Path(__file__).resolve().parent.parent
30
- frames: list[str] = []
31
- for i in range(1, 6):
32
- p = base_dir / f"assets/image_{i}_base64.txt"
33
- if not p.exists():
34
- raise FileNotFoundError(f"Missing spinner frame file: {p}")
35
- frames.append(p.read_text(encoding="utf-8").strip())
36
- return frames
37
-
38
-
39
- _SPINNER_FRAMES_RAW = _load_spinner_frames_for_this_template()
33
+ """
34
+ Looks under: <repo>/mainsequence/dashboards/streamlit/assets/
35
+
36
+ Order of precedence:
37
+ 1) image_1_base64.txt ... image_5_base64.txt
38
+ 2) image_base64.txt (single file, replicated to 5 frames)
39
+ 3) spinner_1.txt ... spinner_5.txt
40
+ 4) Any *base64.txt (sorted) or *.txt (sorted), up to 5 frames
41
+ - If only one file is found, it is replicated to 5 frames.
42
+ - If 2-4 files are found, the last one is repeated to reach 5.
43
+ On total failure, returns five copies of a 1x1 transparent PNG.
44
+ """
45
+ assets = Path(__file__).resolve().parent.parent / "assets"
46
+
47
+ # 1) Named sequence: image_1_base64.txt .. image_5_base64.txt
48
+ seq = [assets / f"image_{i}_base64.txt" for i in range(1, 6)]
49
+ if all(p.exists() for p in seq):
50
+ return [_read_txt(p) for p in seq]
51
+
52
+ # 2) Single file replicated
53
+ single = assets / "image_base64.txt"
54
+ if single.exists():
55
+ s = _read_txt(single)
56
+ return [s] * 5
57
+
58
+ # 3) Alternate sequence
59
+ alt_seq = [assets / f"spinner_{i}.txt" for i in range(1, 6)]
60
+ if all(p.exists() for p in alt_seq):
61
+ return [_read_txt(p) for p in alt_seq]
62
+
63
+ # 4) Any *base64.txt, then any *.txt
64
+ candidates = sorted(assets.glob("*base64.txt")) or sorted(assets.glob("*.txt"))
65
+ frames = [_read_txt(p) for p in candidates[:5]]
66
+ if frames:
67
+ if len(frames) == 1:
68
+ frames = frames * 5
69
+ elif len(frames) < 5:
70
+ frames += [frames[-1]] * (5 - len(frames))
71
+ return frames
72
+
73
+ # Fallback: 1x1 transparent PNG
74
+ transparent_png = ("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8"
75
+ "/w8AAn8B9p7u3t8AAAAASUVORK5CYII=")
76
+ return [transparent_png] * 5
77
+
78
+ try:
79
+ _SPINNER_FRAMES_RAW = _load_spinner_frames_for_this_template()
80
+ except Exception:
81
+ # Never break import due to spinner assets
82
+ transparent_png = ("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8"
83
+ "/w8AAn8B9p7u3t8AAAAASUVORK5CYII=")
84
+ _SPINNER_FRAMES_RAW = [transparent_png] * 5
85
+
86
+ # Public constants (used only within this module, but left as globals for clarity)
87
+ IMAGE_1_B64, IMAGE_2_B64, IMAGE_3_B64, IMAGE_4_B64, IMAGE_5_B64 = _SPINNER_FRAMES_RAW
40
88
 
41
89
 
42
- # Expose constants for the function (keeps the code below simple)
43
- IMAGE_1_B64, IMAGE_2_B64, IMAGE_3_B64, IMAGE_4_B64, IMAGE_5_B64 = _SPINNER_FRAMES_RAW
90
+ # --------------------- spinner override (CSS only) ---------------------
44
91
 
45
92
  def override_spinners(
46
93
  hide_deploy_button: bool = False,
47
94
  *,
48
95
  # Sizes
49
- top_px: int = 35, # top-right toolbar & st.status icon base size
50
- inline_px: int = 288, # animation size when centered
96
+ top_px: int = 20, # top-right toolbar spinner size
97
+ inline_px: int = 96, # inline/status spinner size
51
98
  # Timing
52
99
  duration_ms: int = 900,
53
- # Toolbar nudges / spacing
54
- toolbar_nudge_px: int = -3,
100
+ # Toolbar micro-positioning
101
+ toolbar_nudge_px: int = -2,
55
102
  toolbar_gap_left_px: int = 2,
56
103
  toolbar_left_offset_px: int = 0,
57
- # Centered overlay styling
58
- center_non_toolbar: bool = True, # << keep True to center inline + status
59
- dim_backdrop: bool = True, # << set False to hide the dark veil
60
- overlay_blur_px: float = 1.5,
61
- overlay_opacity: float = 0.35,
62
- overlay_z_index: int = 9990, # keep below toolbar; we also lift toolbar above
104
+ # Overlay options (for inline/status)
105
+ center_non_toolbar: bool = True,
106
+ dim_backdrop: bool = False,
107
+ overlay_blur_px: float = 0.0,
108
+ overlay_opacity: float = 0.0,
109
+ overlay_z_index: int = 9990,
63
110
  ) -> None:
64
- """Override Streamlit spinners with a 4-frame animation.
65
- - Toolbar spinner stays in the toolbar (top-right).
66
- - All other spinners (inline + st.status icon) are centered on screen.
111
+ """Replace Streamlit's spinners with a 5‑frame bitmap animation.
112
+
113
+ This injects CSS only (no JS). It hides native SVGs and applies the frames
114
+ to the toolbar spinner, inline st.spinner, and st.status icon.
67
115
  """
68
116
 
69
- def as_data_uri(s: str, mime="image/png") -> str:
70
- s = s.strip()
117
+ def as_data_uri(s: str, mime: str = "image/png") -> str:
118
+ s = (s or "").strip()
71
119
  return s if s.startswith("data:") else f"data:{mime};base64,{s}"
72
120
 
73
121
  i1 = as_data_uri(IMAGE_1_B64)
74
122
  i2 = as_data_uri(IMAGE_2_B64)
75
123
  i3 = as_data_uri(IMAGE_3_B64)
76
124
  i4 = as_data_uri(IMAGE_4_B64)
77
- i5= as_data_uri(IMAGE_5_B64)
78
-
79
- veil_bg = f"rgba(0,0,0,{overlay_opacity})"
125
+ i5 = as_data_uri(IMAGE_5_B64)
80
126
 
81
127
  st.markdown(f"""
82
128
  <style>
83
- /* ---- 4-frame animation ---- */
84
- @keyframes st-fourframe {{
85
- 0%% {{ background-image:url("{i1}"); }}
86
- 20% {{ background-image:url("{i2}"); }}
87
- 40% {{ background-image:url("{i3}"); }}
88
- 60% {{ background-image:url("{i4}"); }}
89
- 80% {{ background-image:url("{i5}"); }}
90
- 100% {{ background-image:url("{i5}"); }}
129
+ /* ===== 5-frame animation (fixed: do NOT use '0%%') ===== */
130
+ @keyframes st-fiveframe {{
131
+ 0% {{ background-image:url("{i1}"); }}
132
+ 20% {{ background-image:url("{i2}"); }}
133
+ 40% {{ background-image:url("{i3}"); }}
134
+ 60% {{ background-image:url("{i4}"); }}
135
+ 80% {{ background-image:url("{i5}"); }}
136
+ 100%{{ background-image:url("{i5}"); }}
91
137
  }}
92
138
 
93
- /* ---- CSS variables ---- */
94
139
  :root {{
95
- --st-spin-top:{top_px}px; /* toolbar/status base size */
96
- --st-spin-inline:{inline_px}px; /* centered spinner size */
140
+ --st-spin-top:{top_px}px;
141
+ --st-spin-inline:{inline_px}px;
97
142
  --st-spin-dur:{duration_ms}ms;
98
143
 
99
- --st-spin-toolbar-nudge:{toolbar_nudge_px}px;
100
- --st-spin-toolbar-gap:{toolbar_gap_left_px}px;
101
- --st-spin-toolbar-left:{toolbar_left_offset_px}px;
144
+ --st-toolbar-nudge:{toolbar_nudge_px}px;
145
+ --st-toolbar-gap:{toolbar_gap_left_px}px;
146
+ --st-toolbar-left:{toolbar_left_offset_px}px;
102
147
 
103
148
  --st-overlay-z:{overlay_z_index};
104
- --st-overlay-bg:{veil_bg};
149
+ --st-overlay-bg: rgba(0,0,0,{overlay_opacity});
105
150
  --st-overlay-blur:{overlay_blur_px}px;
106
151
  }}
107
152
 
108
- /* Lift toolbar above any overlay so Stop/Deploy remain clickable */
153
+ /* ===== ensure toolbar itself stays clickable above overlays ===== */
109
154
  div[data-testid="stToolbar"],
110
155
  [data-testid="stStatusWidget"] {{
111
156
  position: relative;
112
157
  z-index: calc(var(--st-overlay-z) + 5);
113
158
  }}
114
159
 
115
- /* =======================================================================
116
- 1) Top-right toolbar widget (kept in place, not centered)
117
- ======================================================================= */
160
+ /* ===== hide every built-in spinner glyph (SVG/img) ===== */
161
+ [data-testid="stSpinner"] svg,
162
+ [data-testid="stSpinnerIcon"] svg,
163
+ [data-testid="stStatusWidget"] svg,
164
+ header [data-testid="stSpinner"] svg {{
165
+ display: none !important;
166
+ }}
167
+
168
+ /* ===== toolbar spinner (top-right) ===== */
118
169
  [data-testid="stStatusWidget"] {{
119
170
  position:relative;
120
- padding-left: calc(var(--st-spin-top) + var(--st-spin-toolbar-gap));
171
+ padding-left: calc(var(--st-spin-top) + var(--st-toolbar-gap));
121
172
  }}
122
- [data-testid="stStatusWidget"] svg,
123
- [data-testid="stStatusWidget"] img {{ display:none !important; }}
124
173
  [data-testid="stStatusWidget"]::before {{
125
174
  content:"";
126
175
  position:absolute;
127
- left: var(--st-spin-toolbar-left);
176
+ left: var(--st-toolbar-left);
128
177
  top:50%;
129
- transform:translateY(-50%) translateY(var(--st-spin-toolbar-nudge));
178
+ transform: translateY(calc(-50% + var(--st-toolbar-nudge)));
130
179
  width:var(--st-spin-top);
131
180
  height:var(--st-spin-top);
132
- background:no-repeat center/contain;
133
- animation:st-fourframe var(--st-spin-dur) linear infinite;
181
+ background-image:url("{i1}");
182
+ background-repeat:no-repeat;
183
+ background-position:center center;
184
+ background-size:contain;
185
+ animation: st-fiveframe var(--st-spin-dur) steps(1, end) infinite;
134
186
  }}
135
187
 
136
- /* Hide the entire toolbar if requested */
188
+ /* Optionally hide Deploy/Stop toolbar entirely */
137
189
  {"div[data-testid='stToolbar']{display:none !important;}" if hide_deploy_button else ""}
138
190
 
139
- /* =======================================================================
140
- 2) Inline spinner (st.spinner) — centered overlay
141
- ======================================================================= */
142
- [data-testid="stSpinner"] svg {{ display:none !important; }}
191
+ /* ===== inline st.spinner ===== */
143
192
  [data-testid="stSpinner"] {{
144
- min-height: 0 !important; /* avoid layout jump, since we center globally */
193
+ min-height: 0 !important;
145
194
  }}
146
195
  { "[data-testid='stSpinner']::after { content:''; position:fixed; inset:0; background:var(--st-overlay-bg); backdrop-filter: blur(var(--st-overlay-blur)); z-index: var(--st-overlay-z); pointer-events: none; }" if dim_backdrop else "" }
147
196
  [data-testid="stSpinner"]::before {{
@@ -152,61 +201,31 @@ div[data-testid="stToolbar"],
152
201
  transform: translate(-50%,-50%);
153
202
  width: var(--st-spin-inline);
154
203
  height: var(--st-spin-inline);
155
- background:no-repeat center/contain;
156
- animation:st-fourframe var(--st-spin-dur) linear infinite;
204
+ background-image:url("{i1}");
205
+ background-repeat:no-repeat;
206
+ background-position:center center;
207
+ background-size:contain;
208
+ animation: st-fiveframe var(--st-spin-dur) steps(1, end) infinite;
157
209
  z-index: calc(var(--st-overlay-z) + 1);
158
210
  }}
159
211
 
160
- /* Center the spinner message below the animation (works in sidebar or main) */
161
- [data-testid="stSpinner"] [data-testid="stSpinnerMessage"],
162
- [data-testid="stSpinner"] > div > div:last-child,
163
- [data-testid="stSpinner"] > div > div:only-child {{
164
- position: fixed !important;
165
- left: 50% !important;
166
- top: calc(50% + var(--st-spin-inline) / 2 + 12px) !important;
167
- transform: translateX(-50%) !important;
168
- z-index: calc(var(--st-overlay-z) + 2) !important;
169
- text-align: center !important;
170
- margin: 0 !important;
171
- padding: .25rem .75rem !important;
172
- max-width: min(80vw, 900px) !important; /* keeps long text from stretching off-screen */
173
- white-space: normal !important; /* use `nowrap` if you prefer single-line */
174
- font-weight: 500 !important;
175
- }}
176
-
177
- /* Kill the tiny default glyph wrapper so you don't get a stray dot in the sidebar */
178
- [data-testid="stSpinner"] > div > div:first-child {{
179
- display: none !important;
180
- }}
181
-
182
- /* We still hide the default SVG everywhere */
183
- [data-testid="stSpinner"] svg {{
184
- display: none !important;
185
- }}
186
-
187
- /* =======================================================================
188
- 3) st.status(...) icon — centered overlay
189
- ======================================================================= */
190
- [data-testid="stStatus"] [data-testid="stStatusIcon"] svg,
191
- [data-testid="stStatus"] [data-testid="stStatusIcon"] img {{ display:none !important; }}
192
- {"[data-testid='stStatus']::after { content:''; position:fixed; inset:0; background:var(--st-overlay-bg); backdrop-filter: blur(var(--st-overlay-blur)); z-index: var(--st-overlay-z); pointer-events: none; }" if dim_backdrop else ""}
212
+ /* ===== st.status(...) icon ===== */
213
+ [data-testid="stStatus"] [data-testid="stStatusIcon"] svg {{ display:none !important; }}
214
+ { "[data-testid='stStatus']::after { content:''; position:fixed; inset:0; background:var(--st-overlay-bg); backdrop-filter: blur(var(--st-overlay-blur)); z-index: var(--st-overlay-z); pointer-events: none; }" if dim_backdrop else "" }
193
215
  [data-testid="stStatus"] [data-testid="stStatusIcon"]::before {{
194
216
  content:"";
195
217
  position: fixed;
196
218
  left: 50%;
197
219
  top: 50%;
198
220
  transform: translate(-50%,-50%);
199
- width: var(--st-spin-inline); /* use same size as inline */
221
+ width: var(--st-spin-inline);
200
222
  height: var(--st-spin-inline);
201
- background:no-repeat center/contain;
202
- animation:st-fourframe var(--st-spin-dur) linear infinite;
223
+ background-image:url("{i1}");
224
+ background-repeat:no-repeat;
225
+ background-position:center center;
226
+ background-size:contain;
227
+ animation: st-fiveframe var(--st-spin-dur) steps(1, end) infinite;
203
228
  z-index: calc(var(--st-overlay-z) + 1);
204
229
  }}
205
-
206
- /* Optional: allow 'esc' feel without blocking clicks — achieved via pointer-events:none above. */
207
230
  </style>
208
231
  """, unsafe_allow_html=True)
209
-
210
-
211
-
212
-
@@ -10,6 +10,9 @@ from importlib.resources import files as _pkg_files
10
10
  import sys
11
11
  import os
12
12
 
13
+
14
+
15
+
13
16
  def _detect_app_dir() -> Path:
14
17
  """
15
18
  Best-effort detection of the directory that contains the running Streamlit app.
@@ -1,2 +1,2 @@
1
- from .constants import *
1
+
2
2
  from .instruments import *
@@ -4,7 +4,7 @@ from mainsequence.client import Constant as _C
4
4
  import os
5
5
 
6
6
  def _make_backend():
7
- backend = os.getenv("MSI_DATA_BACKEND", "mock").lower()
7
+ backend = os.getenv("MSI_DATA_BACKEND", "mainsequence").lower()
8
8
  return MSInterface() if backend == "mainsequence" else MockDataInterface()
9
9
 
10
10
  # export a single, uniform instance
@@ -9,8 +9,9 @@ import pandas as pd
9
9
  from pathlib import Path
10
10
 
11
11
 
12
- DISCOUNT_CURVES_TABLE=msc.Constant.get_or_none(name="DISCOUNT_CURVES_TABLE")
13
- REFERENCE_RATES_FIXING_TABLE = msc.Constant.get_or_none(name="REFERENCE_RATES_FIXING_TABLE")
12
+
13
+ DISCOUNT_CURVES_TABLE=msc.Constant.get_value(name="DISCOUNT_CURVES_TABLE")
14
+ REFERENCE_RATES_FIXING_TABLE = msc.Constant.get_value(name="REFERENCE_RATES_FIXING_TABLE")
14
15
 
15
16
  assert DISCOUNT_CURVES_TABLE is not None, "DISCOUNT_CURVES_TABLE not found in constants"
16
17
  assert REFERENCE_RATES_FIXING_TABLE is not None, "REFERENCE_RATES_FIXING_TABLE not found in constants"
@@ -287,20 +288,22 @@ class MSInterface():
287
288
  @cachedmethod(cache=attrgetter("_curve_cache"), lock=attrgetter("_curve_cache_lock"))
288
289
  def get_historical_discount_curve(self, curve_name, target_date):
289
290
  from mainsequence.tdag import APIDataNode
291
+ from mainsequence.logconf import logger
290
292
  data_node = APIDataNode.build_from_identifier(identifier=DISCOUNT_CURVES_TABLE)
291
293
 
292
294
 
293
295
 
294
296
  # for test purposes only get lats observations
295
- update_statistics = data_node.get_update_statistics()
296
- target_date = update_statistics.asset_time_statistics[curve_name]
297
- print("REMOVE ABOCVE ONLU FOR TESTING")
297
+ use_last_observation=os.environ.get("USE_LAST_OBSERVATION_MS_INSTRUMENT","true").lower()=="true"
298
+ if use_last_observation:
299
+ update_statistics = data_node.get_update_statistics()
300
+ target_date = update_statistics.asset_time_statistics[curve_name]
301
+ logger.warning("Curve is using last observation")
298
302
 
299
303
 
300
- try:
301
- limit = target_date + datetime.timedelta(days=1)
302
- except Exception as e:
303
- raise e
304
+
305
+ limit = target_date + datetime.timedelta(days=1)
306
+
304
307
 
305
308
 
306
309
 
@@ -336,19 +339,36 @@ class MSInterface():
336
339
  :return:
337
340
  """
338
341
  from mainsequence.tdag import APIDataNode
339
- from mainsequence.instruments.settings import REFERENCE_RATES_FIXING_TABLE
342
+ from mainsequence.logconf import logger
343
+ import pytz # patch
340
344
 
341
345
  data_node = APIDataNode.build_from_identifier(identifier=REFERENCE_RATES_FIXING_TABLE)
342
346
 
343
- import pytz # patch
344
347
  start_date = datetime.datetime(2024, 9, 10, tzinfo=pytz.utc)
345
348
  end_date=datetime.datetime(2025, 9, 17, tzinfo=pytz.utc)
349
+
350
+
351
+
346
352
 
347
353
  fixings_df = data_node.get_ranged_data_per_asset(
348
354
  range_descriptor={reference_rate_uid: {"start_date": start_date, "start_date_operand": ">=",
349
355
  "end_date": end_date, "end_date_operand": "<=", }}
350
356
  )
351
357
  if fixings_df.empty:
358
+
359
+ use_last_observation = os.environ.get("USE_LAST_OBSERVATION_MS_INSTRUMENT", "true").lower() == "true"
360
+ if use_last_observation:
361
+ logger.warning("Fixings are using last observation and filled forward")
362
+ fixings_df = data_node.get_ranged_data_per_asset(
363
+ range_descriptor={reference_rate_uid: {"start_date": datetime.datetime(1900,1,1,tzinfo=pytz.utc),
364
+ "start_date_operand": ">=",
365
+ }}
366
+
367
+
368
+ )
369
+
370
+ a=5
371
+
352
372
  raise Exception(f"{reference_rate_uid} has not data between {start_date} and {end_date}.")
353
373
  fixings_df = fixings_df.reset_index().rename(columns={"time_index": "date"})
354
374
  fixings_df["date"] = fixings_df["date"].dt.date
@@ -40,6 +40,10 @@ class Bond(InstrumentModel):
40
40
  settlement_days: int = Field(default=2)
41
41
  schedule: Optional[QSchedule] = Field(None)
42
42
 
43
+ benchmark_rate_index_name: Optional[str] = Field(...,description="A default index benchmark rate, helpful when doing"
44
+ "analysis and we want to map the bond to a bencharmk for example to"
45
+ "the SOFR Curve or to de US Treasury curve etc")
46
+
43
47
  model_config = {"arbitrary_types_allowed": True}
44
48
 
45
49
  _bond: Optional[ql.Bond] = PrivateAttr(default=None)
@@ -279,11 +283,15 @@ class FixedRateBond(Bond):
279
283
  """Plain-vanilla fixed-rate bond following the shared Bond lifecycle."""
280
284
 
281
285
  coupon_rate: float = Field(...)
286
+
282
287
  # Optional market curve if you want to discount off a curve instead of a flat yield
283
288
  discount_curve: Optional[ql.YieldTermStructureHandle] = Field(default=None)
284
289
 
285
290
  model_config = {"arbitrary_types_allowed": True}
286
291
 
292
+ def reset_curve(self, curve: ql.YieldTermStructureHandle) -> None:
293
+ self.discount_curve = curve
294
+
287
295
  def _get_default_discount_curve(self) -> Optional[ql.YieldTermStructureHandle]:
288
296
  return self.discount_curve
289
297
 
@@ -48,8 +48,8 @@ def clear_index_cache() -> None:
48
48
  # No tenor tokens; we store the QuantLib Period directly.
49
49
 
50
50
  INDEX_CONFIGS: Dict[str, Dict] = {
51
- _C.get_value(name="TIIE_28_UID"): dict(
52
- curve_uid=_C.get_value(name="TIIE_28_ZERO_CURVE"),
51
+ _C.get_value(name="REFERENCE_RATE__TIIE_28"): dict(
52
+ curve_uid=_C.get_value(name="ZERO_CURVE__VALMER_TIIE_28"),
53
53
  calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
54
54
  day_counter=ql.Actual360(),
55
55
  currency=(ql.MXNCurrency() if hasattr(ql, "MXNCurrency") else ql.USDCurrency()),
@@ -58,8 +58,8 @@ INDEX_CONFIGS: Dict[str, Dict] = {
58
58
  bdc=ql.ModifiedFollowing,
59
59
  end_of_month=False,
60
60
  ),
61
- _C.get_value(name="TIIE_91_UID"): dict(
62
- curve_uid=_C.get_value(name="TIIE_28_ZERO_CURVE"),
61
+ _C.get_value(name="REFERENCE_RATE__TIIE_91"): dict(
62
+ curve_uid=_C.get_value(name="ZERO_CURVE__VALMER_TIIE_28"),
63
63
  calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
64
64
  day_counter=ql.Actual360(),
65
65
  currency=ql.MXNCurrency(),
@@ -68,8 +68,8 @@ INDEX_CONFIGS: Dict[str, Dict] = {
68
68
  bdc=ql.ModifiedFollowing,
69
69
  end_of_month=False,
70
70
  ),
71
- _C.get_value(name="TIIE_182_UID"): dict(
72
- curve_uid=_C.get_value(name="TIIE_28_ZERO_CURVE"),
71
+ _C.get_value(name="REFERENCE_RATE__TIIE_182"): dict(
72
+ curve_uid=_C.get_value(name="ZERO_CURVE__VALMER_TIIE_28"),
73
73
  calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
74
74
  day_counter=ql.Actual360(),
75
75
  currency=ql.MXNCurrency(),
@@ -79,8 +79,8 @@ INDEX_CONFIGS: Dict[str, Dict] = {
79
79
  end_of_month=False,
80
80
  ),
81
81
  # Add more identifiers here as needed.
82
- _C.get_value(name="TIIE_OVERNIGHT_UID"): dict(
83
- curve_uid=_C.get_value(name="TIIE_28_ZERO_CURVE"),
82
+ _C.get_value(name="REFERENCE_RATE__TIIE_OVERNIGHT"): dict(
83
+ curve_uid=_C.get_value(name="ZERO_CURVE__VALMER_TIIE_28"),
84
84
  calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
85
85
  day_counter=ql.Actual360(),
86
86
  currency=ql.MXNCurrency(),
@@ -89,8 +89,8 @@ INDEX_CONFIGS: Dict[str, Dict] = {
89
89
  bdc=ql.ModifiedFollowing,
90
90
  end_of_month=False,
91
91
  ),
92
- _C.get_value(name="CETE_28_UID"): dict(
93
- curve_uid=_C.get_value(name="M_BONOS_ZERO_CURVE"),
92
+ _C.get_value(name="REFERENCE_RATE__CETE_28"): dict(
93
+ curve_uid=_C.get_value(name="ZERO_CURVE__BANXICO_M_BONOS_OTR"),
94
94
  calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
95
95
  day_counter=ql.Actual360(), # BONOS accrue on Act/360
96
96
  currency=ql.MXNCurrency(),
@@ -99,8 +99,8 @@ INDEX_CONFIGS: Dict[str, Dict] = {
99
99
  bdc=ql.Following, # “next banking business day” => Following
100
100
  end_of_month=False, # Irrelevant when scheduling by days
101
101
  ),
102
- _C.get_value(name="CETE_91_UID"): dict(
103
- curve_uid=_C.get_value(name="M_BONOS_ZERO_CURVE"),
102
+ _C.get_value(name="REFERENCE_RATE__CETE_91"): dict(
103
+ curve_uid=_C.get_value(name="ZERO_CURVE__BANXICO_M_BONOS_OTR"),
104
104
  calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
105
105
  day_counter=ql.Actual360(), # BONOS accrue on Act/360
106
106
  currency=ql.MXNCurrency(),
@@ -110,8 +110,8 @@ INDEX_CONFIGS: Dict[str, Dict] = {
110
110
  end_of_month=False, # Irrelevant when scheduling by days
111
111
  ),
112
112
 
113
- _C.get_value(name="CETE_182_UID"): dict(
114
- curve_uid=_C.get_value(name="M_BONOS_ZERO_CURVE"),
113
+ _C.get_value(name="REFERENCE_RATE__CETE_182"): dict(
114
+ curve_uid=_C.get_value(name="ZERO_CURVE__BANXICO_M_BONOS_OTR"),
115
115
  calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
116
116
  day_counter=ql.Actual360(), # BONOS accrue on Act/360
117
117
  currency=ql.MXNCurrency(),
@@ -120,6 +120,18 @@ INDEX_CONFIGS: Dict[str, Dict] = {
120
120
  bdc=ql.Following, # “next banking business day” => Following
121
121
  end_of_month=False, # Irrelevant when scheduling by days
122
122
  ),
123
+
124
+
125
+ _C.get_value(name="REFERENCE_RATE__USD_SOFR"): dict(
126
+ curve_uid=_C.get_value(name="ZERO_CURVE__UST_CMT_ZERO_CURVE_UID"),
127
+ calendar=ql.UnitedStates(ql.UnitedStates.FederalReserve),
128
+ day_counter=ql.Actual360(),
129
+ currency=ql.USDCurrency(),
130
+ period=ql.Period(6, ql.Months), # Semiannual coupons
131
+ settlement_days=1, # T+1
132
+ bdc=ql.ModifiedFollowing,
133
+ end_of_month=False, # Irrelevant when scheduling by days
134
+ ),
123
135
  }
124
136
 
125
137