Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fc28b30
fix: propagate shared calendar declarations throughout validation
Oct 17, 2026
be323a0
Merge qualified intraday grid prerequisite into validation repair
Oct 17, 2026
5b1f658
fix: preserve timestamp import guards and daily continuity aliases
Oct 17, 2026
48f6f87
fix: preserve nanosecond precision at native validation boundaries
Oct 17, 2026
7ad6411
build: consume published ml4t-specs 0.2.0 contracts
Oct 17, 2026
9331160
Merge remote-tracking branch 'origin/main' into build/published-specs…
Oct 17, 2026
5ff004d
fix: report stored observations against shared calendar bar grids
Oct 17, 2026
8de4644
fix: preserve missing market observations instead of imputing prices
Oct 17, 2026
438044e
test: exercise calendar checks without requesting retired imputation
Oct 17, 2026
a1f6141
fix: report bounded file gaps without counting placeholders
Oct 17, 2026
60f6591
Merge branch 'fix/stored-calendar-gap-report' into fix/source-backed-…
Oct 17, 2026
b714ac9
fix: recover calendar gaps from exact Databento source ranges
Oct 17, 2026
5bd368f
Merge branch 'main' into fix/stored-calendar-gap-report
stefan-jansen Oct 18, 2026
c679d7f
fix: reconcile recovered observations after history recording failures
Oct 18, 2026
0878003
fix: treat an empty gap report as a successful no-op
Oct 18, 2026
966d89d
Merge branch 'main' into fix/validation-calendar-contract
Oct 18, 2026
d6076e5
fix: consolidate calendar gap inspection and source-backed recovery
Oct 18, 2026
b3e1e7d
fix: complete shared calendar validation and incremental range planning
Oct 18, 2026
830b451
test: declare continuous calendar in daily incremental fixture
Oct 18, 2026
ca35907
fix: reject SDK-expanded symbols before targeted Databento requests
Oct 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,23 @@ pre-commit run --all-files
For packaging or release changes, also run the verification scripts used by
`.github/workflows/release.yml`. Never bypass hooks or weaken provider-specific checks to make a
shared gate pass.

## Maintainer-directed change batching

For our own fixes and agent work directed by the maintainer, use a bounded, coherent PR rather
than one PR per small defect or implementation step. One PR may close several related issues
when the changes serve the same user workflow, subsystem repair or compatibility objective and
can be reviewed and reverted together. Keep implementation, regressions, documentation and
follow-up repairs together; use focused commits and retain evidence for each defect. Do not
create separate issues or PRs merely for individual files, tests, assertions or platform variants.

Split unrelated changes, independently risky or breaking changes, separate ownership boundaries,
and work that needs independent release or rollback decisions. A general maintenance label alone
does not make a batch coherent. Reuse or safely consolidate existing owned submissions instead of
opening overlapping PRs; preserve contributor attribution, recoverable history and other owners'
active work. Do not rewrite or close someone else's work merely to reduce the queue count.

