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,38 @@
1
+ """DataClass for a futures product."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from mxm.refdata.models.currencies import Currency
6
+ from mxm.refdata.models.periods import PeriodType
7
+ from mxm.refdata.models.products.settlement import SettlementMethod
8
+ from mxm.refdata.models.units import ProductUnit
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class FuturesProduct:
13
+ """Represents a tradeable futures product."""
14
+
15
+ product_id: str # Internal product code (aligned with venue's default code)
16
+ venue: str # The venue (e.g., exchange or counterparty) offering the product
17
+ description: str # Long-form name and/or description
18
+ currency: Currency # Currency of the product (from Currency enum)
19
+ unit: ProductUnit # Physical unit (from ProductUnit enum)
20
+ contract_size: float # Number of physical units per contract
21
+ valid_period_rule: (
22
+ str # Rule for determining valid trading periods (e.g., "FGHJKMNQUVXZ")
23
+ )
24
+ listing_rule: (
25
+ str # Rule for determining available contracts (e.g., "monthly for all months")
26
+ )
27
+ period_types: tuple[PeriodType, ...]
28
+ settlement: SettlementMethod # Settlement type (physical, financial, etc.)
29
+ last_trading_rule: str # Rule for determining last trading day (e.g., "3rd last business day of delivery month")
30
+ expiry_rule: str # Rule for determining contract expiry (e.g., "3rd Friday of delivery month")
31
+ trading_calendar: (
32
+ str # Placeholder for the trading calendar rule (e.g. CME default)
33
+ )
34
+ trading_hours: str | None = None # String representation of trading hours
35
+ tick_size: float | None = None
36
+ tick_value: float | None = None
37
+ initial_margin: float | None = None
38
+ maintenance_margin: float | None = None
@@ -0,0 +1,10 @@
1
+ from enum import Enum
2
+
3
+
4
+ class SettlementMethod(Enum):
5
+ """Settlement methods for FuturesProducts."""
6
+
7
+ PHYSICAL = "physical"
8
+ FINANCIAL = "financial"
9
+ CASH = "cash"
10
+ OTHER = "other"
@@ -0,0 +1,9 @@
1
+ from enum import Enum
2
+
3
+
4
+ class ReferenceEvent(Enum):
5
+ """Enum representing the possible reference events for last trading day rules."""
6
+
7
+ BUSINESS_DAY_OF_PERIOD = "business_day_of_period"
8
+ CALENDAR_DAY_OF_PERIOD = "calendar_day_of_period"
9
+ WEEKDAY_OF_PERIOD = "weekday_of_period"
@@ -0,0 +1,28 @@
1
+ """Physical Units for Product and Contract denomination."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class ProductUnit(Enum):
7
+ """Enumeration of physical units for various financial contracts."""
8
+
9
+ LOT = "Lot" # General unit for futures or contracts representing multiple shares, bonds, or other assets
10
+ NOTIONAL = "Notional Value" # Used to represent the face value for bond contracts
11
+ SHARE = "Share" # Used when trading the underlying equity (e.g., spot transactions)
12
+ BOND = "Bond" # Spot trading of a single bond (typically unusual; bonds are often traded in lots)
13
+ BARREL = "Barrel" # Oil and other liquid commodities
14
+ TONNE = "Tonne" # Bulk commodities like metals, grains
15
+ BUSHEL = "Bushel" # Agricultural commodities
16
+ TROY_OUNCE = "Troy Ounce" # Precious metals (e.g., gold, silver)
17
+ MWH = "Megawatt-hour" # Electricity
18
+ GALLON = "Gallon" # Energy products (e.g., gasoline)
19
+ CUBIC_METER = "Cubic Meter" # Natural gas
20
+ CONTRACT = "Contract" # General for options, swaps, and other derivatives
21
+ OUNCE = "Ounce" # General mass unit (e.g., for agricultural products)
22
+ GRAM = "Gram" # For precious metals and small-scale commodities
23
+ LITER = "Liter" # Volume-based commodities
24
+ METRIC_TON = "Metric Ton" # Alternative to tonne
25
+ CURRENCY_UNIT = "Currency Unit" # For FX contracts (e.g., USD, EUR)
26
+ INDEX_POINT = "Index Point" # For index-based contracts
27
+ MMBTU = "MMBtu" # Natural gas
28
+ GBP = "GBP" # British Pound Sterling (FX Futures)
@@ -0,0 +1,66 @@
1
+ """A data-class encapsulating weekdays and different representations."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ WEEKDAY_STRINGS = {
6
+ 0: "Monday",
7
+ 1: "Tuesday",
8
+ 2: "Wednesday",
9
+ 3: "Thursday",
10
+ 4: "Friday",
11
+ 5: "Saturday",
12
+ 6: "Sunday",
13
+ }
14
+
15
+ WEEKDAY_STRINGS_ABBR = {
16
+ 0: "Mon",
17
+ 1: "Tue",
18
+ 2: "Wed",
19
+ 3: "Thu",
20
+ 4: "Fri",
21
+ 5: "Sat",
22
+ 6: "Sun",
23
+ }
24
+
25
+ WEEKDAY_LOOKUP = {
26
+ **{v.lower(): k for k, v in WEEKDAY_STRINGS.items()}, # Full names
27
+ **{v.lower(): k for k, v in WEEKDAY_STRINGS_ABBR.items()}, # Abbreviations
28
+ }
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class Weekday:
33
+ """Represents a weekday with various representations."""
34
+
35
+ weekday: int # 0 (Monday) to 6 (Sunday)
36
+
37
+ def __post_init__(self):
38
+ if not (0 <= self.weekday <= 6):
39
+ raise ValueError(
40
+ f"Invalid weekday: {self.weekday}. Must be between 0 (Monday) and 6 (Sunday)."
41
+ )
42
+
43
+ @property
44
+ def as_int(self) -> int:
45
+ """Return the weekday as an integer."""
46
+ return self.weekday
47
+
48
+ @property
49
+ def as_str(self) -> str:
50
+ """Return the weekday as a full name."""
51
+ return WEEKDAY_STRINGS[self.weekday]
52
+
53
+ @property
54
+ def as_abbr(self) -> str:
55
+ """Return the weekday as a three-letter abbreviation."""
56
+ return WEEKDAY_STRINGS_ABBR[self.weekday]
57
+
58
+ @classmethod
59
+ def from_str(cls, weekday_str: str) -> "Weekday":
60
+ """Create a Weekday instance from a full name or abbreviation."""
61
+ weekday_int = WEEKDAY_LOOKUP.get(weekday_str.lower())
62
+ if weekday_int is None:
63
+ raise ValueError(
64
+ f"Invalid weekday name: {weekday_str}. Expected one of {list(WEEKDAY_LOOKUP.keys())}."
65
+ )
66
+ return cls(weekday_int)
@@ -0,0 +1 @@
1
+ """Parsing logic from external sources to internal normalised data."""
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+
5
+ from mxm.refdata.models.currencies import Currency
6
+ from mxm.refdata.models.products.futures_product import FuturesProduct, SettlementMethod
7
+ from mxm.refdata.models.units import ProductUnit
8
+ from mxm.refdata.utils.period_types_codec import decode_period_types
9
+
10
+
11
+ def parse_futures_products_csv(csv_file_path: str) -> list[FuturesProduct]:
12
+ """Parse futures_products.csv into FuturesProduct domain objects."""
13
+ required_fields = {
14
+ "product_id",
15
+ "venue",
16
+ "description",
17
+ "currency",
18
+ "unit",
19
+ "contract_size",
20
+ "valid_period_rule",
21
+ "listing_rule",
22
+ "period_types",
23
+ "settlement",
24
+ "last_trading_rule",
25
+ "expiry_rule",
26
+ "trading_calendar",
27
+ "trading_hours",
28
+ "tick_size",
29
+ "tick_value",
30
+ "initial_margin",
31
+ "maintenance_margin",
32
+ }
33
+
34
+ def _fopt(x: str | None) -> float | None:
35
+ s = (x or "").strip()
36
+ return float(s) if s else None
37
+
38
+ def _freq(x: str | None, field: str) -> float:
39
+ s = (x or "").strip()
40
+ if not s:
41
+ raise ValueError(f"required numeric field {field!r} is empty")
42
+ return float(s)
43
+
44
+ with open(csv_file_path, encoding="utf-8-sig") as csvfile:
45
+ reader = csv.DictReader(csvfile)
46
+ products: list[FuturesProduct] = []
47
+
48
+ for row in reader:
49
+ missing_fields = required_fields - set(row.keys())
50
+ if missing_fields:
51
+ raise ValueError(f"Missing required fields in CSV: {missing_fields}")
52
+
53
+ products.append(
54
+ FuturesProduct(
55
+ product_id=row["product_id"],
56
+ venue=row["venue"],
57
+ description=row["description"],
58
+ currency=Currency[row["currency"]],
59
+ unit=ProductUnit[row["unit"]],
60
+ contract_size=_freq(row.get("contract_size"), "contract_size"),
61
+ valid_period_rule=row["valid_period_rule"],
62
+ listing_rule=row["listing_rule"],
63
+ period_types=decode_period_types(row["period_types"]),
64
+ settlement=SettlementMethod[row["settlement"]],
65
+ last_trading_rule=row["last_trading_rule"],
66
+ expiry_rule=row["expiry_rule"],
67
+ trading_calendar=row["trading_calendar"],
68
+ trading_hours=(row.get("trading_hours") or "").strip() or None,
69
+ tick_size=_fopt(row.get("tick_size")),
70
+ tick_value=_fopt(row.get("tick_value")),
71
+ initial_margin=_fopt(row.get("initial_margin")),
72
+ maintenance_margin=_fopt(row.get("maintenance_margin")),
73
+ )
74
+ )
75
+
76
+ return products
mxm/refdata/py.typed ADDED
File without changes
@@ -0,0 +1 @@
1
+ """Scripts using refData functionality."""
@@ -0,0 +1,61 @@
1
+ """CLI script for managing the database."""
2
+
3
+ import argparse
4
+ import logging
5
+
6
+ from mxm.refdata.database.sql_session_manager import SQLSessionManager
7
+
8
+ logging.basicConfig(level=logging.INFO)
9
+
10
+
11
+ def main():
12
+ """Command-line interface for database management."""
13
+ parser = argparse.ArgumentParser(description="Database management script.")
14
+ parser.add_argument(
15
+ "--init", action="store_true", help="Initialize the database schema."
16
+ )
17
+ parser.add_argument(
18
+ "--drop", action="store_true", help="Drop all tables in the database."
19
+ )
20
+ parser.add_argument(
21
+ "--check", action="store_true", help="Check the database connection."
22
+ )
23
+
24
+ args = parser.parse_args()
25
+
26
+ # Use the default SQLSessionManager instance
27
+ session_manager = SQLSessionManager()
28
+
29
+ if args.init:
30
+ success = session_manager.init_db()
31
+ if success:
32
+ logging.info("Database initialized successfully.")
33
+ else:
34
+ logging.error("Failed to initialize the database.")
35
+
36
+ elif args.drop:
37
+ confirmation = input(
38
+ "WARNING: This will delete all data in the database! Type 'yes' to confirm: "
39
+ )
40
+ if confirmation.lower() == "yes":
41
+ success = session_manager.drop_db()
42
+ if success:
43
+ logging.info("Database dropped successfully.")
44
+ else:
45
+ logging.error("Failed to drop the database.")
46
+ else:
47
+ logging.info("Database drop operation aborted.")
48
+
49
+ elif args.check:
50
+ success = session_manager.check_db_connection()
51
+ if success:
52
+ logging.info("Database connection is active.")
53
+ else:
54
+ logging.error("Database connection check failed.")
55
+
56
+ else:
57
+ parser.print_help()
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()
@@ -0,0 +1,73 @@
1
+ """Manage the static reference data in the database."""
2
+
3
+ import argparse
4
+ import logging
5
+ from datetime import date
6
+
7
+ from mxm.refdata.database.sql_session_manager import SQLSessionManager
8
+ from mxm.refdata.services.ref_data_service import RefDataService
9
+ from mxm.refdata.utils.config import load_config
10
+
11
+ # Configure logging
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
15
+ )
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def main():
20
+ """CLI script to manage reference data: reset and initialize database."""
21
+ # Load default config
22
+ config = load_config()
23
+ default_csv_path = (
24
+ config.REFDATA_FUTURES_PRODUCTS_CSV_PATH or "data/futures_products.csv"
25
+ )
26
+
27
+ # Argument parser
28
+ parser = argparse.ArgumentParser(description="Manage the reference data database.")
29
+ parser.add_argument(
30
+ "--reset",
31
+ action="store_true",
32
+ help="Reset the database before initializing (WARNING: This deletes all existing data!).",
33
+ )
34
+ parser.add_argument(
35
+ "--csv",
36
+ type=str,
37
+ default=default_csv_path,
38
+ help=f"Path to the CSV file for futures products (default: {default_csv_path}).",
39
+ )
40
+ parser.add_argument(
41
+ "--start-date",
42
+ type=lambda s: date.fromisoformat(s),
43
+ default=date(2000, 1, 1),
44
+ help="Start date for period and contract initialization (format: YYYY-MM-DD, default: 2000-01-01).",
45
+ )
46
+ parser.add_argument(
47
+ "--end-date",
48
+ type=lambda s: date.fromisoformat(s),
49
+ default=date(2045, 12, 31),
50
+ help="End date for period and contract initialization (format: YYYY-MM-DD, default: 2045-12-31).",
51
+ )
52
+
53
+ args = parser.parse_args()
54
+
55
+ logger.info("Initializing Reference Data Service...")
56
+ session_manager = SQLSessionManager()
57
+ ref_data_service = RefDataService(session_manager=session_manager)
58
+
59
+ # Optionally reset the database
60
+ if args.reset:
61
+ logger.warning("Resetting the database. All existing data will be deleted!")
62
+ ref_data_service.reset_database()
63
+
64
+ # Initialize reference data
65
+ logger.info("Starting reference data setup...")
66
+ ref_data_service.setup_instruments(
67
+ csv_file_path=args.csv, start_date=args.start_date, end_date=args.end_date
68
+ )
69
+ logger.info("Reference data initialization completed successfully.")
70
+
71
+
72
+ if __name__ == "__main__":
73
+ main()
File without changes
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date
4
+
5
+ from sqlalchemy.exc import SQLAlchemyError
6
+
7
+ from mxm.refdata.database.sql_session_manager import SQLSessionManager
8
+ from mxm.refdata.models.orm.futures_products import FuturesProductORM
9
+ from mxm.refdata.utils.config import Config
10
+
11
+
12
+ class RefDataNotInitialisedError(RuntimeError):
13
+ """Raised when refdata is required but DB auto-initialisation is forbidden."""
14
+
15
+
16
+ def _db_has_any_products(session_manager: SQLSessionManager) -> bool:
17
+ try:
18
+ with session_manager.db_session_scope() as session:
19
+ return session.query(FuturesProductORM).limit(1).first() is not None
20
+ except SQLAlchemyError:
21
+ return False
22
+
23
+
24
+ def build_refdata(
25
+ *,
26
+ session_manager: SQLSessionManager | None = None,
27
+ csv_file_path: str | None = None,
28
+ start_date: date | None = None,
29
+ end_date: date | None = None,
30
+ ) -> None:
31
+ """Build the refdata database without first dropping existing tables."""
32
+ from mxm.refdata.services.ref_data_service import RefDataService
33
+
34
+ sm = session_manager or SQLSessionManager()
35
+ sm.init_db()
36
+
37
+ RefDataService(session_manager=sm).setup_instruments(
38
+ csv_file_path=csv_file_path,
39
+ start_date=start_date,
40
+ end_date=end_date,
41
+ )
42
+
43
+
44
+ def rebuild_refdata(
45
+ *,
46
+ session_manager: SQLSessionManager | None = None,
47
+ csv_file_path: str | None = None,
48
+ start_date: date | None = None,
49
+ end_date: date | None = None,
50
+ ) -> None:
51
+ """Destructively rebuild the refdata database."""
52
+ from mxm.refdata.services.ref_data_service import RefDataService
53
+
54
+ sm = session_manager or SQLSessionManager()
55
+ svc = RefDataService(session_manager=sm)
56
+
57
+ svc.reset_database()
58
+ svc.setup_instruments(
59
+ csv_file_path=csv_file_path,
60
+ start_date=start_date,
61
+ end_date=end_date,
62
+ )
63
+
64
+
65
+ def ensure_refdata_ready(session_manager: SQLSessionManager, cfg: Config) -> None:
66
+ """
67
+ Ensure the refdata DB is initialised and populated.
68
+
69
+ If already populated, this is a no-op. If empty and buildable, the packaged
70
+ ontology is materialised. If managed, missing refdata is an error.
71
+ """
72
+ if _db_has_any_products(session_manager):
73
+ return
74
+
75
+ if cfg.REFDATA_DB_MODE != "buildable":
76
+ raise RefDataNotInitialisedError(
77
+ "Refdata database is not initialised and auto-creation is forbidden "
78
+ f"(REFDATA_DB_MODE={cfg.REFDATA_DB_MODE!r}). Initialise refdata explicitly."
79
+ )
80
+
81
+ build_refdata(
82
+ session_manager=session_manager,
83
+ csv_file_path=cfg.REFDATA_FUTURES_PRODUCTS_CSV_PATH,
84
+ )
@@ -0,0 +1,127 @@
1
+ """Factory to create futures contracts."""
2
+
3
+ import datetime
4
+ import threading
5
+ from typing import ClassVar
6
+
7
+ from mxm.refdata.models.contracts.futures_contract import FuturesContract
8
+ from mxm.refdata.models.months import Month
9
+ from mxm.refdata.models.periods import Period, PeriodType
10
+ from mxm.refdata.models.products.futures_product import FuturesProduct
11
+ from mxm.refdata.trading_calendars.first_day_of_interest import (
12
+ calculate_first_day_of_interest,
13
+ )
14
+ from mxm.refdata.trading_calendars.last_trading_day import calculate_last_trading_day
15
+ from mxm.refdata.trading_calendars.trading_calendar import TradingCalendar
16
+
17
+
18
+ class FuturesContractFactory:
19
+ """Factory for generating and retrieving FuturesContract instances."""
20
+
21
+ _instance = None # Singleton instance
22
+ _lock = threading.Lock() # Lock for thread safety
23
+ _cache: ClassVar[dict[str, FuturesContract]] = {} # Cache for created contracts
24
+
25
+ def __new__(cls) -> "FuturesContractFactory":
26
+ """Ensures only one instance of FuturesContractFactory is created."""
27
+ with cls._lock:
28
+ if cls._instance is None:
29
+ cls._instance = super().__new__(cls)
30
+ return cls._instance
31
+
32
+ def create_contracts_for_product(
33
+ self, product: FuturesProduct, available_periods: dict[str, Period]
34
+ ) -> list[FuturesContract]:
35
+ """
36
+ Create all FuturesContract objects for a given product using available periods.
37
+
38
+ Args:
39
+ product (FuturesProduct): The product for which contracts are created.
40
+ available_periods (Dict[str, Period]): Dictionary of period_id -> Period instances.
41
+
42
+ Returns:
43
+ list[FuturesContract]: A list of valid FuturesContract objects.
44
+ """
45
+ contracts: list[FuturesContract] = []
46
+
47
+ for period in available_periods.values():
48
+ if period.period_type not in product.period_types:
49
+ continue # Skip periods that don't match product requirements
50
+
51
+ if self._is_valid_period(product, period):
52
+ contract_id = self._create_contract_id(
53
+ product.product_id, period.period_id
54
+ )
55
+
56
+ if contract_id not in self._cache:
57
+ contract = self.create_contract(product, period)
58
+ self._cache[contract_id] = contract
59
+ else:
60
+ contract = self._cache[contract_id]
61
+
62
+ contracts.append(contract)
63
+
64
+ return contracts
65
+
66
+ def _is_valid_period(self, product: FuturesProduct, period: Period) -> bool:
67
+ """
68
+ Determine whether a period is valid for a given product based on valid_period_rule.
69
+
70
+ Args:
71
+ product (FuturesProduct): The product being evaluated.
72
+ period (Period): The period being checked.
73
+
74
+ Returns:
75
+ bool: True if the period is valid for the product.
76
+ """
77
+ if period.period_type == PeriodType.MONTH:
78
+ return (
79
+ Month(period.first_date.month).as_cme_code in product.valid_period_rule
80
+ )
81
+
82
+ return True
83
+
84
+ def clear_cache(self):
85
+ """Clear the cache of created contracts."""
86
+ self._cache.clear()
87
+
88
+ def _create_contract_id(self, product_id: str, period_id: str) -> str:
89
+ """Create a unique contract ID from product and period IDs."""
90
+ return f"{product_id}.{period_id}"
91
+
92
+ def create_contract(
93
+ self, product: FuturesProduct, period: Period
94
+ ) -> FuturesContract:
95
+ """
96
+ Create a FuturesContract object for a given product and period.
97
+
98
+ Args:
99
+ product (FuturesProduct): The product for which the contract is created.
100
+ period (Period): The delivery period for the contract.
101
+
102
+ Returns:
103
+ FuturesContract: A valid FuturesContract object.
104
+ """
105
+ trading_calendar = TradingCalendar(
106
+ product.trading_calendar,
107
+ start=datetime.date(1980, 1, 1),
108
+ end=datetime.date(2046, 12, 31),
109
+ )
110
+ last_trading_day = calculate_last_trading_day(
111
+ product.product_id, period, trading_calendar
112
+ )
113
+ first_day_of_interest = calculate_first_day_of_interest(
114
+ product.product_id, period, trading_calendar
115
+ )
116
+
117
+ return FuturesContract(
118
+ product_id=product.product_id,
119
+ period_id=period.period_id,
120
+ contract_id=self._create_contract_id(product.product_id, period.period_id),
121
+ contract_size=product.contract_size,
122
+ currency=product.currency,
123
+ unit=product.unit,
124
+ trading_calendar=product.trading_calendar,
125
+ first_day_of_interest=first_day_of_interest,
126
+ last_trading_day=last_trading_day,
127
+ )