mxm-refdata 0.3.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 (64) hide show
  1. mxm/refdata/__init__.py +1 -0
  2. mxm/refdata/api/__init__.py +1 -0
  3. mxm/refdata/api/ref_data_api.py +607 -0
  4. mxm/refdata/cli.py +218 -0
  5. mxm/refdata/data/first_day_of_interest_rule.json +88 -0
  6. mxm/refdata/data/futures_products.csv +6 -0
  7. mxm/refdata/data/last_trading_rule.json +34 -0
  8. mxm/refdata/database/__init__.py +1 -0
  9. mxm/refdata/database/sql_session_manager.py +135 -0
  10. mxm/refdata/mappings/__init__.py +31 -0
  11. mxm/refdata/mappings/futures_contract_vs_orm.py +51 -0
  12. mxm/refdata/mappings/futures_product_vs_orm.py +56 -0
  13. mxm/refdata/mappings/period_cycles_vs_orm.py +102 -0
  14. mxm/refdata/mappings/period_vs_orm.py +40 -0
  15. mxm/refdata/models/__init__.py +31 -0
  16. mxm/refdata/models/contracts/__init__.py +0 -0
  17. mxm/refdata/models/contracts/futures_contract.py +22 -0
  18. mxm/refdata/models/currencies.py +24 -0
  19. mxm/refdata/models/months.py +72 -0
  20. mxm/refdata/models/orm/__init__.py +16 -0
  21. mxm/refdata/models/orm/base.py +3 -0
  22. mxm/refdata/models/orm/futures_contracts.py +53 -0
  23. mxm/refdata/models/orm/futures_products.py +61 -0
  24. mxm/refdata/models/orm/period_cycles.py +84 -0
  25. mxm/refdata/models/orm/periods.py +30 -0
  26. mxm/refdata/models/period_cycles.py +73 -0
  27. mxm/refdata/models/periods.py +64 -0
  28. mxm/refdata/models/products/__init__.py +1 -0
  29. mxm/refdata/models/products/futures_product.py +38 -0
  30. mxm/refdata/models/products/settlement.py +10 -0
  31. mxm/refdata/models/reference_events.py +9 -0
  32. mxm/refdata/models/units.py +28 -0
  33. mxm/refdata/models/weekdays.py +66 -0
  34. mxm/refdata/parsing/__init__.py +1 -0
  35. mxm/refdata/parsing/futures_products_from_csv.py +76 -0
  36. mxm/refdata/py.typed +0 -0
  37. mxm/refdata/scripts/__init__.py +1 -0
  38. mxm/refdata/scripts/db_utils.py +61 -0
  39. mxm/refdata/scripts/manage_static_ref_data.py +73 -0
  40. mxm/refdata/services/__init__.py +0 -0
  41. mxm/refdata/services/bootstrap.py +84 -0
  42. mxm/refdata/services/futures_contract_factory.py +127 -0
  43. mxm/refdata/services/futures_product_factory.py +132 -0
  44. mxm/refdata/services/period_factory.py +315 -0
  45. mxm/refdata/services/ref_data_service.py +322 -0
  46. mxm/refdata/services/smokecheck.py +326 -0
  47. mxm/refdata/trading_calendars/__init__.py +0 -0
  48. mxm/refdata/trading_calendars/first_day_of_interest.py +74 -0
  49. mxm/refdata/trading_calendars/last_trading_day.py +117 -0
  50. mxm/refdata/trading_calendars/nth_business_day.py +52 -0
  51. mxm/refdata/trading_calendars/nth_calendar_day_of_period.py +43 -0
  52. mxm/refdata/trading_calendars/nth_weekday_of_period.py +50 -0
  53. mxm/refdata/trading_calendars/trading_calendar.py +186 -0
  54. mxm/refdata/utils/__init__.py +1 -0
  55. mxm/refdata/utils/cache_manager.py +33 -0
  56. mxm/refdata/utils/config.py +32 -0
  57. mxm/refdata/utils/period_types_codec.py +16 -0
  58. mxm/refdata/utils/regex_patterns.py +18 -0
  59. mxm/refdata/utils/resources.py +29 -0
  60. mxm_refdata-0.3.0.dist-info/METADATA +228 -0
  61. mxm_refdata-0.3.0.dist-info/RECORD +64 -0
  62. mxm_refdata-0.3.0.dist-info/WHEEL +4 -0
  63. mxm_refdata-0.3.0.dist-info/entry_points.txt +3 -0
  64. mxm_refdata-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,102 @@
