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.
Files changed (57) hide show
  1. {commodutil-3.6.1 → commodutil-3.6.2}/PKG-INFO +1 -1
  2. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/stats.py +83 -5
  3. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil.egg-info/PKG-INFO +1 -1
  4. {commodutil-3.6.1 → commodutil-3.6.2}/.coveragerc +0 -0
  5. {commodutil-3.6.1 → commodutil-3.6.2}/.github/workflows/1_tests.yml +0 -0
  6. {commodutil-3.6.1 → commodutil-3.6.2}/.github/workflows/2_coverage.yml +0 -0
  7. {commodutil-3.6.1 → commodutil-3.6.2}/.github/workflows/3_linting.yml +0 -0
  8. {commodutil-3.6.1 → commodutil-3.6.2}/.github/workflows/4_release.yml +0 -0
  9. {commodutil-3.6.1 → commodutil-3.6.2}/.gitignore +0 -0
  10. {commodutil-3.6.1 → commodutil-3.6.2}/.pypirc +0 -0
  11. {commodutil-3.6.1 → commodutil-3.6.2}/azure-build-pipelines.yml +0 -0
  12. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/__init__.py +0 -0
  13. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/arb.py +0 -0
  14. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/convfactors.py +0 -0
  15. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/dates.py +0 -0
  16. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forward/__init__.py +0 -0
  17. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forward/calendar.py +0 -0
  18. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forward/continuous.py +0 -0
  19. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forward/fly.py +0 -0
  20. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forward/quarterly.py +0 -0
  21. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forward/spreads.py +0 -0
  22. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forward/structure.py +0 -0
  23. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forward/util.py +0 -0
  24. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/forwards.py +0 -0
  25. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/pandasutil.py +0 -0
  26. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil/transforms.py +0 -0
  27. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil.egg-info/SOURCES.txt +0 -0
  28. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil.egg-info/dependency_links.txt +0 -0
  29. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil.egg-info/requires.txt +0 -0
  30. {commodutil-3.6.1 → commodutil-3.6.2}/commodutil.egg-info/top_level.txt +0 -0
  31. {commodutil-3.6.1 → commodutil-3.6.2}/pyproject.toml +0 -0
  32. {commodutil-3.6.1 → commodutil-3.6.2}/requirements-test.txt +0 -0
  33. {commodutil-3.6.1 → commodutil-3.6.2}/requirements.txt +0 -0
  34. {commodutil-3.6.1 → commodutil-3.6.2}/requirements_dev.txt +0 -0
  35. {commodutil-3.6.1 → commodutil-3.6.2}/scripts/rbw_structure_scan.py +0 -0
  36. {commodutil-3.6.1 → commodutil-3.6.2}/setup.cfg +0 -0
  37. {commodutil-3.6.1 → commodutil-3.6.2}/tests/__init__.py +0 -0
  38. {commodutil-3.6.1 → commodutil-3.6.2}/tests/conftest.py +0 -0
  39. {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/__init__.py +0 -0
  40. {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/conftest.py +0 -0
  41. {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/test_calendar.py +0 -0
  42. {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/test_continuous.py +0 -0
  43. {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/test_fly.py +0 -0
  44. {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/test_quarterly.py +0 -0
  45. {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/test_spreads.py +0 -0
  46. {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/test_structure.py +0 -0
  47. {commodutil-3.6.1 → commodutil-3.6.2}/tests/forward/test_util.py +0 -0
  48. {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_arb.py +0 -0
  49. {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_cl.csv +0 -0
  50. {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_conv.py +0 -0
  51. {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_dates.py +0 -0
  52. {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_forwards.py +0 -0
  53. {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_pandasutils.py +0 -0
  54. {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_price_conv.py +0 -0
  55. {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_stats.py +0 -0
  56. {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_transforms.py +0 -0
  57. {commodutil-3.6.1 → commodutil-3.6.2}/tests/test_weekly.csv +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commodutil
3
- Version: 3.6.1
3
+ Version: 3.6.2
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
@@ -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
- asof_ts = pd.Timestamp(dft.index.max()) if asof is None else pd.Timestamp(asof)
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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commodutil
3
- Version: 3.6.1
3
+ Version: 3.6.2
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
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