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.
- mainsequence/cli/cli.py +4 -7
- mainsequence/cli/ssh_utils.py +17 -2
- mainsequence/client/__init__.py +3 -3
- mainsequence/client/base.py +3 -3
- mainsequence/client/data_sources_interfaces/timescale.py +20 -19
- mainsequence/client/exceptions.py +11 -0
- mainsequence/client/models_helpers.py +2 -2
- mainsequence/client/models_tdag.py +104 -87
- mainsequence/client/models_vam.py +9 -9
- mainsequence/dashboards/streamlit/core/theme.py +128 -109
- mainsequence/dashboards/streamlit/scaffold.py +3 -0
- mainsequence/instruments/__init__.py +1 -1
- mainsequence/instruments/data_interface/__init__.py +1 -1
- mainsequence/instruments/data_interface/data_interface.py +31 -11
- mainsequence/instruments/instruments/bond.py +8 -0
- mainsequence/instruments/pricing_models/indices.py +26 -14
- mainsequence/instruments/settings.py +2 -162
- mainsequence/tdag/config.py +2 -2
- mainsequence/tdag/data_nodes/build_operations.py +3 -3
- mainsequence/tdag/data_nodes/data_nodes.py +23 -23
- mainsequence/tdag/data_nodes/persist_managers.py +121 -121
- mainsequence/tdag/data_nodes/run_operations.py +25 -25
- mainsequence/virtualfundbuilder/contrib/apps/portfolio_report_app.py +1 -1
- mainsequence/virtualfundbuilder/contrib/prices/data_nodes.py +2 -2
- mainsequence/virtualfundbuilder/data_nodes.py +1 -1
- mainsequence/virtualfundbuilder/portfolio_interface.py +7 -7
- mainsequence/virtualfundbuilder/utils.py +2 -2
- {mainsequence-2.0.4rc0.dist-info → mainsequence-3.0.2.dist-info}/METADATA +1 -1
- {mainsequence-2.0.4rc0.dist-info → mainsequence-3.0.2.dist-info}/RECORD +33 -32
- {mainsequence-2.0.4rc0.dist-info → mainsequence-3.0.2.dist-info}/WHEEL +0 -0
- {mainsequence-2.0.4rc0.dist-info → mainsequence-3.0.2.dist-info}/entry_points.txt +0 -0
- {mainsequence-2.0.4rc0.dist-info → mainsequence-3.0.2.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
#
|
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 =
|
50
|
-
inline_px: int =
|
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
|
54
|
-
toolbar_nudge_px: int = -
|
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
|
-
#
|
58
|
-
center_non_toolbar: bool = True,
|
59
|
-
dim_backdrop: bool =
|
60
|
-
overlay_blur_px: float =
|
61
|
-
overlay_opacity: float = 0.
|
62
|
-
overlay_z_index: int = 9990,
|
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
|
-
"""
|
65
|
-
|
66
|
-
|
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
|
-
/*
|
84
|
-
@keyframes st-
|
85
|
-
0
|
86
|
-
20%
|
87
|
-
40%
|
88
|
-
60%
|
89
|
-
|
90
|
-
|
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;
|
96
|
-
--st-spin-inline:{inline_px}px;
|
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-
|
100
|
-
--st-
|
101
|
-
--st-
|
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:{
|
149
|
+
--st-overlay-bg: rgba(0,0,0,{overlay_opacity});
|
105
150
|
--st-overlay-blur:{overlay_blur_px}px;
|
106
151
|
}}
|
107
152
|
|
108
|
-
/*
|
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
|
-
|
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-
|
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-
|
176
|
+
left: var(--st-toolbar-left);
|
128
177
|
top:50%;
|
129
|
-
transform:translateY(-50%
|
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:
|
133
|
-
|
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
|
-
/*
|
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;
|
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:
|
156
|
-
|
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
|
-
/*
|
161
|
-
[data-testid="
|
162
|
-
[data-testid=
|
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);
|
221
|
+
width: var(--st-spin-inline);
|
200
222
|
height: var(--st-spin-inline);
|
201
|
-
background:
|
202
|
-
|
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
|
-
|
@@ -1,2 +1,2 @@
|
|
1
|
-
|
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", "
|
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
|
-
|
13
|
-
|
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
|
-
|
296
|
-
|
297
|
-
|
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
|
-
|
301
|
-
|
302
|
-
|
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.
|
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="
|
52
|
-
curve_uid=_C.get_value(name="
|
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="
|
62
|
-
curve_uid=_C.get_value(name="
|
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="
|
72
|
-
curve_uid=_C.get_value(name="
|
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="
|
83
|
-
curve_uid=_C.get_value(name="
|
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="
|
93
|
-
curve_uid=_C.get_value(name="
|
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="
|
103
|
-
curve_uid=_C.get_value(name="
|
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="
|
114
|
-
curve_uid=_C.get_value(name="
|
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
|
|