macrotrace 0.2.0__tar.gz → 0.3.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.
Files changed (90) hide show
  1. {macrotrace-0.2.0 → macrotrace-0.3.0}/.github/workflows/docs.yml +2 -2
  2. macrotrace-0.3.0/CHANGELOG.md +75 -0
  3. {macrotrace-0.2.0 → macrotrace-0.3.0}/PKG-INFO +2 -6
  4. {macrotrace-0.2.0 → macrotrace-0.3.0}/README.md +1 -5
  5. macrotrace-0.3.0/macrotrace/_time.py +19 -0
  6. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/models/db.py +10 -15
  7. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/models/mt/analysis.py +8 -3
  8. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/models/mt/plotter.py +6 -15
  9. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/models/mt/time_series.py +466 -83
  10. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/sources/base.py +7 -1
  11. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/sources/fred.py +13 -51
  12. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/sources/ons.py +7 -2
  13. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/sources/rtdsm.py +5 -16
  14. {macrotrace-0.2.0 → macrotrace-0.3.0}/scripts/backstop_ingest.py +5 -11
  15. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/models/mt/series/test_init.py +26 -7
  16. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/models/mt/series/test_series.py +354 -13
  17. macrotrace-0.3.0/tests/models/mt/series/test_window_source_local_bounds.py +124 -0
  18. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/models/mt/test_analysis.py +49 -44
  19. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/models/mt/test_plotter.py +2 -4
  20. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/fred/test_fred_dataset_manager.py +14 -36
  21. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/fred/test_fred_release_manager.py +0 -25
  22. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/fred/test_fred_tz_handling.py +0 -40
  23. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/rtdsm/test_rtdsm_helpers.py +1 -9
  24. macrotrace-0.3.0/tests/test_time.py +40 -0
  25. macrotrace-0.2.0/CHANGELOG.md +0 -35
  26. {macrotrace-0.2.0 → macrotrace-0.3.0}/.github/workflows/ci.yml +0 -0
  27. {macrotrace-0.2.0 → macrotrace-0.3.0}/.github/workflows/release.yml +0 -0
  28. {macrotrace-0.2.0 → macrotrace-0.3.0}/.gitignore +0 -0
  29. {macrotrace-0.2.0 → macrotrace-0.3.0}/.pre-commit-config.yaml +0 -0
  30. {macrotrace-0.2.0 → macrotrace-0.3.0}/.python-version +0 -0
  31. {macrotrace-0.2.0 → macrotrace-0.3.0}/LICENSE +0 -0
  32. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/__init__.py +0 -0
  33. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/_paths.py +0 -0
  34. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/cli.py +0 -0
  35. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/graphing.py +0 -0
  36. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/models/__init__.py +0 -0
  37. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/models/mt/__init__.py +0 -0
  38. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/models/mt/observation.py +0 -0
  39. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/models/mt/series_metadata.py +0 -0
  40. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/ons_cli/__init__.py +0 -0
  41. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/ons_cli/cli.py +0 -0
  42. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/ons_cli/common.py +0 -0
  43. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/ons_cli/tui.py +0 -0
  44. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/py.typed +0 -0
  45. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/sources/__init__.py +0 -0
  46. {macrotrace-0.2.0 → macrotrace-0.3.0}/macrotrace/sources/example.py +0 -0
  47. {macrotrace-0.2.0 → macrotrace-0.3.0}/pyproject.toml +0 -0
  48. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/assets/mt/time_series/expected_vm.csv +0 -0
  49. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/assets/mt/time_series/from_dataframe.csv +0 -0
  50. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/assets/mt/time_series/from_dataframe_with_tz.csv +0 -0
  51. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/models/mt/series/test_db_path_forwarding.py +0 -0
  52. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/models/mt/test_metadata.py +0 -0
  53. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/models/mt/utils.py +0 -0
  54. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/models/test_db_models.py +0 -0
  55. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/ons_cli/test_cli.py +0 -0
  56. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/ons_cli/test_common.py +0 -0
  57. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/ons_cli/test_root_cli.py +0 -0
  58. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/ons_cli/test_tui.py +0 -0
  59. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/ons_cli/utils.py +0 -0
  60. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/base/fixtures.py +0 -0
  61. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/base/test_base_api_client.py +0 -0
  62. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/base/test_base_dataset_manager.py +0 -0
  63. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/base/test_base_observation_manager.py +0 -0
  64. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/base/test_base_release_manager.py +0 -0
  65. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/base/test_base_series_manager.py +0 -0
  66. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/base/test_base_update_manager.py +0 -0
  67. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/base/test_base_update_state.py +0 -0
  68. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/base/test_db_path_resolution.py +0 -0
  69. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/fred/fixtures.py +0 -0
  70. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/fred/test_fred_api_client.py +0 -0
  71. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/fred/test_fred_observation_manager.py +0 -0
  72. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/fred/test_fred_series_manager.py +0 -0
  73. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/fred/test_fred_update_manager.py +0 -0
  74. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/ons/fixtures.py +0 -0
  75. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/ons/test_ons_api_client.py +0 -0
  76. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/ons/test_ons_dataset_manager.py +0 -0
  77. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/ons/test_ons_observation_manager.py +0 -0
  78. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/ons/test_ons_release_manager.py +0 -0
  79. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/ons/test_ons_series_manager.py +0 -0
  80. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/ons/test_ons_update_manager.py +0 -0
  81. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/rtdsm/fixtures.py +0 -0
  82. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/rtdsm/test_rtdsm_api_client.py +0 -0
  83. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/rtdsm/test_rtdsm_dataset_manager.py +0 -0
  84. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/rtdsm/test_rtdsm_observation_manager.py +0 -0
  85. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/rtdsm/test_rtdsm_release_manager.py +0 -0
  86. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/rtdsm/test_rtdsm_series_manager.py +0 -0
  87. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/sources/rtdsm/test_rtdsm_update_manager.py +0 -0
  88. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/test_package_init.py +0 -0
  89. {macrotrace-0.2.0 → macrotrace-0.3.0}/tests/test_paths.py +0 -0
  90. {macrotrace-0.2.0 → macrotrace-0.3.0}/uv.lock +0 -0
