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.
- mxm/refdata/__init__.py +1 -0
- mxm/refdata/api/__init__.py +1 -0
- mxm/refdata/api/ref_data_api.py +607 -0
- mxm/refdata/cli.py +218 -0
- mxm/refdata/data/first_day_of_interest_rule.json +88 -0
- mxm/refdata/data/futures_products.csv +6 -0
- mxm/refdata/data/last_trading_rule.json +34 -0
- mxm/refdata/database/__init__.py +1 -0
- mxm/refdata/database/sql_session_manager.py +135 -0
- mxm/refdata/mappings/__init__.py +31 -0
- mxm/refdata/mappings/futures_contract_vs_orm.py +51 -0
- mxm/refdata/mappings/futures_product_vs_orm.py +56 -0
- mxm/refdata/mappings/period_cycles_vs_orm.py +102 -0
- mxm/refdata/mappings/period_vs_orm.py +40 -0
- mxm/refdata/models/__init__.py +31 -0
- mxm/refdata/models/contracts/__init__.py +0 -0
- mxm/refdata/models/contracts/futures_contract.py +22 -0
- mxm/refdata/models/currencies.py +24 -0
- mxm/refdata/models/months.py +72 -0
- mxm/refdata/models/orm/__init__.py +16 -0
- mxm/refdata/models/orm/base.py +3 -0
- mxm/refdata/models/orm/futures_contracts.py +53 -0
- mxm/refdata/models/orm/futures_products.py +61 -0
- mxm/refdata/models/orm/period_cycles.py +84 -0
- mxm/refdata/models/orm/periods.py +30 -0
- mxm/refdata/models/period_cycles.py +73 -0
- mxm/refdata/models/periods.py +64 -0
- mxm/refdata/models/products/__init__.py +1 -0
- mxm/refdata/models/products/futures_product.py +38 -0
- mxm/refdata/models/products/settlement.py +10 -0
- mxm/refdata/models/reference_events.py +9 -0
- mxm/refdata/models/units.py +28 -0
- mxm/refdata/models/weekdays.py +66 -0
- mxm/refdata/parsing/__init__.py +1 -0
- mxm/refdata/parsing/futures_products_from_csv.py +76 -0
- mxm/refdata/py.typed +0 -0
- mxm/refdata/scripts/__init__.py +1 -0
- mxm/refdata/scripts/db_utils.py +61 -0
- mxm/refdata/scripts/manage_static_ref_data.py +73 -0
- mxm/refdata/services/__init__.py +0 -0
- mxm/refdata/services/bootstrap.py +84 -0
- mxm/refdata/services/futures_contract_factory.py +127 -0
- mxm/refdata/services/futures_product_factory.py +132 -0
- mxm/refdata/services/period_factory.py +315 -0
- mxm/refdata/services/ref_data_service.py +322 -0
- mxm/refdata/services/smokecheck.py +326 -0
- mxm/refdata/trading_calendars/__init__.py +0 -0
- mxm/refdata/trading_calendars/first_day_of_interest.py +74 -0
- mxm/refdata/trading_calendars/last_trading_day.py +117 -0
- mxm/refdata/trading_calendars/nth_business_day.py +52 -0
- mxm/refdata/trading_calendars/nth_calendar_day_of_period.py +43 -0
- mxm/refdata/trading_calendars/nth_weekday_of_period.py +50 -0
- mxm/refdata/trading_calendars/trading_calendar.py +186 -0
- mxm/refdata/utils/__init__.py +1 -0
- mxm/refdata/utils/cache_manager.py +33 -0
- mxm/refdata/utils/config.py +32 -0
- mxm/refdata/utils/period_types_codec.py +16 -0
- mxm/refdata/utils/regex_patterns.py +18 -0
- mxm/refdata/utils/resources.py +29 -0
- mxm_refdata-0.3.0.dist-info/METADATA +228 -0
- mxm_refdata-0.3.0.dist-info/RECORD +64 -0
- mxm_refdata-0.3.0.dist-info/WHEEL +4 -0
- mxm_refdata-0.3.0.dist-info/entry_points.txt +3 -0
- 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,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
|
+
)
|