Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4e47670
fix: preserve shared calendar intent in session-aware labeling (#94)
stefan-jansen Oct 17, 2026
733ee27
docs: align feature task guidance with declared calendar modes (#94)
stefan-jansen Oct 17, 2026
5784963
fix: retain calendar declaration presence in labeling config equality…
stefan-jansen Oct 17, 2026
565a271
fix(api): accept timestamp-free observation rows in explicit row mode…
stefan-jansen Oct 17, 2026
1d624da
fix: use canonical calendar selector on the shared schedule adapter (…
stefan-jansen Oct 17, 2026
665d824
fix: preserve timestamp precision and sparse-event label schemas (#94)
stefan-jansen Oct 17, 2026
9b635b5
fix: keep row label duration metadata portable (#94)
stefan-jansen Oct 17, 2026
49471ff
fix: honor explicit labeling serialization projections (#94)
stefan-jansen Oct 17, 2026
e64dde8
ci: run one full wheel suite and prove failure propagation (#99)
stefan-jansen Oct 17, 2026
1fb082c
ci: verify installed identity and separate native fresh-process stres…
stefan-jansen Oct 17, 2026
f30467d
Merge qualified Hurst prerequisite into CI99
stefan-jansen Oct 17, 2026
6267e95
ci: bound native build and use explicit fail-closed shells (#99)
stefan-jansen Oct 17, 2026
3a26a94
test(ci): reject real installed version and package drift (#99)
stefan-jansen Oct 17, 2026
b95fb29
build: qualify published Specs 0.2 temporal contract (#76)
stefan-jansen Oct 17, 2026
72e856d
fix(grid): validate daily session labels before feature returns (#76)
stefan-jansen Oct 17, 2026
fde92ea
fix(ci): execute Git Bash rather than Windows WSL launcher (#99)
stefan-jansen Oct 17, 2026
ee8c20b
Merge branch 'main' into fix/labeling-shared-calendar-contract
stefan-jansen Oct 17, 2026
17cc533
Merge daily session grid prerequisite into labeling contract (#94)
stefan-jansen Oct 17, 2026
05521bf
feat(labeling): apply scheduled-session horizons through existing bar…
stefan-jansen Oct 17, 2026
6296d2b
Merge remote-tracking branch 'origin/main' into fix/labeling-shared-c…
stefan-jansen Oct 17, 2026
5719993
Merge branch 'main' into ci/focused-wheel-qualification
stefan-jansen Oct 17, 2026
0d34e83
Merge remote-tracking branch 'origin/main' into fix/issue76-timestamp…
stefan-jansen Oct 17, 2026
8fd5949
Merge branch 'fix/labeling-shared-calendar-contract' into fix/issue76…
stefan-jansen Oct 17, 2026
8cd232d
feat(api): distinguish causal session and elapsed feature windows (#76)
stefan-jansen Oct 17, 2026
c056bbb
test(api): retain independent annualization consumer regressions (#76)
stefan-jansen Oct 17, 2026
0fc80a7
fix(labeling): preserve portable schemas when price filters remove al…
stefan-jansen Oct 17, 2026
74b5f59
fix(labeling): keep session deadlines before excluded short-session g…
stefan-jansen Oct 17, 2026
ddc132f
docs(labeling): execute session horizons and preserve the previous gu…
stefan-jansen Oct 17, 2026
543203a
Merge branch 'fix/labeling-shared-calendar-contract' into fix/issue76…
stefan-jansen Oct 17, 2026
d2706f2
Merge branch 'build/published-specs020-consumer' into fix/issue76-tim…
stefan-jansen Oct 17, 2026
9dcc1f6
test(labeling): qualify session horizons across DST and overnight ven…
stefan-jansen Oct 17, 2026
6f05213
Merge branch 'fix/labeling-shared-calendar-contract' into fix/issue76…
stefan-jansen Oct 17, 2026
c8d603c
test(api): retain scheduled sessions with no bars under exclusion pol…
stefan-jansen Oct 17, 2026
2259bae
fix(labeling): keep timestamp-free legacy durations portable and unse…
stefan-jansen Oct 17, 2026
85764b5
Merge branch 'fix/labeling-shared-calendar-contract' into fix/issue76…
stefan-jansen Oct 17, 2026
de4ab2a
docs: remove superseded daily session-label rejection (#94)
stefan-jansen Oct 17, 2026
3ab6510
Merge branch 'fix/labeling-shared-calendar-contract' into fix/issue76…
stefan-jansen Oct 17, 2026
98fe25b
docs: apply maintainer-directed batching to temporal contract mainten…
stefan-jansen Oct 18, 2026
e74e42f
Merge branch 'ci/focused-wheel-qualification' into fix/issue76-timest…
stefan-jansen Oct 18, 2026
6cf32bf
Merge branch 'main' into fix/issue76-timestampless-row-mode
stefan-jansen Oct 18, 2026
47917fd
fix(labeling): distinguish session identifiers from physical elapsed …
stefan-jansen Oct 18, 2026
c69845d
docs(calendars): reconcile helper guidance with daily crypto sessions
stefan-jansen 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 @@ -61,3 +61,23 @@ uv build
uv run python -c "import ml4t.engineer"
uv run mkdocs build --strict
```

## 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.
108 changes: 94 additions & 14 deletions docs/user-guide/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ Common asset columns such as `asset_id`, `symbol`, and `ticker` are detected aut
and every rolling or lagged feature is evaluated independently for each asset.
The examples explicitly select `calendar=None`: their periods count observations and
input order is preserved, even when timestamps are irregular. Sort prices yourself before
deriving returns if chronological order is intended. Without timestamps, also pass
`assume_sorted=True` to assert that the supplied row order is correct.
deriving returns if chronological order is intended. Row mode does not require timestamps
or an additional `assume_sorted=True` assertion. A disabled calendar in shared feed metadata
has the same behavior, even when the feed's timestamp column is absent.

Use `output` when requesting more than one configuration of the same feature:

Expand All @@ -87,23 +88,52 @@ raise `ValueError` before feature execution.
### Calendar and bar grid

The calendar-aware default targets the next minor version (0.2), separate from the 0.1.7 patch.
Development and qualification currently pin the merged Specs v2 implementation at
`701bde68744035050ede386efce498234c661c21`. The same immutable Git dependency is declared
in built package metadata; published Specs 0.1.5 does not provide this API. Before a PyPI
release, replace that source dependency with the verified published Specs version containing
the v2 contract and repeat installed-package qualification.
The shared temporal contract requires published `ml4t-specs>=0.2.0,<0.3.0`, also declared
in built package metadata. Specs 0.1.5 does not provide this API. Engineer's development
branch uses the published dependency; the temporal migration is not part of the 0.1.7 patch.

Calendar selection
uses the explicit `calendar` argument, then declared feed metadata, then NYSE. Explicit
`calendar=None` disables schedule interpretation. A named `"24/7"` calendar instead defines
expected bars and detects outages, including on weekends.

Calendar-aware execution requires a shared `FeedSpec(time_contract_version=2)` with an
explicit fixed intraday frequency and bar-grid policy. The initial implementation supports
time bars anchored at each tradable interval's open. It uses real holidays, early closes and
breaks from the selected schedule. Daily/session-label grids and session or elapsed-duration
windows are not supported by this validator; they fail rather than being treated as row counts.
Naive timestamps need a declared timezone. Aware timestamps already declare their timezone.
Calendar-aware execution requires a shared `FeedSpec(time_contract_version=2)` with declared
frequency and timestamp meaning. Fixed intraday time bars require an explicit bar-grid policy,
anchored at each tradable interval's open. The selected schedule supplies holidays, early closes
and breaks. Naive instants need a declared timezone; aware instants declare their timezone.

Daily bars instead declare `timestamp_semantics="session_label"`, `data_frequency="1d"` and
`bar_type="time"`. Do not supply intraday anchor, label-side or partial-bar fields for session
labels. Date values and datetime representations identify the displayed session date, without
timezone conversion. Validation checks every scheduled session between each asset's first and
last label, including completely missing sessions. A named `24/7` schedule includes weekends;
an NYSE schedule does not. No prices are filled or rows inserted. A daily frequency alone does
not turn `event_time` instants into session labels. Temporal feature windows below use the
same completeness checks; choosing a unit does not permit missing scheduled observations.

<!-- ml4t-exec -->
```python
from datetime import date

import polars as pl
from ml4t.engineer import compute_features
from ml4t.specs import FeedSpec

close = [100.0, 103.0, 106.0, 109.0]
daily_prices = pl.DataFrame({
"timestamp": [date(2025, 7, day) for day in (3, 7, 8, 9)],
"open": close, "high": close, "low": close, "close": close, "volume": 1.0,
})
daily_feed = FeedSpec(
time_contract_version=2, calendar="NYSE", data_frequency="1d",
timestamp_semantics="session_label", bar_type="time",
)
checked = compute_features(daily_prices, [], feed_spec=daily_feed)
returns = checked.with_columns(pl.col("close").log().diff().alias("returns"))
features = compute_features(returns, [{"name": "sma", "params": {"period": 2}}], feed_spec=daily_feed)
assert features["sma"].to_list() == [None, 101.5, 104.5, 107.5]
assert features["timestamp"].equals(daily_prices["timestamp"])
```

`CryptoCalendar` also changes in the next minor version. Install the `calendars` extra
to use its authoritative `24/7` schedule. It enumerates UTC daily session opens, not
Expand Down Expand Up @@ -174,6 +204,56 @@ This does not change legacy Specs bridges or the numerical feature kernels.
Neither means sessions or elapsed hours. A shortened final bar still counts as one expected
bar when `partial_bar_policy="include"`. Annualization remains a separate convention: for
example, `realized_volatility(trading_periods=252)` retains its documented numerical default.

### Session and elapsed feature windows

`sma`, `stddev` and `realized_volatility` accept `window_unit="session"` or `"elapsed"`.
They retain original bars and row counts. They do not first aggregate to session closes.
For sessions, `params.period=N` includes the current session's observations through the
current row and the preceding N-1 scheduled sessions. Earlier rows of the first N-1
observed sessions are null. July 4 holidays and lunch breaks do not create extra sessions.
A one-session average of intraday prices100/110 is therefore100 at the first row and105
at the second row, not the session's eventual closing price110 at both rows.

Elapsed periods use a positive duration such as `params.period="2h"` and the physical
UTC interval `(timestamp - duration, timestamp]`, including through DST changes.
Outputs remain null until observed coverage spans the requested duration. A half-hour
final bar can put three observations inside two elapsed hours. Daily `session_label`
identifiers cannot define elapsed windows; they do support session-count windows.
Windows with insufficient samples for the numerical statistic also remain null.
Annualization parameters and all other supported feature parameters retain their
numerical definitions. Recursive EMA/RSI and other unqualified window definitions
reject temporal units explicitly rather than silently resetting or reseeding.

<!-- ml4t-exec -->
```python
from datetime import date, datetime, timedelta, timezone

import polars as pl
from ml4t.engineer import compute_features
from ml4t.specs import FeedSpec

daily = FeedSpec(time_contract_version=2, calendar="NYSE", data_frequency="1d",
timestamp_semantics="session_label", bar_type="time")
values = [100.0, 101.0, 104.0]
prices = pl.DataFrame({
"timestamp": [date(2025, 7, day) for day in (2, 3, 7)],
"open": values, "high": values, "low": values, "close": values, "volume": 1.0,
})
result = compute_features(prices, [{"name": "sma", "params": {"period": 2}}],
feed_spec=daily, window_unit="session")
assert result["sma"].to_list() == [None, 100.5, 102.5]

hourly = FeedSpec(time_contract_version=2, calendar="24/7", data_frequency="1h",
timestamp_semantics="event_time", timezone="UTC", bar_type="time",
bar_grid_anchor="session_open", bar_label="left", partial_bar_policy="include")
prices = prices.with_columns(pl.Series("timestamp", [
datetime(2025, 7, 5, tzinfo=timezone.utc) + timedelta(hours=i) for i in range(3)
]))
result = compute_features(prices, [{"name": "sma", "params": {"period": "2h"}}],
feed_spec=hourly, window_unit="elapsed")
assert result["sma"].to_list() == [None, None, 102.5]
```
No calendar-derived annualization factor is silently substituted.

### Equity schedule helpers
Expand Down Expand Up @@ -501,7 +581,7 @@ Most features expect a DataFrame with standardized column names (lowercase):

| Column | Type | Required By |
|--------|------|-------------|
| `timestamp` | datetime | Temporal ordering, unless `assume_sorted=True` |
| `timestamp` | datetime | Required for calendar-aware grids; optional in observation-row mode |
| `asset_id` | string | Optional panel grouping, auto-detected when present |
| `open` | float | OHLCV, OHLC features |
| `high` | float | OHLCV, OHLC, HLC, HL features |
Expand Down
112 changes: 112 additions & 0 deletions docs/user-guide/labeling.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,12 @@ result = apply_meta_model(

For futures and other instruments with defined trading sessions, calendar-aware labeling respects session boundaries. A label that would span an overnight gap is handled correctly.

In the next minor version, `calendar_aware_labels` uses the shared temporal contract.
An explicit `calendar` argument wins over a declared `LabelingConfig.calendar`, then
`FeedSpec` metadata, then NYSE. Omitting a selector differs from explicit `None`.
Config JSON/YAML output preserves that distinction and emits only explicitly declared
column overrides, so a round trip does not replace feed column mappings with defaults.

```python
from ml4t.engineer.labeling import calendar_aware_labels

Expand All @@ -433,6 +439,112 @@ former pause because trading is continuous until 4:00 PM.
See the [CME exchange notice](https://www.cmegroup.com/notices/electronic-trading/2021/06/20210621.html)
for the affected equity-index contracts.

<a id="observation-rows-expected-bars-and-elapsed-durations"></a>

### Observation rows, expected bars, sessions and elapsed durations

Use `calendar=None` for observation-row labeling, including timestamp-indexed data:

```python
row_config = LabelingConfig.triple_barrier(
upper_barrier=0.02,
lower_barrier=0.02,
max_holding_period=10,
calendar=None,
window_unit="observation",
)
labels = calendar_aware_labels(df, row_config)
```

Row mode retains input order, processes each asset independently, does not require
timestamps and never loads a schedule provider. Its `label_bars` and `label_time` are
observation counts and per-asset row offsets; `label_duration` is unset. Timestamp
spacing and weekend placement do not alter its numerical labels. Duration holding
periods are incompatible with this mode.

Named calendars segment authoritative sessions and breaks. New shared-feed calls and
omitted selectors default count windows to `"expected_bar"`; an undeclared or incomplete
grid fails before labeling. Supply the complete shared grid
declarations described in the [feature guide](features.md#calendar-and-bar-grid) to reject
missing scheduled bars before labeling. Named `"24/7"` still has daily schedule boundaries
and is not equivalent to row mode. Duration holding periods select `"elapsed"` and remain truncated at session
boundaries, rather than counting sessions or observed dates.

Pass `feed_spec=FeedSpec(...)` directly or through `LabelingConfig.feed_spec`. Shared
`timestamp_semantics` must distinguish intraday instants from `session_label` identifiers.
Naive feed timestamps require an explicit timezone, and ambiguous/nonexistent local
times fail. Right-labeled/bar-close observations at a break boundary end the preceding
tradable interval. Input timestamp representations are retained in the output.
Instant horizons and `label_duration` use physical elapsed time, including across
daylight-saving transitions. Shared `session_label` identifiers do not identify instants;
their `label_time` retains the session identifier and `label_duration` is unset.

Legacy calls without FeedSpec keep naive-UTC handling. Custom protocol objects and
explicit `calendar="auto"` remain compatibility modes without shared feed metadata.
An explicit legacy calendar selector without FeedSpec retains observation-count horizons.
New calls can also select `window_unit="observation"` explicitly to count received rows
within each authoritative segment, without claiming scheduled-bar completeness.
Auto-gap segmentation does not establish market holidays, session completeness or a
bar grid. Standalone labeling kernels retain their documented row-count or elapsed-time
definitions and chronological sorting; they do not acquire calendar interpretation from
timestamps. Use this wrapper when selecting the shared calendar/row contract.
Standalone kernels reject configs declaring calendar/feed/window semantics instead
of silently ignoring those declarations.
Standalone kernels retain date-valued label times and calendar-day differences in
`label_duration` for legacy `Date` inputs. These standalone calls do not validate
the dates against an exchange schedule.

For `window_unit="session"`, an integer `max_holding_period=N` expires at the final
bar of the Nth scheduled session after the entry session. A positive integral count
column can select N per event. July 3, 2025 on NYSE therefore reaches July 7 for N=1,
not the July 4 holiday. Intraday entries reach the following session's last bar,
including an early close; lunch and overnight breaks do not truncate this horizon.
Profit, stop and trailing barriers still use the existing triple-barrier calculation.
The full declared grid is checked before any labels are computed, including whole-session
outages. No prices are filled or rows inserted.

With `partial_bar_policy="exclude"`, a shortened session can have no bars. It still
counts as a scheduled session. The exit uses the last available bar no later than the
deadline, even if that is the entry itself with zero holding bars. The calculation
does not extend the deadline into a later session to obtain another observation.

Daily input uses `FeedSpec(time_contract_version=2, calendar="NYSE",
timestamp_semantics="session_label", data_frequency="1d", bar_type="time")`.
Do not supply intraday anchor/label/partial-bar fields. Date-valued and midnight datetime
identifiers keep their displayed session date; they are not converted to UTC instants.
Daily observation and expected-bar horizons scan across session rows rather than
truncating every label to its entry row. Elapsed horizons reject these identifiers.

As with existing count horizons, a deadline beyond the available dataset is censored
at the last available bar. `label_bars` reports the actual observed holding length and
`label_time` the actual exit, not an inferred future observation. These changes target
the next 0.2 development release, not the historical 0.1.7 patch.

<!-- ml4t-exec -->
```python
from datetime import date
from io import BytesIO

import polars as pl
from ml4t.specs import FeedSpec
from ml4t.engineer.config import LabelingConfig
from ml4t.engineer.labeling import calendar_aware_labels

prices = pl.DataFrame({
"timestamp": [date(2025, 7, day) for day in (3, 7, 8)],
"close": [100.0, 101.0, 103.0],
})
config = LabelingConfig.triple_barrier(
upper_barrier=None, lower_barrier=None, max_holding_period=1, window_unit="session",
feed_spec=FeedSpec(time_contract_version=2, calendar="NYSE", data_frequency="1d",
timestamp_semantics="session_label", bar_type="time"),
)
labels = calendar_aware_labels(prices, config)
assert labels["label_time"].to_list() == [date(2025, 7, 7), date(2025, 7, 8), date(2025, 7, 8)]
assert labels["label_bars"].to_list() == [1, 1, 0]
labels.write_parquet(BytesIO())
labels.head(0).write_parquet(BytesIO())
```
## Sample Weighting

AFML Chapter 4 addresses the problem of overlapping labels creating correlated samples. The library provides the full toolkit:
Expand Down
6 changes: 1 addition & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ build-backend = "hatchling.build"
[tool.hatch.version]
source = "vcs"

[tool.hatch.metadata]
# Qualification pin until the shared time-contract v2 API is published on PyPI.
allow-direct-references = true

[tool.hatch.build.targets.wheel]
packages = ["src/ml4t"]
namespaces = true
Expand Down Expand Up @@ -76,7 +72,7 @@ requires-python = ">=3.12,<3.15"

# Core dependencies
dependencies = [
"ml4t-specs @ git+https://github.com/ml4t/specs.git@701bde68744035050ede386efce498234c661c21",
"ml4t-specs>=0.2.0,<0.3.0",

# Data processing (Polars-first)
"polars>=0.20.0",
Expand Down
Loading
Loading