@@ -37,7 +37,7 @@ jobs:
37
37
  - name: Deploy dev docs (push to main)
38
38
  if: github.ref == 'refs/heads/main'
39
39
  run: |
40
- uv run mike deploy --push --update-aliases dev
40
+ uv run mike deploy --push --update-aliases --prop-set hidden=true dev
41
41
  if ! uv run mike list 2>/dev/null | grep -qE '^[0-9]'; then
42
42
  uv run mike set-default --push dev
43
43
  fi
@@ -49,7 +49,7 @@ jobs:
49
49
  if [[ "$VERSION" =~ (rc|a|b|dev|alpha|beta) ]]; then
50
50
  uv run mike deploy --push "$VERSION"
51
51
  else
52
- uv run mike deploy --push --update-aliases "$VERSION" latest
52
+ uv run mike deploy --push --update-aliases --alias-type copy "$VERSION" latest
53
53
  uv run mike set-default --push latest
54
54
  fi
55
55
 
@@ -0,0 +1,75 @@
1
+ # Changelog
2
+
3
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
4
+ versions follow [SemVer](https://semver.org/).
5
+
6
+ ## 0.3.0 — 2026-06-15
7
+
8
+ - **Breaking:** Naive date inputs (`as_of`, the vintage/data windows,
9
+ `vintage_comparison`) are read on the source's clock instead of UTC.
10
+ This fixes same-day vintages being silently skipped for FRED,
11
+ which stamps releases at midnight US Central:
12
+ `as_of("2018-03-16")` now returns the 2018-03-16 vintage, and a
13
+ `data_end_date` on a period boundary no longer drops the final observation.
14
+ Timezone-aware datetimes still compare as exact instants.
15
+ - **Breaking:** Date strings must be `YYYY-MM-DD`; `datetime.date` objects
16
+ are now accepted. Free-text parsing is gone (along with the `dateutil`
17
+ dependency), so ambiguous formats like `"03/04/2018"` are rejected instead
18
+ of guessed.
19
+ - **Fixed:** `created_at` columns stamped each row with the process start
20
+ time instead of its actual creation time.
21
+
22
+ ## 0.2.2 — 2026-06-12
23
+
24
+ - **Vintage matching:** `identify_vintage` now interprets a tz-naive index in
25
+ the source's native timezone (e.g. midnight US Central for FRED) instead of
26
+ UTC, so plain dates match FRED vintages.
27
+ - **Vintage matching:** Added a `decimals` argument that rounds both sides
28
+ before comparison, for matching data published at a fixed precision.
29
+ - **Vintage matching:** `VintageMatch.failure_reason` now reports why nothing
30
+ matched: timestamps no vintage contains (`"coverage"`) vs value
31
+ disagreements (`"values"`).
32
+ - **Vintage matching:** Numeric/positional indexes are rejected with a clear
33
+ error, and `pd.PeriodIndex` is supported.
34
+ - **Vintage matching:** When nothing matches, `VintageMatch.alignment_hint`
35
+ flags timestamps that would match under a wrong timezone localization, a
36
+ constant time shift, or a month-end vs month-start convention.
37
+
38
+ ## 0.2.1 — 2026-06-11
39
+
40
+ - **Docs:** RTDSM is now listed as an available source on the documentation
41
+ homepage — it had been left under "Coming Soon" when 0.2.0 shipped.
42
+ - **Docs:** The version selector now shows the `latest` label next to the
43
+ release it points at, and the in-development `dev` build is hidden from
44
+ the selector (it is still reachable directly at `/dev/`).
45
+
46
+ ## 0.2.0 — 2026-06-10
47
+
48
+ - **Sources:** Added the Federal Reserve Bank of Philadelphia's Real-Time
49
+ Data Set for Macroeconomists (RTDSM) — vintage-aware ingestion of 115 U.S.
50
+ macroeconomic series. Each series is parsed from the published full-history spreadsheet; an optional `series_key={"frequency": "Q" | "M"}` selects the vintage frequency, and a monthly refresh throttle avoids re-downloading the same series
51
+ within a calendar month as requested by the Philadelphia Federal Reserve Bank.
52
+ - **Vintage matching:** Added `MTTimeSeries.identify_vintage(...)`, which
53
+ recovers which release(s) an undated block of observations came from by
54
+ comparing it against every stored vintage. Returns a `VintageMatch`
55
+ (`matched`, `is_ambiguous`, `release_date` / `release_dates`) — useful for
56
+ pinning down the vintage behind replication-package data.
57
+ - **Time series:** Added `MTTimeSeries.to_series(...)`, the values-only,
58
+ date-indexed pandas `Series` counterpart to `to_dataframe` (supports the
59
+ `default`, `first_difference`, and `pct_change` modes). Exposed
60
+ `VintageMatch` at the package root.
61
+
62
+ ## 0.1.0 — 2026-04-28
63
+
64
+ First public release.
65
+
66
+ - **Sources:** vintage-aware ingestion from FRED and ONS, with a local
67
+ SQLite store (`MacroTrace.db`) and shared request cache.
68
+ - **Time series:** `MTTimeSeries` with `as_of(...)`, vintage- and
69
+ data-window filtering, `from_dataframe`, and pandas / Darts export.
70
+ - **Analysis:** revision metrics, vintage comparison, decomposition
71
+ across vintages, biasedness regression, and revision autocorrelation.
72
+ - **Plotting:** Plotly-based vintage, revision, and decomposition plots
73
+ via `MTTimeSeriesPlotter`.
74
+ - **CLI / TUI:** `macrotrace ons explorer` and `macrotrace ons tui`
75
+ (the latter via the optional `ons-tui` extra).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: macrotrace
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A Python library for managing and analyzing macroeconomic time series data with vintage awareness.
5
5
  Project-URL: Homepage, https://github.com/john-ramsey/macrotrace
6
6
  Project-URL: Repository, https://github.com/john-ramsey/macrotrace
@@ -170,13 +170,9 @@ if match.is_ambiguous:
170
170
  elif match.matched:
171
171
  print(f"Matches the {match.release_date.date()} vintage")
172
172
  else:
173
- print("No matching vintage found")
173
+ print(f"No matching vintage found (failed on: {match.failure_reason})")
174
174
  ```
175
175
 
176
- A match is ambiguous when the data is unchanged across consecutive vintages, so
177
- the values alone cannot pin down a single release; `release_dates` lists every
178
- consistent vintage in that case.
179
-
180
176
  ## Command-Line Tools
181
177
 
182
178
  MacroTrace includes command-line tools for exploring ONS datasets:
@@ -129,13 +129,9 @@ if match.is_ambiguous:
129
129
  elif match.matched:
130
130
  print(f"Matches the {match.release_date.date()} vintage")
131
131
  else:
132
- print("No matching vintage found")
132
+ print(f"No matching vintage found (failed on: {match.failure_reason})")
133
133
  ```
134
134
 
135
- A match is ambiguous when the data is unchanged across consecutive vintages, so
136
- the values alone cannot pin down a single release; `release_dates` lists every
137
- consistent vintage in that case.
138
-
139
135
  ## Command-Line Tools
140
136
 
141
137
  MacroTrace includes command-line tools for exploring ONS datasets:
@@ -0,0 +1,19 @@
1
+ from datetime import datetime, tzinfo
2
+ from typing import Optional
3
+
4
+
5
+ def ensure_timezone(dt: Optional[datetime], tz: tzinfo) -> Optional[datetime]:
6
+ """
7
+ Return the datetime made aware in ``tz``, or None.
8
+
9
+ Naive datetimes keep their wall clock while aware ones are converted. Uses pytz's
10
+ ``localize()`` when available so the real historical offset is picked
11
+ instead of pytz's first entry for the zone (LMT).
12
+ """
13
+ if dt is None:
14
+ return None
15
+ if dt.tzinfo is None:
16
+ if hasattr(tz, "localize"):
17
+ return tz.localize(dt)
18
+ return dt.replace(tzinfo=tz)
19
+ return dt.astimezone(tz)
@@ -40,6 +40,11 @@ def is_valid_dateoffset(value: str) -> bool:
40
40
  return False
41
41
 
42
42
 
43
+ def _utc_now() -> datetime.datetime:
44
+ """Callable default so each row stamps its own creation time, not the import time."""
45
+ return datetime.datetime.now(tz=datetime.timezone.utc)
46
+
47
+
43
48
  class StrictDateTimeField(DateTimeField):
44
49
  """DateTimeField that enforces timezone-aware datetime objects in ISO 8601 format."""
45
50
 
@@ -122,9 +127,7 @@ class DatasetDimension(DataBaseModel):
122
127
  # Validity period for this dimension definition, null valid_to means currently valid
123
128
  valid_from = StrictDateTimeField()
124
129
  valid_to = StrictDateTimeField(null=True)
125
- created_at = StrictDateTimeField(
126
- default=datetime.datetime.now(tz=datetime.timezone.utc)
127
- )
130
+ created_at = StrictDateTimeField(default=_utc_now)
128
131
 
129
132
  class Meta:
130
133
  constraints = [
@@ -152,9 +155,7 @@ class Release(DataBaseModel):
152
155
  )
153
156
  release_date = StrictDateTimeField()
154
157
  additional_metadata = JSONField(null=True)
155
- created_at = StrictDateTimeField(
156
- default=datetime.datetime.now(tz=datetime.timezone.utc)
157
- )
158
+ created_at = StrictDateTimeField(default=_utc_now)
158
159
 
159
160
  class Meta:
160
161
  constraints = [SQL("UNIQUE(dataset_id, release_date)")]
@@ -182,9 +183,7 @@ class ReleaseDimension(DataBaseModel):
182
183
  backref="release_dimensions",
183
184
  on_delete="CASCADE",
184
185
  )
