mainsequence 3.3.1__tar.gz → 3.3.3__tar.gz

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 (125) hide show
  1. {mainsequence-3.3.1 → mainsequence-3.3.3}/PKG-INFO +1 -1
  2. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/instruments/bond.py +232 -0
  3. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/pricing_models/indices.py +0 -2
  4. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence.egg-info/PKG-INFO +1 -1
  5. {mainsequence-3.3.1 → mainsequence-3.3.3}/pyproject.toml +1 -1
  6. {mainsequence-3.3.1 → mainsequence-3.3.3}/LICENSE +0 -0
  7. {mainsequence-3.3.1 → mainsequence-3.3.3}/README.md +0 -0
  8. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/__init__.py +0 -0
  9. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/__main__.py +0 -0
  10. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/cli/__init__.py +0 -0
  11. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/cli/api.py +0 -0
  12. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/cli/cli.py +0 -0
  13. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/cli/config.py +0 -0
  14. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/cli/ssh_utils.py +0 -0
  15. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/client/__init__.py +0 -0
  16. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/client/base.py +0 -0
  17. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  18. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  19. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/client/data_sources_interfaces/timescale.py +0 -0
  20. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/client/exceptions.py +0 -0
  21. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/client/models_helpers.py +0 -0
  22. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/client/models_report_studio.py +0 -0
  23. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/client/models_tdag.py +0 -0
  24. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/client/models_vam.py +0 -0
  25. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/client/utils.py +0 -0
  26. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/dashboards/__init__.py +0 -0
  27. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/dashboards/streamlit/__init__.py +0 -0
  28. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/dashboards/streamlit/assets/config.toml +0 -0
  29. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/dashboards/streamlit/assets/favicon.png +0 -0
  30. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/dashboards/streamlit/assets/image_1_base64.txt +0 -0
  31. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/dashboards/streamlit/assets/image_2_base64.txt +0 -0
  32. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/dashboards/streamlit/assets/image_3_base64.txt +0 -0
  33. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/dashboards/streamlit/assets/image_4_base64.txt +0 -0
  34. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/dashboards/streamlit/assets/image_5_base64.txt +0 -0
  35. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/dashboards/streamlit/assets/logo.png +0 -0
  36. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/dashboards/streamlit/core/__init__.py +0 -0
  37. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/dashboards/streamlit/core/theme.py +0 -0
  38. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/dashboards/streamlit/pages/__init__.py +0 -0
  39. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/dashboards/streamlit/scaffold.py +0 -0
  40. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instrumentation/__init__.py +0 -0
  41. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instrumentation/utils.py +0 -0
  42. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/__init__.py +0 -0
  43. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/data_interface/__init__.py +0 -0
  44. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/data_interface/data_interface.py +0 -0
  45. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/instruments/__init__.py +0 -0
  46. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/instruments/base_instrument.py +0 -0
  47. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/instruments/european_option.py +0 -0
  48. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/instruments/interest_rate_swap.py +0 -0
  49. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/instruments/json_codec.py +0 -0
  50. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/instruments/knockout_fx_option.py +0 -0
  51. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/instruments/position.py +0 -0
  52. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/instruments/ql_fields.py +0 -0
  53. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/instruments/vanilla_fx_option.py +0 -0
  54. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/instruments.default.toml +0 -0
  55. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/pricing_models/__init__.py +0 -0
  56. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/pricing_models/black_scholes.py +0 -0
  57. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/pricing_models/bond_pricer.py +0 -0
  58. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/pricing_models/fx_option_pricer.py +0 -0
  59. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/pricing_models/knockout_fx_pricer.py +0 -0
  60. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/pricing_models/swap_pricer.py +0 -0
  61. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/settings.py +0 -0
  62. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/instruments/utils.py +0 -0
  63. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/logconf.py +0 -0
  64. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/reportbuilder/__init__.py +0 -0
  65. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/reportbuilder/__main__.py +0 -0
  66. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/reportbuilder/examples/ms_template_report.py +0 -0
  67. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/reportbuilder/model.py +0 -0
  68. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/reportbuilder/slide_templates.py +0 -0
  69. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/tdag/__init__.py +0 -0
  70. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/tdag/__main__.py +0 -0
  71. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/tdag/config.py +0 -0
  72. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/tdag/data_nodes/__init__.py +0 -0
  73. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/tdag/data_nodes/build_operations.py +0 -0
  74. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/tdag/data_nodes/data_nodes.py +0 -0
  75. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/tdag/data_nodes/persist_managers.py +0 -0
  76. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/tdag/data_nodes/run_operations.py +0 -0
  77. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/tdag/data_nodes/utils.py +0 -0
  78. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/tdag/future_registry.py +0 -0
  79. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/tdag/utils.py +0 -0
  80. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/__init__.py +0 -0
  81. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/agent_interface.py +0 -0
  82. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/config_handling.py +0 -0
  83. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/__init__.py +0 -0
  84. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/apps/__init__.py +0 -0
  85. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/apps/etf_replicator_app.py +0 -0
  86. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/apps/generate_report.py +0 -0
  87. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/apps/load_external_portfolio.py +0 -0
  88. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/apps/news_app.py +0 -0
  89. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/apps/portfolio_report_app.py +0 -0
  90. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/apps/portfolio_table.py +0 -0
  91. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/apps/run_named_portfolio.py +0 -0
  92. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/apps/run_portfolio.py +0 -0
  93. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/apps/templates/base.html +0 -0
  94. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/apps/templates/report.html +0 -0
  95. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/data_nodes/__init__.py +0 -0
  96. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/data_nodes/external_weights.py +0 -0
  97. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/data_nodes/intraday_trend.py +0 -0
  98. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/data_nodes/market_cap.py +0 -0
  99. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/data_nodes/mock_signal.py +0 -0
  100. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/data_nodes/portfolio_replicator.py +0 -0
  101. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/prices/__init__.py +0 -0
  102. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/prices/data_nodes.py +0 -0
  103. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/prices/utils.py +0 -0
  104. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/rebalance_strategies/__init__.py +0 -0
  105. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/contrib/rebalance_strategies/rebalance_strategies.py +0 -0
  106. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/data_nodes.py +0 -0
  107. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/enums.py +0 -0
  108. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/models.py +0 -0
  109. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/notebook_handling.py +0 -0
  110. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/portfolio_interface.py +0 -0
  111. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/resource_factory/__init__.py +0 -0
  112. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/resource_factory/app_factory.py +0 -0
  113. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/resource_factory/base_factory.py +0 -0
  114. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/resource_factory/rebalance_factory.py +0 -0
  115. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/resource_factory/signal_factory.py +0 -0
  116. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence/virtualfundbuilder/utils.py +0 -0
  117. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence.egg-info/SOURCES.txt +0 -0
  118. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence.egg-info/dependency_links.txt +0 -0
  119. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence.egg-info/entry_points.txt +0 -0
  120. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence.egg-info/requires.txt +0 -0
  121. {mainsequence-3.3.1 → mainsequence-3.3.3}/mainsequence.egg-info/top_level.txt +0 -0
  122. {mainsequence-3.3.1 → mainsequence-3.3.3}/setup.cfg +0 -0
  123. {mainsequence-3.3.1 → mainsequence-3.3.3}/tests/test_agent.py +0 -0
  124. {mainsequence-3.3.1 → mainsequence-3.3.3}/tests/test_portfolio.py +0 -0
  125. {mainsequence-3.3.1 → mainsequence-3.3.3}/tests/test_replicator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 3.3.1
