Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Bug report
description: Report reproducible incorrect behavior in ecosystem policy or tooling
description: Report reproducible incorrect behavior in ml4t-engineer
title: "[Bug]: "
labels:
- "type: bug"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,12 @@ jobs:
name: Qualify (${{ matrix.os }}, Python ${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
needs: build-candidate
timeout-minutes: 45
timeout-minutes: ${{ matrix.python-version == '3.15' && 90 || 45 }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.12", "3.13", "3.14"]
python-version: ["3.12", "3.13", "3.14", "3.15"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/ecosystem.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,3 @@ jobs:
uses: ml4t/ecosystem/.github/workflows/qualify-library.yml@066dc737a879faea51a5b8a900315441712ea89a # 2026-09-25 policy snapshot
with:
import-package: ml4t.engineer
prerelease-exception: python-315-polars
2 changes: 1 addition & 1 deletion .github/workflows/native-stress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
name: Native stress (${{ matrix.os }}, Python ${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
needs: build
timeout-minutes: 30
timeout-minutes: ${{ matrix.python-version == '3.15' && 90 || 30 }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.build.outputs.matrix) }}
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ jobs:
uses: ml4t/ecosystem/.github/workflows/qualify-library.yml@066dc737a879faea51a5b8a900315441712ea89a # 2026-09-25 policy snapshot
with:
import-package: ml4t.engineer
prerelease-exception: python-315-polars

qualification:
name: Qualify Release Candidate
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ is under `docs/user-guide/`, and the generated API reference is under

## Engineering constraints

- Python 3.12, 3.13, and 3.14 are supported.
- Python 3.12, 3.13, 3.14, and 3.15 are supported.
- Feature computation must preserve the documented DataFrame or LazyFrame
return behavior.
- Changes to registered features must keep implementation, registry metadata,
Expand Down
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ml4t-engineer

[![Python 3.12-3.14](https://img.shields.io/badge/python-3.12--3.14-blue.svg)](https://www.python.org/downloads/)
[![Python 3.12-3.15](https://img.shields.io/badge/python-3.12--3.15-blue.svg)](https://www.python.org/downloads/)
[![PyPI](https://img.shields.io/pypi/v/ml4t-engineer)](https://pypi.org/project/ml4t-engineer/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/ml4t/engineer/blob/main/LICENSE)

Expand All @@ -13,12 +13,19 @@ The core interface uses Polars DataFrames.

## Installation

`ml4t-engineer` supports Python 3.12, 3.13, and 3.14 on Linux, macOS, and Windows.
Development source supports Python 3.12, 3.13, 3.14, and 3.15 on Linux, macOS, and Windows.
Python 3.15 support and the calendar API used below are intended for the 0.2 release.
Release 0.1.6 supports Python 3.12-3.14; use its
[version-matched quick start](https://github.com/ml4t/engineer/tree/v0.1.6#quick-start)
instead of passing the new `calendar` argument to that release.

```bash
pip install ml4t-engineer
```

Until 0.2 is available from PyPI, run the examples below from a
[source checkout](#development) with `uv sync --no-dev`, using that environment's Python.

The quick start uses only core dependencies. Optional extras provide TA-Lib validation,
DuckDB and PyArrow storage, market calendars, visualization, statistics, and ML tools:

Expand All @@ -31,9 +38,10 @@ pip install "ml4t-engineer[stats]"
pip install "ml4t-engineer[ml]"
```

TA-Lib requires its native library. The core package does not require an external
service, credentials, or special hardware. Python 3.15 is not supported while the
active Polars compatibility exception applies.
The development `ta` extra uses TA-Lib 0.8.2 or later in the 0.8 series; its wheels
include the native library. For source builds, follow the
[upstream installation guide](https://github.com/TA-Lib/ta-lib-python#installation).
The core package does not require an external service, credentials, or special hardware.

## Quick Start

Expand Down
33 changes: 18 additions & 15 deletions docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ and then move directly into the first workflow.

## Requirements

- Python 3.12, 3.13, or 3.14
- Python 3.12, 3.13, 3.14, or 3.15
- Polars 0.20+

These requirements describe development source for the 0.2 release. Published 0.1.6
supports Python 3.12-3.14 and does not accept the new `calendar` argument. Use its
[version-matched quick start](https://github.com/ml4t/engineer/tree/v0.1.6#quick-start)
for that release. Until 0.2 is available from PyPI, use the source installation below
for Python 3.15 and the calendar-aware examples in this guide.

## Install from PyPI

```bash
Expand All @@ -20,30 +26,27 @@ If you standardize on `uv`, `uv pip install ml4t-engineer` is equivalent.

```bash
git clone https://github.com/ml4t/engineer.git
cd ml4t-engineer
pip install -e .
cd engineer
uv sync --no-dev
```

Run the examples with the created environment, for example `uv run --no-sync python`.

## Optional Dependencies

### TA-Lib (for validation)

Some indicators can be validated against TA-Lib. Install TA-Lib if you need this:
Some indicators can be validated against TA-Lib. The `ta` extra requires TA-Lib 0.8.2
or later in the 0.8 series, whose wheels include the native library and support Python 3.15:

```bash
# macOS
brew install ta-lib
pip install TA-Lib

# Ubuntu/Debian
sudo apt-get install libta-lib-dev
pip install TA-Lib

# Windows
# Download from https://www.ta-lib.org/
pip install TA-Lib
uv pip install "ml4t-engineer[ta]"
```

Use the [upstream installation guide](https://github.com/TA-Lib/ta-lib-python#installation)
when your platform requires a source build. Comparisons must declare matching moving-average
types: Engineer's APO/PPO default remains SMA (`matype=0`), while TA-Lib 0.8 defaults to EMA.

### Statistical diagnostics

`find_optimal_d()` and `fdiff_diagnostics()` use Statsmodels for the augmented
Expand Down
10 changes: 6 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,13 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Topic :: Office/Business :: Financial :: Investment",
"Topic :: Scientific/Engineering :: Information Analysis",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Typing :: Typed",
]
requires-python = ">=3.12,<3.15"
requires-python = ">=3.12"

# Core dependencies
dependencies = [
Expand All @@ -95,7 +96,7 @@ dependencies = [
[project.optional-dependencies]
# Technical analysis libraries
ta = [
"ta-lib>=0.4.0,<0.8",
"ta-lib>=0.8.2,<0.9",
]

# Feature store with DuckDB
Expand Down Expand Up @@ -142,7 +143,7 @@ dev = [
"pre-commit>=3.3.0",
"duckdb>=0.9.0",
"pandas-market-calendars>=5.5.0",
"ta-lib>=0.4.0,<0.8",
"ta-lib>=0.8.2,<0.9",
"yfinance>=0.2.0",
"lightgbm>=4.0.0",
"shap>=0.44.0",
Expand All @@ -160,7 +161,7 @@ docs = [

# Complete installation
all = [
"ta-lib>=0.4.0,<0.8",
"ta-lib>=0.8.2,<0.9",
"duckdb>=0.9.0",
"pandas-market-calendars>=5.5.0",
"plotly>=5.15.0",
Expand Down Expand Up @@ -201,6 +202,7 @@ test = [
"pytest-timeout>=2.1.0",
"pytest-benchmark>=4.0.0",
"hypothesis>=6.80.0",
"pandas-market-calendars>=5.5.0",
]
dev = [
{ include-group = "test" },
Expand Down
8 changes: 4 additions & 4 deletions tests/test_all_indicators_accuracy.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,13 +175,13 @@ def test_momentum_indicators(self, price_data):
assert_allclose(result, expected, rtol=1e-10, equal_nan=True)

# APO
result = apo(close, 12, 26)
expected = talib.APO(close, 12, 26)
result = apo(close, 12, 26, matype=0)
expected = talib.APO(close, 12, 26, matype=0)
assert_allclose(result, expected, rtol=1e-10, equal_nan=True)

# PPO
result = ppo(close, 12, 26)
expected = talib.PPO(close, 12, 26)
result = ppo(close, 12, 26, matype=0)
expected = talib.PPO(close, 12, 26, matype=0)
assert_allclose(result, expected, rtol=1e-10, equal_nan=True)

# AROON
Expand Down
49 changes: 16 additions & 33 deletions tests/test_linearreg_family.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,44 +277,27 @@ def test_linearreg_family_nan_patterns(self):
assert np.all(np.isnan(angle[: period - 1]))

def test_linearreg_family_crypto_accuracy(self, crypto_data_small):
"""Test accuracy on real crypto data."""
"""Keep regression outputs accurate when the upstream rolling reference drifts."""
prices = crypto_data_small["close"].to_numpy()

for period in [5, 10, 14, 20]:
# Test slope
expected_slope = talib.LINEARREG_SLOPE(prices, timeperiod=period)
result_slope = linearreg_slope(prices, timeperiod=period)
valid_idx = ~(np.isnan(expected_slope) | np.isnan(result_slope))
if np.any(valid_idx):
windows = np.lib.stride_tricks.sliding_window_view(prices, period)
design = np.column_stack((np.arange(period), np.ones(period)))
# Translation leaves slope unchanged and avoids a large price offset in the solve.
slope, intercept = np.linalg.lstsq(design, (windows - windows[:, :1]).T, rcond=None)[0]
for function, expected in [
(linearreg_slope, slope),
(linearreg_intercept, intercept + windows[:, 0]),
(linearreg_angle, np.degrees(np.arctan(slope))),
]:
aligned = np.full(len(prices), np.nan)
aligned[period - 1 :] = expected
assert_allclose(
result_slope[valid_idx],
expected_slope[valid_idx],
function(prices, timeperiod=period),
aligned,
rtol=1e-7,
err_msg=f"LINEARREG_SLOPE mismatch on crypto data for period {period}",
)

# Test intercept
expected_intercept = talib.LINEARREG_INTERCEPT(prices, timeperiod=period)
result_intercept = linearreg_intercept(prices, timeperiod=period)
valid_idx = ~(np.isnan(expected_intercept) | np.isnan(result_intercept))
if np.any(valid_idx):
assert_allclose(
result_intercept[valid_idx],
expected_intercept[valid_idx],
rtol=1e-7,
err_msg=f"LINEARREG_INTERCEPT mismatch on crypto data for period {period}",
)

# Test angle
expected_angle = talib.LINEARREG_ANGLE(prices, timeperiod=period)
result_angle = linearreg_angle(prices, timeperiod=period)
valid_idx = ~(np.isnan(expected_angle) | np.isnan(result_angle))
if np.any(valid_idx):
assert_allclose(
result_angle[valid_idx],
expected_angle[valid_idx],
rtol=1e-7,
err_msg=f"LINEARREG_ANGLE mismatch on crypto data for period {period}",
equal_nan=True,
err_msg=f"{function.__name__} differs from least squares for period {period}",
)

@pytest.mark.perf
Expand Down
6 changes: 3 additions & 3 deletions tests/test_release_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def test_ci_qualifies_required_python_platform_matrix() -> None:
matrix = workflow["jobs"]["test"]["strategy"]["matrix"]
actual = set(itertools.product(matrix["os"], matrix["python-version"]))

stable = {"3.12", "3.13", "3.14"}
stable = {"3.12", "3.13", "3.14", "3.15"}
platforms = {"ubuntu-latest", "macos-latest", "windows-latest"}
assert actual == set(itertools.product(platforms, stable))
assert "include" not in matrix
Expand Down Expand Up @@ -254,11 +254,11 @@ def test_core_dependency_requires_published_time_contract_v2() -> None:
assert specs["source"] == {"registry": "https://pypi.org/simple"}


def test_package_supports_python_312_through_314() -> None:
def test_package_supports_python_312_through_315() -> None:
project = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))
dependencies = project["project"]["dependencies"]

assert project["project"]["requires-python"] == ">=3.12,<3.15"
assert project["project"]["requires-python"] == ">=3.12"
assert "numba>=0.57.0" in dependencies
assert "statsmodels>=0.14.0" in project["project"]["optional-dependencies"]["stats"]
assert "pyarrow>=14.0.0" in project["project"]["optional-dependencies"]["store"]
Expand Down
Loading
Loading