185
- created_at = StrictDateTimeField(
186
- default=datetime.datetime.now(tz=datetime.timezone.utc)
187
- )
186
+ created_at = StrictDateTimeField(default=_utc_now)
188
187
 
189
188
  class Meta:
190
189
  constraints = [SQL("UNIQUE(release_id, dimension_id)")]
@@ -199,9 +198,7 @@ class ReleaseDimension(DataBaseModel):
199
198
  class Series(DataBaseModel):
200
199
  dataset = ForeignKeyField(Dataset, backref="series", on_delete="CASCADE")
201
200
  series_key = JSONField()
202
- created_at = StrictDateTimeField(
203
- default=datetime.datetime.now(tz=datetime.timezone.utc)
204
- )
201
+ created_at = StrictDateTimeField(default=_utc_now)
205
202
 
206
203
  def __repr__(self):
207
204
  return (
@@ -249,9 +246,7 @@ class Observation(DataBaseModel):
249
246
 
250
247
  observation_timestamp = StrictDateTimeField()
251
248
  value = FloatField(null=True) # null if the observation is missing
252
- created_at = StrictDateTimeField(
253
- default=datetime.datetime.now(tz=datetime.timezone.utc)
254
- )
249
+ created_at = StrictDateTimeField(default=_utc_now)
255
250
 
256
251
  class Meta:
257
252
  constraints = [SQL("UNIQUE(release_id, observation_timestamp)")]
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING, Union
5
+ import datetime
5
6
  import logging
6
7
 
7
8
  import numpy as np
@@ -510,7 +511,6 @@ class MTTimeSeriesAnalysis:
510
511
  darts_ts = self.ts.to_darts_timeseries()
511
512
 
512
513
  if len(darts_ts) >= (min_train_size + 1):
513
-
514
514
  for i in range(min_train_size, len(darts_ts)):
515
515
  train = darts_ts[:i]
516
516
  test = darts_ts[i : i + 1]
@@ -629,7 +629,10 @@ class MTTimeSeriesAnalysis:
629
629
  )
