diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49d6795..a64bfbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,13 +7,22 @@ on: branches: [main] workflow_dispatch: workflow_call: + inputs: + release_version: + description: Exact version to embed in a release candidate + required: false + type: string -env: - PYTHON_VERSION: "3.12" +permissions: + contents: read concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} + +env: + PYTHON_VERSION: "3.12" + UV_VERSION: "0.10.9" jobs: lint: @@ -21,43 +30,28 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Install uv - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - version: "0.10.9" - - - name: Set up Python - run: uv python install ${{ env.PYTHON_VERSION }} - - - name: Install dependencies - run: uv sync --python ${{ env.PYTHON_VERSION }} --dev - - - name: Run ruff check - run: uv run ruff check src/ tests/ - - - name: Run ruff format check - run: uv run ruff format --check src/ tests/ + version: ${{ env.UV_VERSION }} + - run: uv python install ${{ env.PYTHON_VERSION }} + - run: uv sync --python ${{ env.PYTHON_VERSION }} --dev + - name: Run Ruff lint + run: uv run ruff check src/ tests/ examples/ scripts/ + - name: Run Ruff format check + run: uv run ruff format --check src/ tests/ examples/ scripts/ typecheck: name: Type Check runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Install uv - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - version: "0.10.9" - + version: ${{ env.UV_VERSION }} - name: Set up Python run: uv python install ${{ env.PYTHON_VERSION }} - - name: Install dependencies - run: >- - uv sync --python ${{ env.PYTHON_VERSION }} --dev - --extra store --extra viz - + run: uv sync --python ${{ env.PYTHON_VERSION }} --dev --extra store --extra viz - name: Run ty check run: uv run --no-sync ty check @@ -66,83 +60,153 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Install uv - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - version: "0.10.9" - + version: ${{ env.UV_VERSION }} - name: Set up Python run: uv python install ${{ env.PYTHON_VERSION }} - - name: Export locked environments run: | uv export --locked --no-dev --no-emit-project --no-hashes \ --output-file "${{ runner.temp }}/core.txt" uv export --locked --all-extras --all-groups --no-emit-project --no-hashes \ --output-file "${{ runner.temp }}/complete.txt" + - name: Audit runtime and contributor environments + run: | + uv run --with pip-audit python -m pip_audit \ + --requirement "${{ runner.temp }}/core.txt" --progress-spinner off + uv run --with pip-audit python -m pip_audit \ + --requirement "${{ runner.temp }}/complete.txt" --progress-spinner off + + coverage: + name: Coverage + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: ${{ env.UV_VERSION }} + - name: Set up Python + run: uv python install ${{ env.PYTHON_VERSION }} + - name: Install dependencies + run: uv sync --python ${{ env.PYTHON_VERSION }} --dev --extra ta --extra store --extra viz + - name: Measure line and branch coverage + env: + NUMBA_DISABLE_JIT: "1" + run: >- + uv run pytest tests/ -q --cov-report= + --cov-report=json:coverage.json --cov-report=term:skip-covered + - name: Enforce release thresholds + run: uv run python scripts/check_coverage.py coverage.json - - name: Audit locked environments + documentation: + name: Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: ${{ env.UV_VERSION }} + - run: uv python install ${{ env.PYTHON_VERSION }} + - run: uv sync --python ${{ env.PYTHON_VERSION }} --dev --extra docs + - name: Run documented workflows + run: uv run python tests/documentation_workflows.py + - name: Check every README link + run: uv run python scripts/ci/check_readme_links.py README.md + - name: Build strict documentation + run: uv run mkdocs build --strict + + build-candidate: + name: Build Candidate + runs-on: ubuntu-latest + needs: [lint, typecheck, security, coverage, documentation] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: ${{ env.UV_VERSION }} + - run: uv python install ${{ env.PYTHON_VERSION }} + - run: uv sync --python ${{ env.PYTHON_VERSION }} --dev + - name: Set release candidate version + if: ${{ inputs.release_version != '' }} + env: + RELEASE_VERSION: ${{ inputs.release_version }} + run: echo "SETUPTOOLS_SCM_PRETEND_VERSION=$RELEASE_VERSION" >> "$GITHUB_ENV" + - name: Build source and wheel artifacts once + run: uv build --out-dir candidate/dist + - name: Record candidate commit, tree, version, and SHA256 digests + run: >- + uv run python scripts/ci/candidate.py create candidate + --commit-sha "${{ github.sha }}" + --git-tree "$(git rev-parse HEAD^{tree})" + - name: Validate artifact metadata and manifest run: | - uv run pip-audit --requirement "${{ runner.temp }}/core.txt" - uv run pip-audit --requirement "${{ runner.temp }}/complete.txt" + uv run twine check candidate/dist/* + uv run python scripts/ci/candidate.py verify candidate \ + --expected-commit "${{ github.sha }}" \ + --expected-tree "$(git rev-parse HEAD^{tree})" + - name: Upload release candidate + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-candidate + path: candidate/ + if-no-files-found: error test: name: Qualify (${{ matrix.os }}, Python ${{ matrix.python-version }}) runs-on: ${{ matrix.os }} + needs: build-candidate timeout-minutes: 45 strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.12", "3.13", "3.14"] - steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - - - name: Install uv - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - version: "0.10.9" - + version: ${{ env.UV_VERSION }} - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: uv sync --python python --dev --extra ta --extra store --extra viz - - - name: Import package - run: uv run --no-sync python -c "import ml4t.engineer" - - - name: Run ty check - run: uv run --no-sync ty check --python-version ${{ matrix.python-version }} - - - name: Build package - run: uv build --python python - + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-candidate + path: candidate + - name: Verify candidate identity + shell: bash + run: >- + python scripts/ci/candidate.py verify candidate + --expected-commit "${{ github.sha }}" + --expected-tree "$(git rev-parse HEAD^{tree})" - name: Export installed-wheel test environment run: >- uv export --locked --group dev --extra ta --extra store --extra viz --no-emit-project --no-hashes --output-file "${{ runner.temp }}/test-requirements.txt" - - - name: Install built wheel + - name: Install the candidate wheel in a clean environment shell: bash run: | uv venv --python python .artifact-venv - artifact_wheel="$(find dist -maxdepth 1 -name '*.whl' -print -quit)" + wheel="$(find candidate/dist -maxdepth 1 -name '*.whl' -print -quit)" UV_CACHE_DIR="${{ runner.temp }}/fresh-wheel-cache" \ - uv pip install --python .artifact-venv "$artifact_wheel" - uv pip install --python .artifact-venv --requirements "${{ runner.temp }}/test-requirements.txt" - - - name: Import built wheel + uv pip install --python .artifact-venv "$wheel" + uv pip install --python .artifact-venv \ + --requirements "${{ runner.temp }}/test-requirements.txt" + - name: Import the installed candidate run: uv run --python .artifact-venv --no-project python -c "import ml4t.engineer" - - - name: Run tests + - name: Run ty check + run: >- + uv run --python .artifact-venv --no-project + ty check --python-version ${{ matrix.python-version }} + - name: Run the installed-wheel suite repeatedly shell: bash run: | for iteration in {1..10}; do @@ -150,83 +214,25 @@ jobs: uv run --python .artifact-venv --no-project \ python -m pytest tests/ -q --tb=short --no-cov done - - - name: Run documented workflows from built wheel + - name: Run documented workflows from the installed candidate run: >- uv run --python .artifact-venv --no-project python tests/documentation_workflows.py - coverage: - name: Coverage - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Install uv - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 - with: - version: "0.10.9" - - - name: Set up Python - run: uv python install ${{ env.PYTHON_VERSION }} - - - name: Install dependencies - run: >- - uv sync --python ${{ env.PYTHON_VERSION }} --dev - --extra ta --extra store --extra viz - - - name: Measure line and branch coverage - env: - NUMBA_DISABLE_JIT: "1" - run: >- - uv run pytest tests/ -q - --cov-report= - --cov-report=json:coverage.json - --cov-report=term:skip-covered - - - name: Enforce release thresholds - run: uv run python scripts/check_coverage.py coverage.json - build: name: Build Package runs-on: ubuntu-latest - needs: [lint, typecheck, security, test, coverage] + needs: [build-candidate, test] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - - - name: Install uv - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - version: "0.10.9" - - - name: Set up Python - run: uv python install ${{ env.PYTHON_VERSION }} - - - name: Build package - run: uv build - - - name: Validate package metadata - run: uv run twine check dist/* - - - name: Validate release version - if: startsWith(github.ref, 'refs/tags/') + name: release-candidate + path: candidate + - name: Verify the qualified candidate run: >- - uv run python -c - "import os; from ml4t.engineer import __version__; - tag=os.environ['GITHUB_REF_NAME']; - assert tag == f'v{__version__}', f'{tag} does not match v{__version__}'" - - - name: Validate wheel installation - run: | - uv venv --python ${{ env.PYTHON_VERSION }} .artifact-venv - uv pip install --python .artifact-venv dist/*.whl - uv run --python .artifact-venv --no-project python -c "import ml4t.engineer" - - - name: Upload build artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: dist - path: dist/ + python scripts/ci/candidate.py verify candidate + --expected-commit "${{ github.sha }}" + --expected-tree "$(git rev-parse HEAD^{tree})" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c69b137..26d4fd5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,51 +3,35 @@ name: Docs on: push: branches: [main] + pull_request: + branches: [main] workflow_dispatch: permissions: contents: read concurrency: - group: "docs" + group: docs-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + PYTHON_VERSION: "3.12" + UV_VERSION: "0.10.9" + jobs: build: - name: Build & Deploy Docs + name: Build Documentation runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - fetch-depth: 0 - - - name: Install uv - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 - with: - version: "0.10.9" - - - name: Set up Python - run: uv python install 3.12 - - - name: Install dependencies - run: uv sync --python 3.12 --dev --extra docs - + version: ${{ env.UV_VERSION }} + - run: uv python install ${{ env.PYTHON_VERSION }} + - run: uv sync --python ${{ env.PYTHON_VERSION }} --dev --extra docs - name: Run documented workflows run: uv run python tests/documentation_workflows.py - - - name: Build docs + - name: Check every README link + run: uv run python scripts/ci/check_readme_links.py README.md + - name: Build strict documentation run: uv run mkdocs build --strict - - - name: Deploy to website repo - uses: cpina/github-action-push-to-another-repository@55306faa4ed53b815ae49e564af8cfb359d32ae2 # v1.7.3 - env: - SSH_DEPLOY_KEY: ${{ secrets.DOCS_DEPLOY_KEY }} - with: - source-directory: site/ - destination-github-username: ml4t - destination-repository-name: website - target-directory: static/docs/engineer/ - target-branch: main - commit-message: "docs(engineer): update from ml4t/engineer@${{ github.sha }}" - user-name: ml4t-bot - user-email: bot@ml4trading.io diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b563506..4d4985f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,12 +1,74 @@ name: Release on: - push: - tags: ["v*"] + workflow_dispatch: + inputs: + version: + description: Release version without the v prefix + required: true + type: string + candidate_commit: + description: Full commit SHA currently at the tip of main + required: true + type: string + +permissions: + contents: read + +concurrency: + group: release-${{ inputs.version }} + cancel-in-progress: false jobs: + validate: + name: Validate Release Request + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + commit: ${{ steps.identity.outputs.commit }} + tree: ${{ steps.identity.outputs.tree }} + version: ${{ steps.identity.outputs.version }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.candidate_commit }} + fetch-depth: 0 + - name: Require the current main commit and an unused version + id: identity + env: + CANDIDATE_COMMIT: ${{ inputs.candidate_commit }} + GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ inputs.version }} + run: | + if [[ ! "$CANDIDATE_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then + echo "candidate_commit must be a full lowercase commit SHA" >&2 + exit 1 + fi + if [[ ! "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+)?$ ]]; then + echo "version must be a stable or prerelease PEP 440 version" >&2 + exit 1 + fi + git fetch origin main --no-tags + if [[ "$(git rev-parse HEAD)" != "$CANDIDATE_COMMIT" ]] || \ + [[ "$(git rev-parse origin/main)" != "$CANDIDATE_COMMIT" ]] || \ + [[ "${{ github.sha }}" != "$CANDIDATE_COMMIT" ]]; then + echo "dispatch the workflow from candidate_commit at the current main revision" >&2 + exit 1 + fi + if git show-ref --verify --quiet "refs/tags/v${RELEASE_VERSION}" || \ + gh release view "v${RELEASE_VERSION}" --repo "${{ github.repository }}" >/dev/null 2>&1; then + echo "tag or GitHub release v${RELEASE_VERSION} already exists" >&2 + exit 1 + fi + python scripts/ci/release.py require-absent ml4t-engineer "$RELEASE_VERSION" + echo "commit=$CANDIDATE_COMMIT" >> "$GITHUB_OUTPUT" + echo "tree=$(git rev-parse HEAD^{tree})" >> "$GITHUB_OUTPUT" + echo "version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT" + ecosystem-qualification: name: Ecosystem Qualification + needs: validate permissions: contents: read uses: ml4t/ecosystem/.github/workflows/qualify-library.yml@457678d8860d0c816b1dc376e7e2691daa93af4d # v0.1.0 @@ -15,40 +77,226 @@ jobs: prerelease-exception: python-315-polars qualification: - name: Qualify Release + name: Qualify Release Candidate + needs: validate permissions: contents: read uses: ./.github/workflows/ci.yml + with: + release_version: ${{ needs.validate.outputs.version }} + + select-candidate: + name: Select Qualified Candidate + runs-on: ubuntu-latest + needs: [validate, ecosystem-qualification, qualification] + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.validate.outputs.commit }} + fetch-depth: 0 + - name: Download qualified release candidate + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-candidate + path: candidate + - name: Verify candidate commit, tree, version, and SHA256 manifest + run: >- + python scripts/ci/candidate.py verify candidate + --expected-commit "${{ needs.validate.outputs.commit }}" + --expected-tree "${{ needs.validate.outputs.tree }}" + --expected-tag "v${{ needs.validate.outputs.version }}" + - name: Retain selected candidate + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: qualified-release-candidate + path: candidate/ + if-no-files-found: error + + docs: + name: Publish Qualified Documentation + runs-on: ubuntu-latest + needs: [validate, select-candidate] + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.validate.outputs.commit }} + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: "0.10.9" + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: qualified-release-candidate + path: candidate + - name: Verify selected candidate + run: >- + python scripts/ci/candidate.py verify candidate + --expected-commit "${{ needs.validate.outputs.commit }}" + --expected-tree "${{ needs.validate.outputs.tree }}" + --expected-tag "v${{ needs.validate.outputs.version }}" + - name: Build revision-identified documentation + env: + ML4T_DOCS_COMMIT: ${{ needs.validate.outputs.commit }} + ML4T_DOCS_VERSION: ${{ needs.validate.outputs.version }} + run: | + uv sync --dev --extra docs --python 3.12 + uv run mkdocs build --strict + python - <<'PY' + import json + import os + from pathlib import Path + + Path("site/release.json").write_text( + json.dumps( + { + "commit": os.environ["ML4T_DOCS_COMMIT"], + "library": "engineer", + "version": os.environ["ML4T_DOCS_VERSION"], + }, + sort_keys=True, + ) + + "\n", + encoding="utf-8", + ) + PY + - name: Require deploy credentials + env: + DOCS_DEPLOY_KEY: ${{ secrets.DOCS_DEPLOY_KEY }} + run: | + if [[ -z "$DOCS_DEPLOY_KEY" ]]; then + echo "DOCS_DEPLOY_KEY is required for a stable release" >&2 + exit 1 + fi + - name: Deploy stable documentation + uses: cpina/github-action-push-to-another-repository@55306faa4ed53b815ae49e564af8cfb359d32ae2 # v1.7.3 + env: + SSH_DEPLOY_KEY: ${{ secrets.DOCS_DEPLOY_KEY }} + with: + source-directory: site/ + destination-github-username: ml4t + destination-repository-name: website + target-directory: static/docs/engineer/ + target-branch: main + commit-message: "docs(engineer): publish v${{ needs.validate.outputs.version }} from ${{ needs.validate.outputs.commit }}" + user-name: ml4t-bot + user-email: bot@ml4trading.io + - name: Deploy versioned documentation + uses: cpina/github-action-push-to-another-repository@55306faa4ed53b815ae49e564af8cfb359d32ae2 # v1.7.3 + env: + SSH_DEPLOY_KEY: ${{ secrets.DOCS_DEPLOY_KEY }} + with: + source-directory: site/ + destination-github-username: ml4t + destination-repository-name: website + target-directory: static/docs/engineer/releases/${{ needs.validate.outputs.version }}/ + target-branch: main + commit-message: "docs(engineer): retain v${{ needs.validate.outputs.version }} from ${{ needs.validate.outputs.commit }}" + user-name: ml4t-bot + user-email: bot@ml4trading.io + - name: Verify deployed documentation identity + env: + RELEASE_COMMIT: ${{ needs.validate.outputs.commit }} + RELEASE_VERSION: ${{ needs.validate.outputs.version }} + run: uv run python scripts/ci/verify_docs_deployment.py publish: name: Publish to PyPI runs-on: ubuntu-latest - needs: [ecosystem-qualification, qualification] + needs: [validate, ecosystem-qualification, select-candidate, docs] environment: pypi permissions: + contents: read id-token: write steps: - - name: Download build artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: dist - path: dist/ - - - name: Publish to PyPI + name: qualified-release-candidate + path: candidate + - name: Publish the qualified artifacts uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + with: + packages-dir: candidate/dist/ + print-hash: true github-release: name: Create GitHub Release runs-on: ubuntu-latest - needs: publish + needs: [validate, publish] permissions: contents: write steps: - - name: Create GitHub Release + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: qualified-release-candidate + path: candidate + - name: Create tag and GitHub release from the candidate commit env: + CANDIDATE_COMMIT: ${{ needs.validate.outputs.commit }} GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} + RELEASE_VERSION: ${{ needs.validate.outputs.version }} + run: | + args=( + "v${RELEASE_VERSION}" + --repo "${{ github.repository }}" + --target "$CANDIDATE_COMMIT" + --generate-notes + candidate/dist/* + candidate/candidate.json + ) + if [[ "$RELEASE_VERSION" =~ (a|b|rc)[0-9]+$ ]]; then + args+=(--prerelease) + fi + gh release create "${args[@]}" + + verify-release: + name: Verify Published Release + runs-on: ubuntu-latest + needs: [validate, github-release] + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.validate.outputs.commit }} + fetch-depth: 0 + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: "0.10.9" + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: qualified-release-candidate + path: candidate + - name: Verify PyPI metadata and artifact SHA256 digests + run: python scripts/ci/release.py verify candidate + - name: Install the published wheel and run the README quick start + env: + RELEASE_VERSION: ${{ needs.validate.outputs.version }} run: >- - gh release create "${{ github.ref_name }}" - --generate-notes - --verify-tag + uv run --isolated --no-project + --with "ml4t-engineer==${RELEASE_VERSION}" + python tests/documentation_workflows.py --readme-only + - name: Verify GitHub release artifacts against the manifest + env: + GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ needs.validate.outputs.version }} + run: | + mkdir -p released/dist + gh release download "v${RELEASE_VERSION}" --repo "${{ github.repository }}" \ + --pattern candidate.json --dir released + gh release download "v${RELEASE_VERSION}" --repo "${{ github.repository }}" \ + --pattern '*.whl' --pattern '*.tar.gz' --dir released/dist + python scripts/ci/candidate.py verify released \ + --expected-commit "${{ needs.validate.outputs.commit }}" \ + --expected-tree "${{ needs.validate.outputs.tree }}" \ + --expected-tag "v${RELEASE_VERSION}" + - name: Repeat deployed documentation identity check + env: + RELEASE_COMMIT: ${{ needs.validate.outputs.commit }} + RELEASE_VERSION: ${{ needs.validate.outputs.version }} + run: python scripts/ci/verify_docs_deployment.py diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..7f3da52 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,41 @@ +name: Security + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "23 8 * * 1" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: security-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + dependency-review: + name: Dependency Review + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: high + + codeql: + name: CodeQL + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: github/codeql-action/init@fddeee1a7ece751b577e409a89057319e3172939 # v4 + with: + languages: python + - uses: github/codeql-action/analyze@fddeee1a7ece751b577e409a89057319e3172939 # v4 diff --git a/README.md b/README.md index f41e578..99261b4 100644 --- a/README.md +++ b/README.md @@ -2,204 +2,112 @@ [![Python 3.12-3.14](https://img.shields.io/badge/python-3.12--3.14-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://opensource.org/licenses/MIT) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -Feature engineering for financial machine learning: validated features, labeling -methods, alternative bars, and leakage-safe dataset preparation. +Feature engineering, labeling, alternative bars, and leakage-safe datasets for +financial ML. -## Part of the ML4T Library Ecosystem - -This library is one of six interconnected libraries supporting the machine -learning for trading workflow described in -[Machine Learning for Trading](https://www.ml4trading.io/): - -![ML4T Library Ecosystem](docs/images/ml4t_ecosystem_workflow_color.png) - -Together they cover data infrastructure, feature engineering, modeling, signal evaluation, strategy backtesting, and live deployment. - -## What This Library Does - -Transforming raw price data into predictive features is a core task in -quantitative research. `ml4t-engineer` provides: - -- 120 registry features across 11 categories (momentum, volatility, trend, - microstructure, and more) -- Triple-barrier, ATR-based, percentile, trend-scanning, and meta-labeling - methods from *Advances in Financial Machine Learning* -- Alternative bar sampling (volume bars, dollar bars, tick imbalance bars) -- Dataset building, preprocessing, and feature discovery for leakage-safe ML - workflows - -The library is built on Polars with Numba JIT compilation for numerical -operations. 60 features are validated against TA-Lib at `1e-6` tolerance. - -![ml4t-engineer Architecture](docs/images/ml4t_engineer_architecture_print.jpeg) +`ml4t-engineer` provides 120 registry features across 11 categories, path-dependent +and fixed-horizon labeling, activity-based bar sampling, and train-only preprocessing. +The core interface uses Polars DataFrames. ## Installation +`ml4t-engineer` supports Python 3.12, 3.13, and 3.14 on Linux, macOS, and Windows. + ```bash pip install ml4t-engineer ``` -Optional dependencies: +The quick start uses only core dependencies. Optional extras provide TA-Lib validation, +DuckDB and PyArrow storage, market calendars, visualization, statistics, and ML tools: ```bash -pip install ml4t-engineer[ta] # TA-Lib backend -pip install ml4t-engineer[viz] # Visualization -pip install ml4t-engineer[calendars] # Trading calendars +pip install "ml4t-engineer[ta]" +pip install "ml4t-engineer[store]" +pip install "ml4t-engineer[calendars]" +pip install "ml4t-engineer[viz]" +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. + ## Quick Start + ```python +from datetime import date, timedelta + import polars as pl from ml4t.engineer import compute_features -df = pl.read_parquet("ohlcv.parquet") - -# Compute features with default parameters -result = compute_features(df, ["rsi", "macd", "atr", "obv"]) - -# Or with custom parameters -result = compute_features(df, [ - {"name": "rsi", "params": {"period": 20}}, +close = [100.0 + i * 0.1 + (i % 7) * 0.2 for i in range(100)] +ohlcv = pl.DataFrame( { - "name": "bollinger_bands", - "params": {"period": 20, "nbdevup": 2.0, "nbdevdn": 2.0}, - }, -]) -``` - -## Feature Registry - -```python -from ml4t.engineer.core.registry import get_registry - -registry = get_registry() -print(registry.list_all()) # All 120 features -print(registry.list_by_category("momentum")) # 31 momentum indicators -print(registry.list_ta_lib_compatible()) # 60 TA-Lib validated features -print(registry.list_normalized()) # 37 bounded (0-100, -1 to 1) -``` - -## Feature Categories - -| Category | Count | Examples | -|----------|-------|----------| -| Momentum | 31 | RSI, MACD, Stochastic, CCI, ADX, MFI | -| Microstructure | 15 | Kyle Lambda, VPIN, Amihud, Roll spread | -| Volatility | 15 | ATR, Bollinger, Yang-Zhang, Parkinson | -| Statistics | 14 | Variance, Linear Regression, Correlation | -| ML | 14 | Fractional Diff, Entropy, Lag features | -| Trend | 10 | SMA, EMA, WMA, DEMA, TEMA, KAMA | -| Risk | 6 | Max Drawdown, Sortino, CVaR | -| Price Transform | 5 | Typical Price, Weighted Close | -| Regime | 4 | Hurst Exponent, Choppiness Index | -| Volume | 3 | OBV, AD, ADOSC | -| Math | 3 | MAX, MIN, SUM | - -## Triple-Barrier Labeling - -```python -from ml4t.engineer.config import LabelingConfig -from ml4t.engineer.labeling import triple_barrier_labels, atr_triple_barrier_labels - -# Fixed barriers -tb_config = LabelingConfig.triple_barrier( - upper_barrier=0.02, # 2% profit target - lower_barrier=0.01, # 1% stop loss - max_holding_period=20, # 20 bars -) -labels = triple_barrier_labels( - df, - config=tb_config, + "timestamp": [date(2024, 1, 1) + timedelta(days=i) for i in range(100)], + "open": close, + "high": [price + 1.0 for price in close], + "low": [price - 1.0 for price in close], + "close": close, + "volume": [100_000 + i * 100 for i in range(100)], + } ) -# ATR-based dynamic barriers -atr_config = LabelingConfig.atr_barrier( - atr_tp_multiple=2.0, - atr_sl_multiple=1.0, - atr_period=14, - max_holding_period=20, -) -labels = atr_triple_barrier_labels( - df, - config=atr_config, -) +features = compute_features(ohlcv, ["rsi", "macd", "atr"]) -# Time-based horizons -tb_time_config = LabelingConfig.triple_barrier( - upper_barrier=0.02, - lower_barrier=0.01, - max_holding_period="4h", # 4 hours -) -labels = triple_barrier_labels( - df, - config=tb_time_config, -) +assert {"rsi", "macd", "atr"} <= set(features.columns) +assert features.height == ohlcv.height ``` -## Alternative Bars +`compute_features()` returns the input columns with the requested feature columns +appended. Use the feature registry to inspect categories and parameters before building +larger pipelines. -```python -from ml4t.engineer.bars import VolumeBarSampler, DollarBarSampler, TickImbalanceBarSampler +## Supported Workflows -# Volume bars (equal volume per bar) -vbars = VolumeBarSampler(volume_per_bar=1000).sample(tick_data) +- Technical, volatility, risk, microstructure, statistical, and ML-oriented features +- Triple-barrier, ATR-barrier, percentile, fixed-horizon, trend-scanning, and meta-labels +- Tick, volume, dollar, imbalance, and run bars +- Train/test splitting with train-only scaling +- Feature metadata search and discovery -# Dollar bars (equal dollar volume per bar) -dbars = DollarBarSampler(dollars_per_bar=1_000_000).sample(tick_data) - -# Tick imbalance bars (information-driven) -ibars = TickImbalanceBarSampler(expected_ticks_per_bar=100).sample(tick_data) -``` - -## Documentation - -- [Docs Home](https://www.ml4trading.io/docs/engineer/) - library overview and - workflow map -- [Quickstart](https://www.ml4trading.io/docs/engineer/getting-started/quickstart/) - - first working feature and labeling workflow -- [Features](https://www.ml4trading.io/docs/engineer/user-guide/features/) - - 120 features across 11 registry categories -- [Labeling](https://www.ml4trading.io/docs/engineer/user-guide/labeling/) - - 7 labeling methods for supervised learning -- [Dataset Builder](https://www.ml4trading.io/docs/engineer/user-guide/dataset-builder/) - - leakage-safe train/test preparation -- [Examples](https://github.com/ml4t/engineer/blob/main/examples/README.md) - - runnable scripts for complete workflows and focused features - -## Technical Characteristics - -- **Polars-native**: All computations use Polars expressions -- **Numba-accelerated**: JIT compilation for numerical kernels -- **TA-Lib validated**: 60 features validated at `1e-6` tolerance -- **AFML-compliant**: Labeling methods verified against *Advances in Financial Machine Learning* -- **ML-ready outputs**: 37 features produce bounded outputs (0-100, -1 to 1) for direct model input; remaining features work with standard preprocessing (returns, z-scores, robust scaling) +See the [documentation](https://www.ml4trading.io/docs/engineer/) for tutorials, +task-oriented guides, explanations, and the API reference. Report defects and request +changes through [GitHub Issues](https://github.com/ml4t/engineer/issues). ## Related Libraries -- **ml4t-specs**: Shared feed and artifact schema definitions across the ML4T stack -- **ml4t-data**: Market data acquisition and storage -- **ml4t-diagnostic**: Signal evaluation and statistical validation -- **ml4t-backtest**: Event-driven backtesting -- **ml4t-live**: Live trading with broker integration +- [`ml4t-specs`](https://github.com/ml4t/specs) defines the shared market-data and + artifact contracts used by this package. +- [`ml4t-data`](https://github.com/ml4t/data) supplies validated market data for feature + computation. +- [`ml4t-diagnostic`](https://github.com/ml4t/diagnostic) evaluates features, labels, and + model signals produced from engineered datasets. ## Development ```bash git clone https://github.com/ml4t/engineer.git -cd ml4t-engineer -uv sync -uv run pytest tests/ -q +cd engineer +uv sync --dev --extra docs --extra ta --extra store --extra viz +uv run ruff check src/ tests/ examples/ scripts/ +uv run ruff format --check src/ tests/ examples/ scripts/ uv run ty check +uv run pytest tests/ -q +uv build +uv run mkdocs build --strict ``` -## References - -- Lopez de Prado, M. (2018). *Advances in Financial Machine Learning*. Wiley. -- Lopez de Prado, M. (2020). *Machine Learning for Asset Managers*. Cambridge. +Pull requests must also pass the supported Python and operating-system matrix, +dependency and vulnerability review, clean-wheel installation, documented workflow +tests, and ecosystem qualification. -## License +## Project Information -MIT License - see [LICENSE](LICENSE) for details. +- [Documentation](https://www.ml4trading.io/docs/engineer/) +- [Issue tracker](https://github.com/ml4t/engineer/issues) +- [Releases and changelog](https://github.com/ml4t/engineer/releases) +- [License](LICENSE) diff --git a/api.yaml b/api.yaml index abd00b1..4bac3ec 100644 --- a/api.yaml +++ b/api.yaml @@ -1,7 +1,7 @@ name: ml4t-engineer version: 1.0.0 python_requires: ">=3.12,<3.15" -description: High-performance feature engineering for financial machine learning +description: Feature engineering, labeling, alternative bars, and leakage-safe datasets for financial ML. modules: - path: ml4t.engineer diff --git a/docs/index.md b/docs/index.md index 4ff66ea..fd54e0a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ # ML4T Engineer -Turn OHLCV and tick data into model-ready features, labels, and sampling schemes -without rewriting the same research code in every notebook and pipeline. +Feature engineering, labeling, alternative bars, and leakage-safe datasets for +financial ML. `ml4t-engineer` is the feature-engineering layer in the ML4T stack. It sits between `ml4t-data`, which prepares canonical datasets, and `ml4t-diagnostic`, which diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 1fd85f1..9db7922 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -2,6 +2,9 @@ {% block extrahead %} {{ super() }} + + +
{{ config.site_name }} - is part of the ML4T documentation suite. + + version {{ config.extra.release_version }} at + {{ config.extra.release_commit[:12] }}. +