commodutil 4.3.0__tar.gz → 4.4.0__tar.gz
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.
- {commodutil-4.3.0 → commodutil-4.4.0}/PKG-INFO +3 -2
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/__init__.py +3 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/convfactors.py +201 -122
- commodutil-4.4.0/commodutil/dates.py +104 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/pandasutil.py +5 -10
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/standards/__init__.py +13 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/standards/commodities.py +10 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/standards/currency.py +45 -0
- commodutil-4.4.0/commodutil/standards/price_units.py +99 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/standards/units.py +97 -13
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/stats.py +12 -2
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/transforms.py +1 -1
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil.egg-info/PKG-INFO +3 -2
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil.egg-info/SOURCES.txt +7 -1
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil.egg-info/requires.txt +2 -1
- commodutil-4.4.0/commodutil.egg-info/scm_file_list.json +70 -0
- commodutil-4.4.0/commodutil.egg-info/scm_version.json +8 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/pyproject.toml +2 -1
- commodutil-4.4.0/tests/golden/golden_factors.json +8451 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/test_conv.py +17 -10
- commodutil-4.4.0/tests/test_dates.py +62 -0
- commodutil-4.4.0/tests/test_golden_factors.py +257 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/test_pandasutils.py +40 -5
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/test_price_conv.py +62 -2
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/test_standards_commodities.py +20 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/test_standards_currency.py +17 -0
- commodutil-4.4.0/tests/test_standards_price_units.py +122 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/test_standards_units.py +66 -3
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/test_stats.py +22 -13
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/test_transforms.py +20 -0
- commodutil-4.3.0/commodutil/dates.py +0 -57
- commodutil-4.3.0/tests/test_dates.py +0 -34
- {commodutil-4.3.0 → commodutil-4.4.0}/.coveragerc +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/.github/workflows/1_tests.yml +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/.github/workflows/2_coverage.yml +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/.github/workflows/3_linting.yml +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/.github/workflows/4_release.yml +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/.gitignore +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/.pypirc +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/azure-build-pipelines.yml +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/arb.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/forward/__init__.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/forward/calendar.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/forward/continuous.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/forward/fly.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/forward/quarterly.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/forward/spreads.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/forward/structure.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/forward/util.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/forwards.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/standards/analysis_types.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/standards/commodity_groups.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil/standards/regions.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil.egg-info/dependency_links.txt +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/commodutil.egg-info/top_level.txt +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/requirements-test.txt +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/requirements.txt +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/requirements_dev.txt +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/setup.cfg +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/__init__.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/conftest.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/forward/__init__.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/forward/conftest.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/forward/test_calendar.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/forward/test_continuous.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/forward/test_fly.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/forward/test_quarterly.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/forward/test_spreads.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/forward/test_structure.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/forward/test_util.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/test_arb.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/test_cl.csv +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/test_forwards.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/test_standards_analysis_types.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/test_standards_commodity_groups.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/test_standards_regions.py +0 -0
- {commodutil-4.3.0 → commodutil-4.4.0}/tests/test_weekly.csv +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: commodutil
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4.0
|
|
4
4
|
Summary: common commodity/oil analytics utils
|
|
5
5
|
Author-email: aeorxc <author@example.com>
|
|
6
6
|
Project-URL: Homepage, https://dev.azure.com/RWEST-MFI-TE/Oil/_git/commodutil
|
|
@@ -9,9 +9,10 @@ Classifier: Programming Language :: Python :: 3
|
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Requires-Python: >=3.9
|
|
12
|
-
Requires-Dist: pandas
|
|
12
|
+
Requires-Dist: pandas>=2.1
|
|
13
13
|
Requires-Dist: dask[delayed]
|
|
14
14
|
Requires-Dist: pint
|
|
15
|
+
Requires-Dist: python-dateutil
|
|
15
16
|
Provides-Extra: test
|
|
16
17
|
Requires-Dist: pytest; extra == "test"
|
|
17
18
|
Requires-Dist: pytest-runner; extra == "test"
|
|
@@ -35,9 +35,12 @@ _LAZY_EXPORTS = {
|
|
|
35
35
|
"VALID_CURRENCY_TOKENS": "commodutil.standards.currency",
|
|
36
36
|
"fractional_to_major": "commodutil.standards.currency",
|
|
37
37
|
"is_fractional_currency": "commodutil.standards.currency",
|
|
38
|
+
"normalize_currency_token": "commodutil.standards.currency",
|
|
38
39
|
"split_currency_unit": "commodutil.standards.currency",
|
|
39
40
|
# standards.units (stdlib-only -- cheap, no pint)
|
|
41
|
+
"canonical_price_unit_token": "commodutil.standards.units",
|
|
40
42
|
"canonical_quantity_unit": "commodutil.standards.units",
|
|
43
|
+
"canonical_unit_token": "commodutil.standards.units",
|
|
41
44
|
"quantity_unit_from_price_unit": "commodutil.standards.units",
|
|
42
45
|
# dates
|
|
43
46
|
"curmon": "commodutil.dates",
|
|
@@ -16,7 +16,17 @@ from commodutil.standards.units import to_pint_token as _to_pint_token
|
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
|
-
# Initialize pint with custom definitions
|
|
19
|
+
# Initialize pint with custom definitions.
|
|
20
|
+
#
|
|
21
|
+
# NOTE: kept case-sensitive on purpose. `pint.UnitRegistry(case_sensitive=False)`
|
|
22
|
+
# looks tempting (it would drop the hand-registered casing aliases below), but it
|
|
23
|
+
# is UNSAFE for this registry: pint's case-folded lookup makes our domain units
|
|
24
|
+
# collide with SI units that differ only by case -- `kt`/`mt`/`gal` vs `kT`/`mT`
|
|
25
|
+
# (kilo/milli-tesla, T = tesla) and `Gal` (galileo, cm/s^2), while lowercase
|
|
26
|
+
# `mw`/`mwh` resolve to milli-watt instead of mega-. Collision winners are chosen
|
|
27
|
+
# from a set and are NON-DETERMINISTIC (PYTHONHASHSEED-dependent), so `kt`->`km3`
|
|
28
|
+
# etc. intermittently raise "incompatible dimensions". We therefore register the
|
|
29
|
+
# exact spellings we need explicitly and leave the registry case-sensitive.
|
|
20
30
|
ureg = pint.UnitRegistry()
|
|
21
31
|
|
|
22
32
|
# Define oil & gas specific units.
|
|
@@ -74,62 +84,113 @@ class Commodity:
|
|
|
74
84
|
mass<->volume conversion is undefined and must raise).
|
|
75
85
|
Previously this used `0.0 kg/L` as a sentinel; magnitude==0 checks
|
|
76
86
|
were scattered through the codebase. `None` makes the intent explicit.
|
|
87
|
+
|
|
88
|
+
`energy_content` may be a pint Quantity of EITHER dimensionality:
|
|
89
|
+
- `[energy]/[volume]` (e.g. GJ/m^3) — the default for liquids; registers a
|
|
90
|
+
`[volume] <-> [energy]` transformation (and chains to mass via density).
|
|
91
|
+
- `[energy]/[mass]` (e.g. GJ/t) — for density-less solids like coal;
|
|
92
|
+
registers a DIRECT `[mass] <-> [energy]` transformation so `mt <-> MMBtu`
|
|
93
|
+
works with `density=None`, while `mt <-> bbl` still correctly raises.
|
|
94
|
+
A bare float is coerced to the volume-basis default (GJ/m^3) for back-compat.
|
|
77
95
|
"""
|
|
78
96
|
|
|
79
97
|
name: str
|
|
80
98
|
density: Optional[pint.Quantity] = None # kg/L or API gravity; None = not a liquid
|
|
81
|
-
|
|
99
|
+
# [energy]/[volume] (GJ/m^3, liquids) OR [energy]/[mass] (GJ/t, e.g. coal)
|
|
100
|
+
energy_content: Optional[pint.Quantity] = None
|
|
82
101
|
|
|
83
102
|
def __post_init__(self):
|
|
84
103
|
# Ensure quantities have correct dimensions
|
|
85
104
|
if self.density is not None and not isinstance(self.density, pint.Quantity):
|
|
86
105
|
self.density = self.density * ureg.kg / ureg.liter
|
|
87
|
-
|
|
106
|
+
# Bare float -> volume-basis default (GJ/m^3). Callers wanting a mass
|
|
107
|
+
# basis pass an explicit Quantity (e.g. `26.377 * ureg.GJ / ureg.mt`),
|
|
108
|
+
# which is left untouched here and dispatched on in _commodity_context.
|
|
109
|
+
if self.energy_content is not None and not isinstance(
|
|
110
|
+
self.energy_content, pint.Quantity
|
|
111
|
+
):
|
|
88
112
|
self.energy_content = self.energy_content * ureg.GJ / ureg.m**3
|
|
89
113
|
|
|
90
114
|
|
|
115
|
+
# -----------------------------------------------------------------------------
|
|
116
|
+
# Calorific basis policy: GROSS (higher heating value, HHV).
|
|
117
|
+
#
|
|
118
|
+
# `energy_content` for every liquid commodity below is stated on a GROSS/HHV
|
|
119
|
+
# basis, anchored on the U.S. EIA Monthly Energy Review Appendix A, Table A1
|
|
120
|
+
# ("Approximate Heat Content of Petroleum and Biofuels", explicitly gross —
|
|
121
|
+
# "Gross heat content rates are applied in all Btu calculations for the Monthly
|
|
122
|
+
# Energy Review") and, for the NGL species, GPA/EIA figures (EIA A1 which cites
|
|
123
|
+
# NIST enthalpy-of-combustion + API liquid densities). NGL/natural-gas hubs
|
|
124
|
+
# (Henry Hub, TTF, NBP, JKM) all quote HHV, so a cross-fuel $/MMBtu comparison
|
|
125
|
+
# is only internally consistent if every commodity is on the same gross basis.
|
|
126
|
+
#
|
|
127
|
+
# The stored value is GJ/m^3 (energy per unit VOLUME), set directly from the
|
|
128
|
+
# EIA gross figure in MMBtu/bbl via the fixed, density-independent conversion
|
|
129
|
+
# GJ/m^3 = MMBtu/bbl * 1.055056 GJ/MMBtu / 0.158987294928 m^3/bbl
|
|
130
|
+
# so that convfactor('bbl','MMBtu', <commodity>) reproduces the EIA number
|
|
131
|
+
# exactly. Densities are NOT part of this conversion; they remain the
|
|
132
|
+
# BP/Energy-Institute values unchanged (the resulting GJ/t is therefore a
|
|
133
|
+
# derived reporting figure = energy_content / density). Each entry's comment
|
|
134
|
+
# records the source EIA MMBtu/bbl figure and the implied gross GJ/t.
|
|
135
|
+
#
|
|
136
|
+
# Historical note: several products (diesel/naphtha/jet/gasoline/fuel_oil) and
|
|
137
|
+
# the biofuels previously carried BP-style NET (NCV) energy contents, which
|
|
138
|
+
# understated $/MMBtu by ~1-12% vs gross. Standardising on gross was the
|
|
139
|
+
# owner-approved decision (conversion-architecture-plan.md, decision 2b).
|
|
140
|
+
#
|
|
141
|
+
# NOT covered by this policy (see per-entry comments / flagged for desk review):
|
|
142
|
+
# crude (kept on its documented BP gross basis), the LPG/LNG blends (`lpg`,
|
|
143
|
+
# `natgas`), gaseous `natural_gas`, and `product_basket` — all blend- or
|
|
144
|
+
# construction-dependent rather than single-species EIA rows.
|
|
145
|
+
# -----------------------------------------------------------------------------
|
|
91
146
|
# Define commodities with their properties and correct industry factors
|
|
92
147
|
COMMODITIES = {
|
|
93
148
|
# Crude oil (BP approximate conversion factors)
|
|
94
149
|
# 1 mt ≈ 7.33 bbl and ≈ 1.165 kL => density ≈ 0.85809 kg/L
|
|
95
|
-
"crude": Commodity(
|
|
150
|
+
"crude": Commodity(
|
|
151
|
+
"crude", 0.85809151 * ureg.kg / ureg.L, 39.043 * ureg.GJ / ureg.m**3
|
|
152
|
+
), # HHV; BP/EI ~45.5 GJ/t gross (implies 5.883 MMBtu/bbl). EIA Table A2 crude ~5.69-5.80 MMBtu/bbl (US, lighter) -> commodutil is ~1-3% higher; kept on documented BP world-crude gross basis, not changed
|
|
96
153
|
# Light ends - tuned to match kbbl/kt figures exactly
|
|
97
154
|
"gasoline": Commodity(
|
|
98
|
-
"gasoline", 0.755079324 * ureg.kg / ureg.L,
|
|
99
|
-
), # BP
|
|
155
|
+
"gasoline", 0.755079324 * ureg.kg / ureg.L, 34.653728 * ureg.GJ / ureg.m**3
|
|
156
|
+
), # HHV; EIA motor gasoline blending components (2007+, GREET) 5.222 MMBtu/bbl -> 45.89 GJ/t gross (was BP NCV 44.75; RBOB/blendstock basis, not E10-finished ~5.05)
|
|
100
157
|
"naphtha": Commodity(
|
|
101
|
-
"naphtha", 0.706720311 * ureg.kg / ureg.L,
|
|
102
|
-
), #
|
|
158
|
+
"naphtha", 0.706720311 * ureg.kg / ureg.L, 34.826266 * ureg.GJ / ureg.m**3
|
|
159
|
+
), # HHV; EIA petrochemical naphtha <401F 5.248 MMBtu/bbl -> 49.28 GJ/t gross (was NCV 44.90)
|
|
103
160
|
"ethanol": Commodity(
|
|
104
|
-
"ethanol", 0.755079324 * ureg.kg / ureg.L,
|
|
105
|
-
), #
|
|
161
|
+
"ethanol", 0.755079324 * ureg.kg / ureg.L, 23.485167 * ureg.GJ / ureg.m**3
|
|
162
|
+
), # HHV; EIA fuel ethanol (undenatured) 3.539 MMBtu/bbl -> 31.10 GJ/t gross (was net ~27.81); NOTE density 0.7551 looks copied from gasoline (ethanol ~0.789) - flagged, left unchanged
|
|
106
163
|
# Middle distillates
|
|
107
164
|
"diesel": Commodity(
|
|
108
|
-
"diesel", 0.844269902 * ureg.kg / ureg.L,
|
|
109
|
-
), #
|
|
165
|
+
"diesel", 0.844269902 * ureg.kg / ureg.L, 38.290312 * ureg.GJ / ureg.m**3
|
|
166
|
+
), # HHV; EIA distillate (15 ppm & under) 5.770 MMBtu/bbl -> 45.35 GJ/t gross (was BP NCV 43.38)
|
|
110
167
|
"jet": Commodity(
|
|
111
|
-
"jet", 0.798199336 * ureg.kg / ureg.L,
|
|
112
|
-
), #
|
|
168
|
+
"jet", 0.798199336 * ureg.kg / ureg.L, 37.626702 * ureg.GJ / ureg.m**3
|
|
169
|
+
), # HHV; EIA jet fuel kerosene-type 5.670 MMBtu/bbl -> 47.14 GJ/t gross (was BP NCV 43.92)
|
|
113
170
|
"fame": Commodity(
|
|
114
|
-
"fame", 0.892001564 * ureg.kg / ureg.L,
|
|
115
|
-
), #
|
|
171
|
+
"fame", 0.892001564 * ureg.kg / ureg.L, 35.562874 * ureg.GJ / ureg.m**3
|
|
172
|
+
), # HHV; EIA biodiesel 5.359 MMBtu/bbl -> 39.87 GJ/t gross (was net ~37.00)
|
|
116
173
|
"hvo": Commodity(
|
|
117
|
-
"hvo", 0.781731391 * ureg.kg / ureg.L,
|
|
118
|
-
), #
|
|
174
|
+
"hvo", 0.781731391 * ureg.kg / ureg.L, 36.458748 * ureg.GJ / ureg.m**3
|
|
175
|
+
), # HHV; EIA renewable diesel fuel 5.494 MMBtu/bbl -> 46.64 GJ/t gross (was net ~43.49)
|
|
119
176
|
# Heavy products
|
|
120
177
|
"vgo": Commodity("vgo", 0.911566778 * ureg.kg / ureg.L, None), # 6.90 kbbl/kt
|
|
121
178
|
"fuel_oil": Commodity(
|
|
122
|
-
"fuel_oil", 0.990521381 * ureg.kg / ureg.L, 41.
|
|
123
|
-
), #
|
|
179
|
+
"fuel_oil", 0.990521381 * ureg.kg / ureg.L, 41.721177 * ureg.GJ / ureg.m**3
|
|
180
|
+
), # HHV; EIA residual fuel oil 6.287 MMBtu/bbl -> 42.12 GJ/t gross (was BP NCV 41.57)
|
|
124
181
|
# LPG and Natural gas (liquefied)
|
|
125
182
|
"lpg": Commodity(
|
|
126
183
|
"lpg", 0.541 * ureg.kg / ureg.L, 24.96715 * ureg.GJ / ureg.m**3
|
|
127
|
-
), # BP: LPG 46.15 GJ/t
|
|
184
|
+
), # BP: LPG 46.15 GJ/t (implies 3.762 MMBtu/bbl). FLAGGED: generic propane/butane blend; likely NET-basis, but the gross value is composition-dependent (undocumented mix) -> not changed. Desk: specify a C3/C4 split, or price via the discrete NGL species instead.
|
|
128
185
|
"natgas": Commodity(
|
|
129
186
|
"natgas", 0.542225066 * ureg.kg / ureg.L, 26.137 * ureg.GJ / ureg.m**3
|
|
130
|
-
),
|
|
187
|
+
), # liquefied NG / LNG (alias 'lng'); implies 48.20 GJ/t, 3.939 MMBtu/bbl. FLAGGED: density 0.542 is high for LNG (~0.43-0.47) and basis is unclear -> not changed pending desk clarification of what this represents vs 'natural_gas'.
|
|
131
188
|
# Natural gas (gaseous, pipeline): BP approx 36 PJ per bcm => 0.036 GJ/m**3
|
|
132
189
|
# density=None: not a liquid, so mass<->volume conversion is undefined.
|
|
190
|
+
# FLAGGED (HHV policy): 0.036 GJ/m^3 (=36 MJ/m^3) is a rounded BP approx sitting
|
|
191
|
+
# between net (~34.6) and US-pipeline gross (~38.3 MJ/m^3, ~1028 Btu/scf). Not
|
|
192
|
+
# clearly net, and it has broad blast radius (bcm<->energy in many callers +
|
|
193
|
+
# explicit tests), so left unchanged; desk to confirm gross ~0.0383 if wanted.
|
|
133
194
|
"natural_gas": Commodity(
|
|
134
195
|
"natural_gas", density=None, energy_content=0.036 * ureg.GJ / ureg.m**3
|
|
135
196
|
),
|
|
@@ -138,23 +199,36 @@ COMMODITIES = {
|
|
|
138
199
|
# $/gal <-> $/MMBtu for the MB OPIS futures (AC0, B0, AD0, A8I)).
|
|
139
200
|
"ethane": Commodity(
|
|
140
201
|
"ethane", 0.373 * ureg.kg / ureg.L, 18.4262 * ureg.GJ / ureg.m**3
|
|
141
|
-
), #
|
|
202
|
+
), # HHV; implies 2.777 MMBtu/bbl vs EIA ethane 2.783 (+0.2%, within tol) -> left as-is
|
|
142
203
|
"propane": Commodity(
|
|
143
204
|
"propane", 0.507 * ureg.kg / ureg.L, 25.5375 * ureg.GJ / ureg.m**3
|
|
144
|
-
), #
|
|
205
|
+
), # HHV; implies 3.848 MMBtu/bbl vs EIA propane 3.841 (-0.2%, within tol) -> left as-is
|
|
145
206
|
"butane": Commodity(
|
|
146
|
-
"butane", 0.574 * ureg.kg / ureg.L, 28.
|
|
147
|
-
), # n-butane
|
|
207
|
+
"butane", 0.574 * ureg.kg / ureg.L, 28.887 * ureg.GJ / ureg.m**3
|
|
208
|
+
), # HHV; EIA MER App A n-butane 4.353 MMBtu/bbl gross (was 28.44 -> 4.286 MMBtu/bbl, -1.57% off EIA; aligned 2026-07 per EIA-gross basis decision)
|
|
148
209
|
"isobutane": Commodity(
|
|
149
210
|
"isobutane", 0.557 * ureg.kg / ureg.L, 27.7700 * ureg.GJ / ureg.m**3
|
|
150
|
-
), #
|
|
211
|
+
), # HHV; implies 4.185 MMBtu/bbl vs EIA isobutane 4.183 (-0.0%, within tol) -> left as-is
|
|
151
212
|
"natural_gasoline": Commodity(
|
|
152
|
-
"natural_gasoline", 0.6675 * ureg.kg / ureg.L,
|
|
153
|
-
), # pentanes-plus
|
|
213
|
+
"natural_gasoline", 0.6675 * ureg.kg / ureg.L, 30.778 * ureg.GJ / ureg.m**3
|
|
214
|
+
), # HHV; EIA MER App A pentanes-plus 4.638 MMBtu/bbl gross (was 31.4 -> 4.732 MMBtu/bbl, +1.98% off EIA; aligned 2026-07 per EIA-gross basis decision)
|
|
154
215
|
# BP product basket (optional reference)
|
|
155
216
|
"product_basket": Commodity(
|
|
156
217
|
"product_basket", 0.781 * ureg.kg / ureg.L, 33.642356 * ureg.GJ / ureg.m**3
|
|
157
|
-
),
|
|
218
|
+
), # implies 43.08 GJ/t, 5.070 MMBtu/bbl. FLAGGED: weighted product basket, construction (component weights) undocumented -> not changed; should be recomputed from the now-gross component products with documented weights rather than guessed.
|
|
219
|
+
# Solid fuels: MASS-basis energy_content (GJ/t) with density=None. mt<->energy
|
|
220
|
+
# (the only meaningful jump, $/t <-> $/MMBtu) works; mt<->bbl stays illegal.
|
|
221
|
+
"coal": Commodity(
|
|
222
|
+
"coal",
|
|
223
|
+
density=None,
|
|
224
|
+
energy_content=26.377 * ureg.GJ / ureg.mt,
|
|
225
|
+
), # API2 CIF ARA thermal coal. GROSS/HHV basis: traded spec is 6,000 kcal/kg
|
|
226
|
+
# NAR (net); the gross equivalent is ~6,300 kcal/kg GAR (Argus/CoalIndo NAR/GAR
|
|
227
|
+
# markers show a ~300 kcal/kg NAR->GAR gap for high-rank export coal, e.g.
|
|
228
|
+
# 6,200 NAR = 6,500 GAR). 6,300 kcal/kg x 4.1868 kJ/kcal (IT calorie) = 26.377
|
|
229
|
+
# GJ/t = 25.00 MMBtu/t. Replaces the legacy /27.76 tce divisor in
|
|
230
|
+
# oilpricingcharts energy_mmbtu (generic tonne-of-coal-equivalent, 7,000 kcal/kg
|
|
231
|
+
# GCV = 29.29 GJ/t) which understated coal $/MMBtu by ~11%.
|
|
158
232
|
}
|
|
159
233
|
|
|
160
234
|
# Aliases for compatibility
|
|
@@ -270,97 +344,115 @@ class CommodityConverter:
|
|
|
270
344
|
|
|
271
345
|
return result
|
|
272
346
|
|
|
347
|
+
@lru_cache(maxsize=128)
|
|
348
|
+
def _commodity_context(self, name: str) -> pint.Context:
|
|
349
|
+
"""Build (and cache) a pint Context expressing a commodity's dimensional
|
|
350
|
+
transformations.
|
|
351
|
+
|
|
352
|
+
A commodity's physical properties define which cross-dimension jumps are
|
|
353
|
+
legal:
|
|
354
|
+
- ``density`` (kg/m^3) enables ``[mass] <-> [volume]``.
|
|
355
|
+
- ``energy_content`` on a VOLUME basis (J/m^3) enables
|
|
356
|
+
``[volume] <-> [energy]`` (and chains to ``[mass] <-> [energy]`` via
|
|
357
|
+
density when present).
|
|
358
|
+
- ``energy_content`` on a MASS basis (J/kg) enables ``[mass] <-> [energy]``
|
|
359
|
+
DIRECTLY — for density-less solids (coal) whose only meaningful jump
|
|
360
|
+
is $/t <-> $/MMBtu. No volume transform is registered, so a
|
|
361
|
+
mass<->volume jump still finds no path and raises.
|
|
362
|
+
pint composes the registered transformations into a graph and finds the
|
|
363
|
+
conversion path itself — no manual branching on the path is needed.
|
|
364
|
+
|
|
365
|
+
Properties that are ``None`` register no transformation, so pint simply
|
|
366
|
+
finds no path and raises ``DimensionalityError`` (translated to a domain
|
|
367
|
+
``ValueError`` by the caller). This is how density=None commodities
|
|
368
|
+
(natural_gas, naphtha, vgo) reject any mass<->volume(<->energy) jump.
|
|
369
|
+
"""
|
|
370
|
+
comm = self.get_commodity(name)
|
|
371
|
+
ctx = pint.Context(comm.name)
|
|
372
|
+
if comm.density is not None:
|
|
373
|
+
d = comm.density.to("kg/m^3")
|
|
374
|
+
ctx.add_transformation("[mass]", "[volume]", lambda ureg, x, d=d: x / d)
|
|
375
|
+
ctx.add_transformation("[volume]", "[mass]", lambda ureg, x, d=d: x * d)
|
|
376
|
+
if comm.energy_content is not None:
|
|
377
|
+
ec = comm.energy_content
|
|
378
|
+
if ec.check("[energy]/[volume]"):
|
|
379
|
+
e = ec.to("J/m^3")
|
|
380
|
+
ctx.add_transformation(
|
|
381
|
+
"[volume]", "[energy]", lambda ureg, x, e=e: x * e
|
|
382
|
+
)
|
|
383
|
+
ctx.add_transformation(
|
|
384
|
+
"[energy]", "[volume]", lambda ureg, x, e=e: x / e
|
|
385
|
+
)
|
|
386
|
+
elif ec.check("[energy]/[mass]"):
|
|
387
|
+
em = ec.to("J/kg")
|
|
388
|
+
ctx.add_transformation(
|
|
389
|
+
"[mass]", "[energy]", lambda ureg, x, em=em: x * em
|
|
390
|
+
)
|
|
391
|
+
ctx.add_transformation(
|
|
392
|
+
"[energy]", "[mass]", lambda ureg, x, em=em: x / em
|
|
393
|
+
)
|
|
394
|
+
else:
|
|
395
|
+
raise ValueError(
|
|
396
|
+
f"{comm.name}: energy_content must be [energy]/[volume] or "
|
|
397
|
+
f"[energy]/[mass], got dimensionality {dict(ec.dimensionality)}"
|
|
398
|
+
)
|
|
399
|
+
return ctx
|
|
400
|
+
|
|
273
401
|
def _convert_scalar(
|
|
274
402
|
self, value: float, from_unit: str, to_unit: str, commodity: Optional[str]
|
|
275
403
|
) -> float:
|
|
276
|
-
"""Convert a scalar value across mass/volume/energy using commodity
|
|
404
|
+
"""Convert a scalar value across mass/volume/energy using a commodity's
|
|
405
|
+
pint Context when a cross-dimension jump is needed."""
|
|
277
406
|
from_unit = _to_pint_token(from_unit)
|
|
278
407
|
to_unit = _to_pint_token(to_unit)
|
|
279
408
|
qty = value * self.ureg(from_unit)
|
|
280
409
|
|
|
281
|
-
#
|
|
410
|
+
# Same-dimension conversion (e.g. kt->mt, bbl->L): no commodity needed.
|
|
282
411
|
try:
|
|
283
412
|
return qty.to(to_unit).magnitude
|
|
284
413
|
except DimensionalityError:
|
|
285
414
|
pass
|
|
286
415
|
|
|
287
|
-
#
|
|
416
|
+
# Cross-dimensional: needs commodity physical properties. Decide the
|
|
417
|
+
# right error up front when no commodity was supplied (mirrors the
|
|
418
|
+
# historical messages the callers/tests grep for).
|
|
288
419
|
is_from_energy = self._is_energy(from_unit)
|
|
289
420
|
is_to_energy = self._is_energy(to_unit)
|
|
290
|
-
|
|
291
|
-
is_to_mass = self._is_mass(to_unit)
|
|
292
|
-
is_from_volume = self._is_volume(from_unit)
|
|
293
|
-
is_to_volume = self._is_volume(to_unit)
|
|
294
|
-
|
|
295
|
-
# Energy conversions
|
|
296
|
-
if is_from_energy or is_to_energy:
|
|
297
|
-
if not commodity:
|
|
298
|
-
raise ValueError("Commodity required for energy conversion")
|
|
299
|
-
comm = self.get_commodity(commodity)
|
|
300
|
-
if not comm.energy_content:
|
|
301
|
-
raise ValueError(f"No energy content defined for {commodity}")
|
|
302
|
-
|
|
303
|
-
ec = comm.energy_content.to("J/m^3")
|
|
304
|
-
|
|
305
|
-
if is_from_energy:
|
|
306
|
-
energy_J = qty.to("J")
|
|
307
|
-
# Energy -> Volume or Mass
|
|
308
|
-
volume_m3 = (energy_J / ec).to("m^3")
|
|
309
|
-
if is_to_volume:
|
|
310
|
-
return volume_m3.to(to_unit).magnitude
|
|
311
|
-
elif is_to_mass:
|
|
312
|
-
if comm.density is None:
|
|
313
|
-
raise ValueError(
|
|
314
|
-
f"Mass<->volume conversion not supported for {commodity!r} "
|
|
315
|
-
f"(no density defined); cannot convert energy to mass"
|
|
316
|
-
)
|
|
317
|
-
density_kg_m3 = comm.density.to("kg/m^3")
|
|
318
|
-
mass_kg = (volume_m3 * density_kg_m3).to("kg")
|
|
319
|
-
return mass_kg.to(to_unit).magnitude
|
|
320
|
-
else:
|
|
321
|
-
raise ValueError(f"Cannot convert energy to {to_unit}")
|
|
322
|
-
else:
|
|
323
|
-
# Volume/Mass -> Energy
|
|
324
|
-
if is_from_mass:
|
|
325
|
-
if comm.density is None:
|
|
326
|
-
raise ValueError(
|
|
327
|
-
f"Mass<->volume conversion not supported for {commodity!r} "
|
|
328
|
-
f"(no density defined); cannot convert mass to energy"
|
|
329
|
-
)
|
|
330
|
-
density_kg_m3 = comm.density.to("kg/m^3")
|
|
331
|
-
mass_kg = qty.to("kg")
|
|
332
|
-
volume_m3 = (mass_kg / density_kg_m3).to("m^3")
|
|
333
|
-
elif is_from_volume:
|
|
334
|
-
volume_m3 = qty.to("m^3")
|
|
335
|
-
else:
|
|
336
|
-
raise ValueError(f"Cannot convert {from_unit} to energy")
|
|
337
|
-
energy_J = (volume_m3 * ec).to("J")
|
|
338
|
-
return energy_J.to(to_unit).magnitude
|
|
421
|
+
involves_energy = is_from_energy or is_to_energy
|
|
339
422
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
423
|
+
if not commodity:
|
|
424
|
+
if involves_energy:
|
|
425
|
+
raise ValueError("Commodity required for energy conversion")
|
|
426
|
+
is_from_mass = self._is_mass(from_unit)
|
|
427
|
+
is_to_mass = self._is_mass(to_unit)
|
|
428
|
+
is_from_volume = self._is_volume(from_unit)
|
|
429
|
+
is_to_volume = self._is_volume(to_unit)
|
|
430
|
+
if (is_from_mass and is_to_volume) or (is_from_volume and is_to_mass):
|
|
343
431
|
raise ValueError(f"Commodity required for {from_unit} to {to_unit}")
|
|
344
|
-
|
|
345
|
-
|
|
432
|
+
raise ValueError(
|
|
433
|
+
f"Cannot convert from {from_unit} to {to_unit} - incompatible dimensions"
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Let the commodity's context find the conversion path (including
|
|
437
|
+
# mass<->energy chaining). A missing property leaves no path -> pint
|
|
438
|
+
# raises DimensionalityError, which we translate to the domain error.
|
|
439
|
+
comm = self.get_commodity(commodity)
|
|
440
|
+
ctx = self._commodity_context(commodity)
|
|
441
|
+
try:
|
|
442
|
+
return qty.to(to_unit, ctx).magnitude
|
|
443
|
+
except DimensionalityError as exc:
|
|
444
|
+
if involves_energy and comm.energy_content is None:
|
|
445
|
+
raise ValueError(f"No energy content defined for {commodity}") from exc
|
|
446
|
+
if comm.density is None and (
|
|
447
|
+
self._is_mass(from_unit) or self._is_mass(to_unit)
|
|
448
|
+
):
|
|
346
449
|
raise ValueError(
|
|
347
450
|
f"Mass<->volume conversion not supported for {commodity!r} "
|
|
348
451
|
f"(no density defined)"
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
mass_kg = qty.to("kg")
|
|
354
|
-
volume_L = (mass_kg / density_kg_L).to("L")
|
|
355
|
-
return volume_L.to(to_unit).magnitude
|
|
356
|
-
else:
|
|
357
|
-
volume_L = qty.to("L")
|
|
358
|
-
mass_kg = (volume_L * density_kg_L).to("kg")
|
|
359
|
-
return mass_kg.to(to_unit).magnitude
|
|
360
|
-
|
|
361
|
-
raise ValueError(
|
|
362
|
-
f"Cannot convert from {from_unit} to {to_unit} - incompatible dimensions"
|
|
363
|
-
)
|
|
452
|
+
) from exc
|
|
453
|
+
raise ValueError(
|
|
454
|
+
f"Cannot convert from {from_unit} to {to_unit} - incompatible dimensions"
|
|
455
|
+
) from exc
|
|
364
456
|
|
|
365
457
|
def _convert_series(
|
|
366
458
|
self,
|
|
@@ -450,25 +542,13 @@ class CommodityConverter:
|
|
|
450
542
|
return from_per_day / to_per_day
|
|
451
543
|
|
|
452
544
|
def _is_energy(self, unit: str) -> bool:
|
|
453
|
-
|
|
454
|
-
(1 * self.ureg(unit)).to("J")
|
|
455
|
-
return True
|
|
456
|
-
except DimensionalityError:
|
|
457
|
-
return False
|
|
545
|
+
return self.ureg(unit).check("[energy]")
|
|
458
546
|
|
|
459
547
|
def _is_mass(self, unit: str) -> bool:
|
|
460
|
-
|
|
461
|
-
(1 * self.ureg(unit)).to("kg")
|
|
462
|
-
return True
|
|
463
|
-
except DimensionalityError:
|
|
464
|
-
return False
|
|
548
|
+
return self.ureg(unit).check("[mass]")
|
|
465
549
|
|
|
466
550
|
def _is_volume(self, unit: str) -> bool:
|
|
467
|
-
|
|
468
|
-
(1 * self.ureg(unit)).to("m^3")
|
|
469
|
-
return True
|
|
470
|
-
except DimensionalityError:
|
|
471
|
-
return False
|
|
551
|
+
return self.ureg(unit).check("[length]**3")
|
|
472
552
|
|
|
473
553
|
@property
|
|
474
554
|
def available_commodities(self) -> list:
|
|
@@ -629,12 +709,11 @@ def convert_price(
|
|
|
629
709
|
f"convert_price currently only supports USD/* as target; got '{to_unit}'"
|
|
630
710
|
)
|
|
631
711
|
|
|
632
|
-
# Unit-leg conversion (no FX yet — uses commodity factors)
|
|
712
|
+
# Unit-leg conversion (no FX yet — uses commodity factors).
|
|
713
|
+
# convfactor returns a nonzero float or raises, so no None/zero guard is
|
|
714
|
+
# needed here.
|
|
633
715
|
factor = convfactor(from_bare_unit, to_bare_unit, commodity)
|
|
634
|
-
|
|
635
|
-
unit_converted = value
|
|
636
|
-
else:
|
|
637
|
-
unit_converted = value / factor
|
|
716
|
+
unit_converted = value / factor
|
|
638
717
|
|
|
639
718
|
# Same-base fractional case: USc -> USD, GBp -> GBP, EUc -> EUR, JPy -> JPY.
|
|
640
719
|
# This is a pure /100 scale (or *100 in the reverse direction) — no FX
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import re
|
|
3
|
+
from datetime import datetime, date, time, timedelta
|
|
4
|
+
|
|
5
|
+
from dateutil.relativedelta import relativedelta
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _curmon():
|
|
9
|
+
return datetime.now().month
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _curyear():
|
|
13
|
+
return datetime.now().year
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _curmonyear():
|
|
17
|
+
return datetime(_curyear(), _curmon(), 1)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _curmonyear_str():
|
|
21
|
+
return "%s-%s" % (_curyear(), _curmon()) # get pandas time filtering
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _last_day_of_prev_month():
|
|
25
|
+
return date.today().replace(day=1) - relativedelta(days=1)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _start_day_of_prev_month():
|
|
29
|
+
return date.today().replace(day=1) - relativedelta(months=1)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _prevmon():
|
|
33
|
+
return _start_day_of_prev_month().month
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _prevmon_str():
|
|
37
|
+
sdpm = _start_day_of_prev_month()
|
|
38
|
+
return "%s-%s" % (sdpm.year, sdpm.month) # get pandas time filtering
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _nextyear():
|
|
42
|
+
return _curyear() + 1
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _prevyear():
|
|
46
|
+
return _curyear() - 1
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Compute "current date" values live on each attribute access (PEP 562) so that a
|
|
50
|
+
# long-running process (Prefect worker, Dash app) that imports this module once still
|
|
51
|
+
# sees the correct month/year after a boundary, instead of a stale import-time snapshot.
|
|
52
|
+
_DYNAMIC = {
|
|
53
|
+
"curmon": _curmon,
|
|
54
|
+
"curyear": _curyear,
|
|
55
|
+
"curmonyear": _curmonyear,
|
|
56
|
+
"curmonyear_str": _curmonyear_str,
|
|
57
|
+
"last_day_of_prev_month": _last_day_of_prev_month,
|
|
58
|
+
"start_day_of_prev_month": _start_day_of_prev_month,
|
|
59
|
+
"prevmon": _prevmon,
|
|
60
|
+
"prevmon_str": _prevmon_str,
|
|
61
|
+
"nextyear": _nextyear,
|
|
62
|
+
"prevyear": _prevyear,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def __getattr__(name):
|
|
67
|
+
try:
|
|
68
|
+
return _DYNAMIC[name]()
|
|
69
|
+
except KeyError:
|
|
70
|
+
raise AttributeError(name)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def find_year(df, use_delta=False):
|
|
74
|
+
"""
|
|
75
|
+
Given a dataframe find the years in the column headings. Return a dict of colname to year
|
|
76
|
+
eg { 'Q1 2016' : 2016, 'Q1 2017' : 2017
|
|
77
|
+
"""
|
|
78
|
+
res = {}
|
|
79
|
+
for colname in df:
|
|
80
|
+
colregex = re.findall("\d\d\d\d", str(colname))
|
|
81
|
+
colyear = None
|
|
82
|
+
if len(colregex) >= 1:
|
|
83
|
+
colyear = int(colregex[0])
|
|
84
|
+
|
|
85
|
+
if colyear:
|
|
86
|
+
res[colname] = colyear
|
|
87
|
+
if colyear and use_delta:
|
|
88
|
+
delta = colyear - _curyear()
|
|
89
|
+
res[colname] = delta
|
|
90
|
+
else:
|
|
91
|
+
res[colname] = colname
|
|
92
|
+
|
|
93
|
+
return res
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def time_until_end_of_day(dt=None):
|
|
97
|
+
# type: (datetime.datetime) -> datetime.timedelta
|
|
98
|
+
"""
|
|
99
|
+
Get timedelta until end of day on the datetime passed, or current time.
|
|
100
|
+
"""
|
|
101
|
+
if dt is None:
|
|
102
|
+
dt = datetime.now()
|
|
103
|
+
tomorrow = dt + timedelta(days=1)
|
|
104
|
+
return (datetime.combine(tomorrow, time.min) - dt).seconds
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from functools import reduce
|
|
3
3
|
|
|
4
|
-
import numpy as np
|
|
5
4
|
import pandas as pd
|
|
6
5
|
|
|
7
6
|
|
|
@@ -38,15 +37,11 @@ def fillna_downbet(df):
|
|
|
38
37
|
"""
|
|
39
38
|
Fill weekends/holidays in timeseries but don't extend values beyond the last non-NaN entry.
|
|
40
39
|
"""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
start, end = non_nans.index[0], non_nans.index[-1]
|
|
47
|
-
# Perform ffill only on the portion that has data
|
|
48
|
-
df.loc[start:end, col] = df.loc[start:end, col].ffill()
|
|
49
|
-
return df
|
|
40
|
+
# ffill(limit_area="inside") only fills gaps between existing valid values,
|
|
41
|
+
# never before the first or after the last non-NaN — equivalent to the old
|
|
42
|
+
# per-column ffill over [first_valid:last_valid]. Returns a new frame, so the
|
|
43
|
+
# input is not mutated. Requires pandas>=2.1.
|
|
44
|
+
return df.ffill(limit_area="inside")
|
|
50
45
|
|
|
51
46
|
|
|
52
47
|
def sql_insert_statement_from_dataframe(df, table_name, print_statemnt=False):
|
|
@@ -28,10 +28,15 @@ from commodutil.standards.currency import (
|
|
|
28
28
|
VALID_CURRENCY_TOKENS,
|
|
29
29
|
fractional_to_major,
|
|
30
30
|
is_fractional_currency,
|
|
31
|
+
normalize_currency_token,
|
|
31
32
|
required_fx_pair,
|
|
32
33
|
split_currency_unit,
|
|
33
34
|
to_symbol,
|
|
34
35
|
)
|
|
36
|
+
from commodutil.standards.price_units import (
|
|
37
|
+
resolve_price_unit,
|
|
38
|
+
resolve_price_unit_from_attrs,
|
|
39
|
+
)
|
|
35
40
|
from commodutil.standards.regions import (
|
|
36
41
|
CRUDE_GRADE_REGIONS,
|
|
37
42
|
REGION_PATTERNS,
|
|
@@ -43,7 +48,9 @@ from commodutil.standards.regions import (
|
|
|
43
48
|
)
|
|
44
49
|
from commodutil.standards.units import (
|
|
45
50
|
UNIT_MAP,
|
|
51
|
+
canonical_price_unit_token,
|
|
46
52
|
canonical_quantity_unit,
|
|
53
|
+
canonical_unit_token,
|
|
47
54
|
default_unit_for_commodity,
|
|
48
55
|
quantity_unit_from_price_unit,
|
|
49
56
|
to_pint_token,
|
|
@@ -70,9 +77,13 @@ __all__ = [
|
|
|
70
77
|
"VALID_CURRENCY_TOKENS",
|
|
71
78
|
"fractional_to_major",
|
|
72
79
|
"is_fractional_currency",
|
|
80
|
+
"normalize_currency_token",
|
|
73
81
|
"required_fx_pair",
|
|
74
82
|
"split_currency_unit",
|
|
75
83
|
"to_symbol",
|
|
84
|
+
# price_units
|
|
85
|
+
"resolve_price_unit",
|
|
86
|
+
"resolve_price_unit_from_attrs",
|
|
76
87
|
# regions
|
|
77
88
|
"CRUDE_GRADE_REGIONS",
|
|
78
89
|
"REGION_PATTERNS",
|
|
@@ -83,7 +94,9 @@ __all__ = [
|
|
|
83
94
|
"normalize_region",
|
|
84
95
|
# units
|
|
85
96
|
"UNIT_MAP",
|
|
97
|
+
"canonical_price_unit_token",
|
|
86
98
|
"canonical_quantity_unit",
|
|
99
|
+
"canonical_unit_token",
|
|
87
100
|
"default_unit_for_commodity",
|
|
88
101
|
"quantity_unit_from_price_unit",
|
|
89
102
|
"to_pint_token",
|