3
+ Version: 3.3.3
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License: MainSequence GmbH SDK License Agreement
@@ -38,6 +38,9 @@ BOND_CACHE_PER_INSTRUMENT_LIMIT = 256
38
38
  _BOND_CACHE_LOCK = threading.RLock()
39
39
  _BOND_PRICE_CACHE: dict[str, "OrderedDict[str, float]"] = {}
40
40
  _BOND_ZSPREAD_CACHE: dict[str, "OrderedDict[str, float]"] = {}
41
+ _BOND_DURATION_CACHE: dict[str, "OrderedDict[str, float]"] = {} # <- NEW
42
+
43
+
41
44
 
42
45
  def clear_global_bond_cache() -> None:
43
46
  with _BOND_CACHE_LOCK:
@@ -724,6 +727,235 @@ class Bond(InstrumentModel):
724
727
  )
725
728
  return self._bond
726
729
 
730
+ def duration(
731
+ self,
732
+ with_yield: float | None = None,
733
+ *,
734
+ duration_type = ql.Duration.Modified,
735
+ ) -> float:
736
+ """
737
+ Return bond duration (default: Modified) and cache it using the same
738
+ hashed context design as price()/z_spread().
739
+
740
+ Notes
741
+ -----
742
+ - Zero-coupon bonds: for Modified duration we follow the provided guide and
743
+ return time-to-maturity in year fractions (0 if matured).
744
+ - Coupon-bearing bonds: we compute YTM from the current clean price and then
745
+ call QuantLib's BondFunctions.duration(..).
746
+ - Cache key includes the instrument hash and the pricing context (flat yield
747
+ or default curve + version ticks + valuation date), plus the duration type.
748
+ """
749
+
750
+ if self.valuation_date is None:
751
+ raise ValueError("Set valuation_date before computing duration: set_valuation_date(dt).")
752
+
753
+ # ---------- build cache keys ----------
754
+ inst_key = self._instrument_cache_key()
755
+
756
+ # Build a context key. For zero-coupon we may not need a curve; avoid raising if none.
757
+ try:
758
+ ctx_key = self._price_context_key(with_yield)
759
+ except Exception:
760
+ # e.g. zero-coupon duration with no curve/yield doesn't need a curve context;
761
+ # still include valuation date to remain stable across time.
762
+ ctx_key = f"val:{self._val_ordinal()}"
763
+
764
+ # Tag the duration type to avoid collisions if caller requests different types.
765
+ if duration_type == ql.Duration.Modified:
766
+ dtype_str = "Modified"
767
+ elif duration_type == ql.Duration.Macaulay:
768
+ dtype_str = "Macaulay"
769
+ elif duration_type == ql.Duration.Simple:
770
+ dtype_str = "Simple"
771
+ elif duration_type == ql.Duration.Effective:
772
+ dtype_str = "Effective"
773
+ else:
774
+ dtype_str = f"Type{int(duration_type)}"
775
+
776
+ dur_key = f"dur|{ctx_key}|dtype:{dtype_str}"
777
+
778
+ # ---------- cache hit ----------
779
+ with _BOND_CACHE_LOCK:
780
+ bucket = _BOND_DURATION_CACHE.get(inst_key)
781
+ if bucket is not None and dur_key in bucket:
782
+ val = bucket[dur_key]
783
+ bucket.move_to_end(dur_key) # LRU promote
784
+ return val
785
+
786
+ # ---------- compute ----------
787
+ # Special-case ZeroCouponBond per your guide
788
+ if isinstance(self, ZeroCouponBond) and duration_type == ql.Duration.Modified:
789
+ # Build only the instrument (no engine needed)
790
+ self._ensure_instrument()
791
+
792
+ vd = self.valuation_date
793
+ mty = self.maturity_date
794
+ if mty <= vd:
795
+ dur_val = 0.0
796
+ else:
797
+ dcc: ql.DayCounter = self.day_count
798
+ dur_val = max(0.0, dcc.yearFraction(to_ql_date(vd), to_ql_date(mty)))
799
+
800
+ else:
801
+ # Coupon-bearing bonds (and any non-default type): use QL functions
802
+ # Ensure pricer so cleanPrice/settlementDate/etc. are available
803
+ self._setup_pricer(with_yield=with_yield)
804
+ qb: ql.Bond = self._bond # type: ignore[assignment]
805
+ dcc: ql.DayCounter = self.day_count
806
+
807
+ # Frequency: use instrument's coupon frequency if available; else NoFrequency
808
+ try:
809
+ freq: ql.Frequency = self.coupon_frequency.frequency() # Fixed & Floating have this
810
+ except Exception:
811
+ freq = ql.NoFrequency
812
+
813
+ comp = ql.Compounded
814
+ bp = ql.BondPrice(float(qb.cleanPrice()), ql.BondPrice.Clean)
815
+ settle = qb.settlementDate()
816
+ ytm = qb.bondYield(bp, dcc, comp, freq, settle)
817
+
818
+ try:
819
+ dur_val = float(
820
+ ql.BondFunctions.duration(qb, ytm, dcc, comp, freq, duration_type)
821
+ )
822
+ except Exception as e:
823
+ # Stay robust (consistent with your guide)
824
+ print(e)
825
+ dur_val = 0.0
826
+
827
+ # ---------- store in cache ----------
828
+ with _BOND_CACHE_LOCK:
829
+ bucket = _BOND_DURATION_CACHE.setdefault(inst_key, OrderedDict())
830
+ bucket[dur_key] = float(dur_val)
831
+ bucket.move_to_end(dur_key)
832
+ while len(bucket) > BOND_CACHE_PER_INSTRUMENT_LIMIT:
833
+ bucket.popitem(last=False)
834
+
835
+ return float(dur_val)
836
+
837
+ def carry_roll_down(
838
+ self,
839
+ horizon: ql.Period | int | datetime.timedelta | datetime.date,
840
+ *,
841
+ clean: bool = False,
842
+ ) -> dict[str, float]:
843
+ """
844
+ Compute carry + roll-down over a horizon using the already-built engine & curve.
845
+ - Uses self.analytics(...) for today's clean/dirty/accrued (no rebuild if engine is set).
846
+ - No relinks, no engine setup here. Raises if the bond wasn't priced first.
847
+
848
+ Returns per-100 (except *_ccy which are currency):
849
+ p0_dirty_per100, p0_clean_per100,
850
+ p1_dirty_per100_unchanged_curve, p1_dirty_per100_const_yield,
851
+ cr_dirty, carry_const_dirty, roll_down_dirty,
852
+ coupons_between_ccy, cr_plus_coupons_dirty,
853
+ and (if clean=True) clean-price counterparts and accrued at horizon.
854
+ """
855
+ # ---- Preconditions: must already be priced and linked to a curve/yield ----
856
+ if self.valuation_date is None:
857
+ raise ValueError("Set valuation_date before carry_roll_down().")
858
+ if self._bond is None or self._engine is None or self._last_discount_curve_handle is None:
859
+ raise RuntimeError("Price the bond first (price() or analytics()) before carry_roll_down().")
860
+
861
+ qb: ql.Bond = self._bond
862
+ h = self._last_discount_curve_handle
863
+ scale = 100.0 / float(self.face_value)
864
+
865
+ # --- Today's prices via your analytics() (uses existing engine; no rebuild if with_yield unchanged) ---
866
+ an = self.analytics(with_yield=self._with_yield)
867
+ p0_clean = float(an["clean_price"])
868
+ p0_dirty = float(an["dirty_price"])
869
+ a0_ccy = float(an["accrued_amount"])
870
+
871
+ # Settlement today (uses current QL Settings already set when you priced)
872
+ s0: ql.Date = qb.settlementDate()
873
+
874
+ # Current YTM from today's clean (for constant-yield carry)
875
+ dcc: ql.DayCounter = self.day_count
876
+ try:
877
+ freq: ql.Frequency = self.coupon_frequency.frequency()
878
+ except Exception:
879
+ freq = ql.NoFrequency
880
+ comp = ql.Compounded
881
+ ytm0 = qb.bondYield(ql.BondPrice(p0_clean, ql.BondPrice.Clean), dcc, comp, freq, s0)
882
+
883
+ # ---- Horizon valuation date and settlement (no global Settings change) ----
884
+ asof_qld = to_ql_date(self.valuation_date)
885
+ if isinstance(horizon, ql.Period):
886
+ vd1 = self.calendar.advance(asof_qld, horizon, self.business_day_convention)
887
+ elif isinstance(horizon, int):
888
+ vd1 = self.calendar.advance(asof_qld, ql.Period(int(horizon), ql.Days), self.business_day_convention)
889
+ elif isinstance(horizon, datetime.timedelta):
890
+ vd1 = to_ql_date(self.valuation_date + horizon)
891
+ elif isinstance(horizon, datetime.date):
892
+ vd1 = to_ql_date(horizon)
893
+ else:
894
+ raise ValueError("Unsupported horizon type. Use ql.Period | int(days) | timedelta | date.")
895
+
896
+ s1 = self.calendar.advance(vd1, ql.Period(self.settlement_days, ql.Days), ql.Following)
897
+ if s1 <= s0:
898
+ raise ValueError("Horizon/settlement must be after today's settlement date.")
899
+
900
+ # ---- Unchanged-curve forward dirty at horizon: sum DF(t0,T)*CF / DF(t0,S1) ----
901
+ def _df(d: ql.Date) -> float:
902
+ return float(h.discount(d))
903
+
904
+ pv_after_s1 = 0.0
905
+ coupons_between_ccy = 0.0
906
+ for cf in qb.cashflows():
907
+ d = cf.date()
908
+ amt = float(cf.amount())
909
+ if d > s1:
910
+ pv_after_s1 += amt * _df(d)
911
+ elif d > s0: # cashflows received in (s0, s1]
912
+ coupons_between_ccy += amt
913
+
914
+ df_s1 = _df(s1)
915
+ p1_dirty_curve = 0.0 if df_s1 == 0.0 else (pv_after_s1 / df_s1) * scale
916
+
917
+ # ---- Constant-yield dirty at horizon (no engine; BondFunctions) ----
918
+ try:
919
+ p1_clean_const = float(ql.BondFunctions.cleanPrice(qb, ytm0, dcc, comp, freq, s1))
920
+ except Exception:
921
+ p1_clean_const = 0.0
922
+ try:
923
+ a1_ccy = float(ql.BondFunctions.accruedAmount(qb, s1))
924
+ except Exception:
925
+ a1_ccy = 0.0
926
+ p1_dirty_const = p1_clean_const + a1_ccy * scale
927
+
928
+ # ---- Returns (per 100) ----
929
+ cr_dirty = p1_dirty_curve - p0_dirty # carry + roll (ex-coupon)
930
+ carry_const_dirty = p1_dirty_const - p0_dirty # constant-yield carry (ex-coupon)
931
+ roll_down_dirty = p1_dirty_curve - p1_dirty_const # roll-down
932
+
933
+ out: dict[str, float] = {
934
+ "p0_dirty_per100": p0_dirty,
935
+ "p0_clean_per100": p0_clean,
936
+ "p1_dirty_per100_unchanged_curve": p1_dirty_curve,
937
+ "p1_dirty_per100_const_yield": p1_dirty_const,
938
+ "cr_dirty": cr_dirty,
939
+ "carry_const_dirty": carry_const_dirty,
940
+ "roll_down_dirty": roll_down_dirty,
941
+ "coupons_between_ccy": coupons_between_ccy,
942
+ "cr_plus_coupons_dirty": cr_dirty + coupons_between_ccy * scale,
943
+ }
944
+
945
+ if clean:
946
+ p1_clean_curve = p1_dirty_curve - a1_ccy * scale
947
+ out.update({
948
+ "accrued0_ccy": a0_ccy,
949
+ "accrued1_ccy": a1_ccy,
950
+ "accrued1_per100": a1_ccy * scale,
951
+ "p1_clean_per100_unchanged_curve": p1_clean_curve,
952
+ "p1_clean_per100_const_yield": p1_clean_const,
953
+ "cr_clean": p1_clean_curve - p0_clean,
954
+ "carry_const_clean": p1_clean_const - p0_clean,
955
+ })
956
+
957
+ return out
958
+
727
959
 
728
960
  class FixedRateBond(Bond):
729
961
  """Plain-vanilla fixed-rate bond following the shared Bond lifecycle."""
@@ -122,9 +122,7 @@ _INDEX_TEMPLATES: dict[str, Callable[[], dict[str, Any]]] = {
122
122
  "REFERENCE_RATE__CETE_28": _cete(28),
123
123
  "REFERENCE_RATE__CETE_91": _cete(91),
124
124
  "REFERENCE_RATE__CETE_182": _cete(182),
125
- "REFERENCE_RATE__CETE_1": _cete(1),
126
125
 
127
- "REFERENCE_RATE__TIIE_OVERNIGHT_BONDES":_mx_gov_overnight(1),
128
126
 
129
127
  "REFERENCE_RATE__USD_SOFR": lambda: dict(
130
128
  curve_uid=_const("ZERO_CURVE__UST_CMT_ZERO_CURVE_UID"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 3.3.1
3
+ Version: 3.3.3
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License: MainSequence GmbH SDK License Agreement
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "3.3.1"
7
+ version = "3.3.3"
8
8
  description = "Main Sequence SDK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes
File without changes