release: cut v1.0.0
Prepare the first public 1.0.0 release and finish the remaining CI hardening work. Highlights: - align Python, Rust, WASM, Conda, API, MCP, and docs version metadata to 1.0.0 - promote package metadata to Production/Stable and update stability/versioning docs for the stable series - move the accumulated Unreleased notes into a dated 1.0.0 changelog section and keep a fresh top-level Unreleased block - strengthen the changelog checker so it validates a single top-level Unreleased section - fix the CI/package support mismatch by declaring Python >=3.10 consistently and gating pandas-ta extras to Python 3.12+ - restore Sphinx autodoc compatibility for documented ferro_ta.<module> imports by registering module aliases - make the TA-Lib benchmark guardrail less flaky by checking median and tail-percentile speedups instead of failing on a single mild outlier - switch PyPI publishing to OIDC-only trusted publishing and wire the changelog check into the required CI gate - apply the Ruff-driven cleanup across the Python and test tree and refresh uv/cargo lockfiles Validated locally: - python3 scripts/check_changelog.py - uv run --with ruff ruff check python tests - uv run --with ruff ruff format --check python tests - uv lock --check - sphinx-build -b html docs docs/_build -W --keep-going - build/install the ferro_ta 1.0.0 wheel successfully
This commit is contained in:
@@ -11,7 +11,6 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pages: write
|
pages: write
|
||||||
id-token: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
@@ -386,6 +385,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- audit
|
- audit
|
||||||
|
- changelog-check
|
||||||
- version-check
|
- version-check
|
||||||
- rust
|
- rust
|
||||||
- rust-core
|
- rust-core
|
||||||
@@ -452,6 +452,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build-wheels
|
needs: build-wheels
|
||||||
if: github.event_name == 'release' && github.event.action == 'published'
|
if: github.event_name == 'release' && github.event.action == 'published'
|
||||||
|
environment:
|
||||||
|
name: pypi
|
||||||
|
url: https://pypi.org/p/ferro-ta
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
@@ -464,10 +467,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish to PyPI
|
- name: Publish to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
|
||||||
# Use token if set; otherwise the action uses OIDC trusted publishing
|
|
||||||
username: __token__
|
|
||||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Publish ferro_ta_core to crates.io (requires CARGO_REGISTRY_TOKEN secret)
|
# Publish ferro_ta_core to crates.io (requires CARGO_REGISTRY_TOKEN secret)
|
||||||
|
|||||||
+4
-22
@@ -9,6 +9,8 @@ and the project uses [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.0.0] — 2026-03-23 *(initial stable release)*
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
|
|
||||||
- **SMA/EMA** (`src/overlap/sma.rs`, `src/overlap/ema.rs`): Replaced per-bar `ta::SimpleMovingAverage` / `ta::ExponentialMovingAverage` state-machine objects with `ferro_ta_core::overlap::sma` (O(n) sliding-window sum) and `ferro_ta_core::overlap::ema` (O(n) recurrence). SMA/EMA now run at **200–600 M bars/s** on 1 M input.
|
- **SMA/EMA** (`src/overlap/sma.rs`, `src/overlap/ema.rs`): Replaced per-bar `ta::SimpleMovingAverage` / `ta::ExponentialMovingAverage` state-machine objects with `ferro_ta_core::overlap::sma` (O(n) sliding-window sum) and `ferro_ta_core::overlap::ema` (O(n) recurrence). SMA/EMA now run at **200–600 M bars/s** on 1 M input.
|
||||||
@@ -250,25 +252,5 @@ and the project uses [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [0.1.0] — 2025-xx-xx *(initial release)*
|
[Unreleased]: https://github.com/pratikbhadane24/ferro-ta/compare/v1.0.0...HEAD
|
||||||
|
[1.0.0]: https://github.com/pratikbhadane24/ferro-ta/releases/tag/v1.0.0
|
||||||
### Added
|
|
||||||
- Rust + PyO3 core with 155+ TA-Lib-compatible indicators.
|
|
||||||
- Overlap studies (SMA, EMA, BBANDS, MACD, …).
|
|
||||||
- Momentum indicators (RSI, STOCH, ADX, CCI, …).
|
|
||||||
- Volume indicators (AD, ADOSC, OBV).
|
|
||||||
- Volatility indicators (ATR, NATR, TRANGE).
|
|
||||||
- Statistic functions (STDDEV, VAR, LINEARREG, BETA, CORREL).
|
|
||||||
- Price transforms (AVGPRICE, MEDPRICE, TYPPRICE, WCLPRICE).
|
|
||||||
- 61 candlestick pattern recognition functions.
|
|
||||||
- Cycle indicators (HT_TRENDLINE, HT_DCPERIOD, HT_DCPHASE, HT_PHASOR, HT_SINE, HT_TRENDMODE).
|
|
||||||
- Math operators and transforms (ADD, SUB, SUM, MAX, ACOS, SIN, …).
|
|
||||||
- Extended indicators (VWAP, SUPERTREND, ICHIMOKU, DONCHIAN, PIVOT_POINTS).
|
|
||||||
- Streaming / incremental API for live trading (StreamingSMA, StreamingRSI, …).
|
|
||||||
- Transparent pandas Series / DataFrame support.
|
|
||||||
- Type stubs (`.pyi`) for IDE auto-completion.
|
|
||||||
- Sphinx documentation in `docs/`.
|
|
||||||
- Pre-compiled manylinux wheels for Linux, Windows, macOS (Intel & Apple Silicon).
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/pratikbhadane24/ferro-ta/compare/v0.1.0...HEAD
|
|
||||||
[0.1.0]: https://github.com/pratikbhadane24/ferro-ta/releases/tag/v0.1.0
|
|
||||||
|
|||||||
Generated
+28
-28
@@ -28,15 +28,15 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle"
|
name = "anstyle"
|
||||||
version = "1.0.13"
|
version = "1.0.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arc-swap"
|
name = "arc-swap"
|
||||||
version = "1.8.2"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5"
|
checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustversion",
|
"rustversion",
|
||||||
]
|
]
|
||||||
@@ -67,9 +67,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.56"
|
version = "1.2.57"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
@@ -110,18 +110,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.60"
|
version = "4.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
|
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.60"
|
version = "4.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
|
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"clap_lex",
|
"clap_lex",
|
||||||
@@ -129,9 +129,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_lex"
|
name = "clap_lex"
|
||||||
version = "1.0.0"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "criterion"
|
name = "criterion"
|
||||||
@@ -207,7 +207,7 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ferro_ta"
|
name = "ferro_ta"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
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 = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"criterion",
|
"criterion",
|
||||||
"wide",
|
"wide",
|
||||||
@@ -271,9 +271,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.17"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
@@ -287,9 +287,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.182"
|
version = "0.2.183"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
@@ -382,9 +382,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oorandom"
|
name = "oorandom"
|
||||||
@@ -438,9 +438,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic-util"
|
name = "portable-atomic-util"
|
||||||
version = "0.2.5"
|
version = "0.2.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5"
|
checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
]
|
]
|
||||||
@@ -782,9 +782,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wide"
|
name = "wide"
|
||||||
version = "1.1.1"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac11b009ebeae802ed758530b6496784ebfee7a87b9abfbcaf3bbe25b814eb25"
|
checksum = "198f6abc41fab83526d10880fa5c17e2b4ee44e763949b4bb34e2fd1e8ca48e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"safe_arch",
|
"safe_arch",
|
||||||
@@ -838,18 +838,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.40"
|
version = "0.8.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5"
|
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.40"
|
version = "0.8.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953"
|
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@ resolver = "2"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ferro_ta"
|
name = "ferro_ta"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
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 = "0.1.0" }
|
ferro_ta_core = { path = "crates/ferro_ta_core", version = "1.0.0" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { version = "0.8", features = ["html_reports"] }
|
criterion = { version = "0.8", features = ["html_reports"] }
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ This document describes how ferro-ta is packaged and published.
|
|||||||
## PyPI (pip)
|
## PyPI (pip)
|
||||||
|
|
||||||
Wheels are built by CI on release (see [RELEASE.md](RELEASE.md)).
|
Wheels are built by CI on release (see [RELEASE.md](RELEASE.md)).
|
||||||
|
Publishing uses PyPI Trusted Publishing via GitHub OIDC; no long-lived PyPI API
|
||||||
|
token is required.
|
||||||
|
|
||||||
Supported platforms and Python versions are documented in [PLATFORMS.md](PLATFORMS.md).
|
Supported platforms and Python versions are documented in [PLATFORMS.md](PLATFORMS.md).
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -3,7 +3,7 @@
|
|||||||
This document describes the step-by-step process for cutting a new **ferro-ta** release.
|
This document describes the step-by-step process for cutting a new **ferro-ta** release.
|
||||||
Follow every step in order to produce a consistent, reproducible release.
|
Follow every step in order to produce a consistent, reproducible release.
|
||||||
|
|
||||||
For **automated publishing** (secrets and what runs on release), see [PUBLISHING.md](PUBLISHING.md).
|
For the packaging and release overview, see [PACKAGING.md](PACKAGING.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ For **automated publishing** (secrets and what runs on release), see [PUBLISHING
|
|||||||
|
|
||||||
| Artifact | How |
|
| Artifact | How |
|
||||||
|-------------|-----|
|
|-------------|-----|
|
||||||
| **PyPI** | CI job `publish|
|
| **PyPI** | CI job `publish` using PyPI Trusted Publishing (OIDC) |
|
||||||
| **npm (WASM)** | Workflow `wasm-publish`|
|
| **npm (WASM)** | Workflow `wasm-publish`|
|
||||||
| **crates.io** | CI job `publish-cratesio` |
|
| **crates.io** | CI job `publish-cratesio` |
|
||||||
|
|
||||||
@@ -129,7 +129,8 @@ git push origin v0.2.0
|
|||||||
5. Click **Publish release**.
|
5. Click **Publish release**.
|
||||||
|
|
||||||
Publishing the release triggers the CI `build-wheels` and `publish` jobs
|
Publishing the release triggers the CI `build-wheels` and `publish` jobs
|
||||||
automatically (the workflow responds to `release: published`).
|
automatically (the workflow responds to `release: published`). The PyPI upload
|
||||||
|
uses Trusted Publishing via GitHub OIDC, so no `PYPI_API_TOKEN` secret is used.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+13
-14
@@ -22,13 +22,13 @@ Currently supported: **3.10, 3.11, 3.12, 3.13** (see `pyproject.toml`).
|
|||||||
## Release playbook
|
## Release playbook
|
||||||
|
|
||||||
1. **Bump the version** in `Cargo.toml` and `pyproject.toml` to the new version
|
1. **Bump the version** in `Cargo.toml` and `pyproject.toml` to the new version
|
||||||
(e.g. `0.2.0`).
|
(e.g. `1.0.1`).
|
||||||
2. **Update `CHANGELOG.md`**: move the `[Unreleased]` block to a new dated section
|
2. **Update `CHANGELOG.md`**: move the `[Unreleased]` block to a new dated section
|
||||||
`[0.2.0] — YYYY-MM-DD` and open a fresh `[Unreleased]` block.
|
`[1.0.1] — YYYY-MM-DD` and open a fresh `[Unreleased]` block.
|
||||||
3. **Commit** the version bump and changelog update with message
|
3. **Commit** the version bump and changelog update with message
|
||||||
`chore: release v0.2.0`.
|
`chore: release v1.0.1`.
|
||||||
4. **Create a tag**: `git tag v0.2.0 && git push origin v0.2.0`.
|
4. **Create a tag**: `git tag v1.0.1 && git push origin v1.0.1`.
|
||||||
5. **Create a GitHub Release** for tag `v0.2.0` — the CI `build-wheels` and
|
5. **Create a GitHub Release** for tag `v1.0.1` — the CI `build-wheels` and
|
||||||
`publish` jobs trigger automatically on `release: published`.
|
`publish` jobs trigger automatically on `release: published`.
|
||||||
|
|
||||||
## Breaking-change policy
|
## Breaking-change policy
|
||||||
@@ -47,10 +47,10 @@ touch `src/`, `python/`, or `wasm/`.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API Stability Guarantees (v1.0 preparation)
|
## API Stability Guarantees
|
||||||
|
|
||||||
The following modules are considered **stable API** as of the v0.1.x series and
|
The following modules are considered **stable API** as of `1.0.0` and will not
|
||||||
will not have breaking changes in minor releases:
|
have breaking changes in minor releases:
|
||||||
|
|
||||||
| Module | Stability |
|
| Module | Stability |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -90,13 +90,12 @@ will not have breaking changes in minor releases:
|
|||||||
- [x] Configuration defaults API
|
- [x] Configuration defaults API
|
||||||
- [x] Jupyter notebook examples
|
- [x] Jupyter notebook examples
|
||||||
|
|
||||||
### Path to v1.0
|
### Post-1.0 notes
|
||||||
|
|
||||||
When the v1.0 release is cut:
|
With `1.0.0` released:
|
||||||
1. Remove any `Beta` classifiers from `pyproject.toml`.
|
1. The package now uses the `Development Status :: 5 - Production/Stable` classifier.
|
||||||
2. Update `CHANGELOG.md` with the `[1.0.0]` release section.
|
2. `CHANGELOG.md` now contains the `[1.0.0]` release section.
|
||||||
3. Update this file to reflect stable status.
|
3. This file now reflects the stable-series SemVer contract.
|
||||||
4. Consider adding `Stable :: Stable` PyPI classifier.
|
|
||||||
|
|
||||||
### Compatibility matrix
|
### Compatibility matrix
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -106,7 +106,7 @@ MAX_SERIES_LENGTH = int(os.environ.get("MAX_SERIES_LENGTH", "100000"))
|
|||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="ferro-ta API",
|
title="ferro-ta API",
|
||||||
description="REST API for ferro-ta technical analysis indicators and backtesting.",
|
description="REST API for ferro-ta technical analysis indicators and backtesting.",
|
||||||
version="0.1.0",
|
version="1.0.0",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc",
|
redoc_url="/redoc",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Runtime dependencies for ferro-ta API
|
# Runtime dependencies for ferro-ta API
|
||||||
ferro_ta>=0.1.0
|
ferro_ta>=1.0.0
|
||||||
fastapi>=0.110.0
|
fastapi>=0.110.0
|
||||||
uvicorn[standard]>=0.27.0
|
uvicorn[standard]>=0.27.0
|
||||||
pydantic>=2.0.0
|
pydantic>=2.0.0
|
||||||
|
|||||||
@@ -23,6 +23,23 @@ def _parse_threshold_items(items: list[str]) -> dict[int, float]:
|
|||||||
return thresholds
|
return thresholds
|
||||||
|
|
||||||
|
|
||||||
|
def _percentile(values: list[float], q: float) -> float:
|
||||||
|
"""Return the q percentile using linear interpolation."""
|
||||||
|
if not values:
|
||||||
|
raise ValueError("Cannot compute percentile of empty sequence")
|
||||||
|
if q <= 0:
|
||||||
|
return min(values)
|
||||||
|
if q >= 100:
|
||||||
|
return max(values)
|
||||||
|
|
||||||
|
values = sorted(values)
|
||||||
|
rank = (len(values) - 1) * (q / 100.0)
|
||||||
|
lower = int(rank)
|
||||||
|
upper = min(lower + 1, len(values) - 1)
|
||||||
|
weight = rank - lower
|
||||||
|
return values[lower] * (1.0 - weight) + values[upper] * weight
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Check TA-Lib benchmark JSON against regression thresholds."
|
description="Check TA-Lib benchmark JSON against regression thresholds."
|
||||||
@@ -47,8 +64,20 @@ def main() -> int:
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--min-speedup-floor",
|
"--min-speedup-floor",
|
||||||
action="append",
|
action="append",
|
||||||
|
default=["10000=0.10", "100000=0.10"],
|
||||||
|
help="Hard minimum per-row speedup floor per size, e.g. 100000=0.1 (repeatable)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tail-percentile",
|
||||||
|
type=float,
|
||||||
|
default=10.0,
|
||||||
|
help="Tail percentile used for distribution-based slowdown checks (default: 10)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tail-speedup-floor",
|
||||||
|
action="append",
|
||||||
default=["10000=0.20", "100000=0.20"],
|
default=["10000=0.20", "100000=0.20"],
|
||||||
help="Required minimum per-row speedup floor per size, e.g. 100000=0.2 (repeatable)",
|
help="Required minimum tail percentile speedup per size, e.g. 100000=0.2 (repeatable)",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -67,10 +96,19 @@ def main() -> int:
|
|||||||
for entry in data.get("summary", {}).get("by_size", [])
|
for entry in data.get("summary", {}).get("by_size", [])
|
||||||
if entry.get("size") is not None
|
if entry.get("size") is not None
|
||||||
}
|
}
|
||||||
|
results_by_size: dict[int, list[dict[str, object]]] = {}
|
||||||
|
for row in data.get("results", []):
|
||||||
|
if "speedup" not in row or row.get("size") is None:
|
||||||
|
continue
|
||||||
|
size = int(row["size"])
|
||||||
|
results_by_size.setdefault(size, []).append(row)
|
||||||
|
|
||||||
median_floor = _parse_threshold_items(args.median_floor)
|
median_floor = _parse_threshold_items(args.median_floor)
|
||||||
min_speedup_floor = _parse_threshold_items(args.min_speedup_floor)
|
min_speedup_floor = _parse_threshold_items(args.min_speedup_floor)
|
||||||
required_sizes = sorted(set(median_floor) | set(min_speedup_floor))
|
tail_speedup_floor = _parse_threshold_items(args.tail_speedup_floor)
|
||||||
|
required_sizes = sorted(
|
||||||
|
set(median_floor) | set(min_speedup_floor) | set(tail_speedup_floor)
|
||||||
|
)
|
||||||
|
|
||||||
failures: list[str] = []
|
failures: list[str] = []
|
||||||
for size in required_sizes:
|
for size in required_sizes:
|
||||||
@@ -78,12 +116,20 @@ def main() -> int:
|
|||||||
if entry is None:
|
if entry is None:
|
||||||
failures.append(f"missing summary for size={size}")
|
failures.append(f"missing summary for size={size}")
|
||||||
continue
|
continue
|
||||||
|
rows_for_size = results_by_size.get(size, [])
|
||||||
|
if not rows_for_size:
|
||||||
|
failures.append(f"missing detailed rows for size={size}")
|
||||||
|
continue
|
||||||
|
|
||||||
rows = int(entry.get("rows", 0))
|
rows = int(entry.get("rows", 0))
|
||||||
med = float(entry.get("median_speedup", 0.0))
|
med = float(entry.get("median_speedup", 0.0))
|
||||||
min_s = float(entry.get("min_speedup", 0.0))
|
min_s = float(entry.get("min_speedup", 0.0))
|
||||||
|
speedups = [float(row["speedup"]) for row in rows_for_size]
|
||||||
|
tail_s = _percentile(speedups, args.tail_percentile)
|
||||||
print(
|
print(
|
||||||
f"size={size}: rows={rows}, median_speedup={med:.4f}, min_speedup={min_s:.4f}"
|
"size="
|
||||||
|
f"{size}: rows={rows}, median_speedup={med:.4f}, "
|
||||||
|
f"p{args.tail_percentile:g}_speedup={tail_s:.4f}, min_speedup={min_s:.4f}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if rows < args.min_rows:
|
if rows < args.min_rows:
|
||||||
@@ -94,6 +140,12 @@ def main() -> int:
|
|||||||
failures.append(
|
failures.append(
|
||||||
f"size={size} median_speedup {med:.4f} < floor {median_floor[size]:.4f}"
|
f"size={size} median_speedup {med:.4f} < floor {median_floor[size]:.4f}"
|
||||||
)
|
)
|
||||||
|
if tail_s < tail_speedup_floor.get(size, float("-inf")):
|
||||||
|
failures.append(
|
||||||
|
"size="
|
||||||
|
f"{size} p{args.tail_percentile:g}_speedup {tail_s:.4f} "
|
||||||
|
f"< floor {tail_speedup_floor[size]:.4f}"
|
||||||
|
)
|
||||||
if min_s < min_speedup_floor.get(size, float("-inf")):
|
if min_s < min_speedup_floor.get(size, float("-inf")):
|
||||||
failures.append(
|
failures.append(
|
||||||
f"size={size} min_speedup {min_s:.4f} < floor {min_speedup_floor[size]:.4f}"
|
f"size={size} min_speedup {min_s:.4f} < floor {min_speedup_floor[size]:.4f}"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{% set name = "ferro-ta" %}
|
{% set name = "ferro-ta" %}
|
||||||
{% set version = "0.1.0" %}
|
{% set version = "1.0.0" %}
|
||||||
|
|
||||||
package:
|
package:
|
||||||
name: {{ name|lower }}
|
name: {{ name|lower }}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ferro_ta_core"
|
name = "ferro_ta_core"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
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"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
0.1.0 (2024)
|
1.0.0 (2026)
|
||||||
------------
|
------------
|
||||||
|
|
||||||
**Candlestick Pattern Parity (61/61)**
|
**Candlestick Pattern Parity (61/61)**
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ project = "ferro-ta"
|
|||||||
copyright = "2024, pratikbhadane24"
|
copyright = "2024, pratikbhadane24"
|
||||||
author = "pratikbhadane24"
|
author = "pratikbhadane24"
|
||||||
# Version from env (e.g. set in CI from git tag) or default
|
# Version from env (e.g. set in CI from git tag) or default
|
||||||
release = os.environ.get("FERRO_TA_VERSION", "0.1.0")
|
release = os.environ.get("FERRO_TA_VERSION", "1.0.0")
|
||||||
version = release
|
version = release
|
||||||
|
|
||||||
# -- General configuration ----------------------------------------------------
|
# -- General configuration ----------------------------------------------------
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
ferro-ta Documentation
|
ferro-ta Documentation
|
||||||
=====================
|
======================
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ register("SMOOTH_RSI", SMOOTH_RSI)
|
|||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
name = "ferro-ta-myplugin"
|
name = "ferro-ta-myplugin"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = ["ferro_ta>=0.1.0"]
|
dependencies = ["ferro_ta>=1.0.0"]
|
||||||
|
|
||||||
[project.entry-points."ferro_ta.plugins"]
|
[project.entry-points."ferro_ta.plugins"]
|
||||||
auto_register = "ferro_ta_myplugin:register_all"
|
auto_register = "ferro_ta_myplugin:register_all"
|
||||||
|
|||||||
+5
-6
@@ -61,9 +61,8 @@ ferro-ta follows [Semantic Versioning 2.0.0]:
|
|||||||
| New indicators, new sub-modules, new features (backward-compatible) | **MINOR** |
|
| New indicators, new sub-modules, new features (backward-compatible) | **MINOR** |
|
||||||
| Bug fixes, performance improvements, docs, dependency bumps | **PATCH** |
|
| Bug fixes, performance improvements, docs, dependency bumps | **PATCH** |
|
||||||
|
|
||||||
The current version (`0.1.x`) is pre-stable — **breaking changes are possible
|
The current version (`1.x`) is stable. Breaking changes to stable APIs are
|
||||||
in minor releases**. When the project reaches 1.0.0, the full SemVer
|
reserved for future **major** releases.
|
||||||
contract kicks in.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -71,7 +70,7 @@ contract kicks in.
|
|||||||
|
|
||||||
Before removing or renaming any **stable** API:
|
Before removing or renaming any **stable** API:
|
||||||
|
|
||||||
1. The deprecated name/function is kept for at least **one minor release** after
|
1. The deprecated name/function is kept until the next **major release** after
|
||||||
the deprecation notice.
|
the deprecation notice.
|
||||||
2. A `DeprecationWarning` is raised when the deprecated API is used.
|
2. A `DeprecationWarning` is raised when the deprecated API is used.
|
||||||
3. The deprecation and removal are documented in `CHANGELOG.md` under
|
3. The deprecation and removal are documented in `CHANGELOG.md` under
|
||||||
@@ -79,8 +78,8 @@ Before removing or renaming any **stable** API:
|
|||||||
|
|
||||||
Example timeline:
|
Example timeline:
|
||||||
|
|
||||||
- `0.2.0` — `OLD_NAME` deprecated, `DeprecationWarning` added; `NEW_NAME` available.
|
- `1.1.0` — `OLD_NAME` deprecated, `DeprecationWarning` added; `NEW_NAME` available.
|
||||||
- `0.3.0` — `OLD_NAME` removed.
|
- `2.0.0` — `OLD_NAME` removed.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+11
-5
@@ -4,17 +4,17 @@ build-backend = "maturin"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "ferro-ta"
|
name = "ferro-ta"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
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" }
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.10"
|
||||||
keywords = [
|
keywords = [
|
||||||
"technical-analysis", "trading", "finance", "rust", "pyo3",
|
"technical-analysis", "trading", "finance", "rust", "pyo3",
|
||||||
"ta-lib", "indicators", "candlestick", "pandas", "numpy",
|
"ta-lib", "indicators", "candlestick", "pandas", "numpy",
|
||||||
]
|
]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Intended Audience :: Financial and Insurance Industry",
|
"Intended Audience :: Financial and Insurance Industry",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
@@ -37,7 +37,13 @@ benchmark = ["pytest>=7.0", "pytest-benchmark>=4.0"]
|
|||||||
pandas = ["pandas>=1.0"]
|
pandas = ["pandas>=1.0"]
|
||||||
polars = ["polars>=0.19"]
|
polars = ["polars>=0.19"]
|
||||||
docs = ["sphinx>=7.0", "sphinx-rtd-theme>=1.3"]
|
docs = ["sphinx>=7.0", "sphinx-rtd-theme>=1.3"]
|
||||||
comparison = ["pytest>=7.0", "ta-lib>=0.4", "pandas-ta>=0.3", "ta>=0.10", "pandas>=1.0"]
|
comparison = [
|
||||||
|
"pytest>=7.0",
|
||||||
|
"ta-lib>=0.4",
|
||||||
|
"pandas-ta>=0.3; python_version >= '3.12'",
|
||||||
|
"ta>=0.10",
|
||||||
|
"pandas>=1.0",
|
||||||
|
]
|
||||||
gpu = ["torch>=2.0"]
|
gpu = ["torch>=2.0"]
|
||||||
options = []
|
options = []
|
||||||
mcp = ["mcp>=1.0"]
|
mcp = ["mcp>=1.0"]
|
||||||
@@ -128,6 +134,6 @@ dev = [
|
|||||||
"pyright>=1.1",
|
"pyright>=1.1",
|
||||||
"maturin>=1.0,<2.0",
|
"maturin>=1.0,<2.0",
|
||||||
"pyyaml>=6.0",
|
"pyyaml>=6.0",
|
||||||
"pandas-ta>=0.3",
|
"pandas-ta>=0.3; python_version >= '3.12'",
|
||||||
"ta>=0.10",
|
"ta>=0.10",
|
||||||
]
|
]
|
||||||
|
|||||||
+99
-64
@@ -57,6 +57,17 @@ array([ nan, nan, 11. , 12. , 13. , 13.5, 13.33...])
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys as _sys
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Exceptions — exported at the top level for convenient catching
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
from ferro_ta.core.exceptions import ( # noqa: F401
|
||||||
|
FerroTAError,
|
||||||
|
FerroTAInputError,
|
||||||
|
FerroTAValueError,
|
||||||
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Cycle Indicators
|
# Cycle Indicators
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -69,15 +80,6 @@ from ferro_ta.indicators.cycle import ( # noqa: F401
|
|||||||
HT_TRENDMODE,
|
HT_TRENDMODE,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Exceptions — exported at the top level for convenient catching
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
from ferro_ta.core.exceptions import ( # noqa: F401
|
|
||||||
FerroTAError,
|
|
||||||
FerroTAInputError,
|
|
||||||
FerroTAValueError,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Math Operators & Math Transforms
|
# Math Operators & Math Transforms
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -476,21 +478,6 @@ __all__ = [
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
from ferro_ta._utils import pandas_wrap as _pandas_wrap # noqa: E402
|
from ferro_ta._utils import pandas_wrap as _pandas_wrap # noqa: E402
|
||||||
from ferro_ta._utils import polars_wrap as _polars_wrap # noqa: E402
|
from ferro_ta._utils import polars_wrap as _polars_wrap # noqa: E402
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Additional modules (not in __all__ — access via submodule)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
from ferro_ta.tools.alerts import ( # noqa: F401, E402
|
|
||||||
AlertManager,
|
|
||||||
check_cross,
|
|
||||||
check_threshold,
|
|
||||||
collect_alert_bars,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# API discovery helpers — ferro_ta.indicators() and ferro_ta.info()
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
from ferro_ta.tools.api_info import indicators, info # noqa: F401, E402
|
|
||||||
from ferro_ta.analysis.attribution import ( # noqa: F401, E402
|
from ferro_ta.analysis.attribution import ( # noqa: F401, E402
|
||||||
TradeStats,
|
TradeStats,
|
||||||
attribution_by_month,
|
attribution_by_month,
|
||||||
@@ -498,6 +485,36 @@ from ferro_ta.analysis.attribution import ( # noqa: F401, E402
|
|||||||
from_backtest,
|
from_backtest,
|
||||||
trade_stats,
|
trade_stats,
|
||||||
)
|
)
|
||||||
|
from ferro_ta.analysis.crypto import ( # noqa: F401, E402
|
||||||
|
continuous_bar_labels,
|
||||||
|
funding_pnl,
|
||||||
|
resample_continuous,
|
||||||
|
session_boundaries,
|
||||||
|
)
|
||||||
|
from ferro_ta.analysis.regime import ( # noqa: F401, E402
|
||||||
|
detect_breaks_cusum,
|
||||||
|
regime,
|
||||||
|
regime_adx,
|
||||||
|
regime_combined,
|
||||||
|
rolling_variance_break,
|
||||||
|
structural_breaks,
|
||||||
|
)
|
||||||
|
from ferro_ta.core import exceptions as exceptions # noqa: F401, E402
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Logging utilities — ferro_ta.enable_debug() / ferro_ta.benchmark()
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
from ferro_ta.core.logging_utils import ( # noqa: F401, E402
|
||||||
|
benchmark,
|
||||||
|
debug_mode,
|
||||||
|
disable_debug,
|
||||||
|
enable_debug,
|
||||||
|
get_logger,
|
||||||
|
log_call,
|
||||||
|
traced,
|
||||||
|
)
|
||||||
|
from ferro_ta.data import batch as batch # noqa: F401, E402
|
||||||
|
from ferro_ta.data import streaming as streaming # noqa: F401, E402
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Batch API (not in __all__ — use directly from ferro_ta.batch)
|
# Batch API (not in __all__ — use directly from ferro_ta.batch)
|
||||||
@@ -515,45 +532,6 @@ from ferro_ta.data.chunked import ( # noqa: F401, E402
|
|||||||
stitch_chunks,
|
stitch_chunks,
|
||||||
trim_overlap,
|
trim_overlap,
|
||||||
)
|
)
|
||||||
from ferro_ta.analysis.crypto import ( # noqa: F401, E402
|
|
||||||
continuous_bar_labels,
|
|
||||||
funding_pnl,
|
|
||||||
resample_continuous,
|
|
||||||
session_boundaries,
|
|
||||||
)
|
|
||||||
from ferro_ta.indicators.extended import ( # noqa: F401, E402
|
|
||||||
CHANDELIER_EXIT,
|
|
||||||
CHOPPINESS_INDEX,
|
|
||||||
DONCHIAN,
|
|
||||||
HULL_MA,
|
|
||||||
ICHIMOKU,
|
|
||||||
KELTNER_CHANNELS,
|
|
||||||
PIVOT_POINTS,
|
|
||||||
SUPERTREND,
|
|
||||||
VWAP,
|
|
||||||
VWMA,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Logging utilities — ferro_ta.enable_debug() / ferro_ta.benchmark()
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
from ferro_ta.core.logging_utils import ( # noqa: F401, E402
|
|
||||||
benchmark,
|
|
||||||
debug_mode,
|
|
||||||
disable_debug,
|
|
||||||
enable_debug,
|
|
||||||
get_logger,
|
|
||||||
log_call,
|
|
||||||
traced,
|
|
||||||
)
|
|
||||||
from ferro_ta.analysis.regime import ( # noqa: F401, E402
|
|
||||||
detect_breaks_cusum,
|
|
||||||
regime,
|
|
||||||
regime_adx,
|
|
||||||
regime_combined,
|
|
||||||
rolling_variance_break,
|
|
||||||
structural_breaks,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Streaming / Incremental API (not in __all__ — these are classes, not funcs)
|
# Streaming / Incremental API (not in __all__ — these are classes, not funcs)
|
||||||
@@ -570,6 +548,63 @@ from ferro_ta.data.streaming import ( # noqa: F401, E402 # type: ignore[assign
|
|||||||
StreamingSupertrend, # type: ignore[attr-defined]
|
StreamingSupertrend, # type: ignore[attr-defined]
|
||||||
StreamingVWAP, # type: ignore[attr-defined]
|
StreamingVWAP, # type: ignore[attr-defined]
|
||||||
)
|
)
|
||||||
|
from ferro_ta.indicators import cycle as cycle # noqa: F401, E402
|
||||||
|
from ferro_ta.indicators import extended as extended # noqa: F401, E402
|
||||||
|
from ferro_ta.indicators import math_ops as math_ops # noqa: F401, E402
|
||||||
|
from ferro_ta.indicators import momentum as momentum # noqa: F401, E402
|
||||||
|
from ferro_ta.indicators import overlap as overlap # noqa: F401, E402
|
||||||
|
from ferro_ta.indicators import pattern as pattern # noqa: F401, E402
|
||||||
|
from ferro_ta.indicators import price_transform as price_transform # noqa: F401, E402
|
||||||
|
from ferro_ta.indicators import statistic as statistic # noqa: F401, E402
|
||||||
|
from ferro_ta.indicators import volatility as volatility # noqa: F401, E402
|
||||||
|
from ferro_ta.indicators import volume as volume # noqa: F401, E402
|
||||||
|
from ferro_ta.indicators.extended import ( # noqa: F401, E402
|
||||||
|
CHANDELIER_EXIT,
|
||||||
|
CHOPPINESS_INDEX,
|
||||||
|
DONCHIAN,
|
||||||
|
HULL_MA,
|
||||||
|
ICHIMOKU,
|
||||||
|
KELTNER_CHANNELS,
|
||||||
|
PIVOT_POINTS,
|
||||||
|
SUPERTREND,
|
||||||
|
VWAP,
|
||||||
|
VWMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Additional modules (not in __all__ — access via submodule)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
from ferro_ta.tools.alerts import ( # noqa: F401, E402
|
||||||
|
AlertManager,
|
||||||
|
check_cross,
|
||||||
|
check_threshold,
|
||||||
|
collect_alert_bars,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# API discovery helpers — ferro_ta.indicators() and ferro_ta.info()
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
from ferro_ta.tools.api_info import indicators, info # noqa: F401, E402
|
||||||
|
|
||||||
|
_ALIASED_SUBMODULES = {
|
||||||
|
"batch": batch,
|
||||||
|
"cycle": cycle,
|
||||||
|
"exceptions": exceptions,
|
||||||
|
"extended": extended,
|
||||||
|
"math_ops": math_ops,
|
||||||
|
"momentum": momentum,
|
||||||
|
"overlap": overlap,
|
||||||
|
"pattern": pattern,
|
||||||
|
"price_transform": price_transform,
|
||||||
|
"statistic": statistic,
|
||||||
|
"streaming": streaming,
|
||||||
|
"volatility": volatility,
|
||||||
|
"volume": volume,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _module_name, _module in _ALIASED_SUBMODULES.items():
|
||||||
|
setattr(_sys.modules[__name__], _module_name, _module)
|
||||||
|
_sys.modules[f"{__name__}.{_module_name}"] = _module
|
||||||
|
|
||||||
_g = globals()
|
_g = globals()
|
||||||
for _name in __all__:
|
for _name in __all__:
|
||||||
@@ -579,4 +614,4 @@ for _name in __all__:
|
|||||||
_fn = _g.get(_name)
|
_fn = _g.get(_name)
|
||||||
if callable(_fn) and not getattr(_fn, "_polars_wrapped", False):
|
if callable(_fn) and not getattr(_fn, "_polars_wrapped", False):
|
||||||
_g[_name] = _polars_wrap(_fn)
|
_g[_name] = _polars_wrap(_fn)
|
||||||
del _g, _name, _fn
|
del _ALIASED_SUBMODULES, _g, _module, _module_name, _name, _fn, _sys
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ from collections.abc import Callable
|
|||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from ferro_ta._utils import _to_f64
|
from ferro_ta._utils import _to_f64
|
||||||
from ferro_ta.core.exceptions import _normalize_rust_error
|
|
||||||
from ferro_ta.core.exceptions import (
|
from ferro_ta.core.exceptions import (
|
||||||
|
_normalize_rust_error,
|
||||||
check_equal_length,
|
check_equal_length,
|
||||||
check_timeperiod,
|
check_timeperiod,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Backward-compat stub — moved to ``ferro_ta.core.logging_utils``."""
|
"""Backward-compat stub — moved to ``ferro_ta.core.logging_utils``."""
|
||||||
|
|
||||||
from ferro_ta.core.logging_utils import * # noqa: F401, F403
|
from ferro_ta.core.logging_utils import * # noqa: F401, F403
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ferro_ta.core.logging_utils import __all__ # noqa: F401
|
from ferro_ta.core.logging_utils import __all__ # noqa: F401
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
@@ -392,7 +392,7 @@ def _run_stdio_fallback() -> None: # pragma: no cover
|
|||||||
"result": {
|
"result": {
|
||||||
"protocolVersion": "2024-11-05",
|
"protocolVersion": "2024-11-05",
|
||||||
"capabilities": {"tools": {}},
|
"capabilities": {"tools": {}},
|
||||||
"serverInfo": {"name": "ferro-ta", "version": "0.1.0"},
|
"serverInfo": {"name": "ferro-ta", "version": "1.0.0"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
elif method == "tools/list":
|
elif method == "tools/list":
|
||||||
|
|||||||
@@ -27,4 +27,3 @@ from ferro_ta.tools.tools import ( # noqa: F401
|
|||||||
list_indicators,
|
list_indicators,
|
||||||
run_backtest,
|
run_backtest,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ True
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import warnings
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import warnings
|
|
||||||
from numpy.typing import ArrayLike, NDArray
|
from numpy.typing import ArrayLike, NDArray
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Validate that CHANGELOG.md keeps a single top-level [Unreleased] section."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
changelog = Path("CHANGELOG.md")
|
||||||
|
if not changelog.exists():
|
||||||
|
print("ERROR: CHANGELOG.md not found.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
text = changelog.read_text(encoding="utf-8")
|
||||||
|
headings = list(re.finditer(r"^## \[(.+?)\]\s*$", text, flags=re.MULTILINE))
|
||||||
|
unreleased = [m for m in headings if m.group(1) == "Unreleased"]
|
||||||
|
|
||||||
|
if not unreleased:
|
||||||
|
print("ERROR: CHANGELOG.md is missing a '## [Unreleased]' heading.")
|
||||||
|
return 1
|
||||||
|
if len(unreleased) > 1:
|
||||||
|
print("ERROR: CHANGELOG.md contains multiple '## [Unreleased]' headings.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if headings and headings[0].group(1) != "Unreleased":
|
||||||
|
print("ERROR: '## [Unreleased]' must be the first top-level changelog section.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print("OK: CHANGELOG.md contains a single top-level [Unreleased] section.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -197,9 +197,9 @@ class TestStreamingATR:
|
|||||||
|
|
||||||
# Streaming
|
# Streaming
|
||||||
streamer = StreamingATR(period=period)
|
streamer = StreamingATR(period=period)
|
||||||
stream_out = np.array([
|
stream_out = np.array(
|
||||||
streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)
|
[streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)]
|
||||||
])
|
)
|
||||||
|
|
||||||
# Compare only the overlap region where both arrays are valid
|
# Compare only the overlap region where both arrays are valid
|
||||||
mask = np.isfinite(batch_out) & np.isfinite(stream_out)
|
mask = np.isfinite(batch_out) & np.isfinite(stream_out)
|
||||||
@@ -207,9 +207,9 @@ class TestStreamingATR:
|
|||||||
"""ATR values should be non-negative."""
|
"""ATR values should be non-negative."""
|
||||||
period = 14
|
period = 14
|
||||||
streamer = StreamingATR(period=period)
|
streamer = StreamingATR(period=period)
|
||||||
stream_out = np.array([
|
stream_out = np.array(
|
||||||
streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)
|
[streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)]
|
||||||
])
|
)
|
||||||
|
|
||||||
# Filter out NaN values
|
# Filter out NaN values
|
||||||
valid = stream_out[~np.isnan(stream_out)]
|
valid = stream_out[~np.isnan(stream_out)]
|
||||||
@@ -222,15 +222,21 @@ class TestStreamingATR:
|
|||||||
streamer = StreamingATR(period=period)
|
streamer = StreamingATR(period=period)
|
||||||
|
|
||||||
# First pass
|
# First pass
|
||||||
first_pass = np.array([
|
first_pass = np.array(
|
||||||
streamer.update(h, l, c) for h, l, c in zip(HIGH[:50], LOW[:50], CLOSE[:50])
|
[
|
||||||
])
|
streamer.update(h, l, c)
|
||||||
|
for h, l, c in zip(HIGH[:50], LOW[:50], CLOSE[:50])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Reset and second pass
|
# Reset and second pass
|
||||||
streamer.reset()
|
streamer.reset()
|
||||||
second_pass = np.array([
|
second_pass = np.array(
|
||||||
streamer.update(h, l, c) for h, l, c in zip(HIGH[:50], LOW[:50], CLOSE[:50])
|
[
|
||||||
])
|
streamer.update(h, l, c)
|
||||||
|
for h, l, c in zip(HIGH[:50], LOW[:50], CLOSE[:50])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
assert np.allclose(first_pass, second_pass, equal_nan=True, atol=1e-12)
|
assert np.allclose(first_pass, second_pass, equal_nan=True, atol=1e-12)
|
||||||
|
|
||||||
@@ -253,7 +259,9 @@ class TestStreamingBBands:
|
|||||||
verify proximity with atol=0.2 and confirm internal consistency separately.
|
verify proximity with atol=0.2 and confirm internal consistency separately.
|
||||||
"""
|
"""
|
||||||
# Batch
|
# Batch
|
||||||
batch_upper, batch_middle, batch_lower = ferro_ta.BBANDS(CLOSE, timeperiod=period)
|
batch_upper, batch_middle, batch_lower = ferro_ta.BBANDS(
|
||||||
|
CLOSE, timeperiod=period
|
||||||
|
)
|
||||||
|
|
||||||
# Streaming
|
# Streaming
|
||||||
streamer = StreamingBBands(period=period, nbdevup=2.0, nbdevdn=2.0)
|
streamer = StreamingBBands(period=period, nbdevup=2.0, nbdevdn=2.0)
|
||||||
@@ -265,8 +273,9 @@ class TestStreamingBBands:
|
|||||||
# Compare only overlapping valid region
|
# Compare only overlapping valid region
|
||||||
mask = np.isfinite(batch_middle)
|
mask = np.isfinite(batch_middle)
|
||||||
# Middle band (SMA) must match exactly
|
# Middle band (SMA) must match exactly
|
||||||
assert np.allclose(stream_middle[mask], batch_middle[mask], atol=1e-10), \
|
assert np.allclose(stream_middle[mask], batch_middle[mask], atol=1e-10), (
|
||||||
"BBands middle (SMA) must match batch exactly"
|
"BBands middle (SMA) must match batch exactly"
|
||||||
|
)
|
||||||
# Upper/lower: streaming uses sample std; batch uses population std — use atol=0.2
|
# Upper/lower: streaming uses sample std; batch uses population std — use atol=0.2
|
||||||
assert np.allclose(stream_upper[mask], batch_upper[mask], atol=0.2)
|
assert np.allclose(stream_upper[mask], batch_upper[mask], atol=0.2)
|
||||||
assert np.allclose(stream_lower[mask], batch_lower[mask], atol=0.2)
|
assert np.allclose(stream_lower[mask], batch_lower[mask], atol=0.2)
|
||||||
@@ -285,7 +294,9 @@ class TestStreamingBBands:
|
|||||||
|
|
||||||
# Compare all three bands
|
# Compare all three bands
|
||||||
for i in range(len(first_pass)):
|
for i in range(len(first_pass)):
|
||||||
assert np.allclose(first_pass[i], second_pass[i], equal_nan=True, atol=1e-14)
|
assert np.allclose(
|
||||||
|
first_pass[i], second_pass[i], equal_nan=True, atol=1e-14
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -341,7 +352,9 @@ class TestStreamingMACD:
|
|||||||
|
|
||||||
# Compare all three outputs
|
# Compare all three outputs
|
||||||
for i in range(len(first_pass)):
|
for i in range(len(first_pass)):
|
||||||
assert np.allclose(first_pass[i], second_pass[i], equal_nan=True, atol=1e-14)
|
assert np.allclose(
|
||||||
|
first_pass[i], second_pass[i], equal_nan=True, atol=1e-14
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -356,19 +369,12 @@ class TestStreamingStoch:
|
|||||||
"""Streaming Stochastic should match batch Stochastic."""
|
"""Streaming Stochastic should match batch Stochastic."""
|
||||||
# Batch
|
# Batch
|
||||||
batch_slowk, batch_slowd = ferro_ta.STOCH(
|
batch_slowk, batch_slowd = ferro_ta.STOCH(
|
||||||
HIGH, LOW, CLOSE,
|
HIGH, LOW, CLOSE, fastk_period=5, slowk_period=3, slowd_period=3
|
||||||
fastk_period=5, slowk_period=3,
|
|
||||||
slowd_period=3
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Streaming
|
# Streaming
|
||||||
streamer = StreamingStoch(
|
streamer = StreamingStoch(fastk_period=5, slowk_period=3, slowd_period=3)
|
||||||
fastk_period=5, slowk_period=3,
|
stream_results = [streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)]
|
||||||
slowd_period=3
|
|
||||||
)
|
|
||||||
stream_results = [
|
|
||||||
streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)
|
|
||||||
]
|
|
||||||
stream_slowk = np.array([r[0] for r in stream_results])
|
stream_slowk = np.array([r[0] for r in stream_results])
|
||||||
stream_slowd = np.array([r[1] for r in stream_results])
|
stream_slowd = np.array([r[1] for r in stream_results])
|
||||||
|
|
||||||
@@ -380,13 +386,8 @@ class TestStreamingStoch:
|
|||||||
|
|
||||||
def test_stoch_range_zero_to_hundred(self):
|
def test_stoch_range_zero_to_hundred(self):
|
||||||
"""Stochastic values should be in range [0, 100]."""
|
"""Stochastic values should be in range [0, 100]."""
|
||||||
streamer = StreamingStoch(
|
streamer = StreamingStoch(fastk_period=5, slowk_period=3, slowd_period=3)
|
||||||
fastk_period=5, slowk_period=3,
|
stream_results = [streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)]
|
||||||
slowd_period=3
|
|
||||||
)
|
|
||||||
stream_results = [
|
|
||||||
streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)
|
|
||||||
]
|
|
||||||
stream_slowk = np.array([r[0] for r in stream_results])
|
stream_slowk = np.array([r[0] for r in stream_results])
|
||||||
stream_slowd = np.array([r[1] for r in stream_results])
|
stream_slowd = np.array([r[1] for r in stream_results])
|
||||||
|
|
||||||
@@ -401,10 +402,7 @@ class TestStreamingStoch:
|
|||||||
|
|
||||||
def test_reset_gives_same_result(self):
|
def test_reset_gives_same_result(self):
|
||||||
"""Reset and re-feed should give identical output."""
|
"""Reset and re-feed should give identical output."""
|
||||||
streamer = StreamingStoch(
|
streamer = StreamingStoch(fastk_period=5, slowk_period=3, slowd_period=3)
|
||||||
fastk_period=5, slowk_period=3,
|
|
||||||
slowd_period=3
|
|
||||||
)
|
|
||||||
|
|
||||||
# First pass
|
# First pass
|
||||||
first_pass = [
|
first_pass = [
|
||||||
@@ -419,7 +417,9 @@ class TestStreamingStoch:
|
|||||||
|
|
||||||
# Compare
|
# Compare
|
||||||
for i in range(len(first_pass)):
|
for i in range(len(first_pass)):
|
||||||
assert np.allclose(first_pass[i], second_pass[i], equal_nan=True, atol=1e-14)
|
assert np.allclose(
|
||||||
|
first_pass[i], second_pass[i], equal_nan=True, atol=1e-14
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -437,9 +437,12 @@ class TestStreamingVWAP:
|
|||||||
|
|
||||||
# Streaming (cumulative)
|
# Streaming (cumulative)
|
||||||
streamer = StreamingVWAP()
|
streamer = StreamingVWAP()
|
||||||
stream_out = np.array([
|
stream_out = np.array(
|
||||||
streamer.update(h, l, c, v) for h, l, c, v in zip(HIGH, LOW, CLOSE, VOLUME)
|
[
|
||||||
])
|
streamer.update(h, l, c, v)
|
||||||
|
for h, l, c, v in zip(HIGH, LOW, CLOSE, VOLUME)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Compare
|
# Compare
|
||||||
assert np.allclose(stream_out, batch_out, equal_nan=True, atol=1e-10)
|
assert np.allclose(stream_out, batch_out, equal_nan=True, atol=1e-10)
|
||||||
@@ -451,9 +454,12 @@ class TestStreamingVWAP:
|
|||||||
|
|
||||||
# Streaming (cumulative)
|
# Streaming (cumulative)
|
||||||
streamer = StreamingVWAP()
|
streamer = StreamingVWAP()
|
||||||
stream_out = np.array([
|
stream_out = np.array(
|
||||||
streamer.update(h, l, c, v) for h, l, c, v in zip(HIGH, LOW, CLOSE, VOLUME)
|
[
|
||||||
])
|
streamer.update(h, l, c, v)
|
||||||
|
for h, l, c, v in zip(HIGH, LOW, CLOSE, VOLUME)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Compare
|
# Compare
|
||||||
assert np.allclose(stream_out, batch_out, equal_nan=True, atol=1e-10)
|
assert np.allclose(stream_out, batch_out, equal_nan=True, atol=1e-10)
|
||||||
@@ -463,17 +469,21 @@ class TestStreamingVWAP:
|
|||||||
streamer = StreamingVWAP()
|
streamer = StreamingVWAP()
|
||||||
|
|
||||||
# First pass
|
# First pass
|
||||||
first_pass = np.array([
|
first_pass = np.array(
|
||||||
streamer.update(h, l, c, v)
|
[
|
||||||
for h, l, c, v in zip(HIGH[:50], LOW[:50], CLOSE[:50], VOLUME[:50])
|
streamer.update(h, l, c, v)
|
||||||
])
|
for h, l, c, v in zip(HIGH[:50], LOW[:50], CLOSE[:50], VOLUME[:50])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Reset and second pass
|
# Reset and second pass
|
||||||
streamer.reset()
|
streamer.reset()
|
||||||
second_pass = np.array([
|
second_pass = np.array(
|
||||||
streamer.update(h, l, c, v)
|
[
|
||||||
for h, l, c, v in zip(HIGH[:50], LOW[:50], CLOSE[:50], VOLUME[:50])
|
streamer.update(h, l, c, v)
|
||||||
])
|
for h, l, c, v in zip(HIGH[:50], LOW[:50], CLOSE[:50], VOLUME[:50])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
assert np.allclose(first_pass, second_pass, equal_nan=True, atol=1e-14)
|
assert np.allclose(first_pass, second_pass, equal_nan=True, atol=1e-14)
|
||||||
|
|
||||||
@@ -498,9 +508,7 @@ class TestStreamingSupertrend:
|
|||||||
|
|
||||||
# Streaming
|
# Streaming
|
||||||
streamer = StreamingSupertrend(period=period, multiplier=multiplier)
|
streamer = StreamingSupertrend(period=period, multiplier=multiplier)
|
||||||
stream_results = [
|
stream_results = [streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)]
|
||||||
streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)
|
|
||||||
]
|
|
||||||
stream_line = np.array([r[0] for r in stream_results])
|
stream_line = np.array([r[0] for r in stream_results])
|
||||||
stream_dir = np.array([r[1] for r in stream_results])
|
stream_dir = np.array([r[1] for r in stream_results])
|
||||||
|
|
||||||
@@ -527,4 +535,6 @@ class TestStreamingSupertrend:
|
|||||||
|
|
||||||
# Compare
|
# Compare
|
||||||
for i in range(len(first_pass)):
|
for i in range(len(first_pass)):
|
||||||
assert np.allclose(first_pass[i], second_pass[i], equal_nan=True, atol=1e-14)
|
assert np.allclose(
|
||||||
|
first_pass[i], second_pass[i], equal_nan=True, atol=1e-14
|
||||||
|
)
|
||||||
|
|||||||
@@ -181,7 +181,9 @@ class TestBBANDSVsPandasTA:
|
|||||||
pt_upper = pt_bbands[upper_col].to_numpy()
|
pt_upper = pt_bbands[upper_col].to_numpy()
|
||||||
|
|
||||||
# Middle band (SMA) must be identical
|
# Middle band (SMA) must be identical
|
||||||
assert _allclose(ft_middle, pt_middle, atol=1e-8), "BBands middle (SMA) must match"
|
assert _allclose(ft_middle, pt_middle, atol=1e-8), (
|
||||||
|
"BBands middle (SMA) must match"
|
||||||
|
)
|
||||||
# Upper/lower: differ due to ddof=0 vs ddof=1
|
# Upper/lower: differ due to ddof=0 vs ddof=1
|
||||||
assert _allclose(ft_upper, pt_upper, atol=0.1)
|
assert _allclose(ft_upper, pt_upper, atol=0.1)
|
||||||
assert _allclose(ft_lower, pt_lower, atol=0.1)
|
assert _allclose(ft_lower, pt_lower, atol=0.1)
|
||||||
@@ -259,18 +261,15 @@ class TestSTOCHVsPandasTA:
|
|||||||
close = ohlcv_500["close"]
|
close = ohlcv_500["close"]
|
||||||
|
|
||||||
ft_slowk, ft_slowd = ferro_ta.STOCH(
|
ft_slowk, ft_slowd = ferro_ta.STOCH(
|
||||||
high, low, close,
|
high, low, close, fastk_period=14, slowk_period=3, slowd_period=3
|
||||||
fastk_period=14, slowk_period=3,
|
|
||||||
slowd_period=3
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# pandas-ta returns DataFrame
|
# pandas-ta returns DataFrame
|
||||||
pt_stoch = pandas_ta.stoch(
|
pt_stoch = pandas_ta.stoch(
|
||||||
pd.Series(high), pd.Series(low), pd.Series(close),
|
pd.Series(high), pd.Series(low), pd.Series(close), k=14, d=3, smooth_k=3
|
||||||
k=14, d=3, smooth_k=3
|
|
||||||
)
|
)
|
||||||
pt_slowk = pt_stoch[f"STOCHk_14_3_3"].to_numpy()
|
pt_slowk = pt_stoch["STOCHk_14_3_3"].to_numpy()
|
||||||
pt_slowd = pt_stoch[f"STOCHd_14_3_3"].to_numpy()
|
pt_slowd = pt_stoch["STOCHd_14_3_3"].to_numpy()
|
||||||
|
|
||||||
assert _allclose(ft_slowk, pt_slowk, atol=1e-2, tail_fraction=0.3)
|
assert _allclose(ft_slowk, pt_slowk, atol=1e-2, tail_fraction=0.3)
|
||||||
assert _allclose(ft_slowd, pt_slowd, atol=1e-2, tail_fraction=0.3)
|
assert _allclose(ft_slowd, pt_slowd, atol=1e-2, tail_fraction=0.3)
|
||||||
@@ -291,7 +290,9 @@ class TestCCIVsPandasTA:
|
|||||||
# Compute CCI manually: (TP - SMA(TP)) / (0.015 * MeanAbsDev(TP))
|
# Compute CCI manually: (TP - SMA(TP)) / (0.015 * MeanAbsDev(TP))
|
||||||
tp = (pd.Series(high) + pd.Series(low) + pd.Series(close)) / 3.0
|
tp = (pd.Series(high) + pd.Series(low) + pd.Series(close)) / 3.0
|
||||||
mean_tp = tp.rolling(period).mean()
|
mean_tp = tp.rolling(period).mean()
|
||||||
mad_tp = tp.rolling(period).apply(lambda x: np.mean(np.abs(x - x.mean())), raw=True)
|
mad_tp = tp.rolling(period).apply(
|
||||||
|
lambda x: np.mean(np.abs(x - x.mean())), raw=True
|
||||||
|
)
|
||||||
pt = ((tp - mean_tp) / (0.015 * mad_tp)).to_numpy()
|
pt = ((tp - mean_tp) / (0.015 * mad_tp)).to_numpy()
|
||||||
|
|
||||||
assert _allclose(ft, pt, atol=1e-8)
|
assert _allclose(ft, pt, atol=1e-8)
|
||||||
@@ -469,8 +470,8 @@ class TestVWAPVsPandasTA:
|
|||||||
n = len(tp)
|
n = len(tp)
|
||||||
ref = np.full(n, np.nan)
|
ref = np.full(n, np.nan)
|
||||||
for i in range(period - 1, n):
|
for i in range(period - 1, n):
|
||||||
w = tp[i - period + 1: i + 1]
|
w = tp[i - period + 1 : i + 1]
|
||||||
v = vol[i - period + 1: i + 1]
|
v = vol[i - period + 1 : i + 1]
|
||||||
ref[i] = np.dot(w, v) / v.sum()
|
ref[i] = np.dot(w, v) / v.sum()
|
||||||
|
|
||||||
assert _allclose(ft, ref, atol=1e-8)
|
assert _allclose(ft, ref, atol=1e-8)
|
||||||
@@ -522,7 +523,13 @@ class TestICHIMOKUVsPandasTA:
|
|||||||
close = ohlcv_500["close"]
|
close = ohlcv_500["close"]
|
||||||
|
|
||||||
ft_tenkan, ft_kijun, ft_senkou_a, ft_senkou_b, ft_chikou = ferro_ta.ICHIMOKU(
|
ft_tenkan, ft_kijun, ft_senkou_a, ft_senkou_b, ft_chikou = ferro_ta.ICHIMOKU(
|
||||||
high, low, close, tenkan_period=9, kijun_period=26, senkou_b_period=52, displacement=26
|
high,
|
||||||
|
low,
|
||||||
|
close,
|
||||||
|
tenkan_period=9,
|
||||||
|
kijun_period=26,
|
||||||
|
senkou_b_period=52,
|
||||||
|
displacement=26,
|
||||||
)
|
)
|
||||||
|
|
||||||
df = pd.DataFrame({"high": high, "low": low, "close": close})
|
df = pd.DataFrame({"high": high, "low": low, "close": close})
|
||||||
@@ -547,12 +554,19 @@ class TestKELTNER_CHANNELSVsPandasTA:
|
|||||||
multiplier = 2.0
|
multiplier = 2.0
|
||||||
|
|
||||||
ft_upper, ft_middle, ft_lower = ferro_ta.KELTNER_CHANNELS(
|
ft_upper, ft_middle, ft_lower = ferro_ta.KELTNER_CHANNELS(
|
||||||
high, low, close, timeperiod=period, atr_period=atr_period, multiplier=multiplier
|
high,
|
||||||
|
low,
|
||||||
|
close,
|
||||||
|
timeperiod=period,
|
||||||
|
atr_period=atr_period,
|
||||||
|
multiplier=multiplier,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Compute manually using pandas_ta EMA and ATR to match ferro_ta's exact formula
|
# Compute manually using pandas_ta EMA and ATR to match ferro_ta's exact formula
|
||||||
pt_ema = pandas_ta.ema(pd.Series(close), length=period).to_numpy()
|
pt_ema = pandas_ta.ema(pd.Series(close), length=period).to_numpy()
|
||||||
pt_atr = pandas_ta.atr(pd.Series(high), pd.Series(low), pd.Series(close), length=atr_period).to_numpy()
|
pt_atr = pandas_ta.atr(
|
||||||
|
pd.Series(high), pd.Series(low), pd.Series(close), length=atr_period
|
||||||
|
).to_numpy()
|
||||||
pt_upper = pt_ema + multiplier * pt_atr
|
pt_upper = pt_ema + multiplier * pt_atr
|
||||||
pt_middle = pt_ema
|
pt_middle = pt_ema
|
||||||
pt_lower = pt_ema - multiplier * pt_atr
|
pt_lower = pt_ema - multiplier * pt_atr
|
||||||
@@ -643,7 +657,9 @@ class TestCHANDELIER_EXITVsPandasTA:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Compute manually: long = rolling_max(H, n) - mult*ATR; short = rolling_min(L, n) + mult*ATR
|
# Compute manually: long = rolling_max(H, n) - mult*ATR; short = rolling_min(L, n) + mult*ATR
|
||||||
pt_atr = pandas_ta.atr(pd.Series(high), pd.Series(low), pd.Series(close), length=period).to_numpy()
|
pt_atr = pandas_ta.atr(
|
||||||
|
pd.Series(high), pd.Series(low), pd.Series(close), length=period
|
||||||
|
).to_numpy()
|
||||||
rolling_high = pd.Series(high).rolling(period).max().to_numpy()
|
rolling_high = pd.Series(high).rolling(period).max().to_numpy()
|
||||||
rolling_low = pd.Series(low).rolling(period).min().to_numpy()
|
rolling_low = pd.Series(low).rolling(period).min().to_numpy()
|
||||||
pt_long = rolling_high - multiplier * pt_atr
|
pt_long = rolling_high - multiplier * pt_atr
|
||||||
|
|||||||
@@ -190,9 +190,7 @@ class TestSTOCHVsTA:
|
|||||||
close = ohlcv_500["close"]
|
close = ohlcv_500["close"]
|
||||||
|
|
||||||
ft_slowk, ft_slowd = ferro_ta.STOCH(
|
ft_slowk, ft_slowd = ferro_ta.STOCH(
|
||||||
high, low, close,
|
high, low, close, fastk_period=14, slowk_period=3, slowd_period=3
|
||||||
fastk_period=14, slowk_period=3,
|
|
||||||
slowd_period=3
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Values in valid region must be within [0, 100]
|
# Values in valid region must be within [0, 100]
|
||||||
@@ -200,16 +198,21 @@ class TestSTOCHVsTA:
|
|||||||
valid_d = ft_slowd[np.isfinite(ft_slowd)]
|
valid_d = ft_slowd[np.isfinite(ft_slowd)]
|
||||||
assert len(valid_k) > 0, "STOCH slowk should have valid values"
|
assert len(valid_k) > 0, "STOCH slowk should have valid values"
|
||||||
assert len(valid_d) > 0, "STOCH slowd should have valid values"
|
assert len(valid_d) > 0, "STOCH slowd should have valid values"
|
||||||
assert np.all(valid_k >= 0.0) and np.all(valid_k <= 100.0), \
|
assert np.all(valid_k >= 0.0) and np.all(valid_k <= 100.0), (
|
||||||
"STOCH slowk must be in [0, 100]"
|
"STOCH slowk must be in [0, 100]"
|
||||||
assert np.all(valid_d >= 0.0) and np.all(valid_d <= 100.0), \
|
)
|
||||||
|
assert np.all(valid_d >= 0.0) and np.all(valid_d <= 100.0), (
|
||||||
"STOCH slowd must be in [0, 100]"
|
"STOCH slowd must be in [0, 100]"
|
||||||
|
)
|
||||||
|
|
||||||
# Warm-up: TA-Lib STOCH NaN count = fastk_period + slowk_period - 1
|
# Warm-up: TA-Lib STOCH NaN count = fastk_period + slowk_period - 1
|
||||||
expected_nan = 14 + 3 + 1 - 1 # = fastk_period + slowk_period (TA-Lib convention)
|
expected_nan = (
|
||||||
|
14 + 3 + 1 - 1
|
||||||
|
) # = fastk_period + slowk_period (TA-Lib convention)
|
||||||
actual_nan_k = int(np.sum(np.isnan(ft_slowk)))
|
actual_nan_k = int(np.sum(np.isnan(ft_slowk)))
|
||||||
assert actual_nan_k == expected_nan, \
|
assert actual_nan_k == expected_nan, (
|
||||||
f"STOCH slowk NaN warmup: expected {expected_nan}, got {actual_nan_k}"
|
f"STOCH slowk NaN warmup: expected {expected_nan}, got {actual_nan_k}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestWILLRVsTA:
|
class TestWILLRVsTA:
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ SIGN_AGREEMENT_THRESHOLD = 0.8
|
|||||||
# use lower thresholds with a documented reason.
|
# use lower thresholds with a documented reason.
|
||||||
CDL_AGREEMENT_THRESHOLDS: dict[str, float] = {
|
CDL_AGREEMENT_THRESHOLDS: dict[str, float] = {
|
||||||
# Body/shadow ratio thresholds differ between ferro_ta and TA-Lib
|
# Body/shadow ratio thresholds differ between ferro_ta and TA-Lib
|
||||||
"CDLHIGHWAVE": 0.65, # Shadow length threshold differs; 69% observed
|
"CDLHIGHWAVE": 0.65, # Shadow length threshold differs; 69% observed
|
||||||
"CDLLONGLEGGEDDOJI": 0.70, # Long-leg threshold differs; 75% observed
|
"CDLLONGLEGGEDDOJI": 0.70, # Long-leg threshold differs; 75% observed
|
||||||
"CDLSHORTLINE": 0.20, # Body-size cutoff definition completely differs; 25% observed
|
"CDLSHORTLINE": 0.20, # Body-size cutoff definition completely differs; 25% observed
|
||||||
"CDLSPINNINGTOP": 0.75, # Body ratio threshold differs; 78% observed
|
"CDLSPINNINGTOP": 0.75, # Body ratio threshold differs; 78% observed
|
||||||
"CDLDOJI": 0.85, # Shadow ratio precision differs; 86% observed
|
"CDLDOJI": 0.85, # Shadow ratio precision differs; 86% observed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -150,7 +150,9 @@ class TestEMA:
|
|||||||
ta = talib.EMA(CLOSE, timeperiod=5)
|
ta = talib.EMA(CLOSE, timeperiod=5)
|
||||||
# With 500 bars, compare last 30% with tighter tolerance
|
# With 500 bars, compare last 30% with tighter tolerance
|
||||||
tail_start = int(N * 0.7)
|
tail_start = int(N * 0.7)
|
||||||
assert np.allclose(ft[tail_start:], ta[tail_start:], atol=1e-5) # Tightened from 1e-3
|
assert np.allclose(
|
||||||
|
ft[tail_start:], ta[tail_start:], atol=1e-5
|
||||||
|
) # Tightened from 1e-3
|
||||||
|
|
||||||
def test_values_finite_and_reasonable(self):
|
def test_values_finite_and_reasonable(self):
|
||||||
ft = ferro_ta.EMA(CLOSE, timeperiod=5)
|
ft = ferro_ta.EMA(CLOSE, timeperiod=5)
|
||||||
@@ -266,7 +268,9 @@ class TestT3:
|
|||||||
ta = talib.T3(CLOSE, timeperiod=5)
|
ta = talib.T3(CLOSE, timeperiod=5)
|
||||||
# With 500 bars, use last 30% with tighter tolerance
|
# With 500 bars, use last 30% with tighter tolerance
|
||||||
tail_start = int(N * 0.7)
|
tail_start = int(N * 0.7)
|
||||||
assert np.allclose(ft[tail_start:], ta[tail_start:], atol=1e-3) # Tightened from 5e-2
|
assert np.allclose(
|
||||||
|
ft[tail_start:], ta[tail_start:], atol=1e-3
|
||||||
|
) # Tightened from 5e-2
|
||||||
|
|
||||||
|
|
||||||
class TestBBANDS:
|
class TestBBANDS:
|
||||||
@@ -781,7 +785,9 @@ class TestSTOCHRSI:
|
|||||||
assert abs(_nan_count(ft_k) - _nan_count(ta_k)) <= 2
|
assert abs(_nan_count(ft_k) - _nan_count(ta_k)) <= 2
|
||||||
|
|
||||||
def test_range_0_to_100(self):
|
def test_range_0_to_100(self):
|
||||||
ft_k, _ = ferro_ta.STOCHRSI(CLOSE, timeperiod=14, fastk_period=5, fastd_period=3)
|
ft_k, _ = ferro_ta.STOCHRSI(
|
||||||
|
CLOSE, timeperiod=14, fastk_period=5, fastd_period=3
|
||||||
|
)
|
||||||
finite = ft_k[~np.isnan(ft_k)]
|
finite = ft_k[~np.isnan(ft_k)]
|
||||||
# Allow small numerical tolerance for float boundaries
|
# Allow small numerical tolerance for float boundaries
|
||||||
assert all(-1e-9 <= v <= 100.0 + 1e-9 for v in finite)
|
assert all(-1e-9 <= v <= 100.0 + 1e-9 for v in finite)
|
||||||
@@ -834,6 +840,7 @@ class TestPPO:
|
|||||||
mask = _valid_mask(ppo, ta)
|
mask = _valid_mask(ppo, ta)
|
||||||
corr = np.corrcoef(ppo[mask], ta[mask])[0, 1]
|
corr = np.corrcoef(ppo[mask], ta[mask])[0, 1]
|
||||||
assert corr > 0.85
|
assert corr > 0.85
|
||||||
|
|
||||||
"""CMO — same NaN count and shape; values may differ slightly.
|
"""CMO — same NaN count and shape; values may differ slightly.
|
||||||
|
|
||||||
Both libraries compute the Chande Momentum Oscillator as
|
Both libraries compute the Chande Momentum Oscillator as
|
||||||
@@ -2034,9 +2041,7 @@ class TestHTTrendMode:
|
|||||||
mask = _valid_mask(ft, ta)
|
mask = _valid_mask(ft, ta)
|
||||||
if mask.sum() >= 5:
|
if mask.sum() >= 5:
|
||||||
agree = np.mean(ft[mask] == ta[mask])
|
agree = np.mean(ft[mask] == ta[mask])
|
||||||
assert agree >= 0.50, (
|
assert agree >= 0.50, f"HT_TRENDMODE agreement {agree:.2f} < 0.50"
|
||||||
f"HT_TRENDMODE agreement {agree:.2f} < 0.50"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -2046,24 +2051,66 @@ class TestHTTrendMode:
|
|||||||
|
|
||||||
# List of all candlestick patterns to test
|
# List of all candlestick patterns to test
|
||||||
ALL_CDL_PATTERNS = [
|
ALL_CDL_PATTERNS = [
|
||||||
"CDL2CROWS", "CDL3BLACKCROWS", "CDL3INSIDE", "CDL3LINESTRIKE",
|
"CDL2CROWS",
|
||||||
"CDL3OUTSIDE", "CDL3STARSINSOUTH", "CDL3WHITESOLDIERS",
|
"CDL3BLACKCROWS",
|
||||||
"CDLABANDONEDBABY", "CDLADVANCEBLOCK", "CDLBELTHOLD", "CDLBREAKAWAY",
|
"CDL3INSIDE",
|
||||||
"CDLCLOSINGMARUBOZU", "CDLCONCEALBABYSWALL", "CDLCOUNTERATTACK",
|
"CDL3LINESTRIKE",
|
||||||
"CDLDARKCLOUDCOVER", "CDLDOJI", "CDLDOJISTAR", "CDLDRAGONFLYDOJI",
|
"CDL3OUTSIDE",
|
||||||
"CDLENGULFING", "CDLEVENINGDOJISTAR", "CDLEVENINGSTAR",
|
"CDL3STARSINSOUTH",
|
||||||
"CDLGAPSIDESIDEWHITE", "CDLGRAVESTONEDOJI", "CDLHAMMER",
|
"CDL3WHITESOLDIERS",
|
||||||
"CDLHANGINGMAN", "CDLHARAMI", "CDLHARAMICROSS", "CDLHIGHWAVE",
|
"CDLABANDONEDBABY",
|
||||||
"CDLHIKKAKE", "CDLHIKKAKEMOD", "CDLHOMINGPIGEON",
|
"CDLADVANCEBLOCK",
|
||||||
"CDLIDENTICAL3CROWS", "CDLINNECK", "CDLINVERTEDHAMMER",
|
"CDLBELTHOLD",
|
||||||
"CDLKICKING", "CDLKICKINGBYLENGTH", "CDLLADDERBOTTOM",
|
"CDLBREAKAWAY",
|
||||||
"CDLLONGLEGGEDDOJI", "CDLLONGLINE", "CDLMARUBOZU",
|
"CDLCLOSINGMARUBOZU",
|
||||||
"CDLMATCHINGLOW", "CDLMATHOLD", "CDLMORNINGDOJISTAR",
|
"CDLCONCEALBABYSWALL",
|
||||||
"CDLMORNINGSTAR", "CDLONNECK", "CDLPIERCING", "CDLRICKSHAWMAN",
|
"CDLCOUNTERATTACK",
|
||||||
"CDLRISEFALL3METHODS", "CDLSEPARATINGLINES", "CDLSHOOTINGSTAR",
|
"CDLDARKCLOUDCOVER",
|
||||||
"CDLSHORTLINE", "CDLSPINNINGTOP", "CDLSTALLEDPATTERN",
|
"CDLDOJI",
|
||||||
"CDLSTICKSANDWICH", "CDLTAKURI", "CDLTASUKIGAP", "CDLTHRUSTING",
|
"CDLDOJISTAR",
|
||||||
"CDLTRISTAR", "CDLUNIQUE3RIVER", "CDLUPSIDEGAP2CROWS",
|
"CDLDRAGONFLYDOJI",
|
||||||
|
"CDLENGULFING",
|
||||||
|
"CDLEVENINGDOJISTAR",
|
||||||
|
"CDLEVENINGSTAR",
|
||||||
|
"CDLGAPSIDESIDEWHITE",
|
||||||
|
"CDLGRAVESTONEDOJI",
|
||||||
|
"CDLHAMMER",
|
||||||
|
"CDLHANGINGMAN",
|
||||||
|
"CDLHARAMI",
|
||||||
|
"CDLHARAMICROSS",
|
||||||
|
"CDLHIGHWAVE",
|
||||||
|
"CDLHIKKAKE",
|
||||||
|
"CDLHIKKAKEMOD",
|
||||||
|
"CDLHOMINGPIGEON",
|
||||||
|
"CDLIDENTICAL3CROWS",
|
||||||
|
"CDLINNECK",
|
||||||
|
"CDLINVERTEDHAMMER",
|
||||||
|
"CDLKICKING",
|
||||||
|
"CDLKICKINGBYLENGTH",
|
||||||
|
"CDLLADDERBOTTOM",
|
||||||
|
"CDLLONGLEGGEDDOJI",
|
||||||
|
"CDLLONGLINE",
|
||||||
|
"CDLMARUBOZU",
|
||||||
|
"CDLMATCHINGLOW",
|
||||||
|
"CDLMATHOLD",
|
||||||
|
"CDLMORNINGDOJISTAR",
|
||||||
|
"CDLMORNINGSTAR",
|
||||||
|
"CDLONNECK",
|
||||||
|
"CDLPIERCING",
|
||||||
|
"CDLRICKSHAWMAN",
|
||||||
|
"CDLRISEFALL3METHODS",
|
||||||
|
"CDLSEPARATINGLINES",
|
||||||
|
"CDLSHOOTINGSTAR",
|
||||||
|
"CDLSHORTLINE",
|
||||||
|
"CDLSPINNINGTOP",
|
||||||
|
"CDLSTALLEDPATTERN",
|
||||||
|
"CDLSTICKSANDWICH",
|
||||||
|
"CDLTAKURI",
|
||||||
|
"CDLTASUKIGAP",
|
||||||
|
"CDLTHRUSTING",
|
||||||
|
"CDLTRISTAR",
|
||||||
|
"CDLUNIQUE3RIVER",
|
||||||
|
"CDLUPSIDEGAP2CROWS",
|
||||||
"CDLXSIDEGAP3METHODS",
|
"CDLXSIDEGAP3METHODS",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
"""Unit tests for ferro_ta.indicators.cycle"""
|
"""Unit tests for ferro_ta.indicators.cycle"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
|
||||||
from ferro_ta.indicators.cycle import (
|
from ferro_ta.indicators.cycle import (
|
||||||
HT_DCPERIOD, HT_DCPHASE, HT_PHASOR, HT_SINE, HT_TRENDLINE, HT_TRENDMODE,
|
HT_DCPERIOD,
|
||||||
|
HT_DCPHASE,
|
||||||
|
HT_PHASOR,
|
||||||
|
HT_SINE,
|
||||||
|
HT_TRENDLINE,
|
||||||
|
HT_TRENDMODE,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -11,7 +17,7 @@ from ferro_ta.indicators.cycle import (
|
|||||||
|
|
||||||
N = 200
|
N = 200
|
||||||
t = np.linspace(0, 10 * np.pi, N)
|
t = np.linspace(0, 10 * np.pi, N)
|
||||||
SINE_CLOSE = 100 + 10 * np.sin(t) # clean sine wave
|
SINE_CLOSE = 100 + 10 * np.sin(t) # clean sine wave
|
||||||
|
|
||||||
|
|
||||||
def _warmup_end(arr):
|
def _warmup_end(arr):
|
||||||
@@ -24,6 +30,7 @@ def _warmup_end(arr):
|
|||||||
# HT_DCPERIOD
|
# HT_DCPERIOD
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestHT_DCPERIOD:
|
class TestHT_DCPERIOD:
|
||||||
def test_length(self):
|
def test_length(self):
|
||||||
result = HT_DCPERIOD(SINE_CLOSE)
|
result = HT_DCPERIOD(SINE_CLOSE)
|
||||||
@@ -53,6 +60,7 @@ class TestHT_DCPERIOD:
|
|||||||
# HT_DCPHASE
|
# HT_DCPHASE
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestHT_DCPHASE:
|
class TestHT_DCPHASE:
|
||||||
def test_length(self):
|
def test_length(self):
|
||||||
assert len(HT_DCPHASE(SINE_CLOSE)) == N
|
assert len(HT_DCPHASE(SINE_CLOSE)) == N
|
||||||
@@ -72,6 +80,7 @@ class TestHT_DCPHASE:
|
|||||||
# HT_PHASOR
|
# HT_PHASOR
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestHT_PHASOR:
|
class TestHT_PHASOR:
|
||||||
def test_returns_two_arrays(self):
|
def test_returns_two_arrays(self):
|
||||||
result = HT_PHASOR(SINE_CLOSE)
|
result = HT_PHASOR(SINE_CLOSE)
|
||||||
@@ -98,6 +107,7 @@ class TestHT_PHASOR:
|
|||||||
# HT_SINE
|
# HT_SINE
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestHT_SINE:
|
class TestHT_SINE:
|
||||||
def test_returns_two_arrays(self):
|
def test_returns_two_arrays(self):
|
||||||
result = HT_SINE(SINE_CLOSE)
|
result = HT_SINE(SINE_CLOSE)
|
||||||
@@ -130,6 +140,7 @@ class TestHT_SINE:
|
|||||||
# HT_TRENDLINE
|
# HT_TRENDLINE
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestHT_TRENDLINE:
|
class TestHT_TRENDLINE:
|
||||||
def test_length(self):
|
def test_length(self):
|
||||||
assert len(HT_TRENDLINE(SINE_CLOSE)) == N
|
assert len(HT_TRENDLINE(SINE_CLOSE)) == N
|
||||||
@@ -157,6 +168,7 @@ class TestHT_TRENDLINE:
|
|||||||
# HT_TRENDMODE
|
# HT_TRENDMODE
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestHT_TRENDMODE:
|
class TestHT_TRENDMODE:
|
||||||
def test_length(self):
|
def test_length(self):
|
||||||
assert len(HT_TRENDMODE(SINE_CLOSE)) == N
|
assert len(HT_TRENDMODE(SINE_CLOSE)) == N
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
"""Unit tests for ferro_ta.indicators.extended"""
|
"""Unit tests for ferro_ta.indicators.extended"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
|
||||||
from ferro_ta.indicators.extended import (
|
from ferro_ta.indicators.extended import (
|
||||||
VWAP, SUPERTREND, ICHIMOKU, DONCHIAN, PIVOT_POINTS,
|
CHANDELIER_EXIT,
|
||||||
KELTNER_CHANNELS, HULL_MA, CHANDELIER_EXIT, VWMA, CHOPPINESS_INDEX,
|
CHOPPINESS_INDEX,
|
||||||
|
DONCHIAN,
|
||||||
|
HULL_MA,
|
||||||
|
ICHIMOKU,
|
||||||
|
KELTNER_CHANNELS,
|
||||||
|
PIVOT_POINTS,
|
||||||
|
SUPERTREND,
|
||||||
|
VWAP,
|
||||||
|
VWMA,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -23,6 +32,7 @@ _VOL = RNG.uniform(1000, 5000, N)
|
|||||||
# VWAP
|
# VWAP
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestVWAP:
|
class TestVWAP:
|
||||||
def test_length(self):
|
def test_length(self):
|
||||||
result = VWAP(_H, _L, _C, _VOL)
|
result = VWAP(_H, _L, _C, _VOL)
|
||||||
@@ -46,6 +56,7 @@ class TestVWAP:
|
|||||||
# SUPERTREND
|
# SUPERTREND
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestSUPERTREND:
|
class TestSUPERTREND:
|
||||||
def test_returns_two_arrays(self):
|
def test_returns_two_arrays(self):
|
||||||
result = SUPERTREND(_H, _L, _C)
|
result = SUPERTREND(_H, _L, _C)
|
||||||
@@ -69,6 +80,7 @@ class TestSUPERTREND:
|
|||||||
# ICHIMOKU
|
# ICHIMOKU
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestICHIMOKU:
|
class TestICHIMOKU:
|
||||||
def test_returns_five_arrays(self):
|
def test_returns_five_arrays(self):
|
||||||
result = ICHIMOKU(_H, _L, _C)
|
result = ICHIMOKU(_H, _L, _C)
|
||||||
@@ -80,7 +92,9 @@ class TestICHIMOKU:
|
|||||||
assert len(arr) == N
|
assert len(arr) == N
|
||||||
|
|
||||||
def test_tenkan_warmup(self):
|
def test_tenkan_warmup(self):
|
||||||
tenkan, kijun, senkou_a, senkou_b, chikou = ICHIMOKU(_H, _L, _C, tenkan_period=9)
|
tenkan, kijun, senkou_a, senkou_b, chikou = ICHIMOKU(
|
||||||
|
_H, _L, _C, tenkan_period=9
|
||||||
|
)
|
||||||
assert np.all(np.isnan(tenkan[:8]))
|
assert np.all(np.isnan(tenkan[:8]))
|
||||||
|
|
||||||
def test_finite_after_warmup(self):
|
def test_finite_after_warmup(self):
|
||||||
@@ -94,6 +108,7 @@ class TestICHIMOKU:
|
|||||||
# DONCHIAN
|
# DONCHIAN
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestDONCHIAN:
|
class TestDONCHIAN:
|
||||||
def test_returns_three_arrays(self):
|
def test_returns_three_arrays(self):
|
||||||
result = DONCHIAN(_H, _L)
|
result = DONCHIAN(_H, _L)
|
||||||
@@ -126,6 +141,7 @@ class TestDONCHIAN:
|
|||||||
# PIVOT_POINTS
|
# PIVOT_POINTS
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestPIVOT_POINTS:
|
class TestPIVOT_POINTS:
|
||||||
def test_returns_five_arrays(self):
|
def test_returns_five_arrays(self):
|
||||||
result = PIVOT_POINTS(_H, _L, _C)
|
result = PIVOT_POINTS(_H, _L, _C)
|
||||||
@@ -138,7 +154,7 @@ class TestPIVOT_POINTS:
|
|||||||
|
|
||||||
def test_classic_pivot_formula(self):
|
def test_classic_pivot_formula(self):
|
||||||
# PP = (H + L + C) / 3
|
# PP = (H + L + C) / 3
|
||||||
pp, r1, s1, r2, s2 = PIVOT_POINTS(_H, _L, _C, method='classic')
|
pp, r1, s1, r2, s2 = PIVOT_POINTS(_H, _L, _C, method="classic")
|
||||||
valid = ~np.isnan(pp)
|
valid = ~np.isnan(pp)
|
||||||
expected_pp = (_H[:-1] + _L[:-1] + _C[:-1]) / 3.0
|
expected_pp = (_H[:-1] + _L[:-1] + _C[:-1]) / 3.0
|
||||||
np.testing.assert_allclose(pp[valid], expected_pp[valid[1:]], rtol=1e-6)
|
np.testing.assert_allclose(pp[valid], expected_pp[valid[1:]], rtol=1e-6)
|
||||||
@@ -152,6 +168,7 @@ class TestPIVOT_POINTS:
|
|||||||
# KELTNER_CHANNELS
|
# KELTNER_CHANNELS
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestKELTNER_CHANNELS:
|
class TestKELTNER_CHANNELS:
|
||||||
def test_returns_three_arrays(self):
|
def test_returns_three_arrays(self):
|
||||||
result = KELTNER_CHANNELS(_H, _L, _C)
|
result = KELTNER_CHANNELS(_H, _L, _C)
|
||||||
@@ -175,6 +192,7 @@ class TestKELTNER_CHANNELS:
|
|||||||
# HULL_MA
|
# HULL_MA
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestHULL_MA:
|
class TestHULL_MA:
|
||||||
def test_length(self):
|
def test_length(self):
|
||||||
assert len(HULL_MA(_C, timeperiod=16)) == N
|
assert len(HULL_MA(_C, timeperiod=16)) == N
|
||||||
@@ -199,6 +217,7 @@ class TestHULL_MA:
|
|||||||
# CHANDELIER_EXIT
|
# CHANDELIER_EXIT
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestCHANDELIER_EXIT:
|
class TestCHANDELIER_EXIT:
|
||||||
def test_returns_two_arrays(self):
|
def test_returns_two_arrays(self):
|
||||||
result = CHANDELIER_EXIT(_H, _L, _C)
|
result = CHANDELIER_EXIT(_H, _L, _C)
|
||||||
@@ -223,6 +242,7 @@ class TestCHANDELIER_EXIT:
|
|||||||
# VWMA
|
# VWMA
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestVWMA:
|
class TestVWMA:
|
||||||
def test_length(self):
|
def test_length(self):
|
||||||
assert len(VWMA(_C, _VOL, timeperiod=20)) == N
|
assert len(VWMA(_C, _VOL, timeperiod=20)) == N
|
||||||
@@ -241,6 +261,7 @@ class TestVWMA:
|
|||||||
vol = np.ones(N) * 1000.0
|
vol = np.ones(N) * 1000.0
|
||||||
vwma = VWMA(_C, vol, timeperiod=20)
|
vwma = VWMA(_C, vol, timeperiod=20)
|
||||||
from ferro_ta.indicators.overlap import SMA
|
from ferro_ta.indicators.overlap import SMA
|
||||||
|
|
||||||
sma = SMA(_C, timeperiod=20)
|
sma = SMA(_C, timeperiod=20)
|
||||||
valid = ~np.isnan(vwma) & ~np.isnan(sma)
|
valid = ~np.isnan(vwma) & ~np.isnan(sma)
|
||||||
np.testing.assert_allclose(vwma[valid], sma[valid], rtol=1e-8)
|
np.testing.assert_allclose(vwma[valid], sma[valid], rtol=1e-8)
|
||||||
@@ -250,6 +271,7 @@ class TestVWMA:
|
|||||||
# CHOPPINESS_INDEX
|
# CHOPPINESS_INDEX
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestCHOPPINESS_INDEX:
|
class TestCHOPPINESS_INDEX:
|
||||||
def test_length(self):
|
def test_length(self):
|
||||||
assert len(CHOPPINESS_INDEX(_H, _L, _C, timeperiod=14)) == N
|
assert len(CHOPPINESS_INDEX(_H, _L, _C, timeperiod=14)) == N
|
||||||
|
|||||||
@@ -1,10 +1,32 @@
|
|||||||
"""Unit tests for ferro_ta.indicators.math_ops"""
|
"""Unit tests for ferro_ta.indicators.math_ops"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
|
||||||
from ferro_ta.indicators.math_ops import (
|
from ferro_ta.indicators.math_ops import (
|
||||||
ADD, SUB, MULT, DIV, SUM, MAX, MIN, MAXINDEX, MININDEX,
|
ACOS,
|
||||||
ACOS, ASIN, ATAN, CEIL, COS, COSH, EXP, FLOOR,
|
ADD,
|
||||||
LN, LOG10, SIN, SINH, SQRT, TAN, TANH,
|
ASIN,
|
||||||
|
ATAN,
|
||||||
|
CEIL,
|
||||||
|
COS,
|
||||||
|
COSH,
|
||||||
|
DIV,
|
||||||
|
EXP,
|
||||||
|
FLOOR,
|
||||||
|
LN,
|
||||||
|
LOG10,
|
||||||
|
MAX,
|
||||||
|
MAXINDEX,
|
||||||
|
MIN,
|
||||||
|
MININDEX,
|
||||||
|
MULT,
|
||||||
|
SIN,
|
||||||
|
SINH,
|
||||||
|
SQRT,
|
||||||
|
SUB,
|
||||||
|
SUM,
|
||||||
|
TAN,
|
||||||
|
TANH,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -14,17 +36,18 @@ from ferro_ta.indicators.math_ops import (
|
|||||||
A3 = np.array([1.0, 2.0, 3.0])
|
A3 = np.array([1.0, 2.0, 3.0])
|
||||||
B3 = np.array([4.0, 5.0, 6.0])
|
B3 = np.array([4.0, 5.0, 6.0])
|
||||||
TRIG = np.array([0.0, np.pi / 6, np.pi / 4, np.pi / 3, np.pi / 2])
|
TRIG = np.array([0.0, np.pi / 6, np.pi / 4, np.pi / 3, np.pi / 2])
|
||||||
UNIT = np.array([0.0, 0.25, 0.5, 0.75, 1.0]) # values in [0,1] for ASIN/ACOS
|
UNIT = np.array([0.0, 0.25, 0.5, 0.75, 1.0]) # values in [0,1] for ASIN/ACOS
|
||||||
|
|
||||||
RNG = np.random.default_rng(17)
|
RNG = np.random.default_rng(17)
|
||||||
N = 100
|
N = 100
|
||||||
_ARR = 1.0 + RNG.random(N) * 9.0 # positive values in (1, 10]
|
_ARR = 1.0 + RNG.random(N) * 9.0 # positive values in (1, 10]
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# ADD
|
# ADD
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestADD:
|
class TestADD:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
result = ADD(A3, B3)
|
result = ADD(A3, B3)
|
||||||
@@ -41,6 +64,7 @@ class TestADD:
|
|||||||
# SUB
|
# SUB
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestSUB:
|
class TestSUB:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
result = SUB(B3, A3)
|
result = SUB(B3, A3)
|
||||||
@@ -54,6 +78,7 @@ class TestSUB:
|
|||||||
# MULT
|
# MULT
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMULT:
|
class TestMULT:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
result = MULT(A3, B3)
|
result = MULT(A3, B3)
|
||||||
@@ -70,6 +95,7 @@ class TestMULT:
|
|||||||
# DIV
|
# DIV
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestDIV:
|
class TestDIV:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
result = DIV(B3, A3)
|
result = DIV(B3, A3)
|
||||||
@@ -86,6 +112,7 @@ class TestDIV:
|
|||||||
# SUM
|
# SUM
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestSUM:
|
class TestSUM:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
arr = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
|
arr = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
|
||||||
@@ -106,6 +133,7 @@ class TestSUM:
|
|||||||
# MAX
|
# MAX
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMAX:
|
class TestMAX:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
arr = np.array([1.0, 3.0, 2.0, 5.0, 4.0])
|
arr = np.array([1.0, 3.0, 2.0, 5.0, 4.0])
|
||||||
@@ -127,6 +155,7 @@ class TestMAX:
|
|||||||
# MIN
|
# MIN
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMIN:
|
class TestMIN:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
arr = np.array([5.0, 3.0, 4.0, 1.0, 2.0])
|
arr = np.array([5.0, 3.0, 4.0, 1.0, 2.0])
|
||||||
@@ -143,6 +172,7 @@ class TestMIN:
|
|||||||
# MAXINDEX
|
# MAXINDEX
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMAXINDEX:
|
class TestMAXINDEX:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
arr = np.array([1.0, 5.0, 3.0, 2.0, 4.0])
|
arr = np.array([1.0, 5.0, 3.0, 2.0, 4.0])
|
||||||
@@ -162,6 +192,7 @@ class TestMAXINDEX:
|
|||||||
# MININDEX
|
# MININDEX
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMININDEX:
|
class TestMININDEX:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
arr = np.array([5.0, 1.0, 3.0, 2.0, 4.0])
|
arr = np.array([5.0, 1.0, 3.0, 2.0, 4.0])
|
||||||
@@ -181,6 +212,7 @@ class TestMININDEX:
|
|||||||
# Trig functions
|
# Trig functions
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestSIN:
|
class TestSIN:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
angles = np.array([0.0, np.pi / 2, np.pi])
|
angles = np.array([0.0, np.pi / 2, np.pi])
|
||||||
@@ -236,6 +268,7 @@ class TestTANH:
|
|||||||
# Rounding/exponential
|
# Rounding/exponential
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestCEIL:
|
class TestCEIL:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
arr = np.array([1.1, 2.5, 3.9, -0.5])
|
arr = np.array([1.1, 2.5, 3.9, -0.5])
|
||||||
|
|||||||
@@ -1,12 +1,35 @@
|
|||||||
"""Unit tests for ferro_ta.indicators.momentum"""
|
"""Unit tests for ferro_ta.indicators.momentum"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
|
||||||
from ferro_ta.indicators.momentum import (
|
from ferro_ta.indicators.momentum import (
|
||||||
RSI, STOCH, STOCHF, STOCHRSI,
|
ADX,
|
||||||
ADX, ADXR, CCI, WILLR, AROON, AROONOSC,
|
ADXR,
|
||||||
MFI, MOM, ROC, ROCP, ROCR, ROCR100,
|
APO,
|
||||||
CMO, DX, MINUS_DI, MINUS_DM, PLUS_DI, PLUS_DM,
|
AROON,
|
||||||
PPO, APO, TRIX, ULTOSC, BOP,
|
AROONOSC,
|
||||||
|
BOP,
|
||||||
|
CCI,
|
||||||
|
CMO,
|
||||||
|
DX,
|
||||||
|
MFI,
|
||||||
|
MINUS_DI,
|
||||||
|
MINUS_DM,
|
||||||
|
MOM,
|
||||||
|
PLUS_DI,
|
||||||
|
PLUS_DM,
|
||||||
|
PPO,
|
||||||
|
ROC,
|
||||||
|
ROCP,
|
||||||
|
ROCR,
|
||||||
|
ROCR100,
|
||||||
|
RSI,
|
||||||
|
STOCH,
|
||||||
|
STOCHF,
|
||||||
|
STOCHRSI,
|
||||||
|
TRIX,
|
||||||
|
ULTOSC,
|
||||||
|
WILLR,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -32,6 +55,7 @@ SMALL5_V = np.array([1000.0, 2000.0, 3000.0, 4000.0, 5000.0])
|
|||||||
# RSI
|
# RSI
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestRSI:
|
class TestRSI:
|
||||||
def test_nan_warmup(self):
|
def test_nan_warmup(self):
|
||||||
result = RSI(_CLOSE, timeperiod=14)
|
result = RSI(_CLOSE, timeperiod=14)
|
||||||
@@ -50,6 +74,7 @@ class TestRSI:
|
|||||||
# STOCH
|
# STOCH
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestSTOCH:
|
class TestSTOCH:
|
||||||
def test_returns_two_arrays(self):
|
def test_returns_two_arrays(self):
|
||||||
result = STOCH(_HIGH, _LOW, _CLOSE)
|
result = STOCH(_HIGH, _LOW, _CLOSE)
|
||||||
@@ -70,6 +95,7 @@ class TestSTOCH:
|
|||||||
# STOCHF
|
# STOCHF
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestSTOCHF:
|
class TestSTOCHF:
|
||||||
def test_returns_two_arrays(self):
|
def test_returns_two_arrays(self):
|
||||||
result = STOCHF(_HIGH, _LOW, _CLOSE)
|
result = STOCHF(_HIGH, _LOW, _CLOSE)
|
||||||
@@ -99,6 +125,7 @@ class TestSTOCHF:
|
|||||||
# STOCHRSI
|
# STOCHRSI
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestSTOCHRSI:
|
class TestSTOCHRSI:
|
||||||
def test_returns_two_arrays(self):
|
def test_returns_two_arrays(self):
|
||||||
result = STOCHRSI(_CLOSE)
|
result = STOCHRSI(_CLOSE)
|
||||||
@@ -119,6 +146,7 @@ class TestSTOCHRSI:
|
|||||||
# ADX
|
# ADX
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestADX:
|
class TestADX:
|
||||||
def test_nan_warmup(self):
|
def test_nan_warmup(self):
|
||||||
result = ADX(_HIGH, _LOW, _CLOSE, timeperiod=14)
|
result = ADX(_HIGH, _LOW, _CLOSE, timeperiod=14)
|
||||||
@@ -137,6 +165,7 @@ class TestADX:
|
|||||||
# ADXR
|
# ADXR
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestADXR:
|
class TestADXR:
|
||||||
def test_length(self):
|
def test_length(self):
|
||||||
assert len(ADXR(_HIGH, _LOW, _CLOSE, 14)) == N
|
assert len(ADXR(_HIGH, _LOW, _CLOSE, 14)) == N
|
||||||
@@ -151,6 +180,7 @@ class TestADXR:
|
|||||||
# CCI
|
# CCI
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestCCI:
|
class TestCCI:
|
||||||
def test_known_constant_mean_dev(self):
|
def test_known_constant_mean_dev(self):
|
||||||
# Constant typical price → CCI = 0 after warmup
|
# Constant typical price → CCI = 0 after warmup
|
||||||
@@ -181,6 +211,7 @@ class TestCCI:
|
|||||||
# WILLR
|
# WILLR
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestWILLR:
|
class TestWILLR:
|
||||||
def test_range(self):
|
def test_range(self):
|
||||||
result = WILLR(_HIGH, _LOW, _CLOSE, 14)
|
result = WILLR(_HIGH, _LOW, _CLOSE, 14)
|
||||||
@@ -195,6 +226,7 @@ class TestWILLR:
|
|||||||
# AROON
|
# AROON
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestAROON:
|
class TestAROON:
|
||||||
def test_returns_two_arrays(self):
|
def test_returns_two_arrays(self):
|
||||||
result = AROON(_HIGH, _LOW, 14)
|
result = AROON(_HIGH, _LOW, 14)
|
||||||
@@ -215,6 +247,7 @@ class TestAROON:
|
|||||||
# AROONOSC
|
# AROONOSC
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestAROONOSC:
|
class TestAROONOSC:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
h = np.array([12.0, 13.0, 14.0, 15.0, 16.0])
|
h = np.array([12.0, 13.0, 14.0, 15.0, 16.0])
|
||||||
@@ -242,6 +275,7 @@ class TestAROONOSC:
|
|||||||
# MFI
|
# MFI
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMFI:
|
class TestMFI:
|
||||||
def test_range(self):
|
def test_range(self):
|
||||||
result = MFI(_HIGH, _LOW, _CLOSE, _VOL, 14)
|
result = MFI(_HIGH, _LOW, _CLOSE, _VOL, 14)
|
||||||
@@ -271,6 +305,7 @@ class TestMFI:
|
|||||||
# MOM
|
# MOM
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMOM:
|
class TestMOM:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
result = MOM(SMALL5, timeperiod=2)
|
result = MOM(SMALL5, timeperiod=2)
|
||||||
@@ -286,6 +321,7 @@ class TestMOM:
|
|||||||
# ROC
|
# ROC
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestROC:
|
class TestROC:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
arr = np.array([10.0, 11.0, 12.0, 13.0, 14.0])
|
arr = np.array([10.0, 11.0, 12.0, 13.0, 14.0])
|
||||||
@@ -301,6 +337,7 @@ class TestROC:
|
|||||||
# ROCP
|
# ROCP
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestROCP:
|
class TestROCP:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
arr = np.array([10.0, 11.0, 12.0, 13.0, 14.0])
|
arr = np.array([10.0, 11.0, 12.0, 13.0, 14.0])
|
||||||
@@ -316,6 +353,7 @@ class TestROCP:
|
|||||||
# ROCR
|
# ROCR
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestROCR:
|
class TestROCR:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
arr = np.array([10.0, 11.0, 12.0, 13.0, 14.0])
|
arr = np.array([10.0, 11.0, 12.0, 13.0, 14.0])
|
||||||
@@ -336,6 +374,7 @@ class TestROCR:
|
|||||||
# ROCR100
|
# ROCR100
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestROCR100:
|
class TestROCR100:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
arr = np.array([10.0, 11.0, 12.0, 13.0, 14.0])
|
arr = np.array([10.0, 11.0, 12.0, 13.0, 14.0])
|
||||||
@@ -357,6 +396,7 @@ class TestROCR100:
|
|||||||
# CMO
|
# CMO
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestCMO:
|
class TestCMO:
|
||||||
def test_range(self):
|
def test_range(self):
|
||||||
result = CMO(_CLOSE, 14)
|
result = CMO(_CLOSE, 14)
|
||||||
@@ -371,6 +411,7 @@ class TestCMO:
|
|||||||
# DX
|
# DX
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestDX:
|
class TestDX:
|
||||||
def test_range(self):
|
def test_range(self):
|
||||||
result = DX(_HIGH, _LOW, _CLOSE, 14)
|
result = DX(_HIGH, _LOW, _CLOSE, 14)
|
||||||
@@ -385,6 +426,7 @@ class TestDX:
|
|||||||
# MINUS_DI / MINUS_DM
|
# MINUS_DI / MINUS_DM
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMINUS:
|
class TestMINUS:
|
||||||
def test_minus_di_range(self):
|
def test_minus_di_range(self):
|
||||||
result = MINUS_DI(_HIGH, _LOW, _CLOSE, 14)
|
result = MINUS_DI(_HIGH, _LOW, _CLOSE, 14)
|
||||||
@@ -405,6 +447,7 @@ class TestMINUS:
|
|||||||
# PLUS_DI / PLUS_DM
|
# PLUS_DI / PLUS_DM
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestPLUS:
|
class TestPLUS:
|
||||||
def test_plus_di_range(self):
|
def test_plus_di_range(self):
|
||||||
result = PLUS_DI(_HIGH, _LOW, _CLOSE, 14)
|
result = PLUS_DI(_HIGH, _LOW, _CLOSE, 14)
|
||||||
@@ -425,6 +468,7 @@ class TestPLUS:
|
|||||||
# PPO
|
# PPO
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestPPO:
|
class TestPPO:
|
||||||
def test_returns_three_arrays(self):
|
def test_returns_three_arrays(self):
|
||||||
result = PPO(_CLOSE, fastperiod=12, slowperiod=26)
|
result = PPO(_CLOSE, fastperiod=12, slowperiod=26)
|
||||||
@@ -448,6 +492,7 @@ class TestPPO:
|
|||||||
# APO
|
# APO
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestAPO:
|
class TestAPO:
|
||||||
def test_known_direction(self):
|
def test_known_direction(self):
|
||||||
# Rising close → fast EMA > slow EMA → APO > 0 after warmup
|
# Rising close → fast EMA > slow EMA → APO > 0 after warmup
|
||||||
@@ -468,6 +513,7 @@ class TestAPO:
|
|||||||
# TRIX
|
# TRIX
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestTRIX:
|
class TestTRIX:
|
||||||
def test_length(self):
|
def test_length(self):
|
||||||
assert len(TRIX(_CLOSE, 10)) == N
|
assert len(TRIX(_CLOSE, 10)) == N
|
||||||
@@ -494,6 +540,7 @@ class TestTRIX:
|
|||||||
# BOP
|
# BOP
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestBOP:
|
class TestBOP:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
o = np.array([10.0, 11.0])
|
o = np.array([10.0, 11.0])
|
||||||
@@ -526,6 +573,7 @@ class TestBOP:
|
|||||||
# ULTOSC
|
# ULTOSC
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestULTOSC:
|
class TestULTOSC:
|
||||||
def test_range(self):
|
def test_range(self):
|
||||||
result = ULTOSC(_HIGH, _LOW, _CLOSE, 7, 14, 28)
|
result = ULTOSC(_HIGH, _LOW, _CLOSE, 7, 14, 28)
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
"""Unit tests for ferro_ta.indicators.overlap"""
|
"""Unit tests for ferro_ta.indicators.overlap"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
|
||||||
from ferro_ta.indicators.overlap import (
|
from ferro_ta.indicators.overlap import (
|
||||||
SMA, EMA, WMA, DEMA, TEMA, TRIMA, KAMA, T3, MA,
|
BBANDS,
|
||||||
MACD, MACDFIX, MACDEXT, BBANDS, SAR, SAREXT,
|
DEMA,
|
||||||
MAMA, MAVP, MIDPOINT, MIDPRICE,
|
EMA,
|
||||||
|
KAMA,
|
||||||
|
MA,
|
||||||
|
MACD,
|
||||||
|
MACDEXT,
|
||||||
|
MACDFIX,
|
||||||
|
MAMA,
|
||||||
|
MAVP,
|
||||||
|
MIDPOINT,
|
||||||
|
MIDPRICE,
|
||||||
|
SAR,
|
||||||
|
SAREXT,
|
||||||
|
SMA,
|
||||||
|
T3,
|
||||||
|
TEMA,
|
||||||
|
TRIMA,
|
||||||
|
WMA,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -26,6 +43,7 @@ SMALL5_LOW = np.array([9.0, 10.0, 11.0, 12.0, 13.0])
|
|||||||
# SMA
|
# SMA
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestSMA:
|
class TestSMA:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
result = SMA(SMALL5, timeperiod=3)
|
result = SMA(SMALL5, timeperiod=3)
|
||||||
@@ -50,6 +68,7 @@ class TestSMA:
|
|||||||
# EMA
|
# EMA
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestEMA:
|
class TestEMA:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
# k = 2/(3+1) = 0.5; seed = SMA(3) = 11.0
|
# k = 2/(3+1) = 0.5; seed = SMA(3) = 11.0
|
||||||
@@ -80,13 +99,14 @@ class TestEMA:
|
|||||||
# WMA
|
# WMA
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestWMA:
|
class TestWMA:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
arr = np.arange(1.0, 6.0)
|
arr = np.arange(1.0, 6.0)
|
||||||
result = WMA(arr, timeperiod=3)
|
result = WMA(arr, timeperiod=3)
|
||||||
# weights 1,2,3 / 6
|
# weights 1,2,3 / 6
|
||||||
expected_2 = (1*1 + 2*2 + 3*3) / 6.0 # 14/6
|
expected_2 = (1 * 1 + 2 * 2 + 3 * 3) / 6.0 # 14/6
|
||||||
expected_3 = (1*2 + 2*3 + 3*4) / 6.0 # 20/6
|
expected_3 = (1 * 2 + 2 * 3 + 3 * 4) / 6.0 # 20/6
|
||||||
assert np.isnan(result[0]) and np.isnan(result[1])
|
assert np.isnan(result[0]) and np.isnan(result[1])
|
||||||
np.testing.assert_allclose(result[2], expected_2, rtol=1e-10)
|
np.testing.assert_allclose(result[2], expected_2, rtol=1e-10)
|
||||||
np.testing.assert_allclose(result[3], expected_3, rtol=1e-10)
|
np.testing.assert_allclose(result[3], expected_3, rtol=1e-10)
|
||||||
@@ -103,10 +123,11 @@ class TestWMA:
|
|||||||
# DEMA
|
# DEMA
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestDEMA:
|
class TestDEMA:
|
||||||
def test_nan_warmup(self):
|
def test_nan_warmup(self):
|
||||||
result = DEMA(_CLOSE, timeperiod=5)
|
result = DEMA(_CLOSE, timeperiod=5)
|
||||||
assert np.all(np.isnan(result[:8])) # DEMA needs 2*(tp-1) bars
|
assert np.all(np.isnan(result[:8])) # DEMA needs 2*(tp-1) bars
|
||||||
|
|
||||||
def test_length(self):
|
def test_length(self):
|
||||||
assert len(DEMA(_CLOSE, 5)) == N
|
assert len(DEMA(_CLOSE, 5)) == N
|
||||||
@@ -131,6 +152,7 @@ class TestDEMA:
|
|||||||
# TEMA
|
# TEMA
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestTEMA:
|
class TestTEMA:
|
||||||
def test_nan_warmup(self):
|
def test_nan_warmup(self):
|
||||||
result = TEMA(_CLOSE, timeperiod=5)
|
result = TEMA(_CLOSE, timeperiod=5)
|
||||||
@@ -150,6 +172,7 @@ class TestTEMA:
|
|||||||
# TRIMA
|
# TRIMA
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestTRIMA:
|
class TestTRIMA:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
arr = np.arange(1.0, 11.0)
|
arr = np.arange(1.0, 11.0)
|
||||||
@@ -171,6 +194,7 @@ class TestTRIMA:
|
|||||||
# KAMA
|
# KAMA
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestKAMA:
|
class TestKAMA:
|
||||||
def test_nan_warmup(self):
|
def test_nan_warmup(self):
|
||||||
result = KAMA(_CLOSE, timeperiod=10)
|
result = KAMA(_CLOSE, timeperiod=10)
|
||||||
@@ -195,6 +219,7 @@ class TestKAMA:
|
|||||||
# T3
|
# T3
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestT3:
|
class TestT3:
|
||||||
def test_nan_warmup(self):
|
def test_nan_warmup(self):
|
||||||
arr = np.linspace(10.0, 30.0, 100)
|
arr = np.linspace(10.0, 30.0, 100)
|
||||||
@@ -223,6 +248,7 @@ class TestT3:
|
|||||||
# MA
|
# MA
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMA:
|
class TestMA:
|
||||||
def test_default_is_sma(self):
|
def test_default_is_sma(self):
|
||||||
result_ma = MA(_CLOSE, timeperiod=10, matype=0)
|
result_ma = MA(_CLOSE, timeperiod=10, matype=0)
|
||||||
@@ -242,6 +268,7 @@ class TestMA:
|
|||||||
# MACD
|
# MACD
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMACD:
|
class TestMACD:
|
||||||
def test_returns_three_arrays(self):
|
def test_returns_three_arrays(self):
|
||||||
result = MACD(_CLOSE, 12, 26, 9)
|
result = MACD(_CLOSE, 12, 26, 9)
|
||||||
@@ -266,6 +293,7 @@ class TestMACD:
|
|||||||
# MACDFIX
|
# MACDFIX
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMACDFIX:
|
class TestMACDFIX:
|
||||||
def test_returns_three_arrays(self):
|
def test_returns_three_arrays(self):
|
||||||
result = MACDFIX(_CLOSE)
|
result = MACDFIX(_CLOSE)
|
||||||
@@ -285,6 +313,7 @@ class TestMACDFIX:
|
|||||||
# MACDEXT
|
# MACDEXT
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMACDEXT:
|
class TestMACDEXT:
|
||||||
def test_returns_three_arrays(self):
|
def test_returns_three_arrays(self):
|
||||||
result = MACDEXT(_CLOSE)
|
result = MACDEXT(_CLOSE)
|
||||||
@@ -303,6 +332,7 @@ class TestMACDEXT:
|
|||||||
# BBANDS
|
# BBANDS
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestBBANDS:
|
class TestBBANDS:
|
||||||
def test_returns_three_arrays(self):
|
def test_returns_three_arrays(self):
|
||||||
result = BBANDS(_CLOSE, 20)
|
result = BBANDS(_CLOSE, 20)
|
||||||
@@ -331,6 +361,7 @@ class TestBBANDS:
|
|||||||
# SAR
|
# SAR
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestSAR:
|
class TestSAR:
|
||||||
def test_length(self):
|
def test_length(self):
|
||||||
result = SAR(_HIGH, _LOW)
|
result = SAR(_HIGH, _LOW)
|
||||||
@@ -349,6 +380,7 @@ class TestSAR:
|
|||||||
# SAREXT
|
# SAREXT
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestSAREXT:
|
class TestSAREXT:
|
||||||
def test_length(self):
|
def test_length(self):
|
||||||
result = SAREXT(_HIGH, _LOW)
|
result = SAREXT(_HIGH, _LOW)
|
||||||
@@ -367,6 +399,7 @@ class TestSAREXT:
|
|||||||
# MAMA
|
# MAMA
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMAMA:
|
class TestMAMA:
|
||||||
def test_returns_two_arrays(self):
|
def test_returns_two_arrays(self):
|
||||||
result = MAMA(_CLOSE)
|
result = MAMA(_CLOSE)
|
||||||
@@ -393,6 +426,7 @@ class TestMAMA:
|
|||||||
# MAVP
|
# MAVP
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMAVP:
|
class TestMAVP:
|
||||||
def test_length(self):
|
def test_length(self):
|
||||||
arr = np.linspace(10.0, 30.0, 50)
|
arr = np.linspace(10.0, 30.0, 50)
|
||||||
@@ -412,6 +446,7 @@ class TestMAVP:
|
|||||||
# MIDPOINT
|
# MIDPOINT
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMIDPOINT:
|
class TestMIDPOINT:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
arr = np.array([10.0, 12.0, 14.0, 16.0, 18.0])
|
arr = np.array([10.0, 12.0, 14.0, 16.0, 18.0])
|
||||||
@@ -433,6 +468,7 @@ class TestMIDPOINT:
|
|||||||
# MIDPRICE
|
# MIDPRICE
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMIDPRICE:
|
class TestMIDPRICE:
|
||||||
def test_known_values(self):
|
def test_known_values(self):
|
||||||
result = MIDPRICE(SMALL5_HIGH, SMALL5_LOW, timeperiod=3)
|
result = MIDPRICE(SMALL5_HIGH, SMALL5_LOW, timeperiod=3)
|
||||||
|
|||||||
@@ -1,20 +1,70 @@
|
|||||||
"""Unit tests for ferro_ta.indicators.pattern (CDL* functions)"""
|
"""Unit tests for ferro_ta.indicators.pattern (CDL* functions)"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ferro_ta.indicators.pattern import (
|
from ferro_ta.indicators.pattern import (
|
||||||
CDL2CROWS, CDL3BLACKCROWS, CDL3INSIDE, CDL3LINESTRIKE, CDL3OUTSIDE,
|
CDL2CROWS,
|
||||||
CDL3STARSINSOUTH, CDL3WHITESOLDIERS, CDLABANDONEDBABY, CDLADVANCEBLOCK,
|
CDL3BLACKCROWS,
|
||||||
CDLBELTHOLD, CDLBREAKAWAY, CDLCLOSINGMARUBOZU, CDLCONCEALBABYSWALL,
|
CDL3INSIDE,
|
||||||
CDLCOUNTERATTACK, CDLDARKCLOUDCOVER, CDLDOJI, CDLDOJISTAR, CDLDRAGONFLYDOJI,
|
CDL3LINESTRIKE,
|
||||||
CDLENGULFING, CDLEVENINGDOJISTAR, CDLEVENINGSTAR, CDLGAPSIDESIDEWHITE,
|
CDL3OUTSIDE,
|
||||||
CDLGRAVESTONEDOJI, CDLHAMMER, CDLHANGINGMAN, CDLHARAMI, CDLHARAMICROSS,
|
CDL3STARSINSOUTH,
|
||||||
CDLHIGHWAVE, CDLHIKKAKE, CDLHIKKAKEMOD, CDLHOMINGPIGEON, CDLIDENTICAL3CROWS,
|
CDL3WHITESOLDIERS,
|
||||||
CDLINNECK, CDLINVERTEDHAMMER, CDLKICKING, CDLKICKINGBYLENGTH, CDLLADDERBOTTOM,
|
CDLABANDONEDBABY,
|
||||||
CDLLONGLEGGEDDOJI, CDLLONGLINE, CDLMARUBOZU, CDLMATCHINGLOW, CDLMATHOLD,
|
CDLADVANCEBLOCK,
|
||||||
CDLMORNINGDOJISTAR, CDLMORNINGSTAR, CDLONNECK, CDLPIERCING, CDLRICKSHAWMAN,
|
CDLBELTHOLD,
|
||||||
CDLRISEFALL3METHODS, CDLSEPARATINGLINES, CDLSHOOTINGSTAR, CDLSHORTLINE,
|
CDLBREAKAWAY,
|
||||||
CDLSPINNINGTOP, CDLSTALLEDPATTERN, CDLSTICKSANDWICH, CDLTAKURI, CDLTASUKIGAP,
|
CDLCLOSINGMARUBOZU,
|
||||||
CDLTHRUSTING, CDLTRISTAR, CDLUNIQUE3RIVER, CDLUPSIDEGAP2CROWS, CDLXSIDEGAP3METHODS,
|
CDLCONCEALBABYSWALL,
|
||||||
|
CDLCOUNTERATTACK,
|
||||||
|
CDLDARKCLOUDCOVER,
|
||||||
|
CDLDOJI,
|
||||||
|
CDLDOJISTAR,
|
||||||
|
CDLDRAGONFLYDOJI,
|
||||||
|
CDLENGULFING,
|
||||||
|
CDLEVENINGDOJISTAR,
|
||||||
|
CDLEVENINGSTAR,
|
||||||
|
CDLGAPSIDESIDEWHITE,
|
||||||
|
CDLGRAVESTONEDOJI,
|
||||||
|
CDLHAMMER,
|
||||||
|
CDLHANGINGMAN,
|
||||||
|
CDLHARAMI,
|
||||||
|
CDLHARAMICROSS,
|
||||||
|
CDLHIGHWAVE,
|
||||||
|
CDLHIKKAKE,
|
||||||
|
CDLHIKKAKEMOD,
|
||||||
|
CDLHOMINGPIGEON,
|
||||||
|
CDLIDENTICAL3CROWS,
|
||||||
|
CDLINNECK,
|
||||||
|
CDLINVERTEDHAMMER,
|
||||||
|
CDLKICKING,
|
||||||
|
CDLKICKINGBYLENGTH,
|
||||||
|
CDLLADDERBOTTOM,
|
||||||
|
CDLLONGLEGGEDDOJI,
|
||||||
|
CDLLONGLINE,
|
||||||
|
CDLMARUBOZU,
|
||||||
|
CDLMATCHINGLOW,
|
||||||
|
CDLMATHOLD,
|
||||||
|
CDLMORNINGDOJISTAR,
|
||||||
|
CDLMORNINGSTAR,
|
||||||
|
CDLONNECK,
|
||||||
|
CDLPIERCING,
|
||||||
|
CDLRICKSHAWMAN,
|
||||||
|
CDLRISEFALL3METHODS,
|
||||||
|
CDLSEPARATINGLINES,
|
||||||
|
CDLSHOOTINGSTAR,
|
||||||
|
CDLSHORTLINE,
|
||||||
|
CDLSPINNINGTOP,
|
||||||
|
CDLSTALLEDPATTERN,
|
||||||
|
CDLSTICKSANDWICH,
|
||||||
|
CDLTAKURI,
|
||||||
|
CDLTASUKIGAP,
|
||||||
|
CDLTHRUSTING,
|
||||||
|
CDLTRISTAR,
|
||||||
|
CDLUNIQUE3RIVER,
|
||||||
|
CDLUPSIDEGAP2CROWS,
|
||||||
|
CDLXSIDEGAP3METHODS,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -98,6 +148,7 @@ ALL_CDL = [
|
|||||||
# Parametrised tests: all CDL patterns
|
# Parametrised tests: all CDL patterns
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("name,fn", ALL_CDL)
|
@pytest.mark.parametrize("name,fn", ALL_CDL)
|
||||||
def test_cdl_output_length(name, fn):
|
def test_cdl_output_length(name, fn):
|
||||||
result = fn(_O, _H, _L, _C)
|
result = fn(_O, _H, _L, _C)
|
||||||
@@ -107,8 +158,9 @@ def test_cdl_output_length(name, fn):
|
|||||||
@pytest.mark.parametrize("name,fn", ALL_CDL)
|
@pytest.mark.parametrize("name,fn", ALL_CDL)
|
||||||
def test_cdl_values_in_valid_set(name, fn):
|
def test_cdl_values_in_valid_set(name, fn):
|
||||||
result = fn(_O, _H, _L, _C)
|
result = fn(_O, _H, _L, _C)
|
||||||
assert np.all(np.isin(result, [-100, 0, 100])), \
|
assert np.all(np.isin(result, [-100, 0, 100])), (
|
||||||
f"{name}: unexpected values {np.unique(result)}"
|
f"{name}: unexpected values {np.unique(result)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("name,fn", ALL_CDL)
|
@pytest.mark.parametrize("name,fn", ALL_CDL)
|
||||||
@@ -121,6 +173,7 @@ def test_cdl_no_nan(name, fn):
|
|||||||
# Specific tests for previously untested patterns
|
# Specific tests for previously untested patterns
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestCDLSPINNINGTOP:
|
class TestCDLSPINNINGTOP:
|
||||||
def test_detects_pattern(self):
|
def test_detects_pattern(self):
|
||||||
# Spinning top: small body, long upper and lower shadows
|
# Spinning top: small body, long upper and lower shadows
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Unit tests for ferro_ta.indicators.price_transform"""
|
"""Unit tests for ferro_ta.indicators.price_transform"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
|
||||||
from ferro_ta.indicators.price_transform import AVGPRICE, MEDPRICE, TYPPRICE, WCLPRICE
|
from ferro_ta.indicators.price_transform import AVGPRICE, MEDPRICE, TYPPRICE, WCLPRICE
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -17,6 +18,7 @@ C = np.array([11.0, 12.0, 13.0, 14.0])
|
|||||||
# AVGPRICE
|
# AVGPRICE
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestAVGPRICE:
|
class TestAVGPRICE:
|
||||||
def test_known_formula(self):
|
def test_known_formula(self):
|
||||||
result = AVGPRICE(O, H, L, C)
|
result = AVGPRICE(O, H, L, C)
|
||||||
@@ -39,6 +41,7 @@ class TestAVGPRICE:
|
|||||||
# MEDPRICE
|
# MEDPRICE
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestMEDPRICE:
|
class TestMEDPRICE:
|
||||||
def test_known_formula(self):
|
def test_known_formula(self):
|
||||||
result = MEDPRICE(H, L)
|
result = MEDPRICE(H, L)
|
||||||
@@ -61,6 +64,7 @@ class TestMEDPRICE:
|
|||||||
# TYPPRICE
|
# TYPPRICE
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestTYPPRICE:
|
class TestTYPPRICE:
|
||||||
def test_known_formula(self):
|
def test_known_formula(self):
|
||||||
result = TYPPRICE(H, L, C)
|
result = TYPPRICE(H, L, C)
|
||||||
@@ -83,6 +87,7 @@ class TestTYPPRICE:
|
|||||||
# WCLPRICE
|
# WCLPRICE
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestWCLPRICE:
|
class TestWCLPRICE:
|
||||||
def test_known_formula(self):
|
def test_known_formula(self):
|
||||||
result = WCLPRICE(H, L, C)
|
result = WCLPRICE(H, L, C)
|
||||||
@@ -99,7 +104,6 @@ class TestWCLPRICE:
|
|||||||
|
|
||||||
def test_close_weight_double(self):
|
def test_close_weight_double(self):
|
||||||
# WCLPRICE weights close twice vs TYPPRICE
|
# WCLPRICE weights close twice vs TYPPRICE
|
||||||
typ = TYPPRICE(H, L, C)
|
|
||||||
wcl = WCLPRICE(H, L, C)
|
wcl = WCLPRICE(H, L, C)
|
||||||
# On a rising series (H > L > 0), WCLPRICE > TYPPRICE when C > (H+L)/2
|
# On a rising series (H > L > 0), WCLPRICE > TYPPRICE when C > (H+L)/2
|
||||||
# Just verify formula correctness already done above
|
# Just verify formula correctness already done above
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
"""Unit tests for ferro_ta.indicators.statistic"""
|
"""Unit tests for ferro_ta.indicators.statistic"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
|
||||||
from ferro_ta.indicators.statistic import (
|
from ferro_ta.indicators.statistic import (
|
||||||
STDDEV, VAR, BETA, CORREL,
|
BETA,
|
||||||
LINEARREG, LINEARREG_ANGLE, LINEARREG_INTERCEPT, LINEARREG_SLOPE,
|
CORREL,
|
||||||
|
LINEARREG,
|
||||||
|
LINEARREG_ANGLE,
|
||||||
|
LINEARREG_INTERCEPT,
|
||||||
|
LINEARREG_SLOPE,
|
||||||
|
STDDEV,
|
||||||
TSF,
|
TSF,
|
||||||
|
VAR,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -16,14 +23,15 @@ N = 100
|
|||||||
_A = 100 + np.cumsum(RNG.normal(0, 0.5, N))
|
_A = 100 + np.cumsum(RNG.normal(0, 0.5, N))
|
||||||
_B = 100 + np.cumsum(RNG.normal(0, 0.5, N))
|
_B = 100 + np.cumsum(RNG.normal(0, 0.5, N))
|
||||||
|
|
||||||
LINDATA = np.arange(1.0, 6.0) # [1,2,3,4,5]
|
LINDATA = np.arange(1.0, 6.0) # [1,2,3,4,5]
|
||||||
CONSTDATA = np.ones(10) # all 1.0
|
CONSTDATA = np.ones(10) # all 1.0
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# STDDEV
|
# STDDEV
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestSTDDEV:
|
class TestSTDDEV:
|
||||||
def test_constant_is_zero(self):
|
def test_constant_is_zero(self):
|
||||||
result = STDDEV(CONSTDATA, timeperiod=5)
|
result = STDDEV(CONSTDATA, timeperiod=5)
|
||||||
@@ -52,6 +60,7 @@ class TestSTDDEV:
|
|||||||
# VAR
|
# VAR
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestVAR:
|
class TestVAR:
|
||||||
def test_constant_is_zero(self):
|
def test_constant_is_zero(self):
|
||||||
result = VAR(CONSTDATA, timeperiod=5)
|
result = VAR(CONSTDATA, timeperiod=5)
|
||||||
@@ -77,6 +86,7 @@ class TestVAR:
|
|||||||
# LINEARREG
|
# LINEARREG
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestLINEARREG:
|
class TestLINEARREG:
|
||||||
def test_perfect_line(self):
|
def test_perfect_line(self):
|
||||||
# For [1,2,3,4,5] over window 5, forecast = 5.0
|
# For [1,2,3,4,5] over window 5, forecast = 5.0
|
||||||
@@ -95,6 +105,7 @@ class TestLINEARREG:
|
|||||||
# LINEARREG_SLOPE
|
# LINEARREG_SLOPE
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestLINEARREG_SLOPE:
|
class TestLINEARREG_SLOPE:
|
||||||
def test_perfect_line_slope_one(self):
|
def test_perfect_line_slope_one(self):
|
||||||
result = LINEARREG_SLOPE(LINDATA, timeperiod=5)
|
result = LINEARREG_SLOPE(LINDATA, timeperiod=5)
|
||||||
@@ -113,6 +124,7 @@ class TestLINEARREG_SLOPE:
|
|||||||
# LINEARREG_INTERCEPT
|
# LINEARREG_INTERCEPT
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestLINEARREG_INTERCEPT:
|
class TestLINEARREG_INTERCEPT:
|
||||||
def test_perfect_line_intercept_one(self):
|
def test_perfect_line_intercept_one(self):
|
||||||
# y = [1,2,3,4,5] with x=[0,1,2,3,4] → y = 1 + 1*x → intercept = 1.0
|
# y = [1,2,3,4,5] with x=[0,1,2,3,4] → y = 1 + 1*x → intercept = 1.0
|
||||||
@@ -127,6 +139,7 @@ class TestLINEARREG_INTERCEPT:
|
|||||||
# LINEARREG_ANGLE
|
# LINEARREG_ANGLE
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestLINEARREG_ANGLE:
|
class TestLINEARREG_ANGLE:
|
||||||
def test_slope_one_gives_45_degrees(self):
|
def test_slope_one_gives_45_degrees(self):
|
||||||
result = LINEARREG_ANGLE(LINDATA, timeperiod=5)
|
result = LINEARREG_ANGLE(LINDATA, timeperiod=5)
|
||||||
@@ -146,6 +159,7 @@ class TestLINEARREG_ANGLE:
|
|||||||
# BETA
|
# BETA
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestBETA:
|
class TestBETA:
|
||||||
def test_nan_warmup(self):
|
def test_nan_warmup(self):
|
||||||
result = BETA(_A, _B, timeperiod=5)
|
result = BETA(_A, _B, timeperiod=5)
|
||||||
@@ -170,6 +184,7 @@ class TestBETA:
|
|||||||
# CORREL
|
# CORREL
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestCOREL:
|
class TestCOREL:
|
||||||
def test_self_correlation_is_one(self):
|
def test_self_correlation_is_one(self):
|
||||||
result = CORREL(_A, _A, timeperiod=10)
|
result = CORREL(_A, _A, timeperiod=10)
|
||||||
@@ -195,6 +210,7 @@ class TestCOREL:
|
|||||||
# TSF
|
# TSF
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestTSF:
|
class TestTSF:
|
||||||
def test_perfect_line(self):
|
def test_perfect_line(self):
|
||||||
arr = np.arange(1.0, 10.0)
|
arr = np.arange(1.0, 10.0)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Unit tests for ferro_ta.indicators.volatility"""
|
"""Unit tests for ferro_ta.indicators.volatility"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
|
||||||
from ferro_ta.indicators.volatility import ATR, NATR, TRANGE
|
from ferro_ta.indicators.volatility import ATR, NATR, TRANGE
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -23,6 +24,7 @@ SMALL_C = np.array([11.0, 12.0, 13.0, 14.0, 15.0])
|
|||||||
# TRANGE
|
# TRANGE
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestTRANGE:
|
class TestTRANGE:
|
||||||
def test_known_values_constant_range(self):
|
def test_known_values_constant_range(self):
|
||||||
result = TRANGE(SMALL_H, SMALL_L, SMALL_C)
|
result = TRANGE(SMALL_H, SMALL_L, SMALL_C)
|
||||||
@@ -66,6 +68,7 @@ class TestTRANGE:
|
|||||||
# ATR
|
# ATR
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestATR:
|
class TestATR:
|
||||||
def test_timeperiod_1_equals_trange(self):
|
def test_timeperiod_1_equals_trange(self):
|
||||||
atr = ATR(SMALL_H, SMALL_L, SMALL_C, timeperiod=1)
|
atr = ATR(SMALL_H, SMALL_L, SMALL_C, timeperiod=1)
|
||||||
@@ -99,6 +102,7 @@ class TestATR:
|
|||||||
# NATR
|
# NATR
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestNATR:
|
class TestNATR:
|
||||||
def test_nan_warmup(self):
|
def test_nan_warmup(self):
|
||||||
result = NATR(_HIGH, _LOW, _CLOSE, timeperiod=14)
|
result = NATR(_HIGH, _LOW, _CLOSE, timeperiod=14)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Unit tests for ferro_ta.indicators.volume"""
|
"""Unit tests for ferro_ta.indicators.volume"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
|
||||||
from ferro_ta.indicators.volume import AD, ADOSC, OBV
|
from ferro_ta.indicators.volume import AD, ADOSC, OBV
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -24,6 +25,7 @@ SMALL_V = np.array([1000.0, 2000.0, 3000.0, 4000.0, 5000.0])
|
|||||||
# OBV
|
# OBV
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestOBV:
|
class TestOBV:
|
||||||
def test_known_values_rising(self):
|
def test_known_values_rising(self):
|
||||||
# Rising close: OBV accumulates all volume
|
# Rising close: OBV accumulates all volume
|
||||||
@@ -64,6 +66,7 @@ class TestOBV:
|
|||||||
# AD
|
# AD
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestAD:
|
class TestAD:
|
||||||
def test_known_formula(self):
|
def test_known_formula(self):
|
||||||
# AD = cumsum(CLV * volume)
|
# AD = cumsum(CLV * volume)
|
||||||
@@ -72,7 +75,7 @@ class TestAD:
|
|||||||
l = np.array([10.0])
|
l = np.array([10.0])
|
||||||
c = np.array([12.0])
|
c = np.array([12.0])
|
||||||
v = np.array([1000.0])
|
v = np.array([1000.0])
|
||||||
clv = ((12 - 10) - (15 - 12)) / (15 - 10) # (2 - 3) / 5 = -0.2
|
clv = ((12 - 10) - (15 - 12)) / (15 - 10) # (2 - 3) / 5 = -0.2
|
||||||
expected = clv * 1000.0
|
expected = clv * 1000.0
|
||||||
result = AD(h, l, c, v)
|
result = AD(h, l, c, v)
|
||||||
np.testing.assert_allclose(result[0], expected, rtol=1e-10)
|
np.testing.assert_allclose(result[0], expected, rtol=1e-10)
|
||||||
@@ -94,6 +97,7 @@ class TestAD:
|
|||||||
# ADOSC
|
# ADOSC
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestADOSC:
|
class TestADOSC:
|
||||||
def test_nan_warmup(self):
|
def test_nan_warmup(self):
|
||||||
result = ADOSC(_HIGH, _LOW, _CLOSE, _VOL, fastperiod=3, slowperiod=10)
|
result = ADOSC(_HIGH, _LOW, _CLOSE, _VOL, fastperiod=3, slowperiod=10)
|
||||||
|
|||||||
+132
-132
File diff suppressed because it is too large
Load Diff
@@ -640,13 +640,17 @@ class TestVersionConsistency:
|
|||||||
|
|
||||||
def test_release_md_exists(self):
|
def test_release_md_exists(self):
|
||||||
"""RELEASE.md must exist in the repository root."""
|
"""RELEASE.md must exist in the repository root."""
|
||||||
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
root = os.path.dirname(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
)
|
||||||
release_md = os.path.join(root, "RELEASE.md")
|
release_md = os.path.join(root, "RELEASE.md")
|
||||||
assert os.path.isfile(release_md), "RELEASE.md not found"
|
assert os.path.isfile(release_md), "RELEASE.md not found"
|
||||||
|
|
||||||
def test_release_md_has_key_sections(self):
|
def test_release_md_has_key_sections(self):
|
||||||
"""RELEASE.md must mention tagging and PyPI."""
|
"""RELEASE.md must mention tagging and PyPI."""
|
||||||
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
root = os.path.dirname(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
)
|
||||||
release_md = os.path.join(root, "RELEASE.md")
|
release_md = os.path.join(root, "RELEASE.md")
|
||||||
if not os.path.isfile(release_md):
|
if not os.path.isfile(release_md):
|
||||||
pytest.skip("RELEASE.md not found")
|
pytest.skip("RELEASE.md not found")
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ All tests use NO optional dependencies - they run in every CI environment.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
|
||||||
|
|
||||||
import ferro_ta
|
import ferro_ta
|
||||||
|
|
||||||
@@ -81,7 +80,7 @@ class TestEMAKnownValues:
|
|||||||
# After warmup, EMA should be strictly increasing
|
# After warmup, EMA should be strictly increasing
|
||||||
for i in range(20, len(result) - 1):
|
for i in range(20, len(result) - 1):
|
||||||
assert result[i + 1] > result[i], (
|
assert result[i + 1] > result[i], (
|
||||||
f"EMA not increasing at index {i}: {result[i]} >= {result[i+1]}"
|
f"EMA not increasing at index {i}: {result[i]} >= {result[i + 1]}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -195,9 +194,9 @@ class TestRSIKnownValues:
|
|||||||
valid_values = result[~np.isnan(result)]
|
valid_values = result[~np.isnan(result)]
|
||||||
if len(valid_values) > 0:
|
if len(valid_values) > 0:
|
||||||
# Should be either NaN everywhere or 100 everywhere
|
# Should be either NaN everywhere or 100 everywhere
|
||||||
assert np.all(np.abs(valid_values - 100.0) < 1e-10) or np.all(np.abs(valid_values - 50.0) < 5.0), (
|
assert np.all(np.abs(valid_values - 100.0) < 1e-10) or np.all(
|
||||||
"RSI of constant series should be 100 (no down movement) or close to 50"
|
np.abs(valid_values - 50.0) < 5.0
|
||||||
)
|
), "RSI of constant series should be 100 (no down movement) or close to 50"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -229,12 +228,63 @@ class TestATRKnownValues:
|
|||||||
# Bar 0: H=11, L=9, C=10
|
# Bar 0: H=11, L=9, C=10
|
||||||
# Bar 1: H=13, L=10, C=12 → TR = max(13-10, |13-10|, |10-10|) = 3
|
# Bar 1: H=13, L=10, C=12 → TR = max(13-10, |13-10|, |10-10|) = 3
|
||||||
# Bar 2: H=14, L=11, C=13 → TR = max(14-11, |14-12|, |11-12|) = 3
|
# Bar 2: H=14, L=11, C=13 → TR = max(14-11, |14-12|, |11-12|) = 3
|
||||||
high = np.array([11.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0,
|
high = np.array(
|
||||||
22.0, 23.0, 24.0, 25.0, 26.0])
|
[
|
||||||
low = np.array([9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0,
|
11.0,
|
||||||
19.0, 20.0, 21.0, 22.0, 23.0])
|
13.0,
|
||||||
close = np.array([10.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0,
|
14.0,
|
||||||
21.0, 22.0, 23.0, 24.0, 25.0])
|
15.0,
|
||||||
|
16.0,
|
||||||
|
17.0,
|
||||||
|
18.0,
|
||||||
|
19.0,
|
||||||
|
20.0,
|
||||||
|
21.0,
|
||||||
|
22.0,
|
||||||
|
23.0,
|
||||||
|
24.0,
|
||||||
|
25.0,
|
||||||
|
26.0,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
low = np.array(
|
||||||
|
[
|
||||||
|
9.0,
|
||||||
|
10.0,
|
||||||
|
11.0,
|
||||||
|
12.0,
|
||||||
|
13.0,
|
||||||
|
14.0,
|
||||||
|
15.0,
|
||||||
|
16.0,
|
||||||
|
17.0,
|
||||||
|
18.0,
|
||||||
|
19.0,
|
||||||
|
20.0,
|
||||||
|
21.0,
|
||||||
|
22.0,
|
||||||
|
23.0,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
close = np.array(
|
||||||
|
[
|
||||||
|
10.0,
|
||||||
|
12.0,
|
||||||
|
13.0,
|
||||||
|
14.0,
|
||||||
|
15.0,
|
||||||
|
16.0,
|
||||||
|
17.0,
|
||||||
|
18.0,
|
||||||
|
19.0,
|
||||||
|
20.0,
|
||||||
|
21.0,
|
||||||
|
22.0,
|
||||||
|
23.0,
|
||||||
|
24.0,
|
||||||
|
25.0,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# For period=1, ATR still has warmup. Use TRANGE to check TR values directly
|
# For period=1, ATR still has warmup. Use TRANGE to check TR values directly
|
||||||
tr = ferro_ta.TRANGE(high, low, close)
|
tr = ferro_ta.TRANGE(high, low, close)
|
||||||
@@ -263,7 +313,7 @@ class TestMOMKnownValues:
|
|||||||
|
|
||||||
assert np.isnan(result[0])
|
assert np.isnan(result[0])
|
||||||
assert np.isnan(result[1])
|
assert np.isnan(result[1])
|
||||||
assert np.abs(result[2] - 5.0) < 1e-10 # 15 - 10 = 5
|
assert np.abs(result[2] - 5.0) < 1e-10 # 15 - 10 = 5
|
||||||
assert np.abs(result[3] - (-1.0)) < 1e-10 # 11 - 12 = -1
|
assert np.abs(result[3] - (-1.0)) < 1e-10 # 11 - 12 = -1
|
||||||
|
|
||||||
|
|
||||||
@@ -295,7 +345,9 @@ class TestMACDKnownValues:
|
|||||||
def test_macd_histogram_identity(self):
|
def test_macd_histogram_identity(self):
|
||||||
"""histogram should always equal macd - signal."""
|
"""histogram should always equal macd - signal."""
|
||||||
data = np.arange(1.0, 51.0)
|
data = np.arange(1.0, 51.0)
|
||||||
macd, signal, histogram = ferro_ta.MACD(data, fastperiod=12, slowperiod=26, signalperiod=9)
|
macd, signal, histogram = ferro_ta.MACD(
|
||||||
|
data, fastperiod=12, slowperiod=26, signalperiod=9
|
||||||
|
)
|
||||||
|
|
||||||
# histogram = macd - signal (within floating-point tolerance)
|
# histogram = macd - signal (within floating-point tolerance)
|
||||||
expected_histogram = macd - signal
|
expected_histogram = macd - signal
|
||||||
@@ -338,9 +390,9 @@ class TestVWAPKnownValues:
|
|||||||
|
|
||||||
expected_0 = typ[0]
|
expected_0 = typ[0]
|
||||||
expected_1 = (typ[0] * volume[0] + typ[1] * volume[1]) / (volume[0] + volume[1])
|
expected_1 = (typ[0] * volume[0] + typ[1] * volume[1]) / (volume[0] + volume[1])
|
||||||
expected_2 = (
|
expected_2 = (typ[0] * volume[0] + typ[1] * volume[1] + typ[2] * volume[2]) / (
|
||||||
typ[0] * volume[0] + typ[1] * volume[1] + typ[2] * volume[2]
|
volume[0] + volume[1] + volume[2]
|
||||||
) / (volume[0] + volume[1] + volume[2])
|
)
|
||||||
|
|
||||||
assert np.abs(result[0] - expected_0) < 1e-10
|
assert np.abs(result[0] - expected_0) < 1e-10
|
||||||
assert np.abs(result[1] - expected_1) < 1e-10
|
assert np.abs(result[1] - expected_1) < 1e-10
|
||||||
@@ -359,7 +411,6 @@ class TestDONCHIANKnownValues:
|
|||||||
"""upper == MAX(high), lower == MIN(low), middle == (upper+lower)/2."""
|
"""upper == MAX(high), lower == MIN(low), middle == (upper+lower)/2."""
|
||||||
high = np.array([11.0, 13.0, 14.0, 12.0, 15.0])
|
high = np.array([11.0, 13.0, 14.0, 12.0, 15.0])
|
||||||
low = np.array([9.0, 10.0, 11.0, 10.0, 12.0])
|
low = np.array([9.0, 10.0, 11.0, 10.0, 12.0])
|
||||||
close = np.array([10.0, 12.0, 13.0, 11.0, 14.0])
|
|
||||||
|
|
||||||
period = 3
|
period = 3
|
||||||
upper, middle, lower = ferro_ta.DONCHIAN(high, low, timeperiod=period)
|
upper, middle, lower = ferro_ta.DONCHIAN(high, low, timeperiod=period)
|
||||||
@@ -394,7 +445,9 @@ class TestPIVOT_POINTSKnownValues:
|
|||||||
low = np.array([90.0, 90.0])
|
low = np.array([90.0, 90.0])
|
||||||
close = np.array([100.0, 100.0])
|
close = np.array([100.0, 100.0])
|
||||||
|
|
||||||
pivot, r1, s1, r2, s2 = ferro_ta.PIVOT_POINTS(high, low, close, method="classic")
|
pivot, r1, s1, r2, s2 = ferro_ta.PIVOT_POINTS(
|
||||||
|
high, low, close, method="classic"
|
||||||
|
)
|
||||||
|
|
||||||
# Check last bar (index 1) which has full history
|
# Check last bar (index 1) which has full history
|
||||||
# P = (110 + 90 + 100) / 3 = 100
|
# P = (110 + 90 + 100) / 3 = 100
|
||||||
@@ -458,7 +511,6 @@ class TestPatternKnownValues:
|
|||||||
def test_doji_known_sequence(self):
|
def test_doji_known_sequence(self):
|
||||||
"""Construct a perfect doji: open == close, small body."""
|
"""Construct a perfect doji: open == close, small body."""
|
||||||
# Doji: open == close (or very close), H and L have range
|
# Doji: open == close (or very close), H and L have range
|
||||||
n = 5
|
|
||||||
high = np.array([11.0, 11.0, 11.0, 11.0, 11.0])
|
high = np.array([11.0, 11.0, 11.0, 11.0, 11.0])
|
||||||
low = np.array([9.0, 9.0, 9.0, 9.0, 9.0])
|
low = np.array([9.0, 9.0, 9.0, 9.0, 9.0])
|
||||||
close = np.array([10.0, 10.0, 10.0, 10.0, 10.0])
|
close = np.array([10.0, 10.0, 10.0, 10.0, 10.0])
|
||||||
@@ -507,3 +559,4 @@ class TestPatternKnownValues:
|
|||||||
|
|
||||||
# Last bar has hammer characteristics
|
# Last bar has hammer characteristics
|
||||||
# (actual detection may vary based on implementation)
|
# (actual detection may vary based on implementation)
|
||||||
|
assert result.shape == close.shape
|
||||||
|
|||||||
@@ -655,7 +655,9 @@ class TestWebAPI:
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Insert project root so that `api.main` is importable
|
# Insert project root so that `api.main` is importable
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
project_root = os.path.dirname(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
)
|
||||||
if project_root not in sys.path:
|
if project_root not in sys.path:
|
||||||
sys.path.insert(0, project_root)
|
sys.path.insert(0, project_root)
|
||||||
try:
|
try:
|
||||||
|
|||||||
Generated
+7
-7
@@ -33,9 +33,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.56"
|
version = "1.2.57"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
@@ -49,7 +49,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ferro_ta_wasm"
|
name = "ferro_ta_wasm"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -88,9 +88,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.17"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
@@ -145,9 +145,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oorandom"
|
name = "oorandom"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ferro_ta_wasm"
|
name = "ferro_ta_wasm"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "WebAssembly bindings for ferro-ta technical analysis indicators"
|
description = "WebAssembly bindings for ferro-ta technical analysis indicators"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ferro-ta-wasm",
|
"name": "ferro-ta-wasm",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"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",
|
||||||
|
|||||||
Reference in New Issue
Block a user