630
630
 
631
631
  def vintage_comparison(
632
- self, vintage_dates: List[str], mode: str = "growth", strategy: str = "all"
632
+ self,
633
+ vintage_dates: List[str | datetime.datetime | datetime.date],
634
+ mode: str = "growth",
635
+ strategy: str = "all",
633
636
  ) -> "VintageComparison":
634
637
  """
635
638
  Compare vintages across summary measures describing revisions of a
@@ -677,7 +680,9 @@ class MTTimeSeriesAnalysis:
677
680
  period-over-period change in the level changes sign between vintages.
678
681
 
679
682
  Args:
680
- vintage_dates (List[str]): A list of vintage identifiers to compare.
683
+ vintage_dates (List[str | datetime | date]): The vintages to
684
+ compare, each resolved through ``as_of()`` — a ``YYYY-MM-DD``
685
+ string or date for a calendar day, or a datetime for an exact instant.
681
686
  mode (str): The mode of comparison ("growth" or "levels").
682
687
  strategy (str): The strategy for comparison ("sequential", "final", or "all").
683
688
 
@@ -1,5 +1,5 @@
1
1
  from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
2
- from datetime import datetime
2
+ from datetime import date, datetime
3
3
  import warnings
4
4
 
5
5
  import numpy as np
@@ -147,9 +147,8 @@ class MTTimeSeriesPlotter:
147
147
  go.Figure: Plotly figure showing observation revisions over time.
148
148
  """
