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,74 @@
1
+ """Calculate the first day we are interested in a given FuturesContract."""
2
+
3
+ import json
4
+ from datetime import date
5
+ from importlib.resources import files
6
+
7
+ from mxm.refdata.models import Period, PeriodType
8
+ from mxm.refdata.services.period_factory import PeriodFactory
9
+ from mxm.refdata.trading_calendars.last_trading_day import calculate_last_trading_day
10
+ from mxm.refdata.trading_calendars.trading_calendar import TradingCalendar
11
+
12
+ # Load first_day_of_interest rules from JSON file
13
+ FIRST_DAY_OF_INTEREST_RULES = json.loads(
14
+ files("mxm.refdata")
15
+ .joinpath("data/first_day_of_interest_rule.json")
16
+ .read_text(encoding="utf-8")
17
+ )
18
+
19
+
20
+ def calculate_first_day_of_interest(
21
+ product_id: str, period: Period, trading_calendar: TradingCalendar
22
+ ) -> date:
23
+ """
24
+ Calculate the first_day_of_interest for a given futures contract.
25
+
26
+ Parameters:
27
+ - product_id (str): The product identifier for the futures contract.
28
+ - period (Period): The period representing the contract's expiration/delivery period.
29
+ - trading_calendar (TradingCalendar): The trading calendar to use for business day calculations.
30
+
31
+ Returns:
32
+ - date: The first business day we are interested in a given FuturesContract.
33
+
34
+ Raises:
35
+ - ValueError: If the product_id is not found in the JSON rules.
36
+ - ValueError: If period_type is not MONTH.
37
+ """
38
+ if product_id not in FIRST_DAY_OF_INTEREST_RULES:
39
+ raise ValueError(
40
+ f"No first_day_of_interest rule found for product_id: {product_id}"
41
+ )
42
+
43
+ if period.period_type != PeriodType.MONTH:
44
+ raise ValueError(f"Unsupported period_type: {period.period_type}")
45
+
46
+ product_rules = FIRST_DAY_OF_INTEREST_RULES[product_id]
47
+ month_str = period.first_date.strftime("%b")
48
+ try:
49
+ shift_n = product_rules["shift_rule"]["n_shift"][month_str]
50
+ except KeyError as e:
51
+ raise KeyError(
52
+ f"No shift value found for month '{month_str}' in product_id: {product_id}"
53
+ ) from e
54
+
55
+ shifted_period = PeriodFactory().shift_period_by_n(period, n=-shift_n)
56
+
57
+ # Determine reference date based on reference rule
58
+ reference_rule = product_rules["reference_rule"]
59
+
60
+ if reference_rule == "next_b_day_after_last_trading_day_of_december":
61
+ december_period = PeriodFactory.get_period(
62
+ date_obj=date(shifted_period.last_date.year, 12, 1),
63
+ period_type=PeriodType.MONTH,
64
+ )
65
+
66
+ reference_date = calculate_last_trading_day(
67
+ product_id, december_period, trading_calendar
68
+ )
69
+ elif reference_rule == "next_b_day_after_period":
70
+ reference_date = shifted_period.last_date
71
+ else:
72
+ raise ValueError(f"Unsupported reference_rule: {reference_rule}")
73
+
74
+ return trading_calendar.get_nth_business_day_relative_to_date(reference_date, n=1)
@@ -0,0 +1,117 @@
1
+ import datetime
2
+ import json
3
+ from importlib.resources import files
4
+ from typing import NotRequired, TypedDict, cast
5
+
6
+ from mxm.refdata.models.periods import Period
7
+ from mxm.refdata.models.reference_events import ReferenceEvent
8
+ from mxm.refdata.models.weekdays import Weekday
9
+ from mxm.refdata.services.period_factory import PeriodFactory
10
+ from mxm.refdata.trading_calendars.nth_business_day import (
11
+ get_nth_business_day_of_period,
12
+ )
13
+ from mxm.refdata.trading_calendars.nth_calendar_day_of_period import (
14
+ get_nth_calendar_day_of_period,
15
+ )
16
+ from mxm.refdata.trading_calendars.nth_weekday_of_period import (
17
+ get_nth_weekday_of_period,
18
+ )
19
+ from mxm.refdata.trading_calendars.trading_calendar import TradingCalendar
20
+
21
+
22
+ class LastTradingRule(TypedDict):
23
+ period_offset: int
24
+ reference_event: str
25
+ n_reference: int
26
+ business_day_offset: int
27
+ weekday: NotRequired[str]
28
+
29
+
30
+ type LastTradingRules = dict[str, LastTradingRule]
31
+
32
+ TRADING_RULES: LastTradingRules = cast(
33
+ LastTradingRules,
34
+ json.loads(
35
+ files("mxm.refdata")
36
+ .joinpath("data/last_trading_rule.json")
37
+ .read_text(encoding="utf-8")
38
+ ),
39
+ )
40
+
41
+
42
+ def get_reference_date(
43
+ *,
44
+ product_id: str,
45
+ adjusted_period: Period,
46
+ rule: LastTradingRule,
47
+ trading_calendar: TradingCalendar,
48
+ ) -> datetime.date:
49
+ """Resolve the reference date specified by a product last-trading-day rule."""
50
+ reference_event = ReferenceEvent(rule["reference_event"])
51
+ n_reference = int(rule["n_reference"])
52
+
53
+ match reference_event:
54
+ case ReferenceEvent.BUSINESS_DAY_OF_PERIOD:
55
+ return get_nth_business_day_of_period(
56
+ adjusted_period,
57
+ n_reference,
58
+ trading_calendar,
59
+ )
60
+
61
+ case ReferenceEvent.CALENDAR_DAY_OF_PERIOD:
62
+ return get_nth_calendar_day_of_period(
63
+ adjusted_period,
64
+ n_reference,
65
+ )
66
+
67
+ case ReferenceEvent.WEEKDAY_OF_PERIOD:
68
+ weekday_raw = rule.get("weekday")
69
+ if not isinstance(weekday_raw, str):
70
+ raise ValueError(
71
+ f"Missing or invalid 'weekday' key in trading rule for {product_id}"
72
+ )
73
+
74
+ return get_nth_weekday_of_period(
75
+ adjusted_period,
76
+ Weekday.from_str(weekday_raw).as_int,
77
+ n_reference,
78
+ )
79
+
80
+ case _:
81
+ raise ValueError(f"Unsupported reference_event: {reference_event!r}")
82
+
83
+
84
+ def calculate_last_trading_day(
85
+ product_id: str,
86
+ period: Period,
87
+ trading_calendar: TradingCalendar,
88
+ ) -> datetime.date:
89
+ """
90
+ Calculate the last trading day for a futures contract.
91
+
92
+ The calculation is based on the product-specific last-trading-day rule,
93
+ the contract period, and the relevant trading calendar.
94
+ """
95
+ if product_id not in TRADING_RULES:
96
+ raise KeyError(f"No trading rule found for product: {product_id}")
97
+
98
+ rule = TRADING_RULES[product_id]
99
+
100
+ adjusted_period = PeriodFactory.shift_period_by_n(
101
+ period,
102
+ int(rule["period_offset"]),
103
+ )
104
+
105
+ reference_date = get_reference_date(
106
+ product_id=product_id,
107
+ adjusted_period=adjusted_period,
108
+ rule=rule,
109
+ trading_calendar=trading_calendar,
110
+ )
111
+
112
+ business_day_offset = int(rule.get("business_day_offset", 0))
113
+
114
+ return trading_calendar.get_nth_business_day_relative_to_date(
115
+ reference_date,
116
+ business_day_offset,
117
+ )
@@ -0,0 +1,52 @@
1
+ """Calculate n-th business day of a period."""
2
+
3
+ import datetime
4
+
5
+ from mxm.refdata.models.periods import Period
6
+ from mxm.refdata.trading_calendars.trading_calendar import TradingCalendar
7
+
8
+
9
+ def get_nth_business_day_of_period(
10
+ period: Period, n: int, trading_calendar: TradingCalendar
11
+ ) -> datetime.date:
12
+ """
13
+ Returns the N-th business day of the given period.
14
+
15
+ Supports:
16
+ - **Positive `n` values** (e.g., `n=1` → First business day).
17
+ - **Negative `n` values** (e.g., `n=-1` → Last business day).
18
+
19
+ Args:
20
+ period (Period): The period (e.g., contract month, quarter).
21
+ n (int): The N-th business day (1-based index for forward search, -1-based for reverse search).
22
+ trading_calendar (TradingCalendar): The trading calendar to determine business days.
23
+
24
+ Returns:
25
+ datetime.date: The date of the N-th business day.
26
+
27
+ Raises:
28
+ ValueError: If the N-th business day does not exist in the period.
29
+
30
+ Examples:
31
+ - **Contract Month (June 2025)**:
32
+ - `get_nth_business_day_of_period(Period(2025, 6), 1, trading_calendar)` → First business day of June 2025.
33
+ - `get_nth_business_day_of_period(Period(2025, 6), -1, trading_calendar)` → Last business day of June 2025.
34
+
35
+ - **Quarter (Q3 2025)**:
36
+ - `get_nth_business_day_of_period(Period(2025, 7, 9), 5, trading_calendar)` → Fifth business day of Q3 2025.
37
+ """
38
+ all_dates = period.to_daterange()
39
+
40
+ # Get valid business days within the period
41
+ business_days = [
42
+ date
43
+ for date in trading_calendar.get_sessions_in_range(all_dates[0], all_dates[-1])
44
+ ]
45
+
46
+ # Ensure we return a date object, not a timestamp
47
+ try:
48
+ return business_days[n - 1].date() if n > 0 else business_days[n].date()
49
+ except IndexError as err:
50
+ raise ValueError(
51
+ f"The {n}-th business day does not exist in {period.period_id}."
52
+ ) from err
@@ -0,0 +1,43 @@
1
+ import datetime
2
+
3
+ from mxm.refdata.models.periods import Period
4
+
5
+
6
+ def get_nth_calendar_day_of_period(period: Period, n: int) -> datetime.date:
7
+ """
8
+ Returns the N-th calendar day of the given period.
9
+
10
+ Supports:
11
+ - **Positive `n` values** (e.g., `n=1` → First day of the period).
12
+ - **Negative `n` values** (e.g., `n=-1` → Last day of the period).
13
+
14
+ Args:
15
+ period (Period): The period (e.g., contract month, quarter).
16
+ n (int): The N-th calendar day (1-based index for forward search, -1-based for reverse search).
17
+
18
+ Returns:
19
+ datetime.date: The date of the N-th calendar day.
20
+
21
+ Raises:
22
+ ValueError: If the N-th calendar day does not exist in the period.
23
+
24
+ Examples:
25
+ - **Contract Month (June 2025)**:
26
+ - `get_nth_calendar_day_of_period(Period(2025, 6), 1)` → June 1, 2025.
27
+ - `get_nth_calendar_day_of_period(Period(2025, 6), -1)` → June 30, 2025.
28
+
29
+ - **Quarter (Q3 2025)**:
30
+ - `get_nth_calendar_day_of_period(Period(2025, 7, 9), 5)` → July 5, 2025.
31
+ - `get_nth_calendar_day_of_period(Period(2025, 7, 9), -5)` → September 26, 2025.
32
+ """
33
+ all_days = period.to_daterange()
34
+
35
+ # Ensure valid index and return the correct day
36
+ try:
37
+ return (
38
+ all_days[n - 1].date() if n > 0 else all_days[n].date()
39
+ ) # Adjust for 1-based indexing
40
+ except IndexError as err:
41
+ raise ValueError(
42
+ f"The {n}-th calendar day does not exist in {period.period_id}."
43
+ ) from err
@@ -0,0 +1,50 @@
1
+ """Utility functions for working with calendars."""
2
+
3
+ import datetime
4
+
5
+ import pandas as pd
6
+
7
+ from mxm.refdata.models.periods import Period
8
+ from mxm.refdata.models.weekdays import Weekday
9
+
10
+
11
+ def get_nth_weekday_of_period(
12
+ period: Period, weekday: int | str, n: int
13
+ ) -> datetime.date:
14
+ """
15
+ Returns the N-th occurrence of a given weekday in the specified period.
16
+
17
+ Supports:
18
+ - `weekday` as an **integer** (0=Monday, 6=Sunday).
19
+ - `weekday` as a **full name** ("Tuesday", "Friday").
20
+ - `weekday` as an **abbreviation** ("Tue", "Fri").
21
+
22
+ Args:
23
+ period (Period): The period (e.g., contract month, quarter).
24
+ weekday (int | str): The weekday (0=Monday, 6=Sunday or "Tuesday", "Tue", etc.).
25
+ n (int): The N-th occurrence (1-based index for forward search, -1-based for reverse search).
26
+
27
+ Returns:
28
+ datetime.date: The date of the N-th occurrence of the given weekday.
29
+
30
+ Raises:
31
+ ValueError: If the N-th occurrence does not exist in the period.
32
+ """
33
+ # Convert weekday string to integer if needed
34
+ if isinstance(weekday, str):
35
+ weekday = Weekday.from_str(weekday).as_int
36
+ period_range = period.to_daterange()
37
+ # Get all dates in the period and filter for matching weekdays
38
+ dates: list[pd.Timestamp] = [
39
+ date for date in period_range if date.weekday() == weekday
40
+ ]
41
+
42
+ # Handle both forward (n > 0) and reverse (n < 0) indexing
43
+ try:
44
+ return (
45
+ dates[n - 1].date() if n > 0 else dates[n].date()
46
+ ) # Python negative index works naturally
47
+ except IndexError as err:
48
+ raise ValueError(
49
+ f"The {n}-th occurrence of weekday {weekday} does not exist in {period.period_id}."
50
+ ) from err
@@ -0,0 +1,186 @@
1
+ import datetime
2
+ from typing import cast
3
+
4
+ import exchange_calendars as xcals
5
+ import pandas as pd
6
+ from exchange_calendars.errors import InvalidCalendarName
7
+
8
+
9
+ class TradingCalendar:
10
+ """
11
+ TradingCalendar manages exchange trading sessions and provides session-based utilities.
12
+ Sessions are represented by UTC Timestamps, following `exchange_calendars` conventions.
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ calendar_name: str,
18
+ start: datetime.date | None = None,
19
+ end: datetime.date | None = None,
20
+ ):
21
+ """
22
+ Initializes a TradingCalendar based on an exchange calendar.
23
+
24
+ Args:
25
+ calendar_name (str): The name of the exchange calendar (e.g., "CMES", "NYSE").
26
+ extend_to (datetime.date, optional): Extend the calendar to this date if needed.
27
+
28
+ Raises:
29
+ ValueError: If the calendar name is not recognized by `exchange_calendars`.
30
+ """
31
+ try:
32
+ if start and not end:
33
+ self.calendar = xcals.get_calendar(calendar_name, start=start)
34
+ elif end and not start:
35
+ self.calendar = xcals.get_calendar(calendar_name, end=end)
36
+ elif start and end:
37
+ self.calendar = xcals.get_calendar(calendar_name, start=start, end=end)
38
+ else:
39
+ self.calendar = xcals.get_calendar(calendar_name)
40
+
41
+ except InvalidCalendarName as e:
42
+ raise ValueError(f"Unknown trading calendar: {calendar_name}") from e
43
+
44
+ def get_sessions_in_range(
45
+ self, start: datetime.date, end: datetime.date
46
+ ) -> pd.DatetimeIndex:
47
+ """
48
+ Returns trading sessions in a given range as a DatetimeIndex in UTC.
49
+
50
+ Args:
51
+ start (datetime.date): The start of the range.
52
+ end (datetime.date): The end of the range.
53
+
54
+ Returns:
55
+ pd.DatetimeIndex: A list of session timestamps in UTC.
56
+ """
57
+ return self.calendar.sessions_in_range(start, end)
58
+
59
+ def shift_sessions(self, session: pd.Timestamp, offset: int) -> pd.Timestamp:
60
+ """
61
+ Moves a given session forward or backward by a specified number of sessions.
62
+
63
+ Args:
64
+ session (pd.Timestamp): The session timestamp in UTC.
65
+ offset (int): Number of sessions to shift (+ for forward, - for backward).
66
+
67
+ Returns:
68
+ pd.Timestamp: The shifted session timestamp in UTC.
69
+ """
70
+ return self.calendar.session_offset(session, offset)
71
+
72
+ def get_session_dates_in_range(
73
+ self, start: datetime.date, end: datetime.date
74
+ ) -> list[datetime.date]:
75
+ """
76
+ Returns trading session dates in a given range.
77
+
78
+ Args:
79
+ start (datetime.date): The start date.
80
+ end (datetime.date): The end date.
81
+
82
+ Returns:
83
+ List[datetime.date]: A list of session dates.
84
+ """
85
+ return [ts.date() for ts in self.get_sessions_in_range(start, end)]
86
+
87
+ def shift_session_date(self, date: datetime.date, offset: int) -> datetime.date:
88
+ """
89
+ Moves a given date forward or backward by a specified number of trading sessions.
90
+
91
+ Args:
92
+ date (datetime.date): The reference date.
93
+ offset (int): Number of sessions to shift (+ for forward, - for backward).
94
+
95
+ Returns:
96
+ datetime.date: The shifted session's date.
97
+ """
98
+ return self.shift_sessions(pd.Timestamp(date), offset).date()
99
+
100
+ def get_session_open(self, session: pd.Timestamp) -> pd.Timestamp:
101
+ """
102
+ Returns the opening time of a given session.
103
+
104
+ Args:
105
+ session (pd.Timestamp): The session timestamp in UTC.
106
+
107
+ Returns:
108
+ pd.Timestamp: The market open time for the session (UTC).
109
+ """
110
+ return cast(pd.Timestamp, self.calendar.schedule.loc[session, "open"])
111
+
112
+ def get_session_close(self, session: pd.Timestamp) -> pd.Timestamp:
113
+ """
114
+ Returns the closing time of a given session.
115
+
116
+ Args:
117
+ session (pd.Timestamp): The session timestamp in UTC.
118
+
119
+ Returns:
120
+ pd.Timestamp: The market close time for the session (UTC).
121
+ """
122
+ return cast(pd.Timestamp, self.calendar.schedule.loc[session, "close"])
123
+
124
+ def is_trading_day(self, date: datetime.date) -> bool:
125
+ """
126
+ Checks if a given date is a valid trading day.
127
+
128
+ Args:
129
+ date (datetime.date): The date to check.
130
+
131
+ Returns:
132
+ bool: True if the date is a trading day, False otherwise.
133
+ """
134
+ return self.calendar.is_session(date)
135
+
136
+ def get_last_prior_session_date(self, date: datetime.date) -> datetime.date:
137
+ """
138
+ Finds the last valid session date prior to or on the given date.
139
+
140
+ Args:
141
+ date (datetime.date): The date to check.
142
+
143
+ Returns:
144
+ datetime.date: The last valid trading session date.
145
+
146
+ Raises:
147
+ ValueError: If the date is before the first available session in the trading calendar.
148
+ """
149
+ # If it's already a trading day, return it as is
150
+ if self.is_trading_day(date):
151
+ return date
152
+
153
+ # Start from the given date and move backward until we find a valid session
154
+ while not self.is_trading_day(date):
155
+ date -= datetime.timedelta(days=1)
156
+ if date < self.calendar.first_session.date():
157
+ raise ValueError(f"No prior session found for date {date}")
158
+
159
+ return date
160
+
161
+ def get_nth_business_day_relative_to_date(
162
+ self, date: datetime.date, n: int
163
+ ) -> datetime.date:
164
+ """
165
+ Finds the N-th business day relative to a given date, even if the date is not a business day.
166
+
167
+ If the date is not a business day:
168
+ - When `n < 0` (backward shift), we already moved back once, so shift by `n + 1`.
169
+ - When `n > 0` (forward shift), shift by `n` as usual.
170
+
171
+ Args:
172
+ date (datetime.date): The starting date (can be a business or non-business day).
173
+ n (int): The number of business days to move (+ for forward, - for backward).
174
+
175
+ Returns:
176
+ datetime.date: The computed business day.
177
+
178
+ """
179
+ # If the date is not a business day, find the last valid business day before it
180
+ if not self.is_trading_day(date):
181
+ date = self.get_last_prior_session_date(date)
182
+ if n < 0:
183
+ n += 1 # If moving backward, adjust by 1 because we already moved back
184
+
185
+ # Now shift by `n` business days from a valid business day
186
+ return self.shift_session_date(date, n)
@@ -0,0 +1 @@
1
+ """Utility functions for the refData package."""
@@ -0,0 +1,33 @@
1
+ """A cache manager for the reference data service."""
2
+
3
+ import threading
4
+
5
+ from cachetools import LRUCache
6
+
7
+
8
+ class CacheManager[V]:
9
+ """Thread-safe cache manager using an LRU cache."""
10
+
11
+ def __init__(self, maxsize: int = 1000) -> None:
12
+ self._cache: LRUCache[str, V] = LRUCache(maxsize=maxsize)
13
+ self._lock = threading.Lock()
14
+
15
+ def get(self, key: str) -> V | None:
16
+ """Thread-safe retrieval from the cache."""
17
+ with self._lock:
18
+ return self._cache.get(key)
19
+
20
+ def set(self, key: str, value: V) -> None:
21
+ """Thread-safe insertion into the cache."""
22
+ with self._lock:
23
+ self._cache[key] = value
24
+
25
+ def invalidate(self, key: str) -> None:
26
+ """Thread-safe removal from the cache."""
27
+ with self._lock:
28
+ self._cache.pop(key, None)
29
+
30
+ def clear(self) -> None:
31
+ """Thread-safe clearing of the cache."""
32
+ with self._lock:
33
+ self._cache.clear()
@@ -0,0 +1,32 @@
1
+ """Configuration details for the refData application."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import ClassVar
7
+
8
+ from pydantic_settings import BaseSettings, SettingsConfigDict
9
+
10
+
11
+ def _default_sqlite_db_url() -> str:
12
+ # Stable, user-writable default (avoid process-relative paths)
13
+ db_path = Path.home() / ".mxm" / "refdata" / "refdata.db"
14
+ return f"sqlite:///{db_path}"
15
+
16
+
17
+ class Config(BaseSettings):
18
+ # Overrideable via env; default is safe when imported as a dependency
19
+ SQL_DB_URL: str = _default_sqlite_db_url()
20
+
21
+ REFDATA_DB_MODE: str = "buildable" # "buildable" | "managed"
22
+
23
+ # None means "use packaged CSV resource"
24
+ REFDATA_FUTURES_PRODUCTS_CSV_PATH: str | None = None
25
+
26
+ model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
27
+ env_file=".env", env_file_encoding="utf-8"
28
+ )
29
+
30
+
31
+ def load_config() -> Config:
32
+ return Config()
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from mxm.refdata.models.periods import PeriodType
4
+
5
+
6
+ def encode_period_types(values: tuple[PeriodType, ...]) -> str:
7
+ if not values:
8
+ raise ValueError("period_types must be non-empty")
9
+ return ",".join(v.name for v in values)
10
+
11
+
12
+ def decode_period_types(value: str) -> tuple[PeriodType, ...]:
13
+ parts = [p.strip() for p in value.replace("|", ",").split(",") if p.strip()]
14
+ if not parts:
15
+ raise ValueError(f"invalid period_types string: {value!r}")
16
+ return tuple(PeriodType[p] for p in parts)
@@ -0,0 +1,18 @@
1
+ """Pre-computed regex patterns for use in the refData package."""
2
+
3
+ import re
4
+ from types import MappingProxyType
5
+
6
+ from mxm.refdata.models.periods import PeriodType
7
+
8
+ PERIOD_TYPE_PARSING_MAP = MappingProxyType(
9
+ {
10
+ PeriodType.YEAR: re.compile(r"^\d{4}$"),
11
+ PeriodType.MONTH: re.compile(
12
+ r"^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{4}$"
13
+ ),
14
+ PeriodType.QUARTER: re.compile(r"^\d{4}-Q[1-4]$"),
15
+ PeriodType.WEEK: re.compile(r"^\d{4}-W\d{1,2}$"),
16
+ PeriodType.DAY: re.compile(r"^\d{4}\d{2}\d{2}$"),
17
+ }
18
+ )
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from contextlib import contextmanager
5
+ from importlib.resources import as_file, files
6
+ from pathlib import Path
7
+
8
+ from mxm.refdata.utils.config import Config
9
+
10
+
11
+ @contextmanager
12
+ def futures_products_csv_path(cfg: Config) -> Iterator[Path]:
13
+ """
14
+ Yield a filesystem Path to the futures products CSV.
15
+
16
+ If cfg.REFDATA_FUTURES_PRODUCTS_CSV_PATH is set, yield that path.
17
+ Otherwise, yield a materialised path to the packaged CSV resource.
18
+
19
+ The path may be backed by a temporary file; callers must consume it
20
+ within the context manager.
21
+ """
22
+ override = cfg.REFDATA_FUTURES_PRODUCTS_CSV_PATH
23
+ if override:
24
+ yield Path(override)
25
+ return
26
+
27
+ resource = files("mxm.refdata").joinpath("data/futures_products.csv")
28
+ with as_file(resource) as p:
29
+ yield Path(p)