Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
682bf063ca |
+38
-325
@@ -10,43 +10,8 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pages: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
# Dependency audits — cargo audit (Rust) and pip-audit (Python)
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
audit:
|
|
||||||
name: Dependency audit (cargo + pip)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Install cargo-audit and run cargo audit
|
|
||||||
run: |
|
|
||||||
cargo install cargo-audit
|
|
||||||
cargo audit
|
|
||||||
|
|
||||||
- name: Install cargo-deny and run cargo deny check
|
|
||||||
run: |
|
|
||||||
cargo install cargo-deny --locked
|
|
||||||
cargo deny check
|
|
||||||
|
|
||||||
- name: Set up Python 3.12
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
run: pip install uv
|
|
||||||
|
|
||||||
- name: Install pip-audit via uv and run pip-audit
|
|
||||||
run: |
|
|
||||||
uv run --with pip-audit pip-audit
|
|
||||||
|
|
||||||
- name: Verify uv.lock is up-to-date
|
|
||||||
run: uv lock --check
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Version consistency — Cargo.toml and pyproject.toml must have same version
|
# Version consistency — Cargo.toml and pyproject.toml must have same version
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
@@ -76,285 +41,28 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "OK: versions match ($CARGO_VERSION)"
|
echo "OK: versions match ($CARGO_VERSION)"
|
||||||
|
|
||||||
rust:
|
|
||||||
name: Rust (fmt + clippy)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: dtolnay/rust-toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
components: rustfmt, clippy
|
|
||||||
- run: cargo fmt --all -- --check
|
|
||||||
- run: cargo clippy --release -- -D warnings
|
|
||||||
- name: Verify benchmarks compile (ferro_ta_core)
|
|
||||||
run: cargo bench -p ferro_ta_core --no-run
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Rust coverage — optional, runs in a separate non-blocking job
|
# Modularized CI suites.
|
||||||
|
# Keep release publishing jobs in this file so trusted-publisher bindings
|
||||||
|
# remain anchored to CI.yml for PyPI and crates.io.
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
rust-coverage:
|
rust-suite:
|
||||||
name: Rust coverage (tarpaulin, optional)
|
name: Rust and audit
|
||||||
runs-on: ubuntu-latest
|
uses: ./.github/workflows/ci-rust.yml
|
||||||
continue-on-error: true
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: dtolnay/rust-toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
- name: Install cargo-tarpaulin
|
|
||||||
run: cargo install cargo-tarpaulin --locked
|
|
||||||
- name: Collect Rust coverage (ferro_ta_core)
|
|
||||||
run: cargo tarpaulin -p ferro_ta_core --out Xml --output-dir coverage/
|
|
||||||
- name: Upload Rust coverage artifact
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: rust-coverage
|
|
||||||
path: coverage/
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
lint:
|
python-suite:
|
||||||
name: Lint (ruff)
|
name: Python quality and tests
|
||||||
runs-on: ubuntu-latest
|
uses: ./.github/workflows/ci-python.yml
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Set up Python 3.12
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
run: pip install uv
|
|
||||||
|
|
||||||
- name: Run ruff check via uv
|
|
||||||
run: uv run --with ruff ruff check python/ tests/
|
|
||||||
- name: Run ruff format check via uv
|
|
||||||
run: uv run --with ruff ruff format --check python/ tests/
|
|
||||||
|
|
||||||
typecheck:
|
|
||||||
name: Type checking (mypy + pyright)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Set up Python 3.12
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
run: pip install uv
|
|
||||||
|
|
||||||
- name: Run mypy on ferro_ta via uv
|
|
||||||
run: uv run --with mypy --with numpy mypy python/ferro_ta --ignore-missing-imports --no-error-summary
|
|
||||||
|
|
||||||
- name: Run pyright on ferro_ta via uv
|
|
||||||
run: uv run --with pyright pyright python/ferro_ta
|
|
||||||
|
|
||||||
test:
|
|
||||||
name: Test (ubuntu-latest / Python ${{ matrix.python-version }})
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
# Python 3.10–3.13 supported (requires-python in pyproject.toml)
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
- name: Install maturin and test dependencies
|
|
||||||
run: |
|
|
||||||
pip install maturin numpy pytest pytest-cov pandas polars hypothesis pyyaml
|
|
||||||
|
|
||||||
- name: Build and install ferro_ta (dev mode)
|
|
||||||
run: |
|
|
||||||
maturin build --release --out dist
|
|
||||||
pip install dist/*.whl
|
|
||||||
|
|
||||||
- name: Run unit tests with coverage
|
|
||||||
run: pytest tests/unit/ tests/integration/ -v --cov=ferro_ta --cov-report=xml --cov-report=term-missing --cov-fail-under=65
|
|
||||||
|
|
||||||
- name: Upload coverage report
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
if: matrix.python-version == '3.12'
|
|
||||||
with:
|
|
||||||
name: coverage-python-${{ matrix.python-version }}
|
|
||||||
path: coverage.xml
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
# WASM binding — build and test with wasm-pack
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
wasm:
|
wasm:
|
||||||
name: WASM binding (wasm-pack test --node)
|
name: WASM binding
|
||||||
runs-on: ubuntu-latest
|
uses: ./.github/workflows/ci-wasm.yml
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Install Rust (stable)
|
|
||||||
uses: dtolnay/rust-toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
targets: wasm32-unknown-unknown
|
|
||||||
|
|
||||||
- name: Install wasm-pack
|
|
||||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
|
||||||
|
|
||||||
- name: Build and test WASM binding
|
|
||||||
working-directory: wasm
|
|
||||||
run: wasm-pack test --node
|
|
||||||
|
|
||||||
- name: Build WASM package (nodejs target)
|
|
||||||
working-directory: wasm
|
|
||||||
run: wasm-pack build --target nodejs --out-dir pkg
|
|
||||||
|
|
||||||
- name: Upload WASM package artifact
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: wasm-pkg
|
|
||||||
path: wasm/pkg/
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
# ferro_ta vs TA-Lib speed comparison — required for PRs.
|
|
||||||
# Performance regressions block merging.
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
benchmark-vs-talib:
|
|
||||||
name: Benchmark vs TA-Lib
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Set up Python 3.12
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
|
|
||||||
- name: Install TA-Lib C library (Ubuntu)
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y build-essential curl
|
|
||||||
curl -sL https://sourceforge.net/projects/ta-lib/files/ta-lib/0.4.0/ta-lib-0.4.0-src.tar.gz/download -o ta-lib-0.4.0-src.tar.gz
|
|
||||||
tar -xzf ta-lib-0.4.0-src.tar.gz
|
|
||||||
cd ta-lib
|
|
||||||
./configure --prefix=/usr
|
|
||||||
make
|
|
||||||
sudo make install
|
|
||||||
sudo ldconfig
|
|
||||||
|
|
||||||
- name: Install maturin and ta-lib Python package
|
|
||||||
run: |
|
|
||||||
pip install maturin numpy
|
|
||||||
pip install ta-lib
|
|
||||||
|
|
||||||
- name: Build and install ferro_ta
|
|
||||||
run: |
|
|
||||||
maturin build --release --out dist
|
|
||||||
pip install dist/*.whl
|
|
||||||
|
|
||||||
- name: Run benchmark comparison
|
|
||||||
run: |
|
|
||||||
python benchmarks/bench_vs_talib.py --sizes 10000 100000 --json benchmark_vs_talib.json
|
|
||||||
|
|
||||||
- name: Enforce benchmark regression policy
|
|
||||||
run: |
|
|
||||||
python3 benchmarks/check_vs_talib_regression.py --input benchmark_vs_talib.json
|
|
||||||
|
|
||||||
- name: Upload benchmark results
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: benchmark-vs-talib
|
|
||||||
path: benchmark_vs_talib.json
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
# Pure Rust core library — build and test without PyO3 / numpy
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
rust-core:
|
|
||||||
name: Rust core library (ferro_ta_core)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: dtolnay/rust-toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
- name: Build core crate
|
|
||||||
run: cargo build -p ferro_ta_core
|
|
||||||
- name: Test core crate
|
|
||||||
run: cargo test -p ferro_ta_core
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
# Fuzzing (short run in CI to catch panics) — optional, non-blocking
|
|
||||||
# This job is explicitly marked continue-on-error because cargo-fuzz
|
|
||||||
# requires nightly Rust and may not be available in all environments.
|
|
||||||
# Failures here are visible in the job log but do not block the PR.
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
fuzz:
|
|
||||||
name: Fuzz targets (short CI run, optional)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
continue-on-error: true
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: dtolnay/rust-toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: nightly
|
|
||||||
- name: Install cargo-fuzz
|
|
||||||
run: cargo install cargo-fuzz --locked
|
|
||||||
- name: Run fuzz_sma (10000 iterations)
|
|
||||||
working-directory: fuzz
|
|
||||||
run: cargo fuzz run fuzz_sma -- -runs=10000 -max_len=512
|
|
||||||
- name: Run fuzz_rsi (10000 iterations)
|
|
||||||
working-directory: fuzz
|
|
||||||
run: cargo fuzz run fuzz_rsi -- -runs=10000 -max_len=512
|
|
||||||
- name: Upload fuzz artifacts on crash
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: fuzz-artifacts
|
|
||||||
path: fuzz/artifacts/
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
# Sphinx documentation build
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
docs:
|
docs:
|
||||||
name: Documentation (Sphinx build)
|
name: Documentation
|
||||||
runs-on: ubuntu-latest
|
uses: ./.github/workflows/ci-docs.yml
|
||||||
steps:
|
with:
|
||||||
- uses: actions/checkout@v6
|
upload-pages-artifact: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Set up Python 3.12
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
|
|
||||||
- name: Install maturin and docs dependencies
|
|
||||||
run: |
|
|
||||||
pip install maturin numpy
|
|
||||||
pip install sphinx sphinx-rtd-theme
|
|
||||||
|
|
||||||
- name: Build and install ferro_ta wheel
|
|
||||||
run: |
|
|
||||||
maturin build --release --out dist
|
|
||||||
pip install dist/*.whl
|
|
||||||
|
|
||||||
- name: Build Sphinx documentation
|
|
||||||
run: sphinx-build -b html docs docs/_build -W --keep-going
|
|
||||||
|
|
||||||
- name: Upload docs artifact
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: sphinx-docs
|
|
||||||
path: docs/_build/
|
|
||||||
|
|
||||||
- name: Upload GitHub Pages artifact
|
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
||||||
uses: actions/upload-pages-artifact@v4
|
|
||||||
with:
|
|
||||||
path: docs/_build/
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Deploy docs to GitHub Pages (on push to main only)
|
# Deploy docs to GitHub Pages (on push to main only)
|
||||||
@@ -384,16 +92,11 @@ jobs:
|
|||||||
name: CI complete (required gate)
|
name: CI complete (required gate)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- audit
|
|
||||||
- changelog-check
|
- changelog-check
|
||||||
- version-check
|
- version-check
|
||||||
- rust
|
- rust-suite
|
||||||
- rust-core
|
- python-suite
|
||||||
- lint
|
|
||||||
- typecheck
|
|
||||||
- test
|
|
||||||
- wasm
|
- wasm
|
||||||
- benchmark-vs-talib
|
|
||||||
- docs
|
- docs
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
@@ -401,21 +104,31 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
results='${{ toJSON(needs) }}'
|
results='${{ toJSON(needs) }}'
|
||||||
echo "Job results: $results"
|
echo "Job results: $results"
|
||||||
failed=$(echo "$results" | python3 -c "
|
failed="$(
|
||||||
import json, sys
|
RESULTS_JSON="$results" python3 - <<'PY'
|
||||||
needs = json.load(sys.stdin)
|
import json
|
||||||
failed = [name for name, data in needs.items() if data['result'] not in ('success', 'skipped')]
|
import os
|
||||||
|
|
||||||
|
needs = json.loads(os.environ["RESULTS_JSON"])
|
||||||
|
failed = [
|
||||||
|
name
|
||||||
|
for name, data in needs.items()
|
||||||
|
if data["result"] not in ("success", "skipped")
|
||||||
|
]
|
||||||
if failed:
|
if failed:
|
||||||
print(' '.join(failed))
|
print(" ".join(failed))
|
||||||
")
|
PY
|
||||||
if [ -n \"\$failed\" ]; then
|
)"
|
||||||
echo \"FAILED jobs: \$failed\"
|
if [ -n "$failed" ]; then
|
||||||
|
echo "FAILED jobs: $failed"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo \"All required CI jobs passed.\"
|
echo "All required CI jobs passed."
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Build wheels for all platforms and publish to PyPI on release
|
# Build wheels for all platforms and publish to PyPI on release
|
||||||
|
# Keep release jobs here because trusted-publisher configuration points to
|
||||||
|
# CI.yml specifically.
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
build-wheels:
|
build-wheels:
|
||||||
name: Build wheels (${{ matrix.os }})
|
name: Build wheels (${{ matrix.os }})
|
||||||
@@ -503,7 +216,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
@@ -516,7 +229,7 @@ jobs:
|
|||||||
pip install dist/*.whl
|
pip install dist/*.whl
|
||||||
|
|
||||||
- name: Generate Python SBOM (CycloneDX via anchore/sbom-action)
|
- name: Generate Python SBOM (CycloneDX via anchore/sbom-action)
|
||||||
uses: anchore/sbom-action@v0.17
|
uses: anchore/sbom-action@v0.24.0
|
||||||
with:
|
with:
|
||||||
artifact-name: ferro-ta-python-sbom.spdx.json
|
artifact-name: ferro-ta-python-sbom.spdx.json
|
||||||
output-file: ferro-ta-python-sbom.spdx.json
|
output-file: ferro-ta-python-sbom.spdx.json
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: CI Docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
upload-pages-artifact:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docs:
|
||||||
|
name: Documentation (Sphinx build)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Python 3.12
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install maturin and docs dependencies
|
||||||
|
run: |
|
||||||
|
pip install maturin numpy
|
||||||
|
pip install sphinx sphinx-rtd-theme
|
||||||
|
|
||||||
|
- name: Build and install ferro_ta wheel
|
||||||
|
run: |
|
||||||
|
maturin build --release --out dist
|
||||||
|
pip install dist/*.whl
|
||||||
|
|
||||||
|
- name: Build Sphinx documentation
|
||||||
|
run: sphinx-build -b html docs docs/_build -W --keep-going
|
||||||
|
|
||||||
|
- name: Upload docs artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: sphinx-docs
|
||||||
|
path: docs/_build/
|
||||||
|
|
||||||
|
- name: Upload GitHub Pages artifact
|
||||||
|
if: ${{ inputs.upload-pages-artifact }}
|
||||||
|
uses: actions/upload-pages-artifact@v4
|
||||||
|
with:
|
||||||
|
path: docs/_build/
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
name: CI Python
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint (ruff)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Python 3.12
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
run: pip install uv
|
||||||
|
|
||||||
|
- name: Run ruff check via uv
|
||||||
|
run: uv run --with ruff ruff check python/ tests/
|
||||||
|
- name: Run ruff format check via uv
|
||||||
|
run: uv run --with ruff ruff format --check python/ tests/
|
||||||
|
|
||||||
|
typecheck:
|
||||||
|
name: Type checking (mypy + pyright)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Python 3.12
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
run: pip install uv
|
||||||
|
|
||||||
|
- name: Run mypy on ferro_ta via uv
|
||||||
|
run: uv run --with mypy --with numpy mypy python/ferro_ta --ignore-missing-imports --no-error-summary
|
||||||
|
|
||||||
|
- name: Run pyright on ferro_ta via uv
|
||||||
|
run: uv run --with pyright pyright python/ferro_ta
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Test (ubuntu-latest / Python ${{ matrix.python-version }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install maturin and test dependencies
|
||||||
|
run: |
|
||||||
|
pip install maturin numpy pytest pytest-cov pandas polars hypothesis pyyaml
|
||||||
|
|
||||||
|
- name: Build and install ferro_ta (dev mode)
|
||||||
|
run: |
|
||||||
|
maturin build --release --out dist
|
||||||
|
pip install dist/*.whl
|
||||||
|
|
||||||
|
- name: Run unit tests with coverage
|
||||||
|
run: pytest tests/unit/ tests/integration/ -v --cov=ferro_ta --cov-report=xml --cov-report=term-missing --cov-fail-under=65
|
||||||
|
|
||||||
|
- name: Upload coverage report
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
if: matrix.python-version == '3.12'
|
||||||
|
with:
|
||||||
|
name: coverage-python-${{ matrix.python-version }}
|
||||||
|
path: coverage.xml
|
||||||
|
|
||||||
|
benchmark-vs-talib:
|
||||||
|
name: Benchmark vs TA-Lib
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Python 3.12
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install TA-Lib C library (Ubuntu)
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y build-essential curl
|
||||||
|
curl -sL https://sourceforge.net/projects/ta-lib/files/ta-lib/0.4.0/ta-lib-0.4.0-src.tar.gz/download -o ta-lib-0.4.0-src.tar.gz
|
||||||
|
tar -xzf ta-lib-0.4.0-src.tar.gz
|
||||||
|
cd ta-lib
|
||||||
|
./configure --prefix=/usr
|
||||||
|
make
|
||||||
|
sudo make install
|
||||||
|
sudo ldconfig
|
||||||
|
|
||||||
|
- name: Install maturin and ta-lib Python package
|
||||||
|
run: |
|
||||||
|
pip install maturin numpy
|
||||||
|
pip install ta-lib
|
||||||
|
|
||||||
|
- name: Build and install ferro_ta
|
||||||
|
run: |
|
||||||
|
maturin build --release --out dist
|
||||||
|
pip install dist/*.whl
|
||||||
|
|
||||||
|
- name: Run benchmark comparison
|
||||||
|
run: |
|
||||||
|
python benchmarks/bench_vs_talib.py --sizes 10000 100000 --json benchmark_vs_talib.json
|
||||||
|
|
||||||
|
- name: Enforce benchmark regression policy
|
||||||
|
run: python3 benchmarks/check_vs_talib_regression.py --input benchmark_vs_talib.json
|
||||||
|
|
||||||
|
- name: Upload benchmark results
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: benchmark-vs-talib
|
||||||
|
path: benchmark_vs_talib.json
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
name: CI Rust
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
audit:
|
||||||
|
name: Dependency audit (cargo + pip)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install cargo-audit and run cargo audit
|
||||||
|
run: |
|
||||||
|
cargo install cargo-audit
|
||||||
|
cargo audit
|
||||||
|
|
||||||
|
- name: Install cargo-deny and run cargo deny check
|
||||||
|
run: |
|
||||||
|
cargo install cargo-deny --locked
|
||||||
|
cargo deny check
|
||||||
|
|
||||||
|
- name: Set up Python 3.12
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
run: pip install uv
|
||||||
|
|
||||||
|
- name: Install pip-audit via uv and run pip-audit
|
||||||
|
run: uv run --with pip-audit pip-audit
|
||||||
|
|
||||||
|
- name: Verify uv.lock is up-to-date
|
||||||
|
run: uv lock --check
|
||||||
|
|
||||||
|
rust:
|
||||||
|
name: Rust (fmt + clippy)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
components: rustfmt, clippy
|
||||||
|
- run: cargo fmt --all -- --check
|
||||||
|
- run: cargo clippy --release -- -D warnings
|
||||||
|
- name: Verify benchmarks compile (ferro_ta_core)
|
||||||
|
run: cargo bench -p ferro_ta_core --no-run
|
||||||
|
|
||||||
|
rust-core:
|
||||||
|
name: Rust core library (ferro_ta_core)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
- name: Build core crate
|
||||||
|
run: cargo build -p ferro_ta_core
|
||||||
|
- name: Test core crate
|
||||||
|
run: cargo test -p ferro_ta_core
|
||||||
|
|
||||||
|
rust-coverage:
|
||||||
|
name: Rust coverage (tarpaulin, optional)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
- name: Install cargo-tarpaulin
|
||||||
|
run: cargo install cargo-tarpaulin --locked
|
||||||
|
- name: Collect Rust coverage (ferro_ta_core)
|
||||||
|
run: cargo tarpaulin -p ferro_ta_core --out Xml --output-dir coverage/
|
||||||
|
- name: Upload Rust coverage artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: rust-coverage
|
||||||
|
path: coverage/
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
fuzz:
|
||||||
|
name: Fuzz targets (short CI run, optional)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
- name: Install cargo-fuzz
|
||||||
|
run: cargo install cargo-fuzz --locked
|
||||||
|
- name: Run fuzz_sma (10000 iterations)
|
||||||
|
working-directory: fuzz
|
||||||
|
run: cargo fuzz run fuzz_sma -- -runs=10000 -max_len=512
|
||||||
|
- name: Run fuzz_rsi (10000 iterations)
|
||||||
|
working-directory: fuzz
|
||||||
|
run: cargo fuzz run fuzz_rsi -- -runs=10000 -max_len=512
|
||||||
|
- name: Upload fuzz artifacts on crash
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: fuzz-artifacts
|
||||||
|
path: fuzz/artifacts/
|
||||||
|
if-no-files-found: ignore
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
name: CI WASM
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
wasm:
|
||||||
|
name: WASM binding (wasm-pack test --node)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Rust (stable)
|
||||||
|
uses: dtolnay/rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
targets: wasm32-unknown-unknown
|
||||||
|
|
||||||
|
- name: Install wasm-pack
|
||||||
|
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||||
|
|
||||||
|
- name: Build and test WASM binding
|
||||||
|
working-directory: wasm
|
||||||
|
run: wasm-pack test --node
|
||||||
|
|
||||||
|
- name: Build WASM package (nodejs target)
|
||||||
|
working-directory: wasm
|
||||||
|
run: wasm-pack build --target nodejs --out-dir pkg
|
||||||
|
|
||||||
|
- name: Upload WASM package artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: wasm-pkg
|
||||||
|
path: wasm/pkg/
|
||||||
@@ -4,6 +4,10 @@ on:
|
|||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
name: Build and publish to npm
|
name: Build and publish to npm
|
||||||
@@ -12,13 +16,15 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "24"
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
- name: Install Rust and wasm-pack
|
- name: Install Rust and wasm-pack
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: wasm32-unknown-unknown
|
||||||
- run: cargo install wasm-pack
|
- run: cargo install wasm-pack
|
||||||
|
|
||||||
- name: Build WASM package
|
- name: Build WASM package
|
||||||
@@ -29,5 +35,3 @@ jobs:
|
|||||||
- name: Publish to npm
|
- name: Publish to npm
|
||||||
working-directory: wasm
|
working-directory: wasm
|
||||||
run: npm publish --access public
|
run: npm publish --access public
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
|
|||||||
+24
-1
@@ -9,6 +9,28 @@ and the project uses [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.0.1] — 2026-03-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `crates/ferro_ta_core/README.md` is now shipped with the published Rust crate, and
|
||||||
|
`ferro_ta_core` metadata now points documentation to docs.rs.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- CI has been modularized into focused workflow files (`ci-rust.yml`,
|
||||||
|
`ci-python.yml`, `ci-wasm.yml`, `ci-docs.yml`) while keeping the release
|
||||||
|
publishing jobs in `CI.yml` for PyPI and crates.io trusted-publisher
|
||||||
|
compatibility.
|
||||||
|
- The `ci-complete` gate no longer fails successful runs because of an escaped
|
||||||
|
shell variable, and the release SBOM job now uses a valid `anchore/sbom-action`
|
||||||
|
version.
|
||||||
|
- The npm publish workflow now uses GitHub OIDC trusted publishing, installs
|
||||||
|
the `wasm32-unknown-unknown` target, and no longer depends on an `NPM_TOKEN`
|
||||||
|
secret.
|
||||||
|
- The WASM npm package now removes the generated `pkg/.gitignore` during
|
||||||
|
`prepack`, so the published tarball includes the built `pkg/` artifacts.
|
||||||
|
|
||||||
## [1.0.0] — 2026-03-23 *(initial stable release)*
|
## [1.0.0] — 2026-03-23 *(initial stable release)*
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
@@ -252,5 +274,6 @@ and the project uses [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[Unreleased]: https://github.com/pratikbhadane24/ferro-ta/compare/v1.0.0...HEAD
|
[Unreleased]: https://github.com/pratikbhadane24/ferro-ta/compare/v1.0.1...HEAD
|
||||||
|
[1.0.1]: https://github.com/pratikbhadane24/ferro-ta/compare/v1.0.0...v1.0.1
|
||||||
[1.0.0]: https://github.com/pratikbhadane24/ferro-ta/releases/tag/v1.0.0
|
[1.0.0]: https://github.com/pratikbhadane24/ferro-ta/releases/tag/v1.0.0
|
||||||
|
|||||||
Generated
+2
-2
@@ -207,7 +207,7 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ferro_ta"
|
name = "ferro_ta"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"criterion",
|
"criterion",
|
||||||
"ferro_ta_core",
|
"ferro_ta_core",
|
||||||
@@ -222,7 +222,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ferro_ta_core"
|
name = "ferro_ta_core"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"criterion",
|
"criterion",
|
||||||
"wide",
|
"wide",
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@ resolver = "2"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ferro_ta"
|
name = "ferro_ta"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
publish = false
|
publish = false
|
||||||
@@ -23,7 +23,7 @@ ndarray = "0.16"
|
|||||||
rayon = "1.10"
|
rayon = "1.10"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
pyo3-log = "0.12"
|
pyo3-log = "0.12"
|
||||||
ferro_ta_core = { path = "crates/ferro_ta_core", version = "1.0.0" }
|
ferro_ta_core = { path = "crates/ferro_ta_core", version = "1.0.1" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { version = "0.8", features = ["html_reports"] }
|
criterion = { version = "0.8", features = ["html_reports"] }
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ferro_ta_core"
|
name = "ferro_ta_core"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Pure Rust core indicator library — no PyO3, no numpy dependency"
|
description = "Pure Rust core indicator library — no PyO3, no numpy dependency"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
readme = "README.md"
|
||||||
repository = "https://github.com/pratikbhadane24/ferro-ta"
|
repository = "https://github.com/pratikbhadane24/ferro-ta"
|
||||||
homepage = "https://github.com/pratikbhadane24/ferro-ta#readme"
|
homepage = "https://github.com/pratikbhadane24/ferro-ta#readme"
|
||||||
documentation = "https://github.com/pratikbhadane24/ferro-ta#readme"
|
documentation = "https://docs.rs/ferro_ta_core"
|
||||||
keywords = ["technical-analysis", "trading", "indicators", "finance", "ta-lib"]
|
keywords = ["technical-analysis", "trading", "indicators", "finance", "ta-lib"]
|
||||||
categories = ["finance", "mathematics"]
|
categories = ["finance", "mathematics"]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# ferro_ta_core
|
||||||
|
|
||||||
|
`ferro_ta_core` is the pure Rust indicator engine behind [`ferro-ta`](https://github.com/pratikbhadane24/ferro-ta).
|
||||||
|
|
||||||
|
It provides allocation-friendly indicator functions over `&[f64]` slices without any
|
||||||
|
PyO3, NumPy, or Python runtime dependency, which makes it a good fit for:
|
||||||
|
|
||||||
|
- Rust-native technical analysis workloads
|
||||||
|
- custom services and backtesting engines
|
||||||
|
- future non-Python bindings such as WASM and other FFI layers
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
ferro_ta_core = "1.0.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
- Pure functions over Rust slices
|
||||||
|
- No Python or NumPy dependency
|
||||||
|
- Shared core for the Python package and WASM bindings
|
||||||
|
- Output shape matches TA-Lib-style full-length series with `NaN` warm-up values where applicable
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
- `overlap` - moving averages, MACD, Bollinger Bands
|
||||||
|
- `momentum` - RSI, MOM
|
||||||
|
- `volatility` - ATR, TRANGE
|
||||||
|
- `volume` - OBV
|
||||||
|
- `statistic` - STDDEV
|
||||||
|
- `math` - rolling SUM/MAX/MIN helpers
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use ferro_ta_core::overlap;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let close = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
||||||
|
let sma = overlap::sma(&close, 3);
|
||||||
|
|
||||||
|
assert!(sma[0].is_nan());
|
||||||
|
assert!(sma[1].is_nan());
|
||||||
|
assert!((sma[2] - 2.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Relationship To `ferro-ta`
|
||||||
|
|
||||||
|
The published Python package:
|
||||||
|
|
||||||
|
- crate: `ferro_ta`
|
||||||
|
- PyPI package: `ferro-ta`
|
||||||
|
|
||||||
|
wraps this crate with PyO3 bindings and adds:
|
||||||
|
|
||||||
|
- NumPy conversion
|
||||||
|
- pandas/polars wrappers
|
||||||
|
- streaming classes
|
||||||
|
- batch helpers
|
||||||
|
- higher-level Python tooling
|
||||||
|
|
||||||
|
If you only need Rust indicator functions, use `ferro_ta_core` directly.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
From the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build -p ferro_ta_core
|
||||||
|
cargo test -p ferro_ta_core
|
||||||
|
cargo bench -p ferro_ta_core --no-run
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "ferro-ta"
|
name = "ferro-ta"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
description = "A fast Technical Analysis library — TA-Lib alternative powered by Rust and PyO3"
|
description = "A fast Technical Analysis library — TA-Lib alternative powered by Rust and PyO3"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
|
|||||||
+2
-1
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "ferro-ta-wasm",
|
"name": "ferro-ta-wasm",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "WebAssembly bindings for ferro-ta technical analysis indicators",
|
"description": "WebAssembly bindings for ferro-ta technical analysis indicators",
|
||||||
"main": "pkg/ferro_ta_wasm.js",
|
"main": "pkg/ferro_ta_wasm.js",
|
||||||
"types": "pkg/ferro_ta_wasm.d.ts",
|
"types": "pkg/ferro_ta_wasm.d.ts",
|
||||||
"files": ["pkg"],
|
"files": ["pkg"],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "wasm-pack build --target nodejs --out-dir pkg",
|
"build": "wasm-pack build --target nodejs --out-dir pkg",
|
||||||
|
"prepack": "npm run build && node -e \"require('fs').rmSync('pkg/.gitignore', { force: true })\"",
|
||||||
"test": "wasm-pack test --node"
|
"test": "wasm-pack test --node"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
Reference in New Issue
Block a user