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,239 @@
1
+ # src/instruments/ql_fields.py
2
+ from __future__ import annotations
3
+
4
+ import inspect
5
+ from typing import Optional, Dict, Any
6
+ from typing_extensions import Annotated
7
+ from pydantic import BeforeValidator, PlainSerializer, WithJsonSchema
8
+ import QuantLib as ql
9
+
10
+ # Reuse your existing codec helpers
11
+ from mainsequence.instruments.instruments.json_codec import (
12
+ # Period
13
+ period_to_json, period_from_json,
14
+ # DayCounter
15
+ daycount_to_json, daycount_from_json,
16
+ # Calendar
17
+ calendar_to_json, calendar_from_json,
18
+ # BDC
19
+ bdc_to_json, bdc_from_json,
20
+ # IborIndex (used only for legacy conversion)
21
+ ibor_to_json, ibor_from_json,
22
+ # Schedule
23
+ schedule_to_json, schedule_from_json,
24
+ )
25
+
26
+ # ============================================================================
27
+ # Automatic Calendar Factory
28
+ # ============================================================================
29
+
30
+ def _build_fully_automatic_calendar_factory() -> Dict[str, callable]:
31
+ """
32
+ Build a mapping: <calendar display name> -> zero-arg callable that returns ql.Calendar
33
+ We try both no-arg constructors and Market-enum constructors.
34
+
35
+ Examples of keys (display names):
36
+ - "TARGET"
37
+ - "Mexican stock exchange" (ql.Mexico())
38
+ - "New York stock exchange" (ql.UnitedStates(ql.UnitedStates.NYSE))
39
+ - "United States settlement" (ql.UnitedStates(ql.UnitedStates.Settlement))
40
+ - "London stock exchange" (ql.UnitedKingdom(ql.UnitedKingdom.LSE))
41
+ """
42
+ factory: Dict[str, callable] = {}
43
+
44
+ def _try_register(ctor):
45
+ try:
46
+ inst = ctor()
47
+ name = inst.name()
48
+ # Keep first seen mapping; if duplicates exist, first one wins.
49
+ factory.setdefault(name, ctor)
50
+ return True
51
+ except Exception:
52
+ return False
53
+
54
+ # Iterate over all QuantLib classes; find Calendar subclasses (excluding base Calendar)
55
+ for _, cls in inspect.getmembers(ql, predicate=inspect.isclass):
56
+ try:
57
+ if not issubclass(cls, ql.Calendar) or cls is ql.Calendar:
58
+ continue
59
+ except TypeError:
60
+ # Some SWIG artifacts aren't proper classes for issubclass; skip them
61
+ continue
62
+
63
+ # Case A: try no-arg constructor (e.g., TARGET, Mexico, Turkey, etc.)
64
+ _try_register(lambda c=cls: c())
65
+
66
+ # Case B: try int-valued attributes on the class (likely Market enums)
67
+ for attr_name, attr_val in inspect.getmembers(cls):
68
+ if attr_name.startswith("_"):
69
+ continue
70
+ if isinstance(attr_val, int):
71
+ _try_register(lambda c=cls, e=attr_val: c(e))
72
+
73
+ # Case C: nested 'Market' enum classes (common in recent QuantLib builds)
74
+ for attr_name, attr_val in inspect.getmembers(cls):
75
+ if not inspect.isclass(attr_val):
76
+ continue
77
+ if attr_name.lower().startswith("market"):
78
+ for mname, mval in inspect.getmembers(attr_val):
79
+ if mname.startswith("_"):
80
+ continue
81
+ if isinstance(mval, int):
82
+ _try_register(lambda c=cls, e=mval: c(e))
83
+
84
+ return factory
85
+
86
+
87
+ # Build once; also a case-insensitive mirror for defensive lookups.
88
+ _CAL_FACTORY: Dict[str, callable] = _build_fully_automatic_calendar_factory()
89
+ _CAL_FACTORY_CI: Dict[str, callable] = {k.casefold(): v for k, v in _CAL_FACTORY.items()}
90
+
91
+
92
+ def calendar_from_display_name(name: str) -> ql.Calendar:
93
+ """
94
+ Rebuild a QuantLib calendar from its display name (Calendar::name()).
95
+ Example: "Mexican stock exchange" -> ql.Mexico()
96
+ """
97
+ ctor = _CAL_FACTORY.get(name) or _CAL_FACTORY_CI.get(name.casefold())
98
+ if ctor is None:
99
+ raise ValueError(f"Unsupported calendar display name {name!r}. "
100
+ "Available: " + ", ".join(sorted(_CAL_FACTORY.keys())[:20]) + ("..." if len(_CAL_FACTORY) > 20 else ""))
101
+ return ctor()
102
+
103
+
104
+ # ============================================================================
105
+ # Strict serializers that force the real calendar name via virtual .name()
106
+ # ============================================================================
107
+
108
+ def _calendar_to_json_actual(cal: ql.Calendar) -> Dict[str, Any]:
109
+ """
110
+ Serialize as {'name': cal.name()} using the virtual method.
111
+ Works even if 'cal' is a base Calendar proxy returned by SWIG.
112
+ """
113
+ return {"name": cal.name()}
114
+
115
+ def _schedule_to_json_actual(s: Optional[ql.Schedule]) -> Optional[Dict[str, Any]]:
116
+ """
117
+ Serialize a schedule; ensure its calendar is emitted with the true display name.
118
+ """
119
+ if s is None:
120
+ return None
121
+ data = schedule_to_json(s) # keep your canonical fields (dates, BDCs, EOM, rule, etc.)
122
+ try:
123
+ data["calendar"] = {"name": s.calendar().name()}
124
+ except Exception:
125
+ pass
126
+ return data
127
+
128
+
129
+ # ============================================================================
130
+ # Lenient deserializers that accept {'name': '<display name>'}
131
+ # ============================================================================
132
+
133
+ def _calendar_from_json_auto(v):
134
+ """
135
+ Accept:
136
+ - ql.Calendar (pass-through)
137
+ - {'name': '<display name>'} -> rebuilt via factory
138
+ - str '<display name>' -> rebuilt via factory
139
+ - else -> delegate to existing calendar_from_json
140
+ """
141
+ if isinstance(v, ql.Calendar):
142
+ return v
143
+ if isinstance(v, dict):
144
+ nm = v.get("name")
145
+ if isinstance(nm, str) and nm and nm != "Calendar":
146
+ return calendar_from_display_name(nm)
147
+ if isinstance(v, str) and v and v != "Calendar":
148
+ return calendar_from_display_name(v)
149
+ # Fallback to your existing helper (may accept other legacy formats)
150
+ return calendar_from_json(v)
151
+
152
+ def _schedule_from_json_auto(v):
153
+ """
154
+ If schedule JSON contains {'calendar': {'name': '<display name>'}},
155
+ rebuild a concrete ql.Calendar first, then delegate to schedule_from_json.
156
+ """
157
+ if v is None or isinstance(v, ql.Schedule):
158
+ return v
159
+ if isinstance(v, dict) and "calendar" in v:
160
+ cal_spec = v["calendar"]
161
+ # Rebuild calendar if we have a display name
162
+ try:
163
+ v = dict(v)
164
+ v["calendar"] = _calendar_from_json_auto(cal_spec)
165
+ except Exception:
166
+ # Leave as-is; schedule_from_json may still handle it
167
+ pass
168
+ return schedule_from_json(v)
169
+
170
+
171
+ # ============================================================================
172
+ # Pydantic Annotated field types
173
+ # ============================================================================
174
+
175
+ # ---------- Period -----------------------------------------------------------
176
+ QuantLibPeriod = Annotated[
177
+ ql.Period,
178
+ BeforeValidator(period_from_json),
179
+ PlainSerializer(period_to_json, return_type=str),
180
+ ]
181
+
182
+ # ---------- DayCounter -------------------------------------------------------
183
+ QuantLibDayCounter = Annotated[
184
+ ql.DayCounter,
185
+ BeforeValidator(daycount_from_json),
186
+ PlainSerializer(daycount_to_json, return_type=str),
187
+ ]
188
+
189
+ # ---------- Calendar ---------------------------------------------------------
190
+ QuantLibCalendar = Annotated[
191
+ ql.Calendar,
192
+ BeforeValidator(_calendar_from_json_auto), # <— use factory-based loader
193
+ PlainSerializer(_calendar_to_json_actual, return_type=Dict[str, Any]), # <— always emit true name
194
+ ]
195
+
196
+ # ---------- Business Day Convention (BDC) -----------------------------------
197
+ def _bdc_from_any(v):
198
+ return bdc_from_json(v)
199
+
200
+ def _bdc_to_str(v: int) -> str:
201
+ return str(bdc_to_json(int(v)))
202
+
203
+ QuantLibBDC = Annotated[
204
+ int,
205
+ BeforeValidator(_bdc_from_any),
206
+ PlainSerializer(_bdc_to_str, return_type=str),
207
+ ]
208
+
209
+ # ---------- Schedule ---------------------------------------------------------
210
+ QuantLibSchedule = Annotated[
211
+ Optional[ql.Schedule],
212
+ BeforeValidator(_schedule_from_json_auto), # <— rebuild calendar from display name first
213
+ PlainSerializer(_schedule_to_json_actual, return_type=Optional[Dict[str, Any]]), # <— emit true name
214
+ WithJsonSchema(
215
+ {
216
+ "type": ["object", "null"],
217
+ "properties": {
218
+ "dates": {"type": "array", "items": {"type": "string", "pattern": r"^\d{4}-\d{2}-\d{2}$"}},
219
+ "calendar": {"type": "object"}, # {"name": "<display name from cal.name()>"}
220
+ "business_day_convention": {"type": ["string", "integer"]},
221
+ "termination_business_day_convention": {"type": ["string", "integer"]},
222
+ "end_of_month": {"type": "boolean"},
223
+ "tenor": {"type": "string"},
224
+ "rule": {"type": ["string", "integer"]},
225
+ },
226
+ "required": ["dates"],
227
+ "additionalProperties": True,
228
+ },
229
+ mode="serialization",
230
+ ),
231
+ ]
232
+
233
+ __all__ = [
234
+ "QuantLibPeriod",
235
+ "QuantLibDayCounter",
236
+ "QuantLibCalendar",
237
+ "QuantLibBDC",
238
+ "QuantLibSchedule",
239
+ ]
@@ -0,0 +1,107 @@
1
+ import datetime
2
+ from typing import Optional, Literal
3
+
4
+ import QuantLib as ql
5
+ from pydantic import BaseModel, Field, PrivateAttr
6
+
7
+ from mainsequence.instruments.pricing_models.fx_option_pricer import create_fx_garman_kohlhagen_model, get_fx_market_data
8
+ from mainsequence.instruments.utils import to_ql_date
9
+ from .base_instrument import InstrumentModel
10
+
11
+
12
+ class VanillaFXOption(InstrumentModel):
13
+ """Vanilla FX option priced with Garman-Kohlhagen model."""
14
+
15
+ currency_pair: str = Field(
16
+ ..., description="Currency pair in format 'EURUSD', 'GBPUSD', etc. (6 characters)."
17
+ )
18
+ strike: float = Field(
19
+ ..., description="Option strike price (domestic currency per unit of foreign currency)."
20
+ )
21
+ maturity: datetime.date = Field(
22
+ ..., description="Option expiration date."
23
+ )
24
+ option_type: Literal["call", "put"] = Field(
25
+ ..., description="Option type: 'call' or 'put'."
26
+ )
27
+ notional: float = Field(
28
+ ..., description="Notional amount in foreign currency units."
29
+ )
30
+
31
+
32
+ # Allow QuantLib types & keep runtime attrs out of the schema
33
+ model_config = {"arbitrary_types_allowed": True}
34
+
35
+ # Runtime-only QuantLib objects
36
+ _option: Optional[ql.VanillaOption] = PrivateAttr(default=None)
37
+ _engine: Optional[ql.PricingEngine] = PrivateAttr(default=None)
38
+
39
+ def _setup_pricing_components(self) -> None:
40
+ """Set up the QuantLib pricing components for the FX option."""
41
+ # 1) Validate currency pair format
42
+ if len(self.currency_pair) != 6:
43
+ raise ValueError("Currency pair must be 6 characters (e.g., 'EURUSD')")
44
+
45
+ # 2) Get FX market data
46
+ market_data = get_fx_market_data(self.currency_pair, self.valuation_date)
47
+ spot_fx = market_data["spot_fx_rate"]
48
+ vol = market_data["volatility"]
49
+ domestic_rate = market_data["domestic_rate"]
50
+ foreign_rate = market_data["foreign_rate"]
51
+
52
+ # 3) Convert dates to QuantLib format
53
+ ql_calc = to_ql_date(self.valuation_date)
54
+ ql_mty = to_ql_date(self.maturity)
55
+ ql.Settings.instance().evaluationDate = ql_calc
56
+
57
+ # 4) Create Garman-Kohlhagen process
58
+ process = create_fx_garman_kohlhagen_model(
59
+ ql_calc, spot_fx, vol, domestic_rate, foreign_rate
60
+ )
61
+
62
+ # 5) Create instrument and engine
63
+ payoff = ql.PlainVanillaPayoff(
64
+ ql.Option.Call if self.option_type == "call" else ql.Option.Put,
65
+ self.strike
66
+ )
67
+ exercise = ql.EuropeanExercise(ql_mty)
68
+ self._option = ql.VanillaOption(payoff, exercise)
69
+ self._engine = ql.AnalyticEuropeanEngine(process)
70
+ self._option.setPricingEngine(self._engine)
71
+
72
+ def price(self) -> float:
73
+ """Calculate the option price (NPV)."""
74
+ if not self._option:
75
+ self._setup_pricing_components()
76
+ # Return price multiplied by notional
77
+ return float(self._option.NPV() * self.notional)
78
+
79
+ def get_greeks(self) -> dict:
80
+ """Calculate the option Greeks."""
81
+ if not self._option:
82
+ self._setup_pricing_components()
83
+ self._option.NPV() # Ensure calculations are performed
84
+
85
+ return {
86
+ "delta": self._option.delta() * self.notional,
87
+ "gamma": self._option.gamma() * self.notional,
88
+ "vega": self._option.vega() * self.notional / 100.0, # Convert to 1% vol change
89
+ "theta": self._option.theta() * self.notional / 365.0, # Convert to per day
90
+ "rho_domestic": self._option.rho() * self.notional / 100.0, # Convert to 1% rate change
91
+ }
92
+
93
+ def get_market_info(self) -> dict:
94
+ """Get the market data used for pricing."""
95
+ market_data = get_fx_market_data(self.currency_pair, self.valuation_date)
96
+ foreign_ccy = self.currency_pair[:3]
97
+ domestic_ccy = self.currency_pair[3:]
98
+
99
+ return {
100
+ "currency_pair": self.currency_pair,
101
+ "foreign_currency": foreign_ccy,
102
+ "domestic_currency": domestic_ccy,
103
+ "spot_fx_rate": market_data["spot_fx_rate"],
104
+ "volatility": market_data["volatility"],
105
+ "domestic_rate": market_data["domestic_rate"],
106
+ "foreign_rate": market_data["foreign_rate"]
107
+ }
File without changes
@@ -0,0 +1,49 @@
1
+ import QuantLib as ql
2
+
3
+
4
+ def create_bsm_model(
5
+ calculation_date: ql.Date,
6
+ spot_price: float,
7
+ volatility: float,
8
+ risk_free_rate: float,
9
+ dividend_yield: float
10
+ ) -> ql.BlackScholesMertonProcess:
11
+ """
12
+ Sets up the Black-Scholes-Merton process in QuantLib.
13
+
14
+ Args:
15
+ calculation_date: The date for which the pricing is being performed.
16
+ spot_price: The current price of the underlying asset.
17
+ volatility: The annualized volatility of the underlying asset.
18
+ risk_free_rate: The annualized risk-free interest rate.
19
+ dividend_yield: The annualized dividend yield of the underlying asset.
20
+
21
+ Returns:
22
+ A QuantLib BlackScholesMertonProcess object configured with the market data.
23
+ """
24
+ # Set the evaluation date in QuantLib
25
+ ql.Settings.instance().evaluationDate = calculation_date
26
+
27
+ # Define underlying asset price and curves
28
+ underlying_handle = ql.QuoteHandle(ql.SimpleQuote(spot_price))
29
+
30
+ day_count = ql.Actual365Fixed()
31
+
32
+ flat_ts = ql.YieldTermStructureHandle(
33
+ ql.FlatForward(calculation_date, risk_free_rate, day_count)
34
+ )
35
+
36
+ dividend_ts = ql.YieldTermStructureHandle(
37
+ ql.FlatForward(calculation_date, dividend_yield, day_count)
38
+ )
39
+
40
+ flat_vol_ts = ql.BlackVolTermStructureHandle(
41
+ ql.BlackConstantVol(calculation_date, ql.TARGET(), volatility, day_count)
42
+ )
43
+
44
+ # Create the Black-Scholes-Merton process
45
+ bsm_process = ql.BlackScholesMertonProcess(
46
+ underlying_handle, dividend_ts, flat_ts, flat_vol_ts
47
+ )
48
+
49
+ return bsm_process
@@ -0,0 +1,182 @@
1
+ # mainsequence/instruments/pricing_models/bond_pricer.py
2
+ import QuantLib as ql
3
+ from typing import List, Dict, Any, Optional
4
+ from mainsequence.instruments.data_interface import data_interface
5
+ from mainsequence.instruments.utils import to_ql_date
6
+ import datetime
7
+ import matplotlib.pyplot as plt
8
+
9
+
10
+ def _map_daycount(dc: str) -> ql.DayCounter:
11
+ s = (dc or '').upper()
12
+ if s.startswith('30/360'):
13
+ return ql.Thirty360(ql.Thirty360.USA)
14
+ if s in ('ACT/365', 'ACT/365F', 'ACTUAL/365', 'ACTUAL/365F'):
15
+ return ql.Actual365Fixed()
16
+ if s in ('ACT/ACT', 'ACTUAL/ACTUAL'):
17
+ return ql.ActualActual()
18
+ return ql.Thirty360(ql.Thirty360.USA)
19
+
20
+
21
+
22
+
23
+
24
+
25
+ def create_fixed_rate_bond(
26
+ calculation_date: ql.Date,
27
+ face: float,
28
+ issue_date: ql.Date,
29
+ maturity_date: ql.Date,
30
+ coupon_rate: float,
31
+ coupon_frequency: ql.Period,
32
+ day_count: ql.DayCounter,
33
+ calendar: ql.Calendar = ql.TARGET(),
34
+ business_day_convention: int = ql.Following, # enums are ints in the Python wrapper
35
+ settlement_days: int = 2,
36
+ discount_curve: Optional[ql.YieldTermStructureHandle] = None,
37
+ schedule: Optional[ql.Schedule] = None,
38
+
39
+ ) -> ql.FixedRateBond:
40
+ """Construct and engine-attach."""
41
+ ql.Settings.instance().evaluationDate = calculation_date
42
+
43
+
44
+
45
+
46
+ # --------- Schedule ----------
47
+ if schedule is None:
48
+ schedule = ql.Schedule(
49
+ issue_date, maturity_date, coupon_frequency, calendar,
50
+ business_day_convention, business_day_convention,
51
+ ql.DateGeneration.Forward, False
52
+ )
53
+ else:
54
+ asof = ql.Settings.instance().evaluationDate
55
+ n = len(schedule.dates())
56
+ # True floater periods exist only if schedule has >=2 dates AND at least one period end > as-of date.
57
+ has_periods_left = (n >= 2) and any(schedule.dates()[i + 1] > asof for i in range(n - 1))
58
+ if not has_periods_left:
59
+ # Redemption-only: price as a zero-coupon bond (par redemption by default).
60
+ maturity = schedule.dates()[n-1] if n > 0 else maturity_date
61
+ zcb = ql.ZeroCouponBond(
62
+ settlement_days,
63
+ calendar, # use the same calendar as above
64
+ face, # notional
65
+ maturity, # maturity date
66
+ business_day_convention, # payment convention (for settlement)
67
+ 100.0, # redemption (% of face)
68
+ issue_date # issue date
69
+ )
70
+ zcb.setPricingEngine(ql.DiscountingBondEngine(discount_curve))
71
+ return zcb
72
+
73
+
74
+ bond = ql.FixedRateBond(settlement_days, face, schedule, [coupon_rate], day_count)
75
+ bond.setPricingEngine(ql.DiscountingBondEngine(discount_curve))
76
+ return bond
77
+
78
+
79
+ def create_floating_rate_bond_with_curve(
80
+ *,
81
+ calculation_date: ql.Date,
82
+ face: float,
83
+ issue_date: ql.Date,
84
+ maturity_date: ql.Date,
85
+ floating_rate_index: ql.IborIndex,
86
+ spread: float = 0.0,
87
+ coupon_frequency: ql.Period | None = None,
88
+ day_count: ql.DayCounter | None = None,
89
+ calendar: ql.Calendar | None = None,
90
+ business_day_convention: int = ql.Following,
91
+ settlement_days: int = 2,
92
+ curve: ql.YieldTermStructureHandle,
93
+ seed_past_fixings_from_curve: bool = True,
94
+ discount_curve: Optional[ql.YieldTermStructureHandle] = None,
95
+ schedule: Optional[ql.Schedule] = None,
96
+ ) -> ql.FloatingRateBond:
97
+ """
98
+ Build/prices a floating-rate bond like your swap-with-curve:
99
+ - clone index to 'curve'
100
+ - spot-start safeguard
101
+ - seed past/today fixings from the same curve
102
+ - discount with the same curve
103
+ """
104
+
105
+ # --- evaluation settings (match swap) ---
106
+ ql.Settings.instance().evaluationDate = calculation_date
107
+ ql.Settings.instance().includeReferenceDateEvents = False
108
+ ql.Settings.instance().enforceTodaysHistoricFixings = False
109
+
110
+ if curve is None:
111
+ raise ValueError("create_floating_rate_bond_with_curve: 'curve' is None")
112
+ # Probe the handle by attempting a discount on calculation_date.
113
+ # If the handle is unlinked/invalid this will raise; we convert it to a clear message.
114
+ try:
115
+ _ = curve.discount(calculation_date)
116
+ except Exception as e:
117
+ raise ValueError(
118
+ "create_floating_rate_bond_with_curve: provided curve handle "
119
+ "is not linked or cannot discount on calculation_date"
120
+ ) from e
121
+
122
+ # --- index & calendars ---
123
+ pricing_index = floating_rate_index.clone(curve) # forecast on the provided curve
124
+ cal = calendar or pricing_index.fixingCalendar()
125
+ freq = coupon_frequency or pricing_index.tenor()
126
+ dc = day_count or pricing_index.dayCounter()
127
+
128
+ eff_start = issue_date
129
+ eff_end = maturity_date
130
+
131
+ # --------- Schedule ----------
132
+ if schedule is None:
133
+ schedule = ql.Schedule(
134
+ eff_start, eff_end, freq, cal,
135
+ business_day_convention, business_day_convention,
136
+ ql.DateGeneration.Forward, False
137
+ )
138
+ else:
139
+ asof = ql.Settings.instance().evaluationDate
140
+ n = len(schedule.dates())
141
+ # True floater periods exist only if schedule has >=2 dates AND at least one period end > as-of date.
142
+ has_periods_left = (n >= 2) and any(schedule.dates()[i + 1] > asof for i in range(n - 1))
143
+ if not has_periods_left:
144
+ # Redemption-only: price as a zero-coupon bond (par redemption by default).
145
+ maturity = schedule.dates()[n-1] if n > 0 else eff_end
146
+ zcb = ql.ZeroCouponBond(
147
+ settlement_days,
148
+ cal, # use the same calendar as above
149
+ face, # notional
150
+ maturity, # maturity date
151
+ business_day_convention, # payment convention (for settlement)
152
+ 100.0, # redemption (% of face)
153
+ issue_date # issue date
154
+ )
155
+ zcb.setPricingEngine(ql.DiscountingBondEngine(curve))
156
+ return zcb
157
+
158
+
159
+ # --------- Instrument ----------
160
+ try:
161
+ bond = ql.FloatingRateBond(
162
+ settlement_days,
163
+ face,
164
+ schedule,
165
+ pricing_index,
166
+ dc,
167
+ business_day_convention,
168
+ pricing_index.fixingDays(),
169
+ [1.0], # gearings
170
+ [spread], # spreads
171
+ [], [], # caps, floors
172
+ False, # inArrears
173
+ 100.0, # redemption
174
+ issue_date
175
+ )
176
+ except Exception as e:
177
+ raise e
178
+
179
+
180
+
181
+ return bond
182
+
@@ -0,0 +1,90 @@
1
+ import QuantLib as ql
2
+ from mainsequence.instruments.data_interface import data_interface, DateInfo
3
+
4
+
5
+ def create_fx_garman_kohlhagen_model(
6
+ calculation_date: ql.Date,
7
+ spot_fx_rate: float,
8
+ volatility: float,
9
+ domestic_rate: float,
10
+ foreign_rate: float
11
+ ) -> ql.BlackScholesMertonProcess:
12
+ """
13
+ Sets up the Garman-Kohlhagen process for FX options in QuantLib.
14
+
15
+ The Garman-Kohlhagen model is essentially Black-Scholes where:
16
+ - The underlying is the FX spot rate
17
+ - The "dividend yield" is replaced by the foreign risk-free rate
18
+ - The risk-free rate is the domestic risk-free rate
19
+
20
+ Args:
21
+ calculation_date: The date for which the pricing is being performed.
22
+ spot_fx_rate: The current FX spot rate (domestic per foreign currency).
23
+ volatility: The annualized volatility of the FX rate.
24
+ domestic_rate: The annualized domestic risk-free interest rate.
25
+ foreign_rate: The annualized foreign risk-free interest rate.
26
+
27
+ Returns:
28
+ A QuantLib BlackScholesMertonProcess object configured for FX options.
29
+ """
30
+ # Set the evaluation date in QuantLib
31
+ ql.Settings.instance().evaluationDate = calculation_date
32
+
33
+ # Define FX spot rate handle
34
+ spot_handle = ql.QuoteHandle(ql.SimpleQuote(spot_fx_rate))
35
+
36
+ day_count = ql.Actual365Fixed()
37
+
38
+ # Domestic risk-free rate curve
39
+ domestic_ts = ql.YieldTermStructureHandle(
40
+ ql.FlatForward(calculation_date, domestic_rate, day_count)
41
+ )
42
+
43
+ # Foreign risk-free rate curve (treated as "dividend yield" in BS framework)
44
+ foreign_ts = ql.YieldTermStructureHandle(
45
+ ql.FlatForward(calculation_date, foreign_rate, day_count)
46
+ )
47
+
48
+ # FX volatility surface
49
+ vol_ts = ql.BlackVolTermStructureHandle(
50
+ ql.BlackConstantVol(calculation_date, ql.TARGET(), volatility, day_count)
51
+ )
52
+
53
+ # Create the Garman-Kohlhagen process (using BlackScholesMertonProcess)
54
+ gk_process = ql.BlackScholesMertonProcess(
55
+ spot_handle, foreign_ts, domestic_ts, vol_ts
56
+ )
57
+
58
+ return gk_process
59
+
60
+
61
+ def get_fx_market_data(currency_pair: str, calculation_date) -> dict:
62
+ """
63
+ Fetches FX market data for a given currency pair.
64
+
65
+ Args:
66
+ currency_pair: Currency pair in format "EURUSD", "GBPUSD", etc.
67
+ calculation_date: The calculation date for market data
68
+
69
+ Returns:
70
+ Dictionary containing spot rate, volatility, domestic rate, and foreign rate
71
+ """
72
+ # Extract domestic and foreign currencies from pair
73
+ if len(currency_pair) != 6:
74
+ raise ValueError("Currency pair must be 6 characters (e.g., 'EURUSD')")
75
+
76
+ foreign_ccy = currency_pair[:3] # First 3 characters
77
+ domestic_ccy = currency_pair[3:] # Last 3 characters
78
+
79
+ # Fetch market data using the data interface
80
+ asset_range_map = {currency_pair: DateInfo(start_date=calculation_date)}
81
+ market_data = data_interface.get_historical_data("fx_options", asset_range_map)
82
+
83
+ return {
84
+ "spot_fx_rate": market_data["spot_fx_rate"],
85
+ "volatility": market_data["volatility"],
86
+ "domestic_rate": market_data["domestic_rate"],
87
+ "foreign_rate": market_data["foreign_rate"],
88
+ "foreign_currency": foreign_ccy,
89
+ "domestic_currency": domestic_ccy
90
+ }