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,502 @@
1
+ import QuantLib as ql
2
+ from mainsequence.instruments.data_interface import data_interface
3
+ from mainsequence.instruments.utils import to_py_date,to_ql_date
4
+ import datetime
5
+ from typing import List, Dict, Any, Optional
6
+ import matplotlib.pyplot as plt
7
+
8
+
9
+
10
+
11
+ def _coerce_to_ql_date(x, fallback: ql.Date) -> ql.Date:
12
+ """Coerce various date-like inputs to ql.Date, or return fallback."""
13
+ if x is None:
14
+ return fallback
15
+ if isinstance(x, ql.Date):
16
+ return x
17
+ if isinstance(x, datetime.date):
18
+ return to_ql_date(x)
19
+ if isinstance(x, str):
20
+ try:
21
+ return to_ql_date(datetime.date.fromisoformat(x))
22
+ except Exception:
23
+ return fallback
24
+ # pandas.Timestamp / numpy datetime64
25
+ try:
26
+ if hasattr(x, "to_pydatetime"):
27
+ return to_ql_date(x.to_pydatetime().date())
28
+ except Exception:
29
+ pass
30
+ return fallback
31
+
32
+
33
+
34
+
35
+ def make_ftiie_index(curve: ql.YieldTermStructureHandle,
36
+ settlement_days: int = 1) -> ql.OvernightIndex:
37
+ cal = ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()
38
+ try:
39
+ ccy = ql.MXNCurrency()
40
+ except Exception:
41
+ ccy = ql.USDCurrency() # label only
42
+ return ql.OvernightIndex("F-TIIE", settlement_days, ccy, cal, ql.Actual360(), curve)
43
+
44
+
45
+
46
+ def build_yield_curve(calculation_date: ql.Date) -> ql.YieldTermStructureHandle:
47
+ """
48
+ Builds a piecewise yield curve by bootstrapping over a set of market rates.
49
+ """
50
+ print("Building bootstrapped yield curve from market nodes...")
51
+
52
+ rate_data = data_interface.get_historical_data("interest_rate_swaps", {"USD_rates": {}})
53
+ curve_nodes = rate_data['curve_nodes']
54
+
55
+ calendar = ql.TARGET()
56
+ day_counter = ql.Actual365Fixed()
57
+
58
+ rate_helpers = []
59
+
60
+ swap_fixed_leg_frequency = ql.Annual
61
+ swap_fixed_leg_convention = ql.Unadjusted
62
+ swap_fixed_leg_daycounter = ql.Thirty360(ql.Thirty360.USA)
63
+ yield_curve_handle = ql.YieldTermStructureHandle(ql.FlatForward(calculation_date, 0.05, day_counter))
64
+ ibor_index = ql.USDLibor(ql.Period('3M'), yield_curve_handle)
65
+
66
+ for node in curve_nodes:
67
+ rate = node['rate']
68
+ tenor = ql.Period(node['tenor'])
69
+ quote_handle = ql.QuoteHandle(ql.SimpleQuote(rate))
70
+
71
+ if node['type'] == 'deposit':
72
+ helper = ql.DepositRateHelper(quote_handle, tenor, 2, calendar, ql.ModifiedFollowing, False, day_counter)
73
+ rate_helpers.append(helper)
74
+ elif node['type'] == 'swap':
75
+ helper = ql.SwapRateHelper(quote_handle, tenor, calendar,
76
+ swap_fixed_leg_frequency,
77
+ swap_fixed_leg_convention,
78
+ swap_fixed_leg_daycounter,
79
+ ibor_index)
80
+ rate_helpers.append(helper)
81
+
82
+ yield_curve = ql.PiecewiseLogCubicDiscount(calculation_date, rate_helpers, day_counter)
83
+ yield_curve.enableExtrapolation()
84
+
85
+ print("Yield curve built successfully.")
86
+ return ql.YieldTermStructureHandle(yield_curve)
87
+
88
+ # src/pricing_models/swap_pricer.py
89
+ def price_vanilla_swap_with_curve(
90
+ calculation_date: ql.Date,
91
+ notional: float,
92
+ start_date: ql.Date,
93
+ maturity_date: ql.Date,
94
+ fixed_rate: float,
95
+ fixed_leg_tenor: ql.Period,
96
+ fixed_leg_convention: int,
97
+ fixed_leg_daycount: ql.DayCounter,
98
+ float_leg_tenor: ql.Period,
99
+ float_leg_spread: float,
100
+ ibor_index: ql.IborIndex,
101
+ curve: ql.YieldTermStructureHandle,
102
+ ) -> ql.VanillaSwap:
103
+ # --- evaluation settings ---
104
+ ql.Settings.instance().evaluationDate = calculation_date
105
+ ql.Settings.instance().includeReferenceDateEvents = False
106
+ ql.Settings.instance().enforceTodaysHistoricFixings = False
107
+
108
+ # index linked to the provided curve
109
+ pricing_ibor_index = ibor_index.clone(curve)
110
+ calendar = pricing_ibor_index.fixingCalendar()
111
+
112
+ # --------- EFFECTIVE DATES (spot start safeguard) ----------
113
+ fixingDate = calendar.adjust(calculation_date, ql.Following)
114
+ while not pricing_ibor_index.isValidFixingDate(fixingDate):
115
+ fixingDate = calendar.advance(fixingDate, 1, ql.Days)
116
+ spot_start = pricing_ibor_index.valueDate(fixingDate)
117
+
118
+ eff_start = start_date if start_date > calculation_date else spot_start
119
+ eff_end = maturity_date
120
+ if eff_end <= eff_start:
121
+ eff_end = calendar.advance(eff_start, float_leg_tenor)
122
+
123
+ # --------- Schedules ----------
124
+ fixed_schedule = ql.Schedule(
125
+ eff_start, eff_end, fixed_leg_tenor, calendar,
126
+ fixed_leg_convention, fixed_leg_convention,
127
+ ql.DateGeneration.Forward, False
128
+ )
129
+ float_schedule = ql.Schedule(
130
+ eff_start, eff_end, float_leg_tenor, calendar,
131
+ pricing_ibor_index.businessDayConvention(), pricing_ibor_index.businessDayConvention(),
132
+ ql.DateGeneration.Forward, False
133
+ )
134
+
135
+ # --------- Instrument ----------
136
+ swap = ql.VanillaSwap(
137
+ ql.VanillaSwap.Payer,
138
+ notional,
139
+ fixed_schedule,
140
+ fixed_rate,
141
+ fixed_leg_daycount,
142
+ float_schedule,
143
+ pricing_ibor_index,
144
+ float_leg_spread,
145
+ pricing_ibor_index.dayCounter()
146
+ )
147
+
148
+ # --------- Seed past fixings from the curve (coupon by coupon) ----------
149
+ # For any coupon whose fixing date <= evaluation date, insert the forward
150
+ # implied by *this same curve* for that coupon’s accrual period (ACT/360 simple).
151
+ dc = pricing_ibor_index.dayCounter()
152
+ for cf in swap.leg(1):
153
+ cup = ql.as_floating_rate_coupon(cf)
154
+ fix = cup.fixingDate()
155
+ if fix <= calculation_date:
156
+ # If a real fixing already exists, leave it
157
+ try:
158
+ _ = pricing_ibor_index.fixing(fix)
159
+ except RuntimeError:
160
+ start = cup.accrualStartDate()
161
+ end = cup.accrualEndDate()
162
+ tau = dc.yearFraction(start, end)
163
+ df0 = curve.discount(start)
164
+ df1 = curve.discount(end)
165
+ fwd = (df0 / df1 - 1.0) / tau # simple ACT/360
166
+ pricing_ibor_index.addFixing(fix, fwd)
167
+
168
+ swap.setPricingEngine(ql.DiscountingSwapEngine(curve))
169
+ return swap
170
+
171
+
172
+
173
+
174
+
175
+ def get_swap_cashflows(swap) -> Dict[str, List[Dict[str, Any]]]:
176
+ """
177
+ Analyzes the cashflows of a swap's fixed and floating legs.
178
+ """
179
+ cashflows = {'fixed': [], 'floating': []}
180
+
181
+ for cf in swap.leg(0):
182
+ if not cf.hasOccurred():
183
+ cashflows['fixed'].append({
184
+ 'payment_date': to_py_date(cf.date()),
185
+ 'amount': cf.amount()
186
+ })
187
+
188
+ for cf in swap.leg(1):
189
+ if not cf.hasOccurred():
190
+ coupon = ql.as_floating_rate_coupon(cf)
191
+ cashflows['floating'].append({
192
+ 'payment_date': to_py_date(coupon.date()),
193
+ 'fixing_date': to_py_date(coupon.fixingDate()),
194
+ 'rate': coupon.rate(),
195
+ 'spread': coupon.spread(),
196
+ 'amount': coupon.amount()
197
+ })
198
+
199
+ return cashflows
200
+
201
+ def plot_swap_zero_curve(
202
+ calculation_date: ql.Date | datetime.date,
203
+ max_years: int = 30,
204
+ step_months: int = 3,
205
+ compounding = ql.Continuous, # QuantLib enums are ints; don't type-hint them
206
+ frequency = ql.Annual,
207
+ show: bool = False,
208
+ ax: Optional[plt.Axes] = None,
209
+ ) -> tuple[list[float], list[float]]:
210
+ """
211
+ Plot the zero-coupon (spot) curve implied by the swap-bootstrapped curve.
212
+
213
+ Returns:
214
+ (tenors_in_years, zero_rates) with zero_rates in decimals (e.g., 0.045).
215
+ """
216
+ # normalize date
217
+ ql_calc = to_ql_date(calculation_date) if isinstance(calculation_date, datetime.date) else calculation_date
218
+ ql.Settings.instance().evaluationDate = ql_calc
219
+
220
+ # build curve from the mocked swap/deposit nodes
221
+ ts_handle = build_yield_curve(ql_calc)
222
+
223
+ calendar = ql.TARGET()
224
+ day_count = ql.Actual365Fixed()
225
+
226
+ years: list[float] = []
227
+ zeros: list[float] = []
228
+
229
+ months = 1
230
+ while months <= max_years * 12:
231
+ d = calendar.advance(ql_calc, ql.Period(months, ql.Months))
232
+ T = day_count.yearFraction(ql_calc, d)
233
+ z = ts_handle.zeroRate(d, day_count, compounding, frequency).rate()
234
+ years.append(T)
235
+ zeros.append(z)
236
+ months += step_months
237
+
238
+ # plot
239
+ if ax is None:
240
+ fig, ax = plt.subplots(figsize=(7, 4))
241
+ ax.plot(years, [z * 100 for z in zeros])
242
+ ax.set_xlabel("Maturity (years)")
243
+ ax.set_ylabel("Zero rate (%)")
244
+ ax.set_title("Zero-Coupon Yield Curve (Swaps)")
245
+ ax.grid(True, linestyle="--", alpha=0.4)
246
+
247
+ if show:
248
+ plt.show()
249
+
250
+ return years, zeros
251
+
252
+ # src/pricing_models/swap_pricer.py
253
+ import QuantLib as ql
254
+
255
+ import QuantLib as ql
256
+
257
+ def price_ftiie_ois_with_curve(
258
+ calculation_date: ql.Date,
259
+ notional: float,
260
+ start_date: ql.Date,
261
+ maturity_date: ql.Date,
262
+ fixed_rate: float,
263
+ fixed_leg_tenor: ql.Period, # e.g., 28D
264
+ fixed_leg_convention: int, # ql.ModifiedFollowing
265
+ fixed_leg_daycount: ql.DayCounter, # ql.Actual360()
266
+ on_index: ql.OvernightIndex, # FTIIE overnight index
267
+ curve: ql.YieldTermStructureHandle,
268
+ ) -> ql.OvernightIndexedSwap:
269
+ # Consistent evaluation settings (no ‘today’ leakage)
270
+ ql.Settings.instance().evaluationDate = calculation_date
271
+ ql.Settings.instance().includeReferenceDateEvents = False
272
+ ql.Settings.instance().enforceTodaysHistoricFixings = False
273
+
274
+ cal = on_index.fixingCalendar()
275
+
276
+ # -------- Spot start (T+1 Mexico) with tenor preservation --------
277
+ fixing = cal.adjust(calculation_date, ql.Following)
278
+ while not on_index.isValidFixingDate(fixing):
279
+ fixing = cal.advance(fixing, 1, ql.Days)
280
+ spot_start = on_index.valueDate(fixing)
281
+
282
+ start_shifted = (start_date <= calculation_date)
283
+ eff_start = start_date if not start_shifted else spot_start
284
+
285
+ if start_shifted:
286
+ # shift end by the same calendar-day offset
287
+ try:
288
+ day_offset = int(eff_start - start_date)
289
+ except Exception:
290
+ day_offset = eff_start.serialNumber() - start_date.serialNumber()
291
+ eff_end = cal.advance(maturity_date, int(day_offset), ql.Days)
292
+ else:
293
+ eff_end = maturity_date
294
+
295
+ if eff_end <= eff_start:
296
+ eff_end = cal.advance(eff_start, fixed_leg_tenor)
297
+
298
+ fixed_sched = ql.Schedule(
299
+ eff_start, eff_end, fixed_leg_tenor, cal,
300
+ fixed_leg_convention, fixed_leg_convention,
301
+ ql.DateGeneration.Forward, False
302
+ )
303
+
304
+ ois = ql.OvernightIndexedSwap(
305
+ ql.OvernightIndexedSwap.Payer,
306
+ notional,
307
+ fixed_sched,
308
+ fixed_rate,
309
+ fixed_leg_daycount,
310
+ on_index
311
+ )
312
+ ois.setPricingEngine(ql.DiscountingSwapEngine(curve))
313
+ return ois
314
+
315
+
316
+ import math
317
+
318
+ # --- add in: src/pricing_models/swap_pricer.py ---
319
+ def debug_swap_coupons(
320
+ swap: ql.VanillaSwap,
321
+ curve: ql.YieldTermStructureHandle,
322
+ ibor_index: ql.IborIndex,
323
+ header: str = "[SWAP COUPON DEBUG]",
324
+ show_past: bool = True,
325
+ max_rows: int | None = None,
326
+ ) -> None:
327
+ """
328
+ Print coupon-by-coupon diagnostics for fixed and floating legs:
329
+ accrual dates, fixing date, year fractions, forward/used rate, amount, DF and PV.
330
+ """
331
+ print("\n" + header)
332
+ asof = ql.Settings.instance().evaluationDate
333
+ print(f"evalDate: {_fmt(asof)} notional: {swap.nominal()}")
334
+ print(f"index: {ibor_index.name()} tenor: {ibor_index.tenor().length()} {ibor_index.tenor().units()} "
335
+ f"DC(float)={type(ibor_index.dayCounter()).__name__}")
336
+
337
+ # ---- fixed leg ----
338
+ print("\n[FIXED LEG]")
339
+ fixed_dc = swap.fixedDayCount()
340
+ fixed_rate = swap.fixedRate()
341
+ print(f"fixed rate input: {fixed_rate:.8f} DC(fixed)={type(fixed_dc).__name__}")
342
+
343
+ print(f"{'#':>3} {'accrualStart':>12} {'accrualEnd':>12} {'pay':>12} {'tau':>8} "
344
+ f"{'df':>12} {'amount':>14} {'pv':>14}")
345
+ print("-" * 96)
346
+ pv_fixed = 0.0
347
+ rows = 0
348
+ for i, cf in enumerate(swap.leg(0)):
349
+ c: ql.FixedRateCoupon = ql.as_fixed_rate_coupon(cf)
350
+ if (not show_past) and cf.hasOccurred(asof):
351
+ continue
352
+ tau = fixed_dc.yearFraction(c.accrualStartDate(), c.accrualEndDate())
353
+ df = curve.discount(c.date())
354
+ amt = c.amount() # already notional * rate * tau
355
+ pv = amt * df
356
+ pv_fixed += pv
357
+ print(f"{i:3d} {_fmt(c.accrualStartDate()):>12} {_fmt(c.accrualEndDate()):>12} {_fmt(c.date()):>12} "
358
+ f"{tau:8.5f} {df:12.8f} {amt:14.2f} {pv:14.2f}")
359
+ rows += 1
360
+ if max_rows and rows >= max_rows:
361
+ break
362
+
363
+ # ---- float leg ----
364
+ print("\n[FLOAT LEG]")
365
+ f_dc = ibor_index.dayCounter()
366
+ print(f"spread: {swap.spread():.8f} DC(float)={type(f_dc).__name__}")
367
+ print(f"{'#':>3} {'fix':>12} {'accrualStart':>12} {'accrualEnd':>12} {'pay':>12} {'tau':>8} "
368
+ f"{'hasFix':>6} {'idxUsed':>10} {'fwdCurve':>10} {'rateUsed':>10} {'df':>12} {'amount':>14} {'pv':>14}")
369
+ print("-" * 144)
370
+ pv_float = 0.0
371
+ rows = 0
372
+ for i, cf in enumerate(swap.leg(1)):
373
+ c: ql.FloatingRateCoupon = ql.as_floating_rate_coupon(cf)
374
+ if (not show_past) and cf.hasOccurred(asof):
375
+ continue
376
+ tau = f_dc.yearFraction(c.accrualStartDate(), c.accrualEndDate())
377
+ df = curve.discount(c.date())
378
+
379
+ # curve-implied forward over this exact accrual
380
+ df0 = curve.discount(c.accrualStartDate())
381
+ df1 = curve.discount(c.accrualEndDate())
382
+ fwd = (df0/df1 - 1.0) / max(tau, 1e-12)
383
+
384
+ # fixing available?
385
+ has_fix = False
386
+ idx_used = math.nan
387
+ try:
388
+ idx_used = ibor_index.fixing(c.fixingDate())
389
+ has_fix = True
390
+ except RuntimeError:
391
+ pass
392
+
393
+ rate_used = c.rate() # will be fixing (if present) else forward + spread
394
+ amt = c.amount()
395
+ pv = amt * df
396
+ pv_float += pv
397
+
398
+ print(f"{i:3d} {_fmt(c.fixingDate()):>12} {_fmt(c.accrualStartDate()):>12} {_fmt(c.accrualEndDate()):>12} "
399
+ f"{_fmt(c.date()):>12} {tau:8.5f} {str(has_fix):>6} "
400
+ f"{(idx_used if has_fix else float('nan')):10.6f} {fwd:10.6f} {rate_used:10.6f} "
401
+ f"{df:12.8f} {amt:14.2f} {pv:14.2f}")
402
+
403
+ rows += 1
404
+ if max_rows and rows >= max_rows:
405
+ break
406
+
407
+ # ---- summary / par checks ----
408
+ print("\n[PV DECOMP]")
409
+ print(f"PV_fixed : {pv_fixed:,.2f}")
410
+ print(f"PV_float : {pv_float:,.2f}")
411
+ print(f"NPV (QL) : {swap.NPV():,.2f}")
412
+
413
+ # annuity of the fixed leg (Σ tau_i * df_i on fixed pay dates)
414
+ annuity = 0.0
415
+ for cf in swap.leg(0):
416
+ c = ql.as_fixed_rate_coupon(cf)
417
+ tau = fixed_dc.yearFraction(c.accrualStartDate(), c.accrualEndDate())
418
+ df = curve.discount(c.date())
419
+ annuity += tau * df
420
+
421
+ # coupon-by-coupon float PV using curve forwards (no spread)
422
+ float_pv_curve = 0.0
423
+ for cf in swap.leg(1):
424
+ c = ql.as_floating_rate_coupon(cf)
425
+ tau = f_dc.yearFraction(c.accrualStartDate(), c.accrualEndDate())
426
+ df = curve.discount(c.date())
427
+ df0 = curve.discount(c.accrualStartDate())
428
+ df1 = curve.discount(c.accrualEndDate())
429
+ fwd = (df0/df1 - 1.0) / max(tau, 1e-12)
430
+ float_pv_curve += swap.nominal() * (fwd + c.spread()) * tau * df
431
+
432
+ # par fixed rate from curve coupons
433
+ par_from_coupons = float_pv_curve / max(annuity * swap.nominal(), 1e-12)
434
+
435
+ # also the classic (1 - DF(T)) / annuity formula (works when float = same curve, no stubs)
436
+ # approximate float PV = notional*(DF(start) - DF(end))
437
+ try:
438
+ start = ql.as_floating_rate_coupon(swap.leg(1)[0]).accrualStartDate()
439
+ end = ql.as_floating_rate_coupon(swap.leg(1)[-1]).accrualEndDate()
440
+ par_alt = (curve.discount(start) - curve.discount(end)) / max(annuity, 1e-12)
441
+ except Exception:
442
+ par_alt = float('nan')
443
+
444
+ print(f"Annuity (fixed) : {annuity:,.8f}")
445
+ print(f"Par (from curve coupons) : {par_from_coupons:,.8f}")
446
+ print(f"Par (alt 1-DF/annuity) : {par_alt:,.8f}")
447
+ print(f"Par (QL fairRate) : {swap.fairRate():,.8f}")
448
+ print()
449
+
450
+ def _fmt(qld: ql.Date) -> str:
451
+ return f"{qld.year():04d}-{qld.month():02d}-{qld.dayOfMonth():02d}"
452
+
453
+ def debug_tiie_zero_curve(
454
+ calculation_date: ql.Date,
455
+ curve: ql.YieldTermStructureHandle,
456
+ cal: ql.Calendar | None = None,
457
+ day_count: ql.DayCounter | None = None,
458
+ sample_months: list[int] | None = None,
459
+ header: str = "[TIIE ZERO CURVE DEBUG]"
460
+ ) -> None:
461
+ """
462
+ Print a readable snapshot of the zero curve: sample maturities, DFs and zero rates.
463
+ """
464
+ print("\n" + header)
465
+ ql.Settings.instance().evaluationDate = calculation_date
466
+ cal = cal or (ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET())
467
+ dc = day_count or ql.Actual360()
468
+
469
+ asof = calculation_date
470
+ print(f"asof (evalDate): {_fmt(asof)}")
471
+ try:
472
+ link = curve.currentLink()
473
+ print(f"curve link: {type(link).__name__} (extrap={'Yes' if link.allowsExtrapolation() else 'No'})")
474
+ except Exception:
475
+ pass
476
+
477
+ # sampling grid
478
+ if sample_months is None:
479
+ sample_months = [1, 2, 3, 6, 9, 12,
480
+ 18, 24, 36, 48, 60, 84] # up to 7Y
481
+
482
+ print(f"{'m (M)':>6} {'date':>12} {'T(yr)':>8} {'DF':>12} {'Zero(%)':>10}")
483
+ print("-" * 52)
484
+ for m in sample_months:
485
+ d = cal.advance(asof, ql.Period(m, ql.Months))
486
+ T = dc.yearFraction(asof, d)
487
+ df = curve.discount(d)
488
+ z = curve.zeroRate(d, dc, ql.Continuous, ql.Annual).rate() * 100.0
489
+ print(f"{m:6d} {_fmt(d):>12} {T:8.5f} {df:12.8f} {z:10.4f}")
490
+ print()
491
+
492
+ def debug_tiie_trade(
493
+ valuation_date: ql.Date,
494
+ swap: ql.VanillaSwap,
495
+ curve: ql.YieldTermStructureHandle,
496
+ ibor_index: ql.IborIndex
497
+ ) -> None:
498
+ """
499
+ Convenience wrapper: dump curve snapshot and full coupon drilldown for a TIIE swap.
500
+ """
501
+ debug_tiie_zero_curve(valuation_date, curve)
502
+ debug_swap_coupons(swap, curve, ibor_index)
@@ -0,0 +1,175 @@
1
+ # settings.py
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+ from types import SimpleNamespace
9
+
10
+ # ---------------- App identity & app dirs ----------------
11
+ APP_VENDOR = "mainsequence"
12
+ APP_NAME = "instruments"
13
+ APP_ID = f"{APP_VENDOR}/{APP_NAME}"
14
+
15
+ # All environment variables use this prefix now.
16
+ ENV_PREFIX = "MSI" # e.g., MSI_CONFIG_FILE, MSI_DATA_BACKEND
17
+ ENV_CONFIG_FILE = f"{ENV_PREFIX}_CONFIG_FILE"
18
+
19
+ def _user_config_root() -> Path:
20
+ if sys.platform == "win32":
21
+ base = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
22
+ elif sys.platform == "darwin":
23
+ base = Path.home() / "Library" / "Application Support"
24
+ else:
25
+ base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
26
+ return (base / APP_VENDOR / APP_NAME).resolve()
27
+
28
+ APP_ROOT = _user_config_root()
29
+ # (No POSITIONS_DIR / BUILDS_DIR / DATA_DIR and no bulk mkdir here.)
30
+
31
+ # ---------------- tiny config loader (stdlib only) ----------------
32
+ def _load_toml(text: str) -> dict:
33
+ try:
34
+ import tomllib # py311+
35
+ return tomllib.loads(text)
36
+ except Exception:
37
+ return {}
38
+
39
+ def _load_file_config() -> dict:
40
+ candidates: list[Path] = []
41
+ # 1) explicit path via MSI_CONFIG_FILE
42
+ env_cfg = os.getenv(ENV_CONFIG_FILE)
43
+ if env_cfg:
44
+ candidates.append(Path(env_cfg).expanduser())
45
+
46
+ # 2) project-local
47
+ candidates += [Path("./instruments.toml"), Path("./instruments.json")]
48
+
49
+ # 3) user config root
50
+ candidates += [APP_ROOT / "config.toml", APP_ROOT / "config.json"]
51
+
52
+ for p in candidates:
53
+ try:
54
+ if not p.exists():
55
+ continue
56
+ s = p.read_text(encoding="utf-8")
57
+ if p.suffix.lower() == ".toml":
58
+ return _load_toml(s) or {}
59
+ if p.suffix.lower() == ".json":
60
+ return json.loads(s)
61
+ except Exception:
62
+ pass
63
+ return {}
64
+
65
+ # ---------------- default TOML (written only if no config exists) ----------------
66
+ DEFAULT_TOML = """# instruments.toml — defaults for MainSequence Instruments
67
+
68
+ DISCOUNT_CURVES_TABLE = "discount_curves"
69
+ REFERENCE_RATES_FIXING_TABLE = "fixing_rates_1d"
70
+
71
+ TIIE_28_ZERO_CURVE = "F_TIIE_28_VALMER"
72
+ M_BONOS_ZERO_CURVE = "M_BONOS_ZERO_OTR"
73
+
74
+ TIIE_28_UID = "TIIE_28"
75
+ TIIE_91_UID = "TIIE_91"
76
+ TIIE_182_UID = "TIIE_182"
77
+ TIIE_OVERNIGHT_UID = "TIIE_OVERNIGHT"
78
+
79
+ CETE_28_UID = "CETE_28"
80
+ CETE_91_UID = "CETE_91"
81
+ CETE_182_UID = "CETE_182"
82
+
83
+ [data]
84
+ backend = "mock"
85
+
86
+ [files]
87
+ tiie_zero_csv = ""
88
+ tiie28_fixings_csv = ""
89
+ """
90
+
91
+ def _existing_config_path() -> Path | None:
92
+ env_cfg = os.getenv(ENV_CONFIG_FILE)
93
+ if env_cfg:
94
+ p = Path(env_cfg).expanduser()
95
+ if p.exists():
96
+ return p
97
+ for p in (
98
+ Path("./instruments.toml"),
99
+ Path("./instruments.json"),
100
+ APP_ROOT / "config.toml",
101
+ APP_ROOT / "config.json",
102
+ ):
103
+ if p.exists():
104
+ return p
105
+ return None
106
+
107
+ def _ensure_default_config_file() -> Path | None:
108
+ """If no config exists anywhere, create one. Never overwrites existing."""
109
+ if _existing_config_path() is not None:
110
+ return None
111
+ target = Path(os.getenv(ENV_CONFIG_FILE, APP_ROOT / "config.toml")).expanduser()
112
+ try:
113
+ target.parent.mkdir(parents=True, exist_ok=True) # ensure parent dir only
114
+ if not target.exists():
115
+ target.write_text(DEFAULT_TOML, encoding="utf-8")
116
+ except Exception:
117
+ return None
118
+ return target
119
+
120
+ # Create a default config file if none is present anywhere.
121
+ _ensure_default_config_file()
122
+
123
+ # Now load the config (env still overrides)
124
+ _CFG = _load_file_config()
125
+
126
+ def _get(key: str, default: str) -> str:
127
+ # Env overrides config file (MSI_<KEY>)
128
+ v = os.getenv(f"{ENV_PREFIX}_{key}")
129
+ if v is not None:
130
+ return v
131
+ try:
132
+ section, leaf = key.lower().split(".", 1)
133
+ return _CFG.get(section, {}).get(leaf, default)
134
+ except Exception:
135
+ return _CFG.get(key, default)
136
+
137
+ # ---------------- Your existing constants (with overrides) ----------------
138
+ # Tables
139
+ DISCOUNT_CURVES_TABLE = _get("DISCOUNT_CURVES_TABLE", "discount_curves")
140
+ REFERENCE_RATES_FIXING_TABLE = _get("REFERENCE_RATES_FIXING_TABLE", "fixing_rates_1d")
141
+
142
+ # Curve identifiers
143
+ TIIE_28_ZERO_CURVE = _get("TIIE_28_ZERO_CURVE", "F_TIIE_28_VALMER")
144
+ M_BONOS_ZERO_CURVE = _get("M_BONOS_ZERO_CURVE", "M_BONOS_ZERO_OTR")
145
+
146
+ # Index UIDs
147
+ TIIE_28_UID = _get("TIIE_28_UID", "TIIE_28")
148
+ TIIE_91_UID = _get("TIIE_91_UID", "TIIE_91")
149
+ TIIE_182_UID = _get("TIIE_182_UID", "TIIE_182")
150
+ TIIE_OVERNIGHT_UID = _get("TIIE_OVERNIGHT_UID", "TIIE_OVERNIGHT")
151
+
152
+ CETE_28_UID = _get("CETE_28_UID", "CETE_28")
153
+ CETE_91_UID = _get("CETE_91_UID", "CETE_91")
154
+ CETE_182_UID = _get("CETE_182_UID", "CETE_182")
155
+
156
+ # Optional file locations (let your code decide how to use them)
157
+ TIIE_ZERO_CSV = (_CFG.get("files", {}) or {}).get("tiie_zero_csv")
158
+ TIIE28_FIXINGS_CSV = (_CFG.get("files", {}) or {}).get("tiie28_fixings_csv")
159
+
160
+ # ---------------- Convenience namespaces for legacy import sites ------------
161
+ indices = SimpleNamespace(
162
+ TIIE_28_UID=TIIE_28_UID,
163
+ TIIE_91_UID=TIIE_91_UID,
164
+ TIIE_182_UID=TIIE_182_UID,
165
+ TIIE_OVERNIGHT_UID=TIIE_OVERNIGHT_UID,
166
+ CETE_28_UID=CETE_28_UID,
167
+ CETE_91_UID=CETE_91_UID,
168
+ CETE_182_UID=CETE_182_UID,
169
+ )
170
+ curves = SimpleNamespace(
171
+ TIIE_28_ZERO_CURVE=TIIE_28_ZERO_CURVE,
172
+ M_BONOS_ZERO_CURVE=M_BONOS_ZERO_CURVE,
173
+ )
174
+ DATA_BACKEND = os.getenv(f"{ENV_PREFIX}_DATA_BACKEND", (_CFG.get("data", {}) or {}).get("backend", "mainsequence"))
175
+ data = SimpleNamespace(backend=DATA_BACKEND)
@@ -0,0 +1,29 @@
1
+ import datetime
2
+ import QuantLib as ql
3
+ import pytz
4
+
5
+
6
+ def to_ql_date(dt: datetime.date) -> ql.Date:
7
+ """
8
+ Converts a Python datetime.date object to a QuantLib.Date object.
9
+
10
+ Args:
11
+ dt: The datetime.date object to convert.
12
+
13
+ Returns:
14
+ The corresponding QuantLib.Date object.
15
+ """
16
+ return ql.Date(dt.day, dt.month, dt.year)
17
+
18
+ def to_py_date(qld: ql.Date) -> datetime.date:
19
+ """
20
+ Converts a QuantLib.Date object to a Python datetime.date object.
21
+
22
+ Args:
23
+ qld: The QuantLib.Date object to convert.
24
+
25
+ Returns:
26
+ The corresponding datetime.date object.
27
+ """
28
+ return datetime.datetime(qld.year(), qld.month(), qld.dayOfMonth(),tzinfo=pytz.utc)
29
+