149
149
  if isinstance(observation_datetime, str):
150
- observation_datetime = self.ts._parse_string_date(
151
- observation_datetime
152
- ) # Returns UTC timezone
150
+ # Resolves to the source's midnight on that day
151
+ observation_datetime = self.ts._parse_string_date(observation_datetime)
153
152
  elif not isinstance(observation_datetime, datetime):
154
153
  raise ValueError(
155
154
  f"Invalid observation datetime type: {type(observation_datetime)}. Must be a string or a datetime."
@@ -588,7 +587,7 @@ class MTTimeSeriesPlotter:
588
587
 
589
588
  def timeseries_comparison(
590
589
  self,
591
- vintage_dates: List[str | datetime],
590
+ vintage_dates: List[str | datetime | date],
592
591
  chart_type: str = "bar",
593
592
  mode: str = "default",
594
593
  y_axis_zero_indexed: bool = False,
@@ -597,7 +596,7 @@ class MTTimeSeriesPlotter:
597
596
  Plots a comparison of time series vintages.
598
597
 
599
598
  Args:
600
- vintage_dates (List[str | datetime]): List of vintage identifiers. Ex. '2025-11-01'
599
+ vintage_dates (List[str | datetime | date]): List of vintage identifiers, resolved via ``as_of``. Ex. '2025-11-01'
601
600
  chart_type (str, optional): Type of chart to plot. Either "bar" or "line". Defaults to "bar".
602
601
  mode (str, optional): The mode for which the dataframe is provided. Supports "default", "first_difference", and "pct_change". Defaults to "default".
603
602
  y_axis_zero_indexed (bool, optional): Sets base of the y-axis to zero.
@@ -610,14 +609,6 @@ class MTTimeSeriesPlotter:
610
609
  f"Invalid mode: {mode}. Supported modes are 'default', 'first_difference', and 'pct_change'."
611
610
  )
612
611
 
613
- for vintage_date in vintage_dates:
614
- if (not isinstance(vintage_date, str)) and (
615
- not isinstance(vintage_date, datetime)
616
- ):
617
- raise TypeError(
618
- "Vintage dates must be provided as strings or datetime objects."
619
- )
620
-
621
612
  fig = go.Figure()
622
613
  all_values = []
623
614
  hoverinfo = "x+y+name"
@@ -626,7 +617,7 @@ class MTTimeSeriesPlotter:
626
617
  df = self.ts.as_of(vintage_date).to_dataframe(mode=mode)
627
618
  vintage_date = (
628
619
  vintage_date.strftime("%Y-%m-%d")
629
- if isinstance(vintage_date, datetime)
620
+ if isinstance(vintage_date, date)
630
621
  else vintage_date
631
622
  )
632
623
  if chart_type == "bar":