mainsequence 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mainsequence/__init__.py +0 -0
- mainsequence/__main__.py +9 -0
- mainsequence/cli/__init__.py +1 -0
- mainsequence/cli/api.py +157 -0
- mainsequence/cli/cli.py +442 -0
- mainsequence/cli/config.py +78 -0
- mainsequence/cli/ssh_utils.py +126 -0
- mainsequence/client/__init__.py +17 -0
- mainsequence/client/base.py +431 -0
- mainsequence/client/data_sources_interfaces/__init__.py +0 -0
- mainsequence/client/data_sources_interfaces/duckdb.py +1468 -0
- mainsequence/client/data_sources_interfaces/timescale.py +479 -0
- mainsequence/client/models_helpers.py +113 -0
- mainsequence/client/models_report_studio.py +412 -0
- mainsequence/client/models_tdag.py +2276 -0
- mainsequence/client/models_vam.py +1983 -0
- mainsequence/client/utils.py +387 -0
- mainsequence/dashboards/__init__.py +0 -0
- mainsequence/dashboards/streamlit/__init__.py +0 -0
- mainsequence/dashboards/streamlit/assets/config.toml +12 -0
- mainsequence/dashboards/streamlit/assets/favicon.png +0 -0
- mainsequence/dashboards/streamlit/assets/logo.png +0 -0
- mainsequence/dashboards/streamlit/core/__init__.py +0 -0
- mainsequence/dashboards/streamlit/core/theme.py +212 -0
- mainsequence/dashboards/streamlit/pages/__init__.py +0 -0
- mainsequence/dashboards/streamlit/scaffold.py +220 -0
- mainsequence/instrumentation/__init__.py +7 -0
- mainsequence/instrumentation/utils.py +101 -0
- mainsequence/instruments/__init__.py +1 -0
- mainsequence/instruments/data_interface/__init__.py +10 -0
- mainsequence/instruments/data_interface/data_interface.py +361 -0
- mainsequence/instruments/instruments/__init__.py +3 -0
- mainsequence/instruments/instruments/base_instrument.py +85 -0
- mainsequence/instruments/instruments/bond.py +447 -0
- mainsequence/instruments/instruments/european_option.py +74 -0
- mainsequence/instruments/instruments/interest_rate_swap.py +217 -0
- mainsequence/instruments/instruments/json_codec.py +585 -0
- mainsequence/instruments/instruments/knockout_fx_option.py +146 -0
- mainsequence/instruments/instruments/position.py +475 -0
- mainsequence/instruments/instruments/ql_fields.py +239 -0
- mainsequence/instruments/instruments/vanilla_fx_option.py +107 -0
- mainsequence/instruments/pricing_models/__init__.py +0 -0
- mainsequence/instruments/pricing_models/black_scholes.py +49 -0
- mainsequence/instruments/pricing_models/bond_pricer.py +182 -0
- mainsequence/instruments/pricing_models/fx_option_pricer.py +90 -0
- mainsequence/instruments/pricing_models/indices.py +350 -0
- mainsequence/instruments/pricing_models/knockout_fx_pricer.py +209 -0
- mainsequence/instruments/pricing_models/swap_pricer.py +502 -0
- mainsequence/instruments/settings.py +175 -0
- mainsequence/instruments/utils.py +29 -0
- mainsequence/logconf.py +284 -0
- mainsequence/reportbuilder/__init__.py +0 -0
- mainsequence/reportbuilder/__main__.py +0 -0
- mainsequence/reportbuilder/examples/ms_template_report.py +706 -0
- mainsequence/reportbuilder/model.py +713 -0
- mainsequence/reportbuilder/slide_templates.py +532 -0
- mainsequence/tdag/__init__.py +8 -0
- mainsequence/tdag/__main__.py +0 -0
- mainsequence/tdag/config.py +129 -0
- mainsequence/tdag/data_nodes/__init__.py +12 -0
- mainsequence/tdag/data_nodes/build_operations.py +751 -0
- mainsequence/tdag/data_nodes/data_nodes.py +1292 -0
- mainsequence/tdag/data_nodes/persist_managers.py +812 -0
- mainsequence/tdag/data_nodes/run_operations.py +543 -0
- mainsequence/tdag/data_nodes/utils.py +24 -0
- mainsequence/tdag/future_registry.py +25 -0
- mainsequence/tdag/utils.py +40 -0
- mainsequence/virtualfundbuilder/__init__.py +45 -0
- mainsequence/virtualfundbuilder/__main__.py +235 -0
- mainsequence/virtualfundbuilder/agent_interface.py +77 -0
- mainsequence/virtualfundbuilder/config_handling.py +86 -0
- mainsequence/virtualfundbuilder/contrib/__init__.py +0 -0
- mainsequence/virtualfundbuilder/contrib/apps/__init__.py +8 -0
- mainsequence/virtualfundbuilder/contrib/apps/etf_replicator_app.py +164 -0
- mainsequence/virtualfundbuilder/contrib/apps/generate_report.py +292 -0
- mainsequence/virtualfundbuilder/contrib/apps/load_external_portfolio.py +107 -0
- mainsequence/virtualfundbuilder/contrib/apps/news_app.py +437 -0
- mainsequence/virtualfundbuilder/contrib/apps/portfolio_report_app.py +91 -0
- mainsequence/virtualfundbuilder/contrib/apps/portfolio_table.py +95 -0
- mainsequence/virtualfundbuilder/contrib/apps/run_named_portfolio.py +45 -0
- mainsequence/virtualfundbuilder/contrib/apps/run_portfolio.py +40 -0
- mainsequence/virtualfundbuilder/contrib/apps/templates/base.html +147 -0
- mainsequence/virtualfundbuilder/contrib/apps/templates/report.html +77 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/__init__.py +5 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/external_weights.py +61 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/intraday_trend.py +149 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/market_cap.py +310 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/mock_signal.py +78 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/portfolio_replicator.py +269 -0
- mainsequence/virtualfundbuilder/contrib/prices/__init__.py +1 -0
- mainsequence/virtualfundbuilder/contrib/prices/data_nodes.py +810 -0
- mainsequence/virtualfundbuilder/contrib/prices/utils.py +11 -0
- mainsequence/virtualfundbuilder/contrib/rebalance_strategies/__init__.py +1 -0
- mainsequence/virtualfundbuilder/contrib/rebalance_strategies/rebalance_strategies.py +313 -0
- mainsequence/virtualfundbuilder/data_nodes.py +637 -0
- mainsequence/virtualfundbuilder/enums.py +23 -0
- mainsequence/virtualfundbuilder/models.py +282 -0
- mainsequence/virtualfundbuilder/notebook_handling.py +42 -0
- mainsequence/virtualfundbuilder/portfolio_interface.py +272 -0
- mainsequence/virtualfundbuilder/resource_factory/__init__.py +0 -0
- mainsequence/virtualfundbuilder/resource_factory/app_factory.py +170 -0
- mainsequence/virtualfundbuilder/resource_factory/base_factory.py +238 -0
- mainsequence/virtualfundbuilder/resource_factory/rebalance_factory.py +101 -0
- mainsequence/virtualfundbuilder/resource_factory/signal_factory.py +183 -0
- mainsequence/virtualfundbuilder/utils.py +381 -0
- mainsequence-2.0.0.dist-info/METADATA +105 -0
- mainsequence-2.0.0.dist-info/RECORD +110 -0
- mainsequence-2.0.0.dist-info/WHEEL +5 -0
- mainsequence-2.0.0.dist-info/licenses/LICENSE +40 -0
- mainsequence-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,350 @@
|
|
1
|
+
# pricing_models/indices.py
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
"""
|
4
|
+
Index factory for QuantLib (identifier-driven, no aliases/registry/tenor parsing).
|
5
|
+
|
6
|
+
Usage
|
7
|
+
-----
|
8
|
+
>>> from datetime import date
|
9
|
+
>>> from mainsequence.instruments.settings import TIIE_28_UID
|
10
|
+
>>> from mainsequence.instruments.pricing_models.indices import get_index
|
11
|
+
>>> idx = get_index(TIIE_28_UID, target_date=date(2024, 6, 14)) # As of target_date
|
12
|
+
|
13
|
+
You can also supply a forwarding curve handle:
|
14
|
+
>>> h = ql.RelinkableYieldTermStructureHandle()
|
15
|
+
>>> idx = get_index(TIIE_28_UID, target_date=date(2025, 9, 16), forwarding_curve=h)
|
16
|
+
|
17
|
+
Notes
|
18
|
+
-----
|
19
|
+
- `get_index` is driven ONLY by `index_identifier` and `target_date`.
|
20
|
+
- We set the QuantLib index **name** to `index_identifier`, so `index.name()` **is** your UID.
|
21
|
+
- Curve construction comes from `build_zero_curve(target_date, index_identifier)`.
|
22
|
+
"""
|
23
|
+
|
24
|
+
from __future__ import annotations
|
25
|
+
|
26
|
+
import datetime
|
27
|
+
from typing import Dict, Tuple, Union, Optional
|
28
|
+
|
29
|
+
import QuantLib as ql
|
30
|
+
from functools import lru_cache
|
31
|
+
|
32
|
+
from mainsequence.instruments.data_interface import data_interface
|
33
|
+
from mainsequence.instruments import settings
|
34
|
+
from mainsequence.instruments.utils import to_py_date, to_ql_date
|
35
|
+
|
36
|
+
|
37
|
+
# ----------------------------- Cache (ONLY by identifier + date) ----------------------------- #
|
38
|
+
|
39
|
+
# key: (index_identifier, target_date_py)
|
40
|
+
_IndexCacheKey = Tuple[str, datetime.date]
|
41
|
+
_INDEX_CACHE: Dict[_IndexCacheKey, ql.Index] = {}
|
42
|
+
|
43
|
+
|
44
|
+
def clear_index_cache() -> None:
|
45
|
+
_INDEX_CACHE.clear()
|
46
|
+
|
47
|
+
|
48
|
+
# ----------------------------- Config ----------------------------- #
|
49
|
+
# Put every supported identifier here with its curve + index construction config.
|
50
|
+
# No tenor tokens; we store the QuantLib Period directly.
|
51
|
+
|
52
|
+
INDEX_CONFIGS: Dict[str, Dict] = {
|
53
|
+
settings.indices.TIIE_28_UID: dict(
|
54
|
+
curve_uid=settings.curves.TIIE_28_ZERO_CURVE,
|
55
|
+
calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
|
56
|
+
day_counter=ql.Actual360(),
|
57
|
+
currency=(ql.MXNCurrency() if hasattr(ql, "MXNCurrency") else ql.USDCurrency()),
|
58
|
+
period=ql.Period(28, ql.Days),
|
59
|
+
settlement_days=1,
|
60
|
+
bdc=ql.ModifiedFollowing,
|
61
|
+
end_of_month=False,
|
62
|
+
),
|
63
|
+
settings.indices.TIIE_91_UID: dict(
|
64
|
+
curve_uid=settings.curves.TIIE_28_ZERO_CURVE,
|
65
|
+
calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
|
66
|
+
day_counter=ql.Actual360(),
|
67
|
+
currency=ql.MXNCurrency(),
|
68
|
+
period=ql.Period(91, ql.Days),
|
69
|
+
settlement_days=1,
|
70
|
+
bdc=ql.ModifiedFollowing,
|
71
|
+
end_of_month=False,
|
72
|
+
),
|
73
|
+
settings.indices.TIIE_182_UID: dict(
|
74
|
+
curve_uid=settings.curves.TIIE_28_ZERO_CURVE,
|
75
|
+
calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
|
76
|
+
day_counter=ql.Actual360(),
|
77
|
+
currency=ql.MXNCurrency(),
|
78
|
+
period=ql.Period(182, ql.Days),
|
79
|
+
settlement_days=1,
|
80
|
+
bdc=ql.ModifiedFollowing,
|
81
|
+
end_of_month=False,
|
82
|
+
),
|
83
|
+
# Add more identifiers here as needed.
|
84
|
+
settings.indices.TIIE_OVERNIGHT_UID: dict(
|
85
|
+
curve_uid=settings.curves.TIIE_28_ZERO_CURVE,
|
86
|
+
calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
|
87
|
+
day_counter=ql.Actual360(),
|
88
|
+
currency=ql.MXNCurrency(),
|
89
|
+
period=ql.Period(1, ql.Days),
|
90
|
+
settlement_days=1,
|
91
|
+
bdc=ql.ModifiedFollowing,
|
92
|
+
end_of_month=False,
|
93
|
+
),
|
94
|
+
settings.indices.CETE_28_UID: dict(
|
95
|
+
curve_uid=settings.curves.M_BONOS_ZERO_CURVE,
|
96
|
+
calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
|
97
|
+
day_counter=ql.Actual360(), # BONOS accrue on Act/360
|
98
|
+
currency=ql.MXNCurrency(),
|
99
|
+
period=ql.Period(28, ql.Days), # Coupons every 28 days
|
100
|
+
settlement_days=1, # T+1 in Mexico since May 27–28, 2024
|
101
|
+
bdc=ql.Following, # “next banking business day” => Following
|
102
|
+
end_of_month=False, # Irrelevant when scheduling by days
|
103
|
+
),
|
104
|
+
|
105
|
+
settings.indices.CETE_182_UID: dict(
|
106
|
+
curve_uid=settings.curves.M_BONOS_ZERO_CURVE,
|
107
|
+
calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
|
108
|
+
day_counter=ql.Actual360(), # BONOS accrue on Act/360
|
109
|
+
currency=ql.MXNCurrency(),
|
110
|
+
period=ql.Period(182, ql.Days), # Coupons every 182 days
|
111
|
+
settlement_days=1, # T+1 in Mexico since May 27–28, 2024
|
112
|
+
bdc=ql.Following, # “next banking business day” => Following
|
113
|
+
end_of_month=False, # Irrelevant when scheduling by days
|
114
|
+
),
|
115
|
+
}
|
116
|
+
|
117
|
+
|
118
|
+
# ----------------------------- Utilities ----------------------------- #
|
119
|
+
|
120
|
+
def _ensure_py_date(
|
121
|
+
d: Union[datetime.date, datetime.datetime, ql.Date]
|
122
|
+
) -> datetime.date:
|
123
|
+
"""Return a Python date; target_date is REQUIRED and must not be None."""
|
124
|
+
if d is None:
|
125
|
+
raise ValueError("target_date is required and cannot be None.")
|
126
|
+
if isinstance(d, datetime.datetime):
|
127
|
+
return d.date()
|
128
|
+
if isinstance(d, datetime.date):
|
129
|
+
return d
|
130
|
+
# ql.Date
|
131
|
+
return to_py_date(d)
|
132
|
+
|
133
|
+
|
134
|
+
# ----------------------------- Zero-curve builder (kept) ----------------------------- #
|
135
|
+
|
136
|
+
def build_zero_curve(
|
137
|
+
target_date: Union[datetime.date, datetime.datetime],
|
138
|
+
index_identifier: str,
|
139
|
+
) -> ql.YieldTermStructureHandle:
|
140
|
+
"""
|
141
|
+
Build a discount curve for the given index_identifier as of target_date.
|
142
|
+
Configuration comes from INDEX_CONFIGS[index_identifier].
|
143
|
+
"""
|
144
|
+
cfg = INDEX_CONFIGS.get(index_identifier)
|
145
|
+
if cfg is None:
|
146
|
+
raise KeyError(
|
147
|
+
f"No curve/index config for index_identifier {index_identifier!r}. "
|
148
|
+
f"Add it to INDEX_CONFIGS."
|
149
|
+
)
|
150
|
+
|
151
|
+
dc: ql.DayCounter = cfg["day_counter"]
|
152
|
+
calendar: ql.Calendar = cfg["calendar"]
|
153
|
+
curve_uid: str = cfg["curve_uid"]
|
154
|
+
|
155
|
+
nodes = data_interface.get_historical_discount_curve(curve_uid, target_date)
|
156
|
+
|
157
|
+
base = to_ql_date(target_date)
|
158
|
+
base_py = target_date if isinstance(target_date, datetime.datetime) else datetime.datetime.combine(
|
159
|
+
target_date, datetime.time()
|
160
|
+
)
|
161
|
+
|
162
|
+
dates = [base]
|
163
|
+
discounts = [1.0]
|
164
|
+
seen = {base.serialNumber()}
|
165
|
+
|
166
|
+
for n in sorted(nodes, key=lambda n: int(n["days_to_maturity"])):
|
167
|
+
days = int(n["days_to_maturity"])
|
168
|
+
if days < 0:
|
169
|
+
continue
|
170
|
+
|
171
|
+
d = to_ql_date(base_py + datetime.timedelta(days=days))
|
172
|
+
|
173
|
+
sn = d.serialNumber()
|
174
|
+
if sn in seen:
|
175
|
+
continue
|
176
|
+
seen.add(sn)
|
177
|
+
|
178
|
+
z = n.get("zero", n.get("zero_rate", n.get("rate")))
|
179
|
+
z = float(z)
|
180
|
+
if z > 1.0:
|
181
|
+
z *= 0.01 # percent -> decimal
|
182
|
+
|
183
|
+
T = dc.yearFraction(base, d)
|
184
|
+
df = 1.0 / (1.0 + z * T) # Valmer zero is simple ACT/360
|
185
|
+
|
186
|
+
dates.append(d)
|
187
|
+
discounts.append(df)
|
188
|
+
|
189
|
+
ts = ql.DiscountCurve(dates, discounts, dc, calendar)
|
190
|
+
ts.enableExtrapolation()
|
191
|
+
return ql.YieldTermStructureHandle(ts)
|
192
|
+
|
193
|
+
|
194
|
+
@lru_cache(maxsize=256)
|
195
|
+
def _default_curve_cached(index_identifier: str, date_key: datetime.date) -> ql.YieldTermStructureHandle:
|
196
|
+
"""Small cache for default curves, keyed only by (identifier, date)."""
|
197
|
+
target_dt = datetime.datetime.combine(date_key, datetime.time())
|
198
|
+
return build_zero_curve(target_dt, index_identifier)
|
199
|
+
|
200
|
+
|
201
|
+
def _default_curve(index_identifier: str, target_date: Union[datetime.date, datetime.datetime, ql.Date]) -> ql.YieldTermStructureHandle:
|
202
|
+
dk = _ensure_py_date(target_date)
|
203
|
+
return _default_curve_cached(index_identifier, dk)
|
204
|
+
|
205
|
+
|
206
|
+
# ----------------------------- Historical fixings hydration ----------------------------- #
|
207
|
+
|
208
|
+
def add_historical_fixings(target_date: ql.Date, ibor_index: ql.IborIndex):
|
209
|
+
"""
|
210
|
+
Backfill historical fixings for an index up to (but not including) target_date,
|
211
|
+
restricted to valid fixing dates for that index's calendar.
|
212
|
+
|
213
|
+
We use the index's **name()** as the UID (because we set it to the identifier).
|
214
|
+
"""
|
215
|
+
print("Fetching and adding historical fixings...")
|
216
|
+
|
217
|
+
end_date = to_py_date(target_date) # inclusive endpoint; filter qld < target_date below
|
218
|
+
start_date = end_date - datetime.timedelta(days=365)
|
219
|
+
|
220
|
+
uid = ibor_index.familyName()
|
221
|
+
|
222
|
+
historical_fixings = data_interface.get_historical_fixings(
|
223
|
+
reference_rate_uid=uid,
|
224
|
+
start_date=start_date,
|
225
|
+
end_date=end_date
|
226
|
+
)
|
227
|
+
|
228
|
+
if not historical_fixings:
|
229
|
+
print("No historical fixings found in the given date range.")
|
230
|
+
return
|
231
|
+
|
232
|
+
valid_qld: list[ql.Date] = []
|
233
|
+
valid_rates: list[float] = []
|
234
|
+
|
235
|
+
for dt_py, rate in sorted(historical_fixings.items()):
|
236
|
+
qld = to_ql_date(dt_py)
|
237
|
+
if qld < target_date and ibor_index.isValidFixingDate(qld):
|
238
|
+
valid_qld.append(qld)
|
239
|
+
valid_rates.append(float(rate))
|
240
|
+
|
241
|
+
if not valid_qld:
|
242
|
+
print("No valid fixing dates for the index calendar; skipping addFixings.")
|
243
|
+
return
|
244
|
+
|
245
|
+
ibor_index.addFixings(valid_qld, valid_rates, True)
|
246
|
+
print(f"Successfully added {len(valid_qld)} fixings for {uid}.")
|
247
|
+
|
248
|
+
|
249
|
+
# ----------------------------- Index construction ----------------------------- #
|
250
|
+
|
251
|
+
def _make_index_from_config(index_identifier: str,
|
252
|
+
curve: ql.YieldTermStructureHandle,
|
253
|
+
*,
|
254
|
+
override_settlement_days: Optional[int] = None) -> ql.IborIndex:
|
255
|
+
"""
|
256
|
+
Build a ql.IborIndex using the exact spec stored in INDEX_CONFIGS[index_identifier].
|
257
|
+
No tenor tokens. Period comes from config.
|
258
|
+
"""
|
259
|
+
cfg = INDEX_CONFIGS.get(index_identifier)
|
260
|
+
if cfg is None:
|
261
|
+
raise KeyError(
|
262
|
+
f"No index config for {index_identifier!r}. Add an entry to INDEX_CONFIGS."
|
263
|
+
)
|
264
|
+
|
265
|
+
cal: ql.Calendar = cfg["calendar"]
|
266
|
+
ccy: ql.Currency = cfg["currency"]
|
267
|
+
dc: ql.DayCounter = cfg["day_counter"]
|
268
|
+
period: ql.Period = cfg["period"]
|
269
|
+
bdc: ql.BusinessDayConvention = cfg["bdc"]
|
270
|
+
eom: bool = cfg["end_of_month"]
|
271
|
+
settle: int = override_settlement_days if override_settlement_days is not None else cfg["settlement_days"]
|
272
|
+
|
273
|
+
# IMPORTANT: we set the QuantLib index **name** to the UID
|
274
|
+
return ql.IborIndex(
|
275
|
+
index_identifier, # name == UID
|
276
|
+
period,
|
277
|
+
settle,
|
278
|
+
ccy,
|
279
|
+
cal,
|
280
|
+
bdc,
|
281
|
+
eom,
|
282
|
+
dc,
|
283
|
+
curve
|
284
|
+
)
|
285
|
+
|
286
|
+
|
287
|
+
# ----------------------------- Public API ----------------------------- #
|
288
|
+
|
289
|
+
def get_index(
|
290
|
+
index_identifier: str,
|
291
|
+
target_date: Union[datetime.date, datetime.datetime, ql.Date],
|
292
|
+
*,
|
293
|
+
forwarding_curve: Optional[ql.YieldTermStructureHandle] = None,
|
294
|
+
hydrate_fixings: bool = True,
|
295
|
+
settlement_days: Optional[int] = None
|
296
|
+
) -> ql.Index:
|
297
|
+
"""
|
298
|
+
Return a QuantLib index instance based ONLY on a stable index_identifier and a target_date.
|
299
|
+
|
300
|
+
Parameters
|
301
|
+
----------
|
302
|
+
index_identifier : str
|
303
|
+
A stable UID from your settings/data model (e.g., 'TIIE_28_UID').
|
304
|
+
This becomes the QuantLib index name (so uid == index.name()).
|
305
|
+
target_date : date|datetime|ql.Date
|
306
|
+
As-of date used to build the default curve when no forwarding_curve is supplied.
|
307
|
+
forwarding_curve : Optional[ql.YieldTermStructureHandle]
|
308
|
+
If provided and non-empty, use it; otherwise a default curve is built by identifier.
|
309
|
+
hydrate_fixings : bool
|
310
|
+
If True and the index is Ibor-like, backfill fixings strictly before `target_date`.
|
311
|
+
settlement_days : Optional[int]
|
312
|
+
Optional override for settlement days.
|
313
|
+
|
314
|
+
Returns
|
315
|
+
-------
|
316
|
+
ql.Index
|
317
|
+
"""
|
318
|
+
target_date_py = _ensure_py_date(target_date)
|
319
|
+
if "D" in index_identifier:
|
320
|
+
raise Exception(f"Index identifier {index_identifier!r} cannot have D.")
|
321
|
+
# Cache ONLY by (identifier, date)
|
322
|
+
cache_key: _IndexCacheKey = (index_identifier, target_date_py)
|
323
|
+
cached = _INDEX_CACHE.get(cache_key)
|
324
|
+
if cached is not None:
|
325
|
+
return cached
|
326
|
+
|
327
|
+
# Resolve forwarding curve or build a default one for the identifier
|
328
|
+
if forwarding_curve is not None:
|
329
|
+
use_curve = forwarding_curve
|
330
|
+
else:
|
331
|
+
use_curve = _default_curve(index_identifier, target_date_py)
|
332
|
+
|
333
|
+
# Build the index exactly as configured
|
334
|
+
idx = _make_index_from_config(
|
335
|
+
index_identifier=index_identifier,
|
336
|
+
curve=use_curve,
|
337
|
+
override_settlement_days=settlement_days
|
338
|
+
)
|
339
|
+
|
340
|
+
# Optional: hydrate fixings up to (but not including) target_date
|
341
|
+
if hydrate_fixings and isinstance(idx, ql.IborIndex):
|
342
|
+
add_historical_fixings(to_ql_date(target_date_py), idx)
|
343
|
+
|
344
|
+
_INDEX_CACHE[cache_key] = idx
|
345
|
+
return idx
|
346
|
+
|
347
|
+
|
348
|
+
# ----------------------------- Convenience alias ----------------------------- #
|
349
|
+
|
350
|
+
index_by_name = get_index
|
@@ -0,0 +1,209 @@
|
|
1
|
+
import QuantLib as ql
|
2
|
+
from typing import Tuple, Literal
|
3
|
+
|
4
|
+
from mainsequence.instruments.pricing_models.fx_option_pricer import create_fx_garman_kohlhagen_model, get_fx_market_data
|
5
|
+
|
6
|
+
|
7
|
+
def create_knockout_fx_option(
|
8
|
+
currency_pair: str,
|
9
|
+
calculation_date: ql.Date,
|
10
|
+
maturity_date: ql.Date,
|
11
|
+
strike: float,
|
12
|
+
barrier: float,
|
13
|
+
option_type: Literal["call", "put"],
|
14
|
+
barrier_type: Literal["up_and_out", "down_and_out"],
|
15
|
+
rebate: float = 0.0
|
16
|
+
) -> Tuple[ql.BarrierOption, ql.PricingEngine]:
|
17
|
+
"""
|
18
|
+
Creates a knock-out FX barrier option using QuantLib.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
currency_pair: Currency pair (e.g., "EURUSD")
|
22
|
+
calculation_date: Valuation date
|
23
|
+
maturity_date: Option expiration date
|
24
|
+
strike: Strike price
|
25
|
+
barrier: Barrier level
|
26
|
+
option_type: "call" or "put"
|
27
|
+
barrier_type: "up_and_out" or "down_and_out"
|
28
|
+
rebate: Rebate paid if knocked out (default: 0.0)
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
Tuple of (BarrierOption, PricingEngine)
|
32
|
+
"""
|
33
|
+
# 1) Get market data
|
34
|
+
market_data = get_fx_market_data(currency_pair, calculation_date)
|
35
|
+
spot_fx = market_data["spot_fx_rate"]
|
36
|
+
vol = market_data["volatility"]
|
37
|
+
domestic_rate = market_data["domestic_rate"]
|
38
|
+
foreign_rate = market_data["foreign_rate"]
|
39
|
+
|
40
|
+
# 2) Create the Garman-Kohlhagen process
|
41
|
+
gk_process = create_fx_garman_kohlhagen_model(
|
42
|
+
calculation_date, spot_fx, vol, domestic_rate, foreign_rate
|
43
|
+
)
|
44
|
+
|
45
|
+
# 3) Define the payoff
|
46
|
+
ql_option_type = ql.Option.Call if option_type == "call" else ql.Option.Put
|
47
|
+
payoff = ql.PlainVanillaPayoff(ql_option_type, strike)
|
48
|
+
|
49
|
+
# 4) Define the exercise (European for barrier options)
|
50
|
+
exercise = ql.EuropeanExercise(maturity_date)
|
51
|
+
|
52
|
+
# 5) Define the barrier type
|
53
|
+
if barrier_type == "up_and_out":
|
54
|
+
ql_barrier_type = ql.Barrier.UpOut
|
55
|
+
elif barrier_type == "down_and_out":
|
56
|
+
ql_barrier_type = ql.Barrier.DownOut
|
57
|
+
else:
|
58
|
+
raise ValueError(f"Unsupported barrier type: {barrier_type}")
|
59
|
+
|
60
|
+
# 6) Create the barrier option
|
61
|
+
barrier_option = ql.BarrierOption(
|
62
|
+
ql_barrier_type,
|
63
|
+
barrier,
|
64
|
+
rebate,
|
65
|
+
payoff,
|
66
|
+
exercise
|
67
|
+
)
|
68
|
+
|
69
|
+
# 7) Create the pricing engine
|
70
|
+
# For barrier options, we can use analytical engines for simple cases
|
71
|
+
# or Monte Carlo for more complex scenarios
|
72
|
+
try:
|
73
|
+
# Try analytical engine first (works for European barrier options)
|
74
|
+
engine = ql.AnalyticBarrierEngine(gk_process)
|
75
|
+
except Exception:
|
76
|
+
# Fall back to Monte Carlo if analytical doesn't work
|
77
|
+
engine = create_monte_carlo_barrier_engine(gk_process)
|
78
|
+
|
79
|
+
# 8) Set the pricing engine
|
80
|
+
barrier_option.setPricingEngine(engine)
|
81
|
+
|
82
|
+
return barrier_option, engine
|
83
|
+
|
84
|
+
|
85
|
+
def create_monte_carlo_barrier_engine(
|
86
|
+
process: ql.BlackScholesMertonProcess,
|
87
|
+
time_steps: int = 252,
|
88
|
+
mc_samples: int = 100000,
|
89
|
+
seed: int = 42
|
90
|
+
) -> ql.PricingEngine:
|
91
|
+
"""
|
92
|
+
Creates a Monte Carlo pricing engine for barrier options.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
process: The underlying stochastic process
|
96
|
+
time_steps: Number of time steps for simulation (default: 252 for daily)
|
97
|
+
mc_samples: Number of Monte Carlo samples (default: 100,000)
|
98
|
+
seed: Random seed for reproducibility
|
99
|
+
|
100
|
+
Returns:
|
101
|
+
Monte Carlo pricing engine
|
102
|
+
"""
|
103
|
+
# Set up the random number generator
|
104
|
+
rng = ql.UniformRandomSequenceGenerator(time_steps, ql.UniformRandomGenerator(seed))
|
105
|
+
seq = ql.GaussianRandomSequenceGenerator(rng)
|
106
|
+
|
107
|
+
# Create the Monte Carlo engine
|
108
|
+
engine = ql.MCBarrierEngine(
|
109
|
+
process,
|
110
|
+
"pseudorandom", # or "lowdiscrepancy"
|
111
|
+
time_steps,
|
112
|
+
requiredSamples=mc_samples,
|
113
|
+
seed=seed
|
114
|
+
)
|
115
|
+
|
116
|
+
return engine
|
117
|
+
|
118
|
+
|
119
|
+
def get_barrier_option_analytics(
|
120
|
+
barrier_option: ql.BarrierOption,
|
121
|
+
spot_fx: float,
|
122
|
+
barrier: float,
|
123
|
+
barrier_type: str
|
124
|
+
) -> dict:
|
125
|
+
"""
|
126
|
+
Calculate additional analytics specific to barrier options.
|
127
|
+
|
128
|
+
Args:
|
129
|
+
barrier_option: The QuantLib barrier option object
|
130
|
+
spot_fx: Current spot FX rate
|
131
|
+
barrier: Barrier level
|
132
|
+
barrier_type: Type of barrier ("up_and_out" or "down_and_out")
|
133
|
+
|
134
|
+
Returns:
|
135
|
+
Dictionary with barrier-specific analytics
|
136
|
+
"""
|
137
|
+
# Calculate probability of knock-out (approximation)
|
138
|
+
if barrier_type == "up_and_out":
|
139
|
+
distance_to_barrier = (barrier - spot_fx) / spot_fx
|
140
|
+
barrier_status = "Active" if spot_fx < barrier else "Knocked Out"
|
141
|
+
else: # down_and_out
|
142
|
+
distance_to_barrier = (spot_fx - barrier) / spot_fx
|
143
|
+
barrier_status = "Active" if spot_fx > barrier else "Knocked Out"
|
144
|
+
|
145
|
+
# Get standard option analytics
|
146
|
+
try:
|
147
|
+
npv = barrier_option.NPV()
|
148
|
+
delta = barrier_option.delta()
|
149
|
+
gamma = barrier_option.gamma()
|
150
|
+
vega = barrier_option.vega()
|
151
|
+
theta = barrier_option.theta()
|
152
|
+
rho = barrier_option.rho()
|
153
|
+
except Exception as e:
|
154
|
+
# If analytics fail, return basic info
|
155
|
+
return {
|
156
|
+
"barrier_status": barrier_status,
|
157
|
+
"distance_to_barrier_pct": distance_to_barrier * 100,
|
158
|
+
"error": f"Analytics calculation failed: {str(e)}"
|
159
|
+
}
|
160
|
+
|
161
|
+
return {
|
162
|
+
"npv": npv,
|
163
|
+
"delta": delta,
|
164
|
+
"gamma": gamma,
|
165
|
+
"vega": vega,
|
166
|
+
"theta": theta,
|
167
|
+
"rho": rho,
|
168
|
+
"barrier_status": barrier_status,
|
169
|
+
"distance_to_barrier_pct": distance_to_barrier * 100
|
170
|
+
}
|
171
|
+
|
172
|
+
|
173
|
+
def validate_barrier_parameters(
|
174
|
+
spot_fx: float,
|
175
|
+
strike: float,
|
176
|
+
barrier: float,
|
177
|
+
barrier_type: str,
|
178
|
+
option_type: str
|
179
|
+
) -> None:
|
180
|
+
"""
|
181
|
+
Validate barrier option parameters for logical consistency.
|
182
|
+
|
183
|
+
Args:
|
184
|
+
spot_fx: Current spot FX rate
|
185
|
+
strike: Strike price
|
186
|
+
barrier: Barrier level
|
187
|
+
barrier_type: "up_and_out" or "down_and_out"
|
188
|
+
option_type: "call" or "put"
|
189
|
+
|
190
|
+
Raises:
|
191
|
+
ValueError: If parameters are inconsistent
|
192
|
+
"""
|
193
|
+
if barrier_type == "up_and_out":
|
194
|
+
if barrier <= spot_fx:
|
195
|
+
raise ValueError("Up-and-out barrier must be above current spot rate")
|
196
|
+
if option_type == "call" and barrier <= strike:
|
197
|
+
raise ValueError("Up-and-out call barrier should typically be above strike")
|
198
|
+
|
199
|
+
elif barrier_type == "down_and_out":
|
200
|
+
if barrier >= spot_fx:
|
201
|
+
raise ValueError("Down-and-out barrier must be below current spot rate")
|
202
|
+
if option_type == "put" and barrier >= strike:
|
203
|
+
raise ValueError("Down-and-out put barrier should typically be below strike")
|
204
|
+
|
205
|
+
else:
|
206
|
+
raise ValueError(f"Unsupported barrier type: {barrier_type}")
|
207
|
+
|
208
|
+
if strike <= 0 or barrier <= 0 or spot_fx <= 0:
|
209
|
+
raise ValueError("Strike, barrier, and spot rates must be positive")
|