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
mxm/refdata/cli.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as dt
|
|
4
|
+
from datetime import date
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from mxm.refdata.api.ref_data_api import RefDataAPI
|
|
12
|
+
from mxm.refdata.services.bootstrap import build_refdata, rebuild_refdata
|
|
13
|
+
from mxm.refdata.services.smokecheck import run_smokechecks
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
add_completion=False,
|
|
17
|
+
help="Inspect and materialize MXM reference data.",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_cli_date(value: str) -> date:
|
|
24
|
+
"""Parse a CLI date in YYYY-MM-DD format."""
|
|
25
|
+
try:
|
|
26
|
+
return dt.datetime.strptime(value, "%Y-%m-%d").date()
|
|
27
|
+
except ValueError as err:
|
|
28
|
+
raise typer.BadParameter("Expected date in YYYY-MM-DD format.") from err
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command("build")
|
|
32
|
+
def build() -> None:
|
|
33
|
+
"""Build the local reference-data database."""
|
|
34
|
+
build_refdata()
|
|
35
|
+
console.print("[green]Reference data database built.[/]")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command("rebuild")
|
|
39
|
+
def rebuild() -> None:
|
|
40
|
+
"""Destructively rebuild the local reference-data database."""
|
|
41
|
+
rebuild_refdata()
|
|
42
|
+
console.print("[green]Reference data database rebuilt.[/]")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.command("products")
|
|
46
|
+
def products() -> None:
|
|
47
|
+
"""List available futures products."""
|
|
48
|
+
api = RefDataAPI()
|
|
49
|
+
rows = api.get_all_products()
|
|
50
|
+
|
|
51
|
+
table = Table(title="MXM Futures Products")
|
|
52
|
+
table.add_column("Product ID")
|
|
53
|
+
table.add_column("Venue")
|
|
54
|
+
table.add_column("Currency")
|
|
55
|
+
table.add_column("Unit")
|
|
56
|
+
table.add_column("Period Types")
|
|
57
|
+
|
|
58
|
+
for product in rows:
|
|
59
|
+
table.add_row(
|
|
60
|
+
product.product_id,
|
|
61
|
+
product.venue,
|
|
62
|
+
product.currency.value,
|
|
63
|
+
product.unit.value,
|
|
64
|
+
", ".join(period_type.value for period_type in product.period_types),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
console.print(table)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command("product")
|
|
71
|
+
def product(
|
|
72
|
+
product_id: Annotated[str, typer.Argument(help="Canonical product ID")],
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Show one futures product."""
|
|
75
|
+
api = RefDataAPI()
|
|
76
|
+
product_obj = api.get_product_by_id(product_id)
|
|
77
|
+
|
|
78
|
+
table = Table(title=f"Product: {product_obj.product_id}")
|
|
79
|
+
table.add_column("Field")
|
|
80
|
+
table.add_column("Value")
|
|
81
|
+
|
|
82
|
+
table.add_row("product_id", product_obj.product_id)
|
|
83
|
+
table.add_row("venue", product_obj.venue)
|
|
84
|
+
table.add_row("description", product_obj.description)
|
|
85
|
+
table.add_row("currency", product_obj.currency.value)
|
|
86
|
+
table.add_row("unit", product_obj.unit.value)
|
|
87
|
+
table.add_row("contract_size", str(product_obj.contract_size))
|
|
88
|
+
table.add_row("listing_rule", product_obj.listing_rule)
|
|
89
|
+
table.add_row(
|
|
90
|
+
"period_types",
|
|
91
|
+
", ".join(period_type.value for period_type in product_obj.period_types),
|
|
92
|
+
)
|
|
93
|
+
table.add_row("settlement", product_obj.settlement.value)
|
|
94
|
+
table.add_row("last_trading_rule", product_obj.last_trading_rule)
|
|
95
|
+
table.add_row("expiry_rule", product_obj.expiry_rule)
|
|
96
|
+
table.add_row("trading_calendar", product_obj.trading_calendar)
|
|
97
|
+
table.add_row("tick_size", str(product_obj.tick_size))
|
|
98
|
+
table.add_row("tick_value", str(product_obj.tick_value))
|
|
99
|
+
table.add_row("valid_period_rule", product_obj.valid_period_rule)
|
|
100
|
+
|
|
101
|
+
console.print(table)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@app.command("contracts")
|
|
105
|
+
def contracts(
|
|
106
|
+
product_id: Annotated[str, typer.Argument(help="Canonical product ID")],
|
|
107
|
+
) -> None:
|
|
108
|
+
"""List contracts for a futures product."""
|
|
109
|
+
api = RefDataAPI()
|
|
110
|
+
rows = api.get_contracts_for_product(product_id)
|
|
111
|
+
|
|
112
|
+
table = Table(title=f"Contracts: {product_id}")
|
|
113
|
+
table.add_column("Contract ID")
|
|
114
|
+
table.add_column("Period")
|
|
115
|
+
table.add_column("First Interest")
|
|
116
|
+
table.add_column("Last Trading")
|
|
117
|
+
|
|
118
|
+
for contract in rows:
|
|
119
|
+
table.add_row(
|
|
120
|
+
contract.contract_id,
|
|
121
|
+
contract.period_id,
|
|
122
|
+
contract.first_day_of_interest.isoformat(),
|
|
123
|
+
contract.last_trading_day.isoformat(),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
console.print(table)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@app.command("active")
|
|
130
|
+
def active(
|
|
131
|
+
as_of_date: Annotated[str, typer.Argument(help="Date in YYYY-MM-DD format")],
|
|
132
|
+
product_id: Annotated[str | None, typer.Option(help="Optional product ID")] = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""List active contracts as of a date."""
|
|
135
|
+
parsed_date = parse_cli_date(as_of_date)
|
|
136
|
+
api = RefDataAPI()
|
|
137
|
+
rows = api.get_active_contracts(parsed_date, product_id=product_id)
|
|
138
|
+
|
|
139
|
+
table = Table(title=f"Active contracts: {parsed_date.isoformat()}")
|
|
140
|
+
table.add_column("Product ID")
|
|
141
|
+
table.add_column("Contract ID")
|
|
142
|
+
table.add_column("Period")
|
|
143
|
+
table.add_column("Last Trading")
|
|
144
|
+
|
|
145
|
+
for contract in rows:
|
|
146
|
+
table.add_row(
|
|
147
|
+
contract.product_id,
|
|
148
|
+
contract.contract_id,
|
|
149
|
+
contract.period_id,
|
|
150
|
+
contract.last_trading_day.isoformat(),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
console.print(table)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@app.command("coverage")
|
|
157
|
+
def coverage() -> None:
|
|
158
|
+
"""Show contract coverage by product."""
|
|
159
|
+
api = RefDataAPI()
|
|
160
|
+
products = api.get_all_products()
|
|
161
|
+
|
|
162
|
+
table = Table(title="MXM Refdata Coverage")
|
|
163
|
+
table.add_column("Product ID")
|
|
164
|
+
table.add_column("Venue")
|
|
165
|
+
table.add_column("First Contract")
|
|
166
|
+
table.add_column("Last Contract")
|
|
167
|
+
table.add_column("Count", justify="right")
|
|
168
|
+
|
|
169
|
+
for product_obj in products:
|
|
170
|
+
product_contracts = api.get_contracts_for_product(product_obj.product_id)
|
|
171
|
+
|
|
172
|
+
if not product_contracts:
|
|
173
|
+
table.add_row(product_obj.product_id, product_obj.venue, "-", "-", "0")
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
table.add_row(
|
|
177
|
+
product_obj.product_id,
|
|
178
|
+
product_obj.venue,
|
|
179
|
+
product_contracts[0].contract_id,
|
|
180
|
+
product_contracts[-1].contract_id,
|
|
181
|
+
str(len(product_contracts)),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
console.print(table)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@app.command("smokecheck")
|
|
188
|
+
def smokecheck() -> None:
|
|
189
|
+
"""Run operational smoke checks against the local refdata database."""
|
|
190
|
+
report = run_smokechecks()
|
|
191
|
+
|
|
192
|
+
console.print("[bold]MXM Refdata Smokecheck[/bold]")
|
|
193
|
+
console.print(
|
|
194
|
+
"Counts: "
|
|
195
|
+
f"products={report.counts.products}, "
|
|
196
|
+
f"periods={report.counts.periods}, "
|
|
197
|
+
f"contracts={report.counts.contracts}, "
|
|
198
|
+
f"cycles={report.counts.cycles}, "
|
|
199
|
+
f"memberships={report.counts.memberships}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
table = Table(title="Smokecheck Results")
|
|
203
|
+
table.add_column("Status")
|
|
204
|
+
table.add_column("Check")
|
|
205
|
+
table.add_column("Message")
|
|
206
|
+
|
|
207
|
+
for result in report.results:
|
|
208
|
+
status = "[green]PASS[/]" if result.status == "pass" else "[red]FAIL[/]"
|
|
209
|
+
table.add_row(status, result.name, result.message)
|
|
210
|
+
|
|
211
|
+
console.print(table)
|
|
212
|
+
|
|
213
|
+
if not report.passed:
|
|
214
|
+
raise typer.Exit(code=1)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
app()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"comex_gold_futures": {
|
|
3
|
+
"shift_rule": {
|
|
4
|
+
"shift_period_type": "MONTH",
|
|
5
|
+
"n_shift": {
|
|
6
|
+
"Jan": 72,
|
|
7
|
+
"Feb": 24,
|
|
8
|
+
"Mar": 24,
|
|
9
|
+
"Apr": 24,
|
|
10
|
+
"May": 24,
|
|
11
|
+
"Jun": 72,
|
|
12
|
+
"Jul": 24,
|
|
13
|
+
"Aug": 24,
|
|
14
|
+
"Sep": 24,
|
|
15
|
+
"Oct": 24,
|
|
16
|
+
"Nov": 24,
|
|
17
|
+
"Dec": 72
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"reference_rule": "next_b_day_after_period"
|
|
21
|
+
},
|
|
22
|
+
"cbot_corn_futures": {
|
|
23
|
+
"shift_rule": {
|
|
24
|
+
"shift_period_type": "MONTH",
|
|
25
|
+
"n_shift": {
|
|
26
|
+
"Mar": 36,
|
|
27
|
+
"May": 36,
|
|
28
|
+
"Sep": 36,
|
|
29
|
+
"Jul": 48,
|
|
30
|
+
"Dec": 48
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"reference_rule": "next_b_day_after_last_trading_day_of_december"
|
|
34
|
+
},
|
|
35
|
+
"cme_gbp_futures": {
|
|
36
|
+
"shift_rule": {
|
|
37
|
+
"shift_period_type": "MONTH",
|
|
38
|
+
"n_shift": {
|
|
39
|
+
"Mar": 60,
|
|
40
|
+
"Jun": 60,
|
|
41
|
+
"Sep": 60,
|
|
42
|
+
"Dec": 60,
|
|
43
|
+
"Jan": 3,
|
|
44
|
+
"Feb": 3,
|
|
45
|
+
"Apr": 3,
|
|
46
|
+
"May": 3,
|
|
47
|
+
"Jul": 3,
|
|
48
|
+
"Aug": 3,
|
|
49
|
+
"Oct": 3,
|
|
50
|
+
"Nov": 3
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"reference_rule": "next_b_day_after_period"
|
|
54
|
+
},
|
|
55
|
+
"cme_emini_snp500_futures": {
|
|
56
|
+
"shift_rule": {
|
|
57
|
+
"shift_period_type": "MONTH",
|
|
58
|
+
"n_shift": {
|
|
59
|
+
"Mar": 63,
|
|
60
|
+
"Jun": 63,
|
|
61
|
+
"Sep": 63,
|
|
62
|
+
"Dec": 63
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"reference_rule": "next_b_day_after_period"
|
|
66
|
+
},
|
|
67
|
+
"nymex_natural_gas_futures": {
|
|
68
|
+
"shift_rule": {
|
|
69
|
+
"shift_period_type": "MONTH",
|
|
70
|
+
"n_shift": {
|
|
71
|
+
"Jan": 144,
|
|
72
|
+
"Feb": 144,
|
|
73
|
+
"Mar": 144,
|
|
74
|
+
"Apr": 144,
|
|
75
|
+
"May": 144,
|
|
76
|
+
"Jun": 144,
|
|
77
|
+
"Jul": 144,
|
|
78
|
+
"Aug": 144,
|
|
79
|
+
"Sep": 144,
|
|
80
|
+
"Oct": 144,
|
|
81
|
+
"Nov": 144,
|
|
82
|
+
"Dec": 144
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"reference_rule": "next_b_day_after_last_trading_day_of_december"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
product_id,venue,description,currency,unit,contract_size,valid_period_rule,listing_rule,period_types,settlement,last_trading_rule,expiry_rule,trading_calendar,trading_hours,tick_size,tick_value,initial_margin,maintenance_margin
|
|
2
|
+
comex_gold_futures,COMEX,Gold Futures,USD,TROY_OUNCE,100,FGHJKMNQUVXZ,Monthly contracts listed for 24 consecutive months and any Jun and Dec in the nearest 72 months.,MONTH,PHYSICAL,Trading terminates at 12:30 p.m. CT on the third last business day of the contract month.,last business day of the contract month.,CMES,Sunday - Friday 6:00 p.m. - 5:00 p.m. (5:00 p.m. - 4:00 p.m. /CT) with a 60-minute break each day beginning at 5:00 p.m. (4:00 p.m. CT),0.1,10,,
|
|
3
|
+
cbot_corn_futures,CBOT,Corn Futures,USD,BUSHEL,5000,HKNUZ,"9 monthly contracts of Mar, May, Sep and 8 monthly contracts of Jul and Dec listed annually after the termination of trading in the December contract of the current year.",MONTH,PHYSICAL,Trading terminates on the business day prior to the 15th day of the contract month.,Second business day following the last trading day of the delivery month.,CMES,Sunday - Friday: 7:00 p.m. - 7:45 a.m. CT and Monday - Friday: 8:30 a.m. - 1:20 p.m. CT,0.0025,12.5,,
|
|
4
|
+
cme_gbp_futures,CME,British Pound Futures,USD,GBP,62500,FGHJKMNQUVXZ,"Quarterly contracts (Mar, Jun, Sep, Dec) listed for 20 consecutive quarters and serial contracts listed for 3 months",MONTH,PHYSICAL,"Trading terminates at 9:16 a.m. CT, 2 business days prior to the third Wednesday of the contract month.",2 business days prior to the third Wednesday of the contract month.,CMES,Sunday - Friday 6:00 p.m. - 5:00 p.m. (5:00 p.m. - 4:00 p.m. CT) with a 60-minute break each day beginning at 5:00 p.m. (4:00 p.m. CT).,0.0001,6.25,,
|
|
5
|
+
cme_emini_snp500_futures,CME,S&P 500 E-mini Futures,USD,INDEX_POINT,50,HMUZ,"Quarterly contracts (Mar, Jun, Sep, Dec) listed for 21 consecutive quarters",MONTH,FINANCIAL,Trading terminates at 9:30 a.m. ET on the 3rd Friday of the contract month.,3rd Friday of the contract month.,CMES,Sunday 6:00 p.m. - Friday 5:00 p.m. ET (5:00 p.m. - 4:00 p.m. CT) with a daily maintenance period from 5:00 p.m. - 6:00 p.m. ET (4:00 p.m. - 5:00 p.m. CT),0.25,12.5,,
|
|
6
|
+
nymex_natural_gas_futures,NYMEX,HH Natural Gas Futures,USD,MMBTU,10000,FGHJKMNQUVXZ,Monthly contracts listed for the current year and the next 12 calendar years. List monthly contracts for a new calendar year following the termination of trading in the December contract of the current year.,MONTH,PHYSICAL,Trading terminates on the 3rd last business day of the month prior to the contract month. ,3rd last business day of the contract month.,CMES,Sunday - Friday 6:00 p.m. - 5:00 p.m. (5:00 p.m. - 4:00 p.m. CT) with a 60-minute break each day beginning at 5:00 p.m. (4:00 p.m. CT).,0.001,10,,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"comex_gold_futures": {
|
|
3
|
+
"period_offset": 0,
|
|
4
|
+
"reference_event": "business_day_of_period",
|
|
5
|
+
"n_reference": -3,
|
|
6
|
+
"business_day_offset": 0
|
|
7
|
+
},
|
|
8
|
+
"cbot_corn_futures": {
|
|
9
|
+
"period_offset": 0,
|
|
10
|
+
"reference_event": "calendar_day_of_period",
|
|
11
|
+
"n_reference": 15,
|
|
12
|
+
"business_day_offset": -1
|
|
13
|
+
},
|
|
14
|
+
"cme_gbp_futures": {
|
|
15
|
+
"period_offset": 0,
|
|
16
|
+
"reference_event": "weekday_of_period",
|
|
17
|
+
"n_reference": 3,
|
|
18
|
+
"weekday": "Wednesday",
|
|
19
|
+
"business_day_offset": -2
|
|
20
|
+
},
|
|
21
|
+
"cme_emini_snp500_futures": {
|
|
22
|
+
"period_offset": 0,
|
|
23
|
+
"reference_event": "weekday_of_period",
|
|
24
|
+
"n_reference": 3,
|
|
25
|
+
"weekday": "Friday",
|
|
26
|
+
"business_day_offset": 0
|
|
27
|
+
},
|
|
28
|
+
"nymex_natural_gas_futures": {
|
|
29
|
+
"period_offset": -1,
|
|
30
|
+
"reference_event": "business_day_of_period",
|
|
31
|
+
"n_reference": -3,
|
|
32
|
+
"business_day_offset": 0
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Database integration for refData."""
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""SQL Session Manager for handling database interactions."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Callable, Iterator
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import Engine, create_engine
|
|
9
|
+
from sqlalchemy.orm import Session, sessionmaker
|
|
10
|
+
from sqlalchemy.sql import text
|
|
11
|
+
|
|
12
|
+
from mxm.refdata.models.orm import Base
|
|
13
|
+
from mxm.refdata.utils.config import load_config
|
|
14
|
+
|
|
15
|
+
logging.basicConfig(level=logging.INFO)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SQLSessionManager:
|
|
19
|
+
"""Manages database sessions while ensuring consistent engine initialization."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
db_url: str | None = None,
|
|
24
|
+
engine: Engine | None = None,
|
|
25
|
+
session_factory: Callable[[], Session] | None = None,
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
Initialize the SQLSessionManager with a specific engine and session factory.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
engine (Optional[Engine]): The database engine to use.
|
|
32
|
+
session_factory (Optional[Callable[[], Session]]): A callable that provides new sessions.
|
|
33
|
+
"""
|
|
34
|
+
config = load_config()
|
|
35
|
+
sql_db_url = db_url or config.SQL_DB_URL
|
|
36
|
+
|
|
37
|
+
# Ensure sqlite parent directory exists before engine creation
|
|
38
|
+
_ensure_sqlite_parent_dir(sql_db_url)
|
|
39
|
+
|
|
40
|
+
self.engine: Engine = engine or create_engine(sql_db_url, echo=False)
|
|
41
|
+
self.session_factory: Callable[[], Session] = session_factory or sessionmaker(
|
|
42
|
+
autocommit=False, autoflush=False, bind=self.engine
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def get_engine(self) -> Engine:
|
|
46
|
+
"""Return the configured database engine."""
|
|
47
|
+
return self.engine
|
|
48
|
+
|
|
49
|
+
def get_session_factory(self) -> Callable[[], Session]:
|
|
50
|
+
"""Return the configured session factory."""
|
|
51
|
+
return self.session_factory
|
|
52
|
+
|
|
53
|
+
def get_db_session(self) -> Session:
|
|
54
|
+
"""
|
|
55
|
+
Provide a new database session.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Session: A new SQLAlchemy session.
|
|
59
|
+
"""
|
|
60
|
+
return self.session_factory()
|
|
61
|
+
|
|
62
|
+
@contextmanager
|
|
63
|
+
def db_session_scope(self) -> Iterator[Session]:
|
|
64
|
+
"""
|
|
65
|
+
Provide a transactional scope for database operations.
|
|
66
|
+
|
|
67
|
+
Yields:
|
|
68
|
+
Session: A transactional database session.
|
|
69
|
+
"""
|
|
70
|
+
db_session = self.get_db_session()
|
|
71
|
+
try:
|
|
72
|
+
yield db_session
|
|
73
|
+
db_session.commit()
|
|
74
|
+
except Exception:
|
|
75
|
+
db_session.rollback()
|
|
76
|
+
raise
|
|
77
|
+
finally:
|
|
78
|
+
db_session.close()
|
|
79
|
+
|
|
80
|
+
def init_db(self) -> bool:
|
|
81
|
+
"""Initialize the database schema."""
|
|
82
|
+
try:
|
|
83
|
+
logging.info("Initializing database schema...")
|
|
84
|
+
Base.metadata.create_all(bind=self.engine)
|
|
85
|
+
logging.info(f"Tables created: {Base.metadata.tables.keys()}")
|
|
86
|
+
return True
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logging.error(f"Failed to initialize database schema: {e}")
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def drop_db(self) -> bool:
|
|
92
|
+
"""Drop all tables in the database."""
|
|
93
|
+
try:
|
|
94
|
+
logging.info("Dropping all tables in the database...")
|
|
95
|
+
Base.metadata.drop_all(bind=self.engine)
|
|
96
|
+
logging.info("All tables dropped successfully.")
|
|
97
|
+
return True
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logging.error(f"Failed to drop database: {e}")
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
def check_db_connection(self) -> bool:
|
|
103
|
+
"""Check if the database connection is active."""
|
|
104
|
+
try:
|
|
105
|
+
with self.engine.connect() as connection:
|
|
106
|
+
connection.execute(text("SELECT 1"))
|
|
107
|
+
logging.info("Database connection is active.")
|
|
108
|
+
return True
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logging.error(f"Database connection check failed: {e}")
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _ensure_sqlite_parent_dir(sql_db_url: str) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Ensure the parent directory exists for sqlite database URLs.
|
|
117
|
+
|
|
118
|
+
Only applies to absolute-path sqlite URLs of the form:
|
|
119
|
+
sqlite:////absolute/path/to/db.sqlite
|
|
120
|
+
|
|
121
|
+
Relative sqlite paths are intentionally not supported as defaults.
|
|
122
|
+
"""
|
|
123
|
+
prefix = "sqlite:///"
|
|
124
|
+
if not sql_db_url.startswith(prefix):
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
raw_path = sql_db_url[len(prefix) :]
|
|
128
|
+
if not raw_path:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
db_path = Path(raw_path)
|
|
132
|
+
|
|
133
|
+
# Only create directories for absolute paths
|
|
134
|
+
if db_path.is_absolute():
|
|
135
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from mxm.refdata.mappings.futures_contract_vs_orm import (
|
|
2
|
+
futures_contract_from_orm,
|
|
3
|
+
futures_contract_to_orm,
|
|
4
|
+
)
|
|
5
|
+
from mxm.refdata.mappings.futures_product_vs_orm import (
|
|
6
|
+
futures_product_from_orm,
|
|
7
|
+
futures_product_to_orm,
|
|
8
|
+
)
|
|
9
|
+
from mxm.refdata.mappings.period_cycles_vs_orm import (
|
|
10
|
+
period_cycle_from_orm,
|
|
11
|
+
period_cycle_membership_from_orm,
|
|
12
|
+
period_cycle_membership_to_orm,
|
|
13
|
+
period_cycle_to_orm,
|
|
14
|
+
)
|
|
15
|
+
from mxm.refdata.mappings.period_vs_orm import (
|
|
16
|
+
period_from_orm,
|
|
17
|
+
period_to_orm,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"futures_contract_from_orm",
|
|
22
|
+
"futures_contract_to_orm",
|
|
23
|
+
"futures_product_from_orm",
|
|
24
|
+
"futures_product_to_orm",
|
|
25
|
+
"period_cycle_from_orm",
|
|
26
|
+
"period_cycle_membership_from_orm",
|
|
27
|
+
"period_cycle_membership_to_orm",
|
|
28
|
+
"period_cycle_to_orm",
|
|
29
|
+
"period_from_orm",
|
|
30
|
+
"period_to_orm",
|
|
31
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Mapping FuturesContract instances to and from FuturesContractORM instances."""
|
|
2
|
+
|
|
3
|
+
from mxm.refdata.models.contracts.futures_contract import FuturesContract
|
|
4
|
+
from mxm.refdata.models.orm import FuturesContractORM
|
|
5
|
+
from mxm.refdata.models.products.futures_product import Currency, ProductUnit
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def futures_contract_to_orm(contract: FuturesContract) -> FuturesContractORM:
|
|
9
|
+
"""
|
|
10
|
+
Map a FuturesContract object to a FuturesContractORM instance.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
contract (FuturesContract): The internal FuturesContract object.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
FuturesContractORM: The corresponding ORM instance.
|
|
17
|
+
"""
|
|
18
|
+
return FuturesContractORM(
|
|
19
|
+
contract_id=contract.contract_id,
|
|
20
|
+
product_id=contract.product_id,
|
|
21
|
+
period_id=contract.period_id,
|
|
22
|
+
contract_size=contract.contract_size,
|
|
23
|
+
unit=contract.unit,
|
|
24
|
+
currency=contract.currency,
|
|
25
|
+
trading_calendar=contract.trading_calendar,
|
|
26
|
+
first_day_of_interest=contract.first_day_of_interest,
|
|
27
|
+
last_trading_day=contract.last_trading_day,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def futures_contract_from_orm(orm: FuturesContractORM) -> FuturesContract:
|
|
32
|
+
"""
|
|
33
|
+
Map a FuturesContractORM object to a FuturesContract object.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
orm (FuturesContractORM): The ORM object to map from.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
FuturesContract: The mapped internal representation.
|
|
40
|
+
"""
|
|
41
|
+
return FuturesContract(
|
|
42
|
+
product_id=orm.product_id, # Directly use product_id
|
|
43
|
+
period_id=orm.period_id, # Directly use period_id
|
|
44
|
+
contract_id=orm.contract_id,
|
|
45
|
+
contract_size=orm.contract_size,
|
|
46
|
+
currency=Currency[orm.currency.name], # Map string to Currency Enum
|
|
47
|
+
unit=ProductUnit[orm.unit.name], # Map string to ProductUnit Enum
|
|
48
|
+
trading_calendar=orm.trading_calendar,
|
|
49
|
+
first_day_of_interest=orm.first_day_of_interest,
|
|
50
|
+
last_trading_day=orm.last_trading_day,
|
|
51
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Mapping FututesProduct instances to and from FuturesProductORM instances."""
|
|
2
|
+
|
|
3
|
+
from mxm.refdata.models.orm import FuturesProductORM
|
|
4
|
+
from mxm.refdata.models.products.futures_product import FuturesProduct
|
|
5
|
+
from mxm.refdata.utils.period_types_codec import (
|
|
6
|
+
decode_period_types,
|
|
7
|
+
encode_period_types,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def futures_product_to_orm(product: FuturesProduct) -> FuturesProductORM:
|
|
12
|
+
"""Map a FuturesProduct to a FuturesProductORM instance."""
|
|
13
|
+
return FuturesProductORM(
|
|
14
|
+
product_id=product.product_id,
|
|
15
|
+
venue=product.venue,
|
|
16
|
+
description=product.description,
|
|
17
|
+
currency=product.currency,
|
|
18
|
+
unit=product.unit,
|
|
19
|
+
contract_size=product.contract_size,
|
|
20
|
+
valid_period_rule=product.valid_period_rule,
|
|
21
|
+
listing_rule=product.listing_rule,
|
|
22
|
+
period_types=encode_period_types(product.period_types),
|
|
23
|
+
settlement=product.settlement,
|
|
24
|
+
last_trading_rule=product.last_trading_rule,
|
|
25
|
+
expiry_rule=product.expiry_rule,
|
|
26
|
+
trading_calendar=product.trading_calendar,
|
|
27
|
+
trading_hours=product.trading_hours,
|
|
28
|
+
tick_size=product.tick_size,
|
|
29
|
+
tick_value=product.tick_value,
|
|
30
|
+
initial_margin=product.initial_margin,
|
|
31
|
+
maintenance_margin=product.maintenance_margin,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def futures_product_from_orm(orm_instance: FuturesProductORM) -> FuturesProduct:
|
|
36
|
+
"""Map a FuturesProductORM instance to a FuturesProduct."""
|
|
37
|
+
return FuturesProduct(
|
|
38
|
+
product_id=orm_instance.product_id,
|
|
39
|
+
venue=orm_instance.venue,
|
|
40
|
+
description=orm_instance.description,
|
|
41
|
+
currency=orm_instance.currency,
|
|
42
|
+
unit=orm_instance.unit,
|
|
43
|
+
contract_size=orm_instance.contract_size,
|
|
44
|
+
valid_period_rule=orm_instance.valid_period_rule,
|
|
45
|
+
listing_rule=orm_instance.listing_rule,
|
|
46
|
+
period_types=decode_period_types(orm_instance.period_types),
|
|
47
|
+
settlement=orm_instance.settlement,
|
|
48
|
+
last_trading_rule=orm_instance.last_trading_rule,
|
|
49
|
+
expiry_rule=orm_instance.expiry_rule,
|
|
50
|
+
trading_calendar=orm_instance.trading_calendar,
|
|
51
|
+
trading_hours=orm_instance.trading_hours,
|
|
52
|
+
tick_size=orm_instance.tick_size,
|
|
53
|
+
tick_value=orm_instance.tick_value,
|
|
54
|
+
initial_margin=orm_instance.initial_margin,
|
|
55
|
+
maintenance_margin=orm_instance.maintenance_margin,
|
|
56
|
+
)
|