Run focused checks while iterating, then qualify the complete final revision with every applicable
required gate. Group related work before expensive CI; do not weaken checks, bypass review or
claim old-head checks qualify changed code. This maintainer-directed batching rule takes precedence
over a literal one-issue-per-PR reading of contributor intake guidance.
31 changes: 31 additions & 0 deletions docs/providers/databento.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,37 @@ df = provider.fetch_continuous_futures(

---

## Exact Timestamp Ranges

`fetch_ohlcv_range()` requests one series between timezone-aware instants, including `start` and
excluding `end`. Unlike the date-based API, it never rounds to dates or applies
`adjust_session_dates`. The wrapper supports native minute, hourly and daily OHLCV schemas and
rejects unsupported frequencies, parent expansion and `ALL_SYMBOLS` before a request.

```python
from datetime import UTC, datetime

with DataBentoProvider(stype_in="continuous") as provider:
rows = provider.fetch_ohlcv_range(
"BTC.c.0",
datetime(2026, 9, 5, 12, 1, tzinfo=UTC),
datetime(2026, 9, 5, 12, 2, tzinfo=UTC),
"minute",
)
```

The returned `symbol` identifies the requested series; `contract` retains the resolved contract
when the SDK supplies it. Select the same dataset, continuous roll rule and price convention used
for the stored series. This method does not reconstruct adjusted continuous prices or select another
contract to fill an absence. Databento timestamps identify interval starts, and no-trade intervals
can have no OHLCV record. A download may therefore leave gaps unresolved.

For a stored report, see [targeted source recovery](../user-guide/incremental-updates.md#download-real-observations-for-a-selected-report).
For raw Parquet files, this method returns a frame for explicit review and merging; it does not
rewrite those files. Requests are metered. Use the native client's cost estimate before broad
requests. The endpoint and bar semantics follow the [historical range API](https://databento.com/docs/api-reference-historical/timeseries/timeseries-get-range)
and [OHLCV specification](https://databento.com/docs/schemas-and-data-formats/ohlcv).

## API Key Setup

```bash
Expand Down
9 changes: 6 additions & 3 deletions docs/tutorials/03_incremental_updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,10 @@ Today: Jan 15
Missing: [Jan 11] [Jan 12] [Jan 13] [Jan 14]
```

**Solution**: Incremental updates automatically detect and fill gaps!
Incremental updates request actual observations from the configured source. Missing periods remain
missing when the source supplies no row; they are never replaced with synthetic market values.
Use the [stored calendar report](../user-guide/incremental-updates.md#inspect-missing-observations-on-disk)
to inspect internal gaps independently of acquisition.

```python
# Even after 5 days without updates, incremental mode works
Expand All @@ -296,7 +299,7 @@ print(result)
# 'records_added': 5,
# 'gap_detected': True,
# 'gap_days': 5,
# 'message': 'Filled 5-day gap'
# 'message': 'Downloaded available source observations'
# }
```

Expand Down Expand Up @@ -410,7 +413,7 @@ def catch_up_all_symbols():

if result.get('gap_detected'):
logger.warning(
f"{symbol}: Filled {result['gap_days']}-day gap "
f"{symbol}: Downloaded source observations for {result['gap_days']} days "
f"({result['records_added']} records)"
)
elif result.get('skip_reason') == 'already_up_to_date':
Expand Down
22 changes: 6 additions & 16 deletions docs/tutorials/04_data_quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,23 +274,13 @@ data = provider.fetch_ohlcv("AAPL", start, end)
data = clean_ohlcv_data(data)
```

### Option 3: Fill Forward (Last Resort)
### Preserve and Investigate Missing Observations

```python
def fill_missing_data(df):
"""Fill missing values with forward fill (use sparingly)."""
# Fill nulls with previous valid value
df = df.with_columns([
pl.col("open").fill_null(strategy="forward"),
pl.col("high").fill_null(strategy="forward"),
pl.col("low").fill_null(strategy="forward"),
pl.col("close").fill_null(strategy="forward"),
pl.col("volume").fill_null(0), # Volume = 0 for missing days
])

logger.warning("Applied forward fill to missing data")
return df
```
Do not replace absent OHLCV values with copied prices or zero volume. Preserve the missing data,
use the [calendar-based disk report](../user-guide/incremental-updates.md#inspect-missing-observations-on-disk)
to identify absent scheduled observations, and request actual rows from the original source when
its API and coverage permit. Intervals without trades may legitimately have no candle. A null price
inside a present row remains a validation finding, not permission to invent a replacement.

## Provider-Specific Validation

Expand Down
3 changes: 2 additions & 1 deletion docs/user-guide/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ ml4t-data update -s AAPL --config ml4t-data.yaml
| `--frequency` | `daily`, `hourly`, or `weekly` |
| `--asset-class` | `equities`, `crypto`, `forex`, or `futures` |
| `--lookback-days` | Existing-data overlap used for revisions |
| `--fill-gaps`, `--no-fill-gaps` | Enable or disable gap filling |
| `--check-gaps`, `--no-check-gaps` | Enable or disable read-only calendar gap checks |
| `--fill-gaps`, `--no-fill-gaps` | Legacy: filling is rejected; `--no-fill-gaps` disables checks |
| `--provider`, `-p` | Provider to use for fetching |
| `--start`, `--end` | Date bounds used only for a first load |
| `--initial-load-days` | First-load history when `--start` is omitted |
Expand Down
3 changes: 2 additions & 1 deletion docs/user-guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ provider, frequency, asset class, and bootstrap range.
| `frequency` | `daily` | Bar frequency passed to `DataManager.update()` |
| `asset_class` | `equity` | Storage key prefix and validation context |
| `lookback_days` | `7` | Overlap fetched for incremental updates |
| `fill_gaps` | `true` | Enable gap detection and filling after merge |
| `check_gaps` | `true` | Report calendar gaps without changing observations |
| `fill_gaps` | unset | Legacy `false` disables checks; `true` is rejected |
| `start` / `start_date` | unset | Explicit first-load start date when storage is empty |
| `end` / `end_date` | today | Explicit first-load end date when storage is empty |
| `initial_load_days` | `365` | First-load history length when no start date is set |
Expand Down
55 changes: 53 additions & 2 deletions docs/user-guide/data-quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ from ml4t.data.validation.rules import ValidationRulePresets

# Crypto is more volatile - wider thresholds
crypto_rules = ValidationRulePresets.crypto_rules()
# max_return_threshold=0.5, staleness_threshold=3, no market hours check
# max_return_threshold=0.5, staleness_threshold=3

# Forex is less volatile - tighter thresholds
forex_rules = ValidationRulePresets.forex_rules()
# max_return_threshold=0.1, staleness_threshold=10, 24/5 market
# max_return_threshold=0.1, staleness_threshold=10

# Strict mode for production data
strict_rules = ValidationRulePresets.strict_rules()
Expand All @@ -74,6 +74,57 @@ Available presets: `equity_rules()`, `crypto_rules()`, `forex_rules()`,
`commodity_rules()`, `futures_rules()`, `strict_rules()`, `relaxed_rules()`.
Commodity and futures presets report negative prices as warnings. The
`negative_price_policy` setting accepts `"forbid"`, `"warn"`, or `"allow"`.
Presets adjust numerical thresholds, not calendar selection. Crypto and forex presets no longer
disable schedule checks implicitly; declare the market's actual schedule. Explicit per-check
opt-outs remain available.

### Validate Calendar Membership and Continuity

`CrossValidator`, `AssetValidator` and `CryptoSessionValidator` accept the canonical `calendar`
keyword and a shared `feed_spec` on construction and validation. A call's explicit calendar
overrides its constructor setting; the effective explicit setting overrides feed metadata, then
NYSE applies. Version-2 metadata can also explicitly disable calendars. Neither a descriptive
asset class nor an exchange name selects a schedule.

```python
from ml4t.data.providers import SyntheticProvider
from ml4t.data.validation.cross_validation import CrossValidator
from ml4t.specs import FeedSpec

with SyntheticProvider(calendar_mode="continuous") as provider:
spot = provider.fetch_ohlcv("BTCUSD", "2025-07-12", "2025-07-12", "minute")

feed = FeedSpec(calendar="24/7", data_frequency="minute", timestamp_semantics="event_time")
report = CrossValidator(feed_spec=feed).validate(spot)
row_only = CrossValidator(calendar=None).validate(spot)
```

Calendar membership uses the same assignment as [native sessions](sessions.md), including DST,
holidays, early closes, overnight dates, breaks and registered custom schedules. Nanosecond instants
retain their precision in membership comparisons. Daily session
labels are not wall-clock observations; explicit event or close semantics override the daily-label
heuristic. Invalid calendars and incompatible temporal declarations fail clearly. Reported sample
indices refer to the original input positions. `calendar=None` disables temporal checks without
disabling price, volume, schema or reference-price checks.

`CrossValidator` checks membership, not missing-bar completeness. `CryptoSessionValidator` checks
continuity through the shared gap detector: explicit `"24/7"` still detects real outages, whereas
`None` performs no schedule-derived gap check. Intraday continuity requires the complete version-2
bar declaration described in [incremental updates](incremental-updates.md). An explicitly supplied
legacy `CryptoCalendar` object retains its elapsed continuous-gap and maintenance behavior; omission
no longer constructs that object automatically. `CrossValidator(is_crypto=True)` remains a legacy
24/7 hint only when neither an explicit calendar nor a feed calendar is declared. It cannot override
a canonical selection.

`DataManager.load()` and `import_data()` pass their effective declaration and acquisition frequency
to validation before storage. For spot data, declare `"24/7"`; use the selected exchange calendar
for futures. Explicit calendar disable does not bypass OHLCV validation.

`ValidationRuleConfig(calendar=...)` preserves omitted, named and explicitly disabled selections
through model copying, JSON and `ValidationRuleSet` YAML. New rule configurations use version 2;
version-1 payloads with a null calendar still inherit the default. Pass a restored configuration
as `CrossValidator(feed_spec=rules)` to apply its declaration, or override it with `calendar=...`.
Use ordinary `model_dump_json()` or `model_dump(mode="json")` without excluding explicit nulls.

### Persistent Rule Sets

Expand Down
Loading
Loading