commodutil 3.6.1__tar.gz → 3.6.2__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-3.6.1 → commodutil-3.6.2}/PKG-INFO +1 -1
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/stats.py +83 -5
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil.egg-info/PKG-INFO +1 -1
- {commodutil-3.6.1 → commodutil-3.6.2}/.coveragerc +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/.github/workflows/1_tests.yml +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/.github/workflows/2_coverage.yml +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/.github/workflows/3_linting.yml +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/.github/workflows/4_release.yml +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/.gitignore +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/.pypirc +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/azure-build-pipelines.yml +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/__init__.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/arb.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/convfactors.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/dates.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forward/__init__.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forward/calendar.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forward/continuous.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forward/fly.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forward/quarterly.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forward/spreads.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forward/structure.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forward/util.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forwards.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/pandasutil.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/transforms.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil.egg-info/SOURCES.txt +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil.egg-info/dependency_links.txt +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil.egg-info/requires.txt +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/commodutil.egg-info/top_level.txt +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/pyproject.toml +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/requirements-test.txt +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/requirements.txt +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/requirements_dev.txt +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/scripts/rbw_structure_scan.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/setup.cfg +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/__init__.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/conftest.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/__init__.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/conftest.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/test_calendar.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/test_continuous.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/test_fly.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/test_quarterly.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/test_spreads.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/test_structure.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/test_util.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_arb.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_cl.csv +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_conv.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_dates.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_forwards.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_pandasutils.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_price_conv.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_stats.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_transforms.py +0 -0
- {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_weekly.csv +0 -0
|
@@ -211,9 +211,6 @@ def reindex_year_point_stats(
|
|
|
211
211
|
|
|
212
212
|
dft = transforms.reindex_year(df)
|
|
213
213
|
|
|
214
|
-
if trim_expiry:
|
|
215
|
-
dft = trim_expiry_noise(dft)
|
|
216
|
-
|
|
217
214
|
if dft is None or dft.empty:
|
|
218
215
|
return PointStats(
|
|
219
216
|
asof=pd.NaT,
|
|
@@ -227,9 +224,14 @@ def reindex_year_point_stats(
|
|
|
227
224
|
percentile=None,
|
|
228
225
|
)
|
|
229
226
|
|
|
230
|
-
|
|
231
|
-
|
|
227
|
+
# Select prompt BEFORE trimming so rollover logic sees complete data
|
|
232
228
|
prompt_col = select_reindex_prompt_column(dft, within_days=within_days)
|
|
229
|
+
|
|
230
|
+
if trim_expiry:
|
|
231
|
+
exclude = [prompt_col] if prompt_col is not None else None
|
|
232
|
+
dft = trim_expiry_noise(dft, exclude_columns=exclude)
|
|
233
|
+
|
|
234
|
+
asof_ts = pd.Timestamp(dft.index.max()) if asof is None else pd.Timestamp(asof)
|
|
233
235
|
year_map = dates.find_year(dft)
|
|
234
236
|
prompt_year = year_map.get(prompt_col) if prompt_col is not None else None
|
|
235
237
|
prompt_year_int = prompt_year if isinstance(prompt_year, int) else None
|
|
@@ -387,6 +389,66 @@ def _base_label_from_column(col) -> str | None:
|
|
|
387
389
|
return s or None
|
|
388
390
|
|
|
389
391
|
|
|
392
|
+
# ---------------------------------------------------------------------------
|
|
393
|
+
# Expired-structure detection
|
|
394
|
+
# ---------------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
_QUARTER_LAST_MONTH = {"q1": 3, "q2": 6, "q3": 9, "q4": 12}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _front_delivery_month(base_label):
|
|
400
|
+
"""Extract front delivery month number from label like 'JanFeb', 'Q1Q2'.
|
|
401
|
+
|
|
402
|
+
Uses month_abbr_inv from commodutil.forward.util for month parsing
|
|
403
|
+
(same pattern as spread_combination_fly in forwards.py).
|
|
404
|
+
"""
|
|
405
|
+
from commodutil.forward.util import month_abbr_inv
|
|
406
|
+
|
|
407
|
+
s = base_label.strip().lower()
|
|
408
|
+
if not s or len(s) < 2:
|
|
409
|
+
return None
|
|
410
|
+
# Monthly: starts with 3-letter month abbreviation
|
|
411
|
+
if len(s) >= 3 and s[:3] in month_abbr_inv:
|
|
412
|
+
return month_abbr_inv[s[:3]]
|
|
413
|
+
# Quarterly: starts with Q + digit
|
|
414
|
+
m = re.match(r"q(\d)", s)
|
|
415
|
+
if m:
|
|
416
|
+
return {"1": 1, "2": 4, "3": 7, "4": 10}.get(m.group(1))
|
|
417
|
+
return None # CAL, H1H2, SummerWinter — don't filter
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _is_structure_expired(base_label, year, ref_date=None):
|
|
421
|
+
"""Check if a structure's front delivery leg has expired.
|
|
422
|
+
|
|
423
|
+
Monthly structures: expired when front month has passed.
|
|
424
|
+
Quarterly structures: expired when entire front quarter has passed.
|
|
425
|
+
CAL/H1/H2/Summer/Winter: never filtered (partial expiry is normal).
|
|
426
|
+
"""
|
|
427
|
+
if ref_date is None:
|
|
428
|
+
ref_date = datetime.now()
|
|
429
|
+
if isinstance(ref_date, pd.Timestamp):
|
|
430
|
+
ref_date = ref_date.to_pydatetime()
|
|
431
|
+
front_month = _front_delivery_month(base_label)
|
|
432
|
+
if front_month is None:
|
|
433
|
+
return False
|
|
434
|
+
s = base_label.strip().lower()
|
|
435
|
+
# Quarterly: expired after the last month of the front quarter
|
|
436
|
+
q_match = re.match(r"q(\d)", s)
|
|
437
|
+
if q_match:
|
|
438
|
+
last_m = _QUARTER_LAST_MONTH.get(f"q{q_match.group(1)}", front_month)
|
|
439
|
+
boundary = (
|
|
440
|
+
datetime(year + 1, 1, 1) if last_m == 12 else datetime(year, last_m + 1, 1)
|
|
441
|
+
)
|
|
442
|
+
else:
|
|
443
|
+
# Monthly: expired after the front month ends
|
|
444
|
+
boundary = (
|
|
445
|
+
datetime(year + 1, 1, 1)
|
|
446
|
+
if front_month == 12
|
|
447
|
+
else datetime(year, front_month + 1, 1)
|
|
448
|
+
)
|
|
449
|
+
return ref_date >= boundary
|
|
450
|
+
|
|
451
|
+
|
|
390
452
|
def reindex_year_point_stats_table(
|
|
391
453
|
df: pd.DataFrame,
|
|
392
454
|
*,
|
|
@@ -395,6 +457,7 @@ def reindex_year_point_stats_table(
|
|
|
395
457
|
within_days: int = 10,
|
|
396
458
|
min_columns: int = 3,
|
|
397
459
|
trim_expiry: bool = False,
|
|
460
|
+
skip_expired: bool = True,
|
|
398
461
|
) -> pd.DataFrame:
|
|
399
462
|
"""
|
|
400
463
|
Compute prompt-vs-history point stats for many structures in one dataframe.
|
|
@@ -408,6 +471,8 @@ def reindex_year_point_stats_table(
|
|
|
408
471
|
- returns a sortable table (z-score/percentile) for scanning cheap/rich structures.
|
|
409
472
|
|
|
410
473
|
If ``trim_expiry=True``, each per-group call applies expiry noise trimming.
|
|
474
|
+
If ``skip_expired=True`` (default), structures whose front delivery month has
|
|
475
|
+
already expired are excluded from results.
|
|
411
476
|
|
|
412
477
|
Notes:
|
|
413
478
|
- Columns must include a 4-digit year somewhere for `dates.find_year` to work reliably.
|
|
@@ -435,10 +500,16 @@ def reindex_year_point_stats_table(
|
|
|
435
500
|
continue
|
|
436
501
|
groups.setdefault(key, []).append(col)
|
|
437
502
|
|
|
503
|
+
ref_date = datetime.now() if asof is None else pd.Timestamp(asof).to_pydatetime()
|
|
504
|
+
|
|
438
505
|
rows: list[dict] = []
|
|
439
506
|
for key, cols in groups.items():
|
|
440
507
|
if len(cols) < min_columns:
|
|
441
508
|
continue
|
|
509
|
+
if skip_expired and _is_structure_expired(
|
|
510
|
+
key, dates.curyear, ref_date=ref_date
|
|
511
|
+
):
|
|
512
|
+
continue
|
|
442
513
|
stats_res = reindex_year_point_stats(
|
|
443
514
|
df[cols],
|
|
444
515
|
asof=asof,
|
|
@@ -672,6 +743,7 @@ def detect_expiry_noise_cutoff(
|
|
|
672
743
|
def trim_expiry_noise(
|
|
673
744
|
df: pd.DataFrame,
|
|
674
745
|
*,
|
|
746
|
+
exclude_columns: Iterable | None = None,
|
|
675
747
|
threshold_std: float = 2.0,
|
|
676
748
|
min_stable_frac: float = 0.6,
|
|
677
749
|
calm_streak: int = 3,
|
|
@@ -683,10 +755,16 @@ def trim_expiry_noise(
|
|
|
683
755
|
For each column, detects the expiry-noise cutoff via
|
|
684
756
|
``detect_expiry_noise_cutoff`` and sets values after that date to NaN.
|
|
685
757
|
|
|
758
|
+
Columns listed in ``exclude_columns`` are left untouched (useful for
|
|
759
|
+
preserving the prompt/current year column).
|
|
760
|
+
|
|
686
761
|
Returns a copy of the DataFrame with noisy tails removed.
|
|
687
762
|
"""
|
|
688
763
|
out = df.copy()
|
|
764
|
+
skip = set(exclude_columns) if exclude_columns else set()
|
|
689
765
|
for col in out.columns:
|
|
766
|
+
if col in skip:
|
|
767
|
+
continue
|
|
690
768
|
s = pd.to_numeric(out[col], errors="coerce")
|
|
691
769
|
cutoff = detect_expiry_noise_cutoff(
|
|
692
770
|
s,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|