1
+ """
2
+ mxm.refdata.mappings.period_cycles_vs_orm
3
+
4
+ ORM ↔ domain mappings for PeriodCycle and PeriodCycleMembership.
5
+
6
+ Purpose
7
+ -------
8
+ `mxm-refdata` stores Period cycles as authoritative reference-data artifacts.
9
+ A PeriodCycle is a definition of a cycle over delivery periods, and a
10
+ PeriodCycleMembership places a specific Period into a given cycle with a
11
+ (cycle_instance, cycle_element) assignment.
12
+
13
+ This module provides the pure mapping functions between:
14
+ - ORM models in `mxm.refdata.models.orm.period_cycles`
15
+ and
16
+ - domain models in `mxm.refdata.models.period_cycles`
17
+
18
+ Conventions
19
+ -----------
20
+ - Enums are stored in the database as strings.
21
+ - PeriodType is stored as `PeriodType.name` (e.g. "MONTH", "QUARTER").
22
+ - CycleInstanceKind is stored as its `.value` (e.g. "YEAR").
23
+
24
+ These conventions should be kept stable to preserve refdata portability and
25
+ auditability.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from mxm.refdata.models.orm.period_cycles import (
31
+ PeriodCycleMembershipORM,
32
+ PeriodCycleORM,
33
+ )
34
+ from mxm.refdata.models.period_cycles import (
35
+ CycleInstanceKind,
36
+ PeriodCycle,
37
+ PeriodCycleMembership,
38
+ )
39
+ from mxm.refdata.models.periods import PeriodType
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # PeriodCycle
43
+ # ---------------------------------------------------------------------------
44
+
45
+
46
+ def period_cycle_from_orm(orm: PeriodCycleORM) -> PeriodCycle:
47
+ """
48
+ Map ORM -> domain PeriodCycle.
49
+ """
50
+ return PeriodCycle(
51
+ cycle_id=orm.cycle_id,
52
+ name=orm.name,
53
+ period_type=PeriodType[orm.period_type], # stored as PeriodType.name
54
+ cycle_size=int(orm.cycle_size),
55
+ instance_kind=CycleInstanceKind(orm.instance_kind), # stored as .value
56
+ )
57
+
58
+
59
+ def period_cycle_to_orm(model: PeriodCycle) -> PeriodCycleORM:
60
+ """
61
+ Map domain -> ORM PeriodCycleORM.
62
+ """
63
+ return PeriodCycleORM(
64
+ cycle_id=model.cycle_id,
65
+ name=model.name,
66
+ period_type=model.period_type.name, # store as PeriodType.name
67
+ instance_kind=model.instance_kind.value, # store as .value
68
+ cycle_size=int(model.cycle_size),
69
+ )
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # PeriodCycleMembership
74
+ # ---------------------------------------------------------------------------
75
+
76
+
77
+ def period_cycle_membership_from_orm(
78
+ orm: PeriodCycleMembershipORM,
79
+ ) -> PeriodCycleMembership:
80
+ """
81
+ Map ORM -> domain PeriodCycleMembership.
82
+ """
83
+ return PeriodCycleMembership(
84
+ cycle_id=orm.cycle_id,
85
+ period_id=orm.period_id,
86
+ cycle_instance=int(orm.cycle_instance),
87
+ cycle_element=int(orm.cycle_element),
88
+ )
89
+
90
+
91
+ def period_cycle_membership_to_orm(
92
+ model: PeriodCycleMembership,
93
+ ) -> PeriodCycleMembershipORM:
94
+ """
95
+ Map domain -> ORM PeriodCycleMembershipORM.
96
+ """
97
+ return PeriodCycleMembershipORM(
98
+ cycle_id=model.cycle_id,
99
+ period_id=model.period_id,
100
+ cycle_instance=int(model.cycle_instance),
101
+ cycle_element=int(model.cycle_element),
102
+ )
@@ -0,0 +1,40 @@
1
+ """Mapping Period instances to and from PeriodORM instances."""
2
+
3
+ from mxm.refdata.models.orm.periods import PeriodORM
4
+ from mxm.refdata.models.periods import Period, PeriodType
5
+
6
+
7
+ def period_to_orm(period: Period) -> PeriodORM:
8
+ """
9
+ Map an internal Period object to a PeriodORM instance.
10
+
11
+ Args:
12
+ period (Period): Internal Period instance.
13
+
14
+ Returns:
15
+ PeriodORM: Corresponding ORM representation.
16
+ """
17
+ return PeriodORM(
18
+ period_id=period.period_id,
19
+ period_type=period.period_type,
20
+ first_date=period.first_date,
21
+ last_date=period.last_date,
22
+ )
23
+
24
+
25
+ def period_from_orm(orm: PeriodORM) -> Period:
26
+ """
27
+ Map a PeriodORM instance to an internal Period object.
28
+
29
+ Args:
30
+ orm (PeriodORM): ORM representation of a Period.
31
+
32
+ Returns:
33
+ Period: Internal representation of the Period.
34
+ """
35
+ return Period(
36
+ period_id=orm.period_id,
37
+ period_type=PeriodType[orm.period_type.name],
38
+ first_date=orm.first_date,
39
+ last_date=orm.last_date,
40
+ )
@@ -0,0 +1,31 @@
1
+ """Internal Data Models and ORM for the reference data service."""
2
+
3
+ from .contracts.futures_contract import FuturesContract
4
+ from .currencies import Currency
5
+ from .months import Month
6
+ from .orm.base import Base
7
+ from .orm.futures_contracts import FuturesContractORM
8
+ from .orm.futures_products import FuturesProductORM
9
+ from .orm.periods import PeriodORM
10
+ from .periods import Period, PeriodType
11
+ from .products.futures_product import FuturesProduct, SettlementMethod
12
+ from .reference_events import ReferenceEvent
13
+ from .units import ProductUnit
14
+ from .weekdays import Weekday
15
+
16
+ __all__ = [
17
+ "Base",
18
+ "Currency",
19
+ "FuturesContract",
20
+ "FuturesContractORM",
21
+ "FuturesProduct",
22
+ "FuturesProductORM",
23
+ "Month",
24
+ "Period",
25
+ "PeriodORM",
26
+ "PeriodType",
27
+ "ProductUnit",
28
+ "ReferenceEvent",
29
+ "SettlementMethod",
30
+ "Weekday",
31
+ ]
File without changes
@@ -0,0 +1,22 @@
1
+ """Future contract class."""
2
+
3
+ import datetime
4
+ from dataclasses import dataclass
5
+
6
+ from mxm.refdata.models.currencies import Currency
7
+ from mxm.refdata.models.units import ProductUnit
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class FuturesContract:
12
+ """Represents an individual futures contract."""
13
+
14
+ contract_id: str # Unique identifier for the contract
15
+ product_id: str # Reference to the product ID
16
+ period_id: str # Reference to the period ID
17
+ contract_size: float # The specific size of this contract
18
+ unit: ProductUnit # Pre-populated unit of the product
19
+ currency: Currency # Pre-populated currency of the product
20
+ trading_calendar: str # Pre-populated trading calendar of the product
21
+ first_day_of_interest: datetime.date # The first day of interest for the contract
22
+ last_trading_day: datetime.date # The last trading day for the contract
@@ -0,0 +1,24 @@
1
+ """This module defines the Currency Enum for ISO 4217 currency codes."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class Currency(Enum):
7
+ """Currency Enum for ISO 4217 currency codes."""
8
+
9
+ AUD = "Australian Dollar"
10
+ CAD = "Canadian Dollar"
11
+ CHF = "Swiss Franc"
12
+ EUR = "Euro"
13
+ GBP = "Pound Sterling"
14
+ HKD = "Hong Kong Dollar"
15
+ INR = "Indian Rupee"
16
+ JPY = "Yen"
17
+ MXN = "Mexican Peso"
18
+ NOK = "Norwegian Krone"
19
+ NZD = "New Zealand Dollar"
20
+ SEK = "Swedish Krona"
21
+ SGD = "Singapore Dollar"
22
+ USD = "US Dollar"
23
+ BRL = "Brazilian Real"
24
+ CNY = "Yuan Renminbi"
@@ -0,0 +1,72 @@
1
+ """A data-class encapsulating calendar months and different representations."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ CME_MONTH_CODES = {
6
+ 1: "F",
7
+ 2: "G",
8
+ 3: "H",
9
+ 4: "J",
10
+ 5: "K",
11
+ 6: "M",
12
+ 7: "N",
13
+ 8: "Q",
14
+ 9: "U",
15
+ 10: "V",
16
+ 11: "X",
17
+ 12: "Z",
18
+ }
19
+
20
+ MONTH_STRINGS = {
21
+ 1: "Jan",
22
+ 2: "Feb",
23
+ 3: "Mar",
24
+ 4: "Apr",
25
+ 5: "May",
26
+ 6: "Jun",
27
+ 7: "Jul",
28
+ 8: "Aug",
29
+ 9: "Sep",
30
+ 10: "Oct",
31
+ 11: "Nov",
32
+ 12: "Dec",
33
+ }
34
+
35
+ MONTH_STRINGS_REVERSE = {v: k for k, v in MONTH_STRINGS.items()}
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class Month:
40
+ """Represents a month with various representations."""
41
+
42
+ month: int # 1 (January) to 12 (December)
43
+
44
+ def __post_init__(self):
45
+ if not (1 <= self.month <= 12):
46
+ raise ValueError(f"Invalid month: {self.month}. Must be between 1 and 12.")
47
+
48
+ @property
49
+ def as_int(self) -> int:
50
+ """Return the month as an integer."""
51
+ return self.month
52
+
53
+ @property
54
+ def as_str(self) -> str:
55
+ """Return the month as a three-letter abbreviation."""
56
+ return MONTH_STRINGS[self.month]
57
+
58
+ @property
59
+ def as_cme_code(self) -> str:
60
+ """Return the month as a CME code."""
61
+ return CME_MONTH_CODES[self.month]
62
+
63
+ @classmethod
64
+ def from_str(cls, month_str: str) -> "Month":
65
+ """Create a Month instance from a three-letter abbreviation."""
66
+ month_int = MONTH_STRINGS_REVERSE.get(month_str)
67
+ if not month_int:
68
+ raise ValueError(
69
+ f"Invalid month abbreviation: {month_str}. "
70
+ f"Expected one of {list(MONTH_STRINGS_REVERSE.keys())}."
71
+ )
72
+ return cls(month_int)
@@ -0,0 +1,16 @@
1
+ """ORM models for DB integration."""
2
+
3
+ from .base import Base
4
+ from .futures_contracts import FuturesContractORM
5
+ from .futures_products import FuturesProductORM
6
+ from .period_cycles import PeriodCycleMembershipORM, PeriodCycleORM
7
+ from .periods import PeriodORM
8
+
9
+ __all__ = [
10
+ "Base",
11
+ "FuturesContractORM",
12
+ "FuturesProductORM",
13
+ "PeriodCycleMembershipORM",
14
+ "PeriodCycleORM",
15
+ "PeriodORM",
16
+ ]
@@ -0,0 +1,3 @@
1
+ from sqlalchemy.orm import declarative_base
2
+
3
+ Base = declarative_base()
@@ -0,0 +1,53 @@
1
+ """
2
+ ORM model for the futures_contracts table.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from datetime import date
8
+ from typing import TYPE_CHECKING
9
+
10
+ from sqlalchemy import Date, Enum, Float, ForeignKey, String
11
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
12
+
13
+ from mxm.refdata.models.orm.base import Base
14
+ from mxm.refdata.models.products.futures_product import Currency, ProductUnit
15
+
16
+ if TYPE_CHECKING:
17
+ from mxm.refdata.models.orm.futures_products import FuturesProductORM
18
+ from mxm.refdata.models.orm.periods import PeriodORM
19
+
20
+
21
+ class FuturesContractORM(Base):
22
+ """ORM model for the futures_contracts table."""
23
+
24
+ __tablename__ = "futures_contracts"
25
+
26
+ contract_id: Mapped[str] = mapped_column(String, primary_key=True, nullable=False)
27
+
28
+ product_id: Mapped[str] = mapped_column(
29
+ String,
30
+ ForeignKey("futures_products.product_id"),
31
+ nullable=False,
32
+ )
33
+
34
+ period_id: Mapped[str] = mapped_column(
35
+ String,
36
+ ForeignKey("periods.period_id"),
37
+ nullable=False,
38
+ )
39
+
40
+ contract_size: Mapped[float] = mapped_column(Float, nullable=False)
41
+ currency: Mapped[Currency] = mapped_column(Enum(Currency), nullable=False)
42
+ unit: Mapped[ProductUnit] = mapped_column(Enum(ProductUnit), nullable=False)
43
+
44
+ trading_calendar: Mapped[str] = mapped_column(String, nullable=False)
45
+
46
+ first_day_of_interest: Mapped[date] = mapped_column(Date, nullable=False)
47
+ last_trading_day: Mapped[date] = mapped_column(Date, nullable=False)
48
+
49
+ # Relationships
50
+ product: Mapped[FuturesProductORM] = relationship(
51
+ "FuturesProductORM", back_populates="contracts"
52
+ )
53
+ period: Mapped[PeriodORM] = relationship("PeriodORM", back_populates="contracts")
@@ -0,0 +1,61 @@
1
+ """
2
+ ORM model for the futures_products table.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING
8
+
9
+ from sqlalchemy import Enum, Float, String, Text
10
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
11
+
12
+ from mxm.refdata.models.orm.base import Base
13
+ from mxm.refdata.models.products.futures_product import (
14
+ Currency,
15
+ ProductUnit,
16
+ SettlementMethod,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from mxm.refdata.models.orm.futures_contracts import FuturesContractORM
21
+
22
+
23
+ class FuturesProductORM(Base):
24
+ """ORM model for the futures_products table."""
25
+
26
+ __tablename__ = "futures_products"
27
+
28
+ product_id: Mapped[str] = mapped_column(String, primary_key=True, nullable=False)
29
+ venue: Mapped[str] = mapped_column(String, nullable=False)
30
+ description: Mapped[str] = mapped_column(Text, nullable=False)
31
+
32
+ currency: Mapped[Currency] = mapped_column(Enum(Currency), nullable=False)
33
+ unit: Mapped[ProductUnit] = mapped_column(Enum(ProductUnit), nullable=False)
34
+ contract_size: Mapped[float] = mapped_column(Float, nullable=False)
35
+
36
+ valid_period_rule: Mapped[str] = mapped_column(Text, nullable=False)
37
+ listing_rule: Mapped[str] = mapped_column(Text, nullable=False)
38
+
39
+ period_types: Mapped[str] = mapped_column(Text, nullable=False)
40
+
41
+ settlement: Mapped[SettlementMethod] = mapped_column(
42
+ Enum(SettlementMethod), nullable=False
43
+ )
44
+ last_trading_rule: Mapped[str] = mapped_column(Text, nullable=False)
45
+ expiry_rule: Mapped[str] = mapped_column(Text, nullable=False)
46
+
47
+ trading_calendar: Mapped[str] = mapped_column(String, nullable=False)
48
+
49
+ trading_hours: Mapped[str | None] = mapped_column(Text, nullable=True)
50
+
51
+ tick_size: Mapped[float | None] = mapped_column(Float, nullable=True)
52
+ tick_value: Mapped[float | None] = mapped_column(Float, nullable=True)
53
+
54
+ initial_margin: Mapped[float | None] = mapped_column(Float, nullable=True)
55
+ maintenance_margin: Mapped[float | None] = mapped_column(Float, nullable=True)
56
+
57
+ contracts: Mapped[list[FuturesContractORM]] = relationship(
58
+ "FuturesContractORM",
59
+ back_populates="product",
60
+ cascade="all, delete-orphan",
61
+ )
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlalchemy import ForeignKey, UniqueConstraint
4
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
5
+
6
+ from mxm.refdata.models.orm.base import Base
7
+ from mxm.refdata.models.orm.periods import PeriodORM
8
+
9
+
10
+ class PeriodCycleORM(Base):
11
+ __tablename__ = "period_cycles"
12
+
13
+ cycle_id: Mapped[str] = mapped_column(primary_key=True)
14
+ name: Mapped[str] = mapped_column(nullable=False)
15
+ period_type: Mapped[str] = mapped_column(nullable=False)
16
+ instance_kind: Mapped[str] = mapped_column(nullable=False)
17
+ cycle_size: Mapped[int] = mapped_column(nullable=False)
18
+
19
+ # Relationship: one cycle -> many memberships
20
+ memberships = relationship(
21
+ "PeriodCycleMembershipORM",
22
+ back_populates="cycle",
23
+ cascade="all, delete-orphan",
24
+ lazy="select",
25
+ )
26
+
27
+ def __repr__(self) -> str:
28
+ return (
29
+ f"PeriodCycleORM(cycle_id={self.cycle_id!r}, name={self.name!r}, "
30
+ f"period_type={self.period_type!r}, instance_kind={self.instance_kind!r}, "
31
+ f"cycle_size={self.cycle_size!r})"
32
+ )
33
+
34
+
35
+ class PeriodCycleMembershipORM(Base):
36
+ """
37
+ ORM table: period_cycle_memberships
38
+
39
+ Membership relation: places a specific Period into a given PeriodCycle.
40
+ """
41
+
42
+ __tablename__ = "period_cycle_memberships"
43
+
44
+ # Composite primary key: one period per cycle
45
+ cycle_id: Mapped[str] = mapped_column(
46
+ ForeignKey("period_cycles.cycle_id", ondelete="CASCADE"),
47
+ primary_key=True,
48
+ )
49
+
50
+ period_id: Mapped[str] = mapped_column(
51
+ ForeignKey("periods.period_id", ondelete="CASCADE"),
52
+ primary_key=True,
53
+ )
54
+
55
+ cycle_instance: Mapped[int] = mapped_column(nullable=False)
56
+ cycle_element: Mapped[int] = mapped_column(nullable=False)
57
+
58
+ # Relationships
59
+ cycle: Mapped[PeriodCycleORM] = relationship(
60
+ back_populates="memberships",
61
+ lazy="select",
62
+ )
63
+
64
+ period: Mapped[PeriodORM] = relationship(
65
+ lazy="select",
66
+ )
67
+
68
+ __table_args__ = (
69
+ UniqueConstraint(
70
+ "cycle_id",
71
+ "cycle_instance",
72
+ "cycle_element",
73
+ name="uq_cycle_instance_element",
74
+ ),
75
+ )
76
+
77
+ def __repr__(self) -> str:
78
+ return (
79
+ "PeriodCycleMembershipORM("
80
+ f"cycle_id={self.cycle_id!r}, "
81
+ f"period_id={self.period_id!r}, "
82
+ f"cycle_instance={self.cycle_instance!r}, "
83
+ f"cycle_element={self.cycle_element!r})"
84
+ )
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date
4
+ from typing import TYPE_CHECKING
5
+
6
+ from sqlalchemy import Date, Enum, String
7
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
8
+
9
+ from mxm.refdata.models.orm.base import Base
10
+ from mxm.refdata.models.periods import PeriodType
11
+
12
+ if TYPE_CHECKING:
13
+ from mxm.refdata.models.orm.futures_contracts import FuturesContractORM
14
+
15
+
16
+ class PeriodORM(Base):
17
+ """ORM model for the periods table."""
18
+
19
+ __tablename__ = "periods"
20
+
21
+ period_id: Mapped[str] = mapped_column(
22
+ String, primary_key=True, unique=True, nullable=False
23
+ )
24
+ period_type: Mapped[PeriodType] = mapped_column(Enum(PeriodType), nullable=False)
25
+ first_date: Mapped[date] = mapped_column(Date, nullable=False)
26
+ last_date: Mapped[date] = mapped_column(Date, nullable=False)
27
+
28
+ contracts: Mapped[list[FuturesContractORM]] = relationship(
29
+ "FuturesContractORM", back_populates="period"
30
+ )
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+ from mxm.refdata.models.periods import PeriodType
7
+
8
+
9
+ class CycleInstanceKind(str, Enum):
10
+ """
11
+ Defines what 'cycle_instance' means for a membership row.
12
+
13
+ For calendar cycles, the instance is the calendar year.
14
+ Other instance kinds may be added later (e.g. gas_year).
15
+ """
16
+
17
+ YEAR = "YEAR"
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class PeriodCycle:
22
+ """
23
+ Defines a cycle over periods.
24
+
25
+ A cycle is an interpretation layer over Periods, not a property of Period itself.
26
+
27
+ Example cycles:
28
+ - CALENDAR_MONTHS: element=1..12, instance=year, applies to PeriodType.MONTH
29
+ - CALENDAR_QUARTERS: element=1..4, instance=year, applies to PeriodType.QUARTER
30
+ """
31
+
32
+ cycle_id: str # stable identifier, e.g. "CALENDAR_MONTHS"
33
+ name: str # human readable
34
+ period_type: PeriodType # the period type this cycle classifies
35
+ cycle_size: int # number of elements in one cycle (12, 4, ...)
36
+ instance_kind: CycleInstanceKind = CycleInstanceKind.YEAR
37
+
38
+ def __post_init__(self) -> None:
39
+ if not self.cycle_id:
40
+ raise ValueError("cycle_id must be non-empty")
41
+ if not self.name:
42
+ raise ValueError("name must be non-empty")
43
+ if self.cycle_size < 1:
44
+ raise ValueError("cycle_size must be >= 1")
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class PeriodCycleMembership:
49
+ """
50
+ Membership relation: places a specific Period into a given cycle.
51
+
52
+ A Period may be a member of multiple cycles.
53
+ A cycle may contain many periods.
54
+
55
+ cycle_instance:
56
+ For YEAR instance_kind cycles, this is the calendar year.
57
+ (Other instance kinds may be introduced later.)
58
+ """
59
+
60
+ cycle_id: str
61
+ period_id: str
62
+ cycle_instance: int
63
+ cycle_element: int # 1..cycle_size
64
+
65
+ def __post_init__(self) -> None:
66
+ if not self.cycle_id:
67
+ raise ValueError("cycle_id must be non-empty")
68
+ if not self.period_id:
69
+ raise ValueError("period_id must be non-empty")
70
+ if self.cycle_instance <= 0:
71
+ raise ValueError("cycle_instance must be > 0")
72
+ if self.cycle_element < 1:
73
+ raise ValueError("cycle_element must be >= 1")
@@ -0,0 +1,64 @@
1
+ """Reference periods for futures contracts."""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import date
5
+ from enum import Enum
6
+ from functools import total_ordering
7
+
8
+ import pandas as pd
9
+
10
+
11
+ class PeriodType(Enum):
12
+ """Period types for futures contracts."""
13
+
14
+ YEAR = "year"
15
+ QUARTER = "quarter"
16
+ MONTH = "month"
17
+ WEEK = "week"
18
+ DAY = "day"
19
+
20
+
21
+ PERIOD_PRIORITY = {
22
+ PeriodType.YEAR: 1,
23
+ PeriodType.QUARTER: 2,
24
+ PeriodType.MONTH: 3,
25
+ PeriodType.WEEK: 4,
26
+ PeriodType.DAY: 5,
27
+ }
28
+
29
+
30
+ @total_ordering
31
+ @dataclass(frozen=True)
32
+ class Period:
33
+ """Represents a specific calendar period."""
34
+
35
+ period_id: (
36
+ str # A unique identifier, like "2024", "Jan-2024", "2024-Q1", or "2024-W24"
37
+ )
38
+ period_type: PeriodType # The type of the period (year, month, quarter, week)
39
+ first_date: date # The first day of the period
40
+ last_date: date # The last day of the period
41
+
42
+ def __post_init__(self):
43
+ if self.first_date > self.last_date:
44
+ raise ValueError("first_date must not be after last_date")
45
+
46
+ def __str__(self):
47
+ return f"{self.period_type.name} Period: {self.period_id} ({self.first_date} to {self.last_date})"
48
+
49
+ def __repr__(self):
50
+ return f"Period(period_id='{self.period_id}', period_type={self.period_type}, first_date={self.first_date}, last_date={self.last_date})"
51
+
52
+ def __lt__(self, other: "Period") -> bool:
53
+ """Define sorting: higher-level periods (Year > Month) first, then by start date."""
54
+
55
+ if PERIOD_PRIORITY[self.period_type] != PERIOD_PRIORITY[other.period_type]:
56
+ return (
57
+ PERIOD_PRIORITY[self.period_type] < PERIOD_PRIORITY[other.period_type]
58
+ )
59
+
60
+ return self.first_date < other.first_date
61
+
62
+ def to_daterange(self) -> pd.DatetimeIndex:
63
+ """Return a pandas date_range from first_date to last_date, with daily frequency."""
64
+ return pd.date_range(self.first_date, self.last_date, freq="D")
@@ -0,0 +1 @@
1
+ """Models for financial products."""