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,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")