[
  {
    "path": ".gitattributes",
    "content": "# Basic settings\n* text=auto eol=lf\n\n# Source code files\n*.rs    text diff=rust\n*.toml  text diff=toml\n\n# Scripts and configuration files\n*.sh    text eol=lf\n*.bat   text eol=crlf\n*.cmd   text eol=crlf\n*.json  text\n*.yaml  text\n*.xml   text\n*.md    text\n*.txt   text\n\n# Visual Studio files\n*.sln        text eol=crlf\n*.vcxproj    text eol=crlf\n*.vcxproj.filters text eol=crlf\n\n# Binary files\n*.png   binary\n*.jpg   binary\n*.gif   binary\n*.ico   binary\n*.zip   binary\n*.dll   binary\n*.exe   binary\n*.lib   binary\n*.pdf   binary\n\n# Special handling\n*.json  merge=union\n*.yaml  merge=union\n*.xml   merge=union\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file\n\nversion: 2\nupdates:\n  - package-ecosystem: \"cargo\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/CI.yml",
    "content": "# This file is autogenerated by maturin v1.9.3\n# To update, run\n#\n#    maturin generate-ci github\n#\nname: CI\n\non:\n  push:\n    branches:\n      - main\n      - master\n    tags:\n      - '*'\n  pull_request:\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  linux:\n    runs-on: ${{ matrix.platform.runner }}\n    strategy:\n      matrix:\n        platform:\n          - runner: ubuntu-22.04\n            target: x86_64\n          - runner: ubuntu-22.04\n            target: x86\n          - runner: ubuntu-22.04\n            target: aarch64\n          - runner: ubuntu-22.04\n            target: armv7\n          - runner: ubuntu-22.04\n            target: s390x\n          - runner: ubuntu-22.04\n            target: ppc64le\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: 3.x\n      - name: Build wheels\n        uses: PyO3/maturin-action@v1\n        with:\n          target: ${{ matrix.platform.target }}\n          args: --release --out dist --find-interpreter\n          sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}\n          manylinux: auto\n      - name: Upload wheels\n        uses: actions/upload-artifact@v4\n        with:\n          name: wheels-linux-${{ matrix.platform.target }}\n          path: dist\n\n  musllinux:\n    runs-on: ${{ matrix.platform.runner }}\n    strategy:\n      matrix:\n        platform:\n          - runner: ubuntu-22.04\n            target: x86_64\n          - runner: ubuntu-22.04\n            target: x86\n          - runner: ubuntu-22.04\n            target: aarch64\n          - runner: ubuntu-22.04\n            target: armv7\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: 3.x\n      - name: Build wheels\n        uses: PyO3/maturin-action@v1\n        with:\n          target: ${{ matrix.platform.target }}\n          args: --release --out dist --find-interpreter\n          sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}\n          manylinux: musllinux_1_2\n      - name: Upload wheels\n        uses: actions/upload-artifact@v4\n        with:\n          name: wheels-musllinux-${{ matrix.platform.target }}\n          path: dist\n\n  windows:\n    runs-on: ${{ matrix.platform.runner }}\n    strategy:\n      matrix:\n        platform:\n          - runner: windows-latest\n            target: x64\n          - runner: windows-latest\n            target: x86\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: 3.x\n          architecture: ${{ matrix.platform.target }}\n      - name: Build wheels\n        uses: PyO3/maturin-action@v1\n        with:\n          target: ${{ matrix.platform.target }}\n          args: --release --out dist --find-interpreter\n          sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}\n      - name: Upload wheels\n        uses: actions/upload-artifact@v4\n        with:\n          name: wheels-windows-${{ matrix.platform.target }}\n          path: dist\n\n  macos:\n    runs-on: ${{ matrix.platform.runner }}\n    strategy:\n      matrix:\n        platform:\n          - runner: macos-13\n            target: x86_64\n          - runner: macos-14\n            target: aarch64\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: 3.x\n      - name: Build wheels\n        uses: PyO3/maturin-action@v1\n        with:\n          target: ${{ matrix.platform.target }}\n          args: --release --out dist --find-interpreter\n          sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}\n      - name: Upload wheels\n        uses: actions/upload-artifact@v4\n        with:\n          name: wheels-macos-${{ matrix.platform.target }}\n          path: dist\n\n  sdist:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Build sdist\n        uses: PyO3/maturin-action@v1\n        with:\n          command: sdist\n          args: --out dist\n      - name: Upload sdist\n        uses: actions/upload-artifact@v4\n        with:\n          name: wheels-sdist\n          path: dist\n\n  release:\n    name: Release\n    runs-on: ubuntu-latest\n    if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}\n    needs: [linux, musllinux, windows, macos, sdist]\n    permissions:\n      # Use to sign the release artifacts\n      id-token: write\n      # Used to upload release artifacts\n      contents: write\n      # Used to generate artifact attestation\n      attestations: write\n    steps:\n      - uses: actions/download-artifact@v4\n      - name: Generate artifact attestation\n        uses: actions/attest-build-provenance@v2\n        with:\n          subject-path: 'wheels-*/*'\n      - name: Publish to PyPI\n        if: ${{ startsWith(github.ref, 'refs/tags/') }}\n        uses: PyO3/maturin-action@v1\n        env:\n          MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}\n        with:\n          command: upload\n          args: --non-interactive --skip-existing wheels-*/*\n"
  },
  {
    "path": ".github/workflows/publish-doc.yml",
    "content": "name: Deploy Documentation\non:\n  push:\n    branches:\n      - master\n      - main\npermissions:\n  contents: write\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Configure Git Credentials\n        run: |\n          git config user.name github-actions[bot]\n          git config user.email 41898282+github-actions[bot]@users.noreply.github.com\n      - uses: actions/setup-python@v5\n        with:\n          python-version: 3.x\n      - uses: dtolnay/rust-toolchain@stable\n      - run: echo \"cache_id=$(date --utc '+%V')\" >> $GITHUB_ENV\n      - uses: actions/cache@v4\n        with:\n          key: mkdocs-material-${{ env.cache_id }}\n          path: .cache\n          restore-keys: |\n            mkdocs-material-\n      - name: Install dependencies\n        run: |\n          pip install mkdocs-material mkdocstrings mkdocstrings-python mkdocs-minify-plugin\n          pip install .\n      - name: Copy and process README.md\n        run: |\n          cp CHANGELOG.md docs/changelog.md\n          cp README.md docs/index.md\n          sed -i 's|docs/assets/logo.png|assets/logo.png|g' docs/index.md\n          sed -i 's|docs/assets/bench_ema.png|assets/bench_ema.png|g' docs/index.md\n      - run: mkdocs gh-deploy --force\n"
  },
  {
    "path": ".github/workflows/publish-docker.yml",
    "content": "name: Docker Package\n\non:\n  push:\n    tags: [ 'v*.*.*' ]\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Log into registry ${{ env.REGISTRY }}\n        if: github.event_name != 'pull_request'\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract Docker metadata\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n          tags: |\n            type=ref,event=branch\n            type=ref,event=pr\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          platforms: linux/amd64,linux/arm64\n          push: ${{ github.event_name != 'pull_request' }}\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n"
  },
  {
    "path": ".github/workflows/publish-rust.yml",
    "content": "name: Publish to crates.io\n\non:\n  push:\n    tags:\n      - 'v*.*.*'\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs:\n  publish:\n    name: Build, Test & Publish\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Build\n        run: cargo build --release --verbose\n      - name: Test\n        run: cargo test --verbose\n      - name: Publish to crates.io\n        env:\n          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}\n        run: cargo publish -p kand\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Automatic Release\n\non:\n  push:\n    tags:\n      - 'v*.*.*'\n\npermissions:\n  contents: write  # Grants permission to create releases\n\njobs:\n  generate-changelog:\n    name: Generate Changelog\n    runs-on: ubuntu-latest\n    outputs:\n      release_body: ${{ steps.git-cliff.outputs.content }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Generate changelog\n        id: git-cliff\n        uses: orhun/git-cliff-action@v2\n        with:\n          config: cliff.toml\n          args: -vv --latest --strip all\n\n  create-release:\n    name: Create GitHub Release\n    needs: generate-changelog\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Set release version\n        shell: bash\n        run: echo \"RELEASE_VERSION=${GITHUB_REF#refs/tags/v}\" >> $GITHUB_ENV\n\n      - name: Create Release\n        uses: softprops/action-gh-release@v1\n        with:\n          name: Release v${{ env.RELEASE_VERSION }}\n          body: ${{ needs.generate-changelog.outputs.release_body }}\n          draft: false\n          prerelease: ${{ contains(github.ref, '-') }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/rust.yml",
    "content": "name: Rust\n\non:\n  push:\n    branches: [ \"main\" ]\n  pull_request:\n    branches: [ \"main\" ]\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs:\n  call-ci:\n    uses: qntx/workflows/.github/workflows/rust.yml@main\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: Close Stale Issues and PRs\n\non:\n  schedule:\n    - cron: \"30 1 * * *\"\n  workflow_dispatch:\n\njobs:\n  close-stale:\n    uses: qntx/workflows/.github/workflows/stale.yml@main\n"
  },
  {
    "path": ".gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\ndebug/\ntarget/\n\n# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries\n# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html\n# Cargo.lock\n\n# These are backup files generated by rustfmt\n**/*.rs.bk\n\n# MSVC Windows builds of rustc generate these, which store debugging information\n*.pdb\n\n# RustRover\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n\n# Added by cargo\n\n/target\n\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n#   in version control.\n#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control\n.pdm.toml\n.pdm-python\n.pdm-build/\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n"
  },
  {
    "path": ".python-version",
    "content": "3.11\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nThis document records all significant updates and changes to the Kand project.\n\n## [unreleased]\n\n### 🚀 Features\n\n- Update readme\n- Update readme\n- Update readme\n- Add CONTRIBUTING\n- Update Readme add Disclaimer\n- Add CORREL (Pearson's Correlation Coefficient) indicator (#27)\n\n### 🐛 Bug Fixes\n\n- *(ci)* Fix publish-rust\n- *(.editorconfig)* Fix path\n- Update mkdocs.yml\n\n### 💼 Other\n\n- *(deps)* Bump serde_json from 1.0.140 to 1.0.141 (#28)\n- *(deps)* Bump rand from 0.9.0 to 0.9.2 (#29)\n\n## [0.2.2] - 2025-03-04\n\n### 🚀 Features\n\n- *(precision)* Add f32 floating-point precision support (#10)\n\n### 🐛 Bug Fixes\n\n- *(tema)* Resolve ambiguous numeric type errors in TEMA calculation\n- *(tema)* Resolve ambiguous numeric type errors in TEMA calculation\n- *(willr)* Resolve Clippy warnings for strict float comparisons\n- *(stats)* Resolve Clippy warnings for strict float comparisons in max/min\n- *(ci)* Fix test-rust\n\n### 🚜 Refactor\n\n- Use _inc instead of _incremental\n\n## [0.2.1] - 2025-03-02\n\n### 🚀 Features\n\n- *(precision)* Add f32 floating-point precision support\n\n## [0.2.0] - 2025-03-02\n\n### 🚀 Features\n\n- [**breaking**] Release v0.2.0 with major type system refactoring\n\n### 🐛 Bug Fixes\n\n- *(ci:publish-doc)* Update publish-doc\n- *(makefile)* Fix uv-sync, add params for gen_stub.py\n\n### 💼 Other\n\n- Update the types and lib type\n\n## [0.1.3] - 2025-02-27\n\n### 🚜 Refactor\n\n- *(ci:release)* Refactor release ci\n\n## [0.1.2] - 2025-02-27\n\n### 🐛 Bug Fixes\n\n- *(makefile)* Update makefile\n- *(bench)* Added #[allow(clippy::expect_used)] to suppress clippy warnings\n- *(cdl_gravestone_doji)* Optimize T::from(100).unwrap() to T::from(100).ok_or(KandError::ConversionError)?\n- *(var)* Replace unwrap with safe conversion using ok_or(KandError::ConversionError)?\n\n### 🚜 Refactor\n\n- *(ci)* Simplify release workflow and customize changelog footer\n- *(tpo)* Replace as f64 with f64::from(u8::try_from(i).unwrap()) for type conversion\n\n### 📚 Documentation\n\n- Update rust doc\n- *(helper)* Add missing error documentation for lowest_bars and highest_bars functions\n\n## [0.1.1] - 2025-02-27\n\n### 🚀 Features\n\n- *(ci)* Add changelog ci.\n\n### 🐛 Bug Fixes\n\n- *(aroonosc)* Optimize precision conversion by replacing 'as' with 'T::from' for safety\n\n## [0.0.4] - 2025-02-23\n\n---\n\n> \"Quantitative trading begins with data, thrives on strategy, and succeeds through execution. Kand, making trading simpler.\"\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Kand\n\nFirst off, thank you for considering contributing to Kand! It's people like you that make Kand such a great tool.\n\nFollowing these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue or assessing patches and features.\n\n## Development Setup\n\nBefore you start developing, you need to install several required tools:\n\n### Required Tools\n\n1. **Install Rust development tools**:\n\n   ```bash\n   cargo install cargo-udeps git-cliff wasm-pack\n   ```\n\n   - `cargo-udeps`: Finds unused dependencies\n   - `git-cliff`: Generates changelogs\n   - `wasm-pack`: WebAssembly package builder\n\n2. **Install uv** (Python package manager):\n   Follow the installation guide at: <https://github.com/astral-sh/uv>\n\n3. **Install maturin** (Python-Rust bindings):\n\n   ```bash\n   uv tool install maturin\n   ```\n\n4. **Install pre-commit** (Git hooks framework):\n\n   ```bash\n   pip install pre-commit\n   ```\n\n5. **Install make** (Build automation tool):\n   - **Linux/macOS**: Usually pre-installed, or install via package manager\n   - **Windows**: Install via one of the following options:\n     - Install [Git for Windows](https://git-scm.com/download/win) (includes make)\n     - Install [Chocolatey](https://chocolatey.org/) and run `choco install make`\n     - Install [MSYS2](https://www.msys2.org/) and run `pacman -S make`\n\nMake sure all these tools are properly installed before proceeding with development.\n\n## Development Workflow\n\nOur project is structured into three main parts:\n\n1. `kand`: The core library written in Rust, containing all the technical indicator implementations.\n2. `kand-py`: The Python bindings for the core library, allowing Kand to be used in Python.\n3. `kand-wasm`: The WebAssembly bindings for the core library, enabling its use in JavaScript/TypeScript environments (web and Node.js).\n\nWhen making changes, please follow this general workflow:\n\n1. **Modify the Rust code**: All logic for technical indicators resides in the `kand/` directory. Make your changes or additions here first.\n2. **Ensure tests pass**: Run the Rust test suite to make sure your changes haven't broken anything.\n3. **Update bindings**: If you've added a new indicator or changed a function signature, update the corresponding bindings in the `kand-py/` and/or `kand-wasm/` directories.\n4. **Run all checks**: Use the provided `Makefile` to run a full suite of checks, including building, testing, linting, and formatting.\n\n### Using the Makefile\n\nWe have a `Makefile` that simplifies the development process. The most important command is:\n\n```bash\nmake\n```\n\nRunning `make` by default executes the `pre-commit` target, which will:\n\n- Build the project (`build`)\n- Run tests (`test`)\n- Run the linter (`clippy`)\n- Format the code (`fmt`)\n- Generate the changelog (`cliff`)\n- Check for unused dependencies (`udeps-check`)\n- Build the Wasm package (`wasm-build`)\n- Sync the Python environment and generate stubs (`uv-sync`)\n\nPlease ensure all checks pass before submitting a pull request.\n\n## Coding Guidelines\n\nTo maintain consistency and readability across the codebase, adhere to the following coding guidelines when implementing or modifying technical indicators:\n\n### Parameter Naming and Order\n\n- **Parameter Order**: Function parameters for technical indicators should follow the `(input data, optimization parameters, output data)` pattern:\n  - **Input Data**: Includes raw input data (e.g., `input: &[TAFloat]` for a price series, `input: TAFloat` for a new price, `prev_input: TAFloat` for an old price) and computation state (e.g., `prev_sma: TAFloat` for the previous SMA value). Raw input data should precede state data.\n  - **Optimization Parameters**: Configuration parameters like `opt_period: TAPeriod` that control the indicator's behavior.\n  - **Output Data**: Typically a mutable output parameter (e.g., `output: &mut [TAFloat]`) for full calculations or the function's return value (e.g., `TAFloat`) for incremental calculations.\n  - **Examples**:\n    - For full SMA calculation: Use `(input, opt_period, output)` as seen in `sma(input: &[TAFloat], opt_period: TAPeriod, output: &mut [TAFloat])`.\n    - For incremental SMA calculation: Use `(input, prev_input, prev_sma, opt_period)` as seen in `sma_inc(input: TAFloat, prev_input: TAFloat, prev_sma: TAFloat, opt_period: TAPeriod)`.\n- **Naming Conventions**:\n  - Use descriptive names for input data, such as `input` (price series or new price), `prev_input` (old price), or `prev_sma` (previous SMA value).\n  - Use domain-specific terms with the `opt_` prefix for optimization parameters, such as `opt_period`, `opt_weight`, or `opt_smoothing`, to clearly indicate their role as configuration parameters.\n  - For output data, use clear names like `output` or `sma_values` to indicate the result's purpose.\n- **Consistency**: Apply this parameter order and naming style to all technical indicator functions, including full calculation functions (e.g., `sma`) and incremental calculation functions (e.g., `sma_inc`), to ensure a cohesive codebase.\n\n## Modifying Existing Indicators\n\n1. Locate the indicator's code in the `kand/src/` directory and apply your changes.\n2. Run the tests to ensure correctness: `make test`.\n3. If you have changed any function signatures, update the corresponding code in `kand-py` and/or `kand-wasm`.\n4. Run `make` to perform all pre-commit checks.\n\n## Adding New Indicators\n\nAdding a new indicator is exciting! To ensure quality and consistency, please follow these steps:\n\n1. Implement the new indicator in the `kand/` directory.\n2. Add a new test module for your indicator.\n3. **Provide accurate test data**: This is a critical step.\n    - **If the indicator exists in TA-Lib**, your test data **must** align with the output of TA-Lib. This ensures compatibility and correctness.\n    - **If the indicator is not in TA-Lib**, you must provide a reference to verify the accuracy of your implementation and test data. This can be:\n        - A Python implementation of the indicator.\n        - A link to a trading website, academic paper, or another reliable source that defines the indicator and provides example calculations.\n4. Add the Python bindings for your new indicator in the `kand-py/` directory.\n5. Run `make` to ensure everything is in order.\n\n## Pull Request Process\n\n1. Fork the repository and create your branch from `main`.\n2. Make your changes, adhering to the workflow described above.\n3. Ensure the test suite passes and that all `make` checks are green.\n4. Issue that pull request! We'll review it as soon as we can.\n\nThank you for your contribution!\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nmembers = [\"kand\", \"kand-py\", \"kand-wasm\"]\n# Only check / build main crates by default (check all with `--workspace`)\ndefault-members = [\"kand\"]\n\nresolver = \"3\"\n\n[workspace.dependencies]\nkand = { path = \"./kand\", default-features = false, features = [\"f64\", \"i64\", \"check\"] }\n\napprox = \"^0.5.1\"\ncriterion = \"0.7.0\"\ncsv = \"^1.3.1\"\nndarray = \"^0.16.1\"\nnum_enum = \"^0.7.3\"\nnum-traits = \"^0.2.19\"\nnumpy = \"0.25.0\"\npyo3 = \"0.25.1\"\nrand = \"^0.9.2\"\nrayon = \"^1.10.0\"\nrust_decimal = \"^1.36.0\"\nserde = { version = \"^1.0.219\", features = [\"derive\"] }\nserde_json = \"^1.0.141\"\nthiserror = \"^2.0.12\"\nwasm-bindgen = \"^0.2.100\"\n\n[profile.release]\nlto = true           # Enable Link Time Optimization to remove unused code\ncodegen-units = 1    # Maximize optimization at the cost of slower compilation\npanic = 'abort'      # Remove panic handling to reduce binary size\nstrip = true         # Fully strip debug symbols; debugging in production will be difficult\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM --platform=$BUILDPLATFORM python:3.12-slim-bullseye AS build\nENV HOME=\"/root\"\nWORKDIR $HOME\n\n# Install build dependencies and set up Python environment\nRUN apt-get update && \\\n    apt-get install -y --no-install-recommends \\\n    build-essential \\\n    curl \\\n    cmake \\\n    gcc-aarch64-linux-gnu \\\n    g++-aarch64-linux-gnu \\\n    patchelf && \\\n    rm -rf /var/lib/apt/lists/* && \\\n    python -m venv $HOME/.venv && \\\n    .venv/bin/pip install --no-cache-dir \"maturin>=1.7,<2.0\"\nENV PATH=\"$HOME/.venv/bin:$PATH\"\n\n# Set Rust target based on the target platform\nARG TARGETPLATFORM\nRUN case \"$TARGETPLATFORM\" in \\\n  \"linux/arm64\") echo \"aarch64-unknown-linux-gnu\" > rust_target.txt ;; \\\n  \"linux/amd64\") echo \"x86_64-unknown-linux-gnu\" > rust_target.txt ;; \\\n  *) exit 1 ;; \\\n  esac\n\n# Install Rust toolchain\nCOPY rust-toolchain.toml rust-toolchain.toml\nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --target $(cat rust_target.txt) --profile minimal --default-toolchain none && \\\n    . $HOME/.cargo/env && \\\n    rustup target add $(cat rust_target.txt)\n\n# Copy project files and build\nCOPY kand kand\nCOPY kand-py kand-py\nCOPY Cargo.toml Cargo.lock pyproject.toml README.md LICENSE-MIT LICENSE-APACHE ./\nCOPY python python\n\n# Build Python extension\nRUN case \"${TARGETPLATFORM}\" in \\\n  \"linux/arm64\") \\\n    export JEMALLOC_SYS_WITH_LG_PAGE=16; \\\n    export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc; \\\n    export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc; \\\n    export CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++; \\\n    ;; \\\n  *) \\\n    ;; \\\n  esac && \\\n  . $HOME/.cargo/env && \\\n  maturin build --release --target $(cat rust_target.txt) -i python3.12 && \\\n  mkdir -p /wheels && \\\n  cp target/wheels/*.whl /wheels/ && \\\n  rm -rf target $HOME/.cargo/registry $HOME/.cargo/git\n\n# Final image: Provide Python runtime\nFROM python:3.12-slim-bullseye\nWORKDIR /app\n\n# Copy and install the wheel file\nCOPY --from=build /wheels/ /wheels/\nRUN pip install --no-cache-dir /wheels/*.whl && rm -rf \\\n    /wheels \\\n    /root/.cache \\\n    /usr/share/doc \\\n    /usr/share/man \\\n    /usr/share/locale \\\n    /var/lib/apt/lists/* \\\n    /var/cache/apt/archives/*\n\n# Set Python path\nENV PYTHONPATH=/app\n"
  },
  {
    "path": "LICENSE-APACHE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "LICENSE-MIT",
    "content": "MIT License\n\nCopyright (c) 2025 Kand TA\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "# Makefile for Rust project using Cargo\n\n.PHONY: all\nall: pre-commit\n\n# Build the project with all features enabled in release mode\n.PHONY: build\nbuild:\n\tcargo build --release --all-features\n\n# Update dependencies to their latest compatible versions\n.PHONY: update\nupdate:\n\tcargo update\n\n# Run the project with all features enabled in release mode\n.PHONY: run\nrun:\n\tcargo run --release --all-features\n\n# Run all tests with all features enabled\n.PHONY: test\ntest:\n\tcargo test --all-features\n\n# Run benchmarks with all features enabled\n.PHONY: bench\nbench:\n\tcargo bench --all-features\n\n# Run Clippy linter with nightly toolchain, fixing issues automatically\n# and applying strict linting rules\n.PHONY: clippy\nclippy:\n\tcargo +nightly clippy --fix \\\n\t\t--all-targets \\\n\t\t--all-features \\\n\t\t--allow-dirty \\\n\t\t--allow-staged \\\n\t\t-- -D warnings \\\n\t\t-W clippy::pedantic \\\n\t\t-W clippy::nursery \\\n\t\t-W clippy::unwrap_used \\\n\t\t-W clippy::expect_used\n\n# Format the code using rustfmt with nightly toolchain\n.PHONY: fmt\nfmt:\n\tcargo +nightly fmt\n\n# Generate documentation for all crates and open it in the browser\n.PHONY: doc\ndoc:\n\tcargo +nightly doc --all-features --no-deps --open\n\n# Generate CHANGELOG.md using git-cliff\n.PHONY: cliff\ncliff:\n\tgit-cliff\n\tgit cliff --output CHANGELOG.md\n\n# Sync Python environment using uv\n.PHONY: uv-sync\nuv-sync:\n\tuv venv\n\tuv lock --upgrade\n\tuv sync\n\tuv run \"./scripts/gen_stub.py\" kand kand-py/python/kand/_kand.pyi\n\n# Check for unused dependencies using cargo-udeps with nightly toolchain\n.PHONY: udeps\nudeps:\n\tcargo +nightly udeps --all-features\n\n# Update and run udeps to check for unused dependencies\n.PHONY: udeps-check\nudeps-check:\n\tcargo update\n\tcargo +nightly udeps --all-features\n\n\n# Build the wasm package\n.PHONY: wasm-build\nwasm-build:\n\t@echo \"Building WASM package...\"\n\t(cd kand-wasm && wasm-pack build --target web && wasm-pack pack pkg)\n\n# Publish the wasm package to npm\n# Note: You must be logged in to npm for this to work (`npm login`)\n.PHONY: wasm-publish\nwasm-publish: wasm-build\n\t@echo \"Publishing WASM package to npm...\"\n\t(cd kand-wasm/pkg && npm pkg fix && npm pkg set name=\"kand\" && npm publish --access public)\n\n# Convenience target to build and publish wasm\n.PHONY: wasm\nwasm: wasm-publish\n\n# Run pre-commit hooks on all files\n.PHONY: pre-commit\npre-commit:\n\t$(MAKE) build\n\t$(MAKE) test\n\t$(MAKE) clippy\n\t$(MAKE) fmt\n\t$(MAKE) cliff\n\t$(MAKE) udeps-check\n\t$(MAKE) wasm-build\n\t$(MAKE) uv-sync\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">\n  <img src=\"docs/assets/logo.png\" alt=\"Kand Logo\" width=\"250\">\n</h1>\n<div align=\"center\">\n  <a href=\"https://crates.io/crates/kand\">\n    <img src=\"https://img.shields.io/crates/v/kand.svg\" alt=\"Crates.io\"/>\n  </a>\n  <a href=\"https://pypi.python.org/pypi/kand\">\n    <img src=\"https://img.shields.io/pypi/v/kand.svg\" alt=\"PyPI Version\"/>\n  </a>\n  <a href=\"https://www.npmjs.com/package/kand\">\n    <img src=\"https://img.shields.io/npm/v/kand.svg\" alt=\"NPM Version\"/>\n  </a>\n  <a href=\"https://pypi.python.org/pypi/kand\">\n    <img src=\"https://img.shields.io/pypi/pyversions/kand.svg\" alt=\"Python Versions\"/>\n  </a>\n  <a href=\"https://github.com/kand-ta/kand/actions/workflows/CI.yml\">\n    <img src=\"https://github.com/kand-ta/kand/actions/workflows/CI.yml/badge.svg\" alt=\"CI Status\"/>\n  </a>\n  <a href=\"https://docs.rs/kand\">\n    <img src=\"https://docs.rs/kand/badge.svg\" alt=\"Docs.rs\"/>\n  </a>\n  <a href=\"https://pypi.python.org/pypi/kand\">\n    <img src=\"https://img.shields.io/pypi/l/kand.svg\" alt=\"License\"/>\n  </a>\n</div>\n<p align=\"center\">\n  <b>Documentation</b>:\n  <a href=\"https://docs.rs/kand\">Rust</a>\n  -\n  <a href=\"https://kand-ta.github.io/kand/\">Python</a>\n  |\n  <b>Repository</b>:\n  <a href=\"https://github.com/kand-ta/kand\">GitHub</a>\n</p>\n<h2 align=\"center\">\n  <b>Kand: A Modern, High-Performance Technical Analysis Library</b>\n</h2>\n\n> [!WARNING]\n> This project is under active development. APIs may change, and some features might not be fully implemented or tested yet. Contributions and feedback are welcome!\n\n<p align=\"center\">\n  <picture align=\"center\">\n    <img alt=\"EMA Performance Comparison\" src=\"docs/assets/bench_ema.png\" width=\"600\">\n  </picture>\n</p>\n\n<p align=\"center\">\n  <i>EMA calculation performance comparison across different implementations.</i>\n</p>\n\n## Why Kand?\n\n`Kand` is engineered as a modern replacement for `TA-Lib`, addressing its core limitations—such as single-threaded execution, Python GIL constraints, memory overhead, and inefficient real-time processing—while preserving its strengths in comprehensive indicator support and ease of integration. Built in Rust, `Kand` delivers superior performance, safety, and flexibility for quantitative trading, data science, and financial analysis.\n\n- **⚡ Superior Performance with Memory Safety**\n  Leveraging Rust's efficiency, `Kand` achieves speeds rivaling or exceeding `TA-Lib`'s peak performance, but with built-in memory safety that eliminates common vulnerabilities and reduces overhead in `TA-Lib`'s C-based implementation.\n\n- **🔓 True Multithreading Capabilities**\n  Unlike `TA-Lib`, which is hindered by Python's GIL and single-threaded design, `Kand` enables seamless parallel processing across multiple cores, unlocking significant gains in multi-threaded environments for large-scale computations.\n\n- **⚙️ Efficient Real-Time Incremental Updates**\n  `Kand` introduces O(1) complexity for incremental calculations, ideal for streaming data and real-time applications—overcoming `TA-Lib`'s reliance on batch processing, which introduces latency and inefficiency in dynamic scenarios.\n\n- **🚀 Zero-Copy NumPy Integration**\n  With native, zero-copy data sharing via Rust-NumPy bindings, `Kand` ensures lossless, high-speed data flow between Python and Rust, addressing `TA-Lib`'s memory copying overhead and enabling ultra-low latency (~7ns) operations.\n\n- **📊 Expanded Indicator Suite**\n  Kand supports a wide array of standard indicators (e.g., EMA, RSI, MACD) like ``TA-Lib``, while pioneering advanced ones such as Vegas, VWAP, and Supertrend, extending analytical capabilities beyond `TA-Lib`'s traditional scope.\n\n- **📦 Streamlined Installation and Lightweight Design**\n  Install with a single `pip install kand` command, featuring precompiled wheels and no complex C dependencies—solving `TA-Lib`'s notoriously cumbersome setup and reducing package bloat for effortless deployment.\n\n- **💻 Broad Cross-Platform Compatibility**\n  Seamlessly runs on macOS, Linux, and Windows, with additional support for JavaScript/TypeScript via `WebAssembly`, providing greater universality than `TA-Lib`'s platform-specific challenges.\n\n> *If you truly understand `TA-Lib`'s limitations, you'll appreciate Kand's innovations.* `Kand` isn't just about fixing what's broken—it's about enabling what's possible. Dive deeper at [**why `kand`**](https://kand-ta.github.io/kand/about).\n\n### Python API\n\nThe Python interface of `kand` leverages PyO3 for ultra-low latency bindings to the Rust core, seamlessly integrating with NumPy for zero-copy operations and true thread-safe calculations. Below are examples for batch and incremental usage.\n\n```python\nimport numpy as np\nfrom kand import ema\n\n# Batch EMA computation with zero-copy NumPy integration\nprices = np.array([10.0, 11.0, 12.0, 13.0, 14.0], dtype=np.float64)\nema_values = ema(prices, period=3)\n\n# Incremental EMA update for streaming data\nprev_ema = 13.5\nnew_price = 15.0\nnew_ema = ema_inc(new_price, prev_ema, period=3)\n```\n\n**Key Features:**\n\n- **Zero-Copy**: Operates directly on NumPy arrays, avoiding memory duplication.\n- **GIL-Free**: Rust backend releases the Python GIL, enabling parallel execution.\n- **Incremental Updates**: O(1) complexity for real-time applications.\n\n---\n\n### Rust API\n\nThe Rust interface in `kand` provides a high-performance, type-safe implementation of EMA with flexible parameter control. It supports both Vec and ndarray inputs for batch and incremental calculations, as shown below.\n\n```rust\nuse kand::ohlcv::ema;\nuse ndarray::Array1;\n\n// Batch EMA calculation over a price series\nlet prices = vec![10.0, 11.0, 12.0, 13.0, 14.0];\nlet mut ema_values = vec![0.0; prices.len()];\nema::ema(&prices, 3, None, &mut ema_values)?;\n\n// Batch EMA with ndarray for scientific workflows\nlet prices = Array1::from_vec(vec![10.0, 11.0, 12.0, 13.0, 14.0]);\nlet mut ema_values = Array1::zeros(prices.len());\nema::ema(&prices, 3, None, &mut ema_values)?;\n\n// Constant-time incremental EMA update\nlet prev_ema = 13.5;\nlet new_price = 15.0;\nlet new_ema = ema::ema_inc(new_price, prev_ema, 3, None)?;\n```\n\n**Key Features:**\n\n- **Memory Efficiency**: Leverages mutable buffers (`&mut Vec<f64>` or `&mut Array1<f64>`) to store results, slashing memory allocations.\n- **Error Handling**: Returns `Result<(), KandError>` or `Result<f64, KandError>` for reliable failure detection (e.g., invalid period, NaN inputs).\n- **Incremental Design**: O(1) updates tailored for real-time systems.\n\n---\n\n### JavaScript/TypeScript API\n\nThe JavaScript/TypeScript interface provides WebAssembly bindings for high-performance technical analysis in web applications and Node.js projects. It delivers near-native performance with a clean, synchronous API.\n\n```typescript\nimport { ema, emaInc } from 'kand';\n\n// Batch EMA computation for price series\nconst prices = new Float64Array([10.0, 11.0, 12.0, 13.0, 14.0]);\nconst emaValues = ema(prices, 3, null);\nconsole.log(emaValues);\n\n// Incremental EMA update for streaming data\nconst prevEma = 13.5;\nconst newPrice = 15.0;\nconst newEma = emaInc(newPrice, prevEma, 3, null);\nconsole.log(newEma);\n\n// Custom smoothing factor\nconst customK = 0.5;\nconst customEma = emaInc(newPrice, prevEma, 3, customK);\n```\n\n**Key Features:**\n\n- **WebAssembly Performance**: Near-native speed through optimized WASM bindings.\n- **Type Safety**: Full TypeScript definitions with comprehensive JSDoc documentation.\n- **ES Module Standard**: Adheres to the ES module standard for native integration with modern JavaScript environments.\n\n---\n\n## Setup\n\n### Python\n\nGet started with Kand in one command - no extra configuration needed:\n\n```bash\npip install kand\n```\n\n### Rust\n\nYou can take latest release from [`crates.io`](https://crates.io/crates/kand), or if you want to use the latest features / performance improvements point to the `main` branch of this repo.\n\n```bash\ncargo add kand\n```\n\nRecommend Rust version `>=1.80`.\n\n### JavaScript/TypeScript\n\nFor web applications and Node.js projects, install Kand via npm:\n\n```bash\nnpm i kand\n```\n\nThe package provides WebAssembly bindings for high-performance technical analysis in JavaScript and TypeScript environments.\n\n## Functions List\n\n### OHLCV Based\n\n- [x] **AD** - Chaikin A/D Line\n- [x] **ADOSC** - Chaikin A/D Oscillator\n- [x] **ADR** - Average Daily Range\n- [x] **ADX** - Average Directional Movement Index\n- [x] **ADXR** - Average Directional Movement Index Rating\n- [x] **AROON** - Aroon\n- [x] **AROONOSC** - Aroon Oscillator\n- [x] **ATR** - Average True Range\n- [x] **BBANDS** - Bollinger Bands\n- [x] **BOP** - Balance Of Power\n- [x] **CCI** - Commodity Channel Index\n- [x] **CDL_DOJI** - Doji\n- [x] **CDL_DRAGONFLY_DOJI** - Dragonfly Doji\n- [x] **CDL_GRAVESTONE_DOJI** - Gravestone Doji\n- [x] **CDL_HAMMER** - Hammer\n- [x] **CDL_INVERTED_HAMMER** - Inverted Hammer\n- [x] **CDL_LONG_LOWER_SHADOW** - Long Lower Shadow\n- [x] **CDL_LONG_UPPER_SHADOW** - Long Upper Shadow\n- [x] **CDL_MARUBOZU** - Marubozu\n- [x] **DEMA** - Double Exponential Moving Average\n- [x] **DX** - Directional Movement Index\n- [x] **EMA** - Exponential Moving Average\n- [x] **ECL** - Expanded Camarilla Levels **[Untested]**\n- [x] **HA** - Heikin Ashi Chart\n- [x] **MACD** - Moving Average Convergence/Divergence **[Unstable]**\n- [x] **MEDPRICE** - Median Price\n- [x] **MFI** - Money Flow Index **[No Incremental]**\n- [x] **MIDPOINT** - MidPoint over period\n- [x] **MIDPRICE** - Midpoint Price over period\n- [x] **MINUS_DI** - Minus Directional Indicator\n- [x] **MINUS_DM** - Minus Directional Movement\n- [x] **MOM** - Momentum\n- [x] **NATR** - Normalized Average True Range\n- [x] **OBV** - On Balance Volume\n- [x] **PLUS_DI** - Plus Directional Indicator\n- [x] **PLUS_DM** - Plus Directional Movement\n- [x] **RMA** - Rolling Moving Average\n- [x] **ROC** - Rate of change : ((price/prevPrice)-1)*100\n- [x] **ROCP** - Rate of change Percentage: (price-prevPrice)/prevPrice\n- [x] **ROCR** - Rate of change ratio: (price/prevPrice)\n- [x] **ROCR100** - Rate of change ratio 100 scale: (price/prevPrice)*100\n- [x] **RSI** - Relative Strength Index\n- [x] **SAR** - Parabolic SAR\n- [x] **SMA** - Simple Moving Average\n- [x] **STOCH** - Stochastic **[No Incremental]**\n- [x] **SUPERTREND** - Super Trend Indicator\n- [x] **T3** - Triple Exponential Moving Average (T3)\n- [x] **TEMA** - Triple Exponential Moving Average\n- [x] **TRANGE** - True Range\n- [x] **TRIMA** - Triangular Moving Average\n- [x] **TRIX** - 1-day Rate-Of-Change (ROC) of a Triple Smooth EMA\n- [x] **TYPPRICE** - Typical Price\n- [x] **VEGAS** - VEGAS Channel and Trend Boundary EMAs **[Untested]**\n- [x] **VWAP** - Volume Weighted Average Price\n- [x] **WCLPRICE** - Weighted Close Price\n- [x] **WILLR** - Williams' %R\n- [x] **WMA** - Weighted Moving Average\n\n### Statistical Analysis\n\n- [ ] **ALPHA** - Alpha: Measures excess returns over market\n- [ ] **BETA** - Beta: Measures sensitivity to market volatility\n- [ ] **CALMAR** - Calmar Ratio: Annual return to maximum drawdown ratio\n- [x] **CORREL** - Pearson's Correlation Coefficient\n- [ ] **DRAWDOWN** - Maximum Drawdown: Maximum potential loss\n- [ ] **KELLY** - Kelly Criterion: Optimal position sizing\n- [x] **MAX** - Highest value over a specified period\n- [x] **MIN** - Lowest value over a specified period\n- [ ] **SHARPE** - Sharpe Ratio: Risk-adjusted return measure\n- [ ] **SORTINO** - Sortino Ratio: Downside risk-adjusted returns\n- [x] **STDDEV** - Standard Deviation\n- [x] **SUM** - Summation\n- [x] **VAR** - Variance\n- [ ] **WINRATE** - Win Rate: Strategy success probability\n\n## Contributing\n\nWe are passionate about supporting contributors of all levels of experience and would love to see\nyou get involved in the project. See the\n[contributing guide](https://github.com/rust-ta/kand/blob/main/CONTRIBUTING.md) to get started.\n\n## License\n\nThis project is licensed under either of the following licenses, at your option:\n\n- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0))\n- MIT license ([LICENSE-MIT](LICENSE-MIT) or [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT))\n\nUnless you explicitly state otherwise, any contribution intentionally submitted for inclusion in kand by you, as defined in the Apache-2.0 license, shall be dually licensed as above, without any additional terms or conditions.\n\n## Disclaimer\n\nThis project is provided \"as is\" without any warranties or guarantees of any kind, express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, accuracy, or non-infringement. The authors and contributors of this project are not liable for any damages, losses, or liabilities resulting from the use of this project, including but not limited to financial losses, investment decisions, or trading outcomes.\n\nTrading or investing in financial instruments involves high risks, including the potential loss of some or all of your investment amount, and may not be suitable for all users. Before using this project for any financial purposes, you should carefully consider your investment objectives, level of experience, and risk tolerance, and seek independent professional advice if needed.\n\nThe information, data, and calculations provided by this project are for informational purposes only and do not constitute financial, investment, or trading advice, recommendations, or solicitations to buy or sell any securities or assets. We do not guarantee the accuracy, completeness, or timeliness of any output, and users assume all risks and liabilities associated with its use.\n"
  },
  {
    "path": "cliff.toml",
    "content": "# git-cliff ~ default configuration file\n# https://git-cliff.org/docs/configuration\n#\n# Lines starting with \"#\" are comments.\n# Configuration options are organized into tables and keys.\n# See documentation for more information on available options.\n\n[changelog]\n# template for the changelog header\nheader = \"\"\"\n# Changelog\\n\nThis document records all significant updates and changes to the Kand project. \\n\n\"\"\"\n# template for the changelog body\n# https://keats.github.io/tera/docs/#introduction\nbody = \"\"\"\n{% if version %}\\\n    ## [{{ version | trim_start_matches(pat=\"v\") }}] - {{ timestamp | date(format=\"%Y-%m-%d\") }}\n{% else %}\\\n    ## [unreleased]\n{% endif %}\\\n{% for group, commits in commits | group_by(attribute=\"group\") %}\n    ### {{ group | striptags | trim | upper_first }}\n    {% for commit in commits %}\n        - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\\\n            {% if commit.breaking %}[**breaking**] {% endif %}\\\n            {{ commit.message | upper_first }}\\\n    {% endfor %}\n{% endfor %}\\n\n\"\"\"\n# template for the changelog footer\nfooter = \"\"\"\n---\n\n> \"Quantitative trading begins with data, thrives on strategy, and succeeds through execution. Kand, making trading simpler.\"\n\"\"\"\n# remove the leading and trailing s\ntrim = true\n# postprocessors\npostprocessors = [\n  # { pattern = '<REPO>', replace = \"https://github.com/orhun/git-cliff\" }, # replace repository URL\n]\n# render body even when there are no releases to process\n# render_always = true\n# output file path\n# output = \"test.md\"\n\n[git]\n# parse the commits based on https://www.conventionalcommits.org\nconventional_commits = true\n# filter out the commits that are not conventional\nfilter_unconventional = true\n# process each line of a commit as an individual commit\nsplit_commits = false\n# regex for preprocessing the commit messages\ncommit_preprocessors = [\n  # Replace issue numbers\n  #{ pattern = '\\((\\w+\\s)?#([0-9]+)\\)', replace = \"([#${2}](<REPO>/issues/${2}))\"},\n  # Check spelling of the commit with https://github.com/crate-ci/typos\n  # If the spelling is incorrect, it will be automatically fixed.\n  #{ pattern = '.*', replace_command = 'typos --write-changes -' },\n]\n# regex for parsing and grouping commits\ncommit_parsers = [\n  { message = \"^feat\", group = \"<!-- 0 -->🚀 Features\" },\n  { message = \"^fix\", group = \"<!-- 1 -->🐛 Bug Fixes\" },\n  { message = \"^doc\", group = \"<!-- 3 -->📚 Documentation\" },\n  { message = \"^perf\", group = \"<!-- 4 -->⚡ Performance\" },\n  { message = \"^refactor\", group = \"<!-- 2 -->🚜 Refactor\" },\n  { message = \"^style\", group = \"<!-- 5 -->🎨 Styling\" },\n  { message = \"^test\", group = \"<!-- 6 -->🧪 Testing\" },\n  { message = \"^chore\\\\(release\\\\): prepare for\", skip = true },\n  { message = \"^chore\\\\(deps.*\\\\)\", skip = true },\n  { message = \"^chore\\\\(pr\\\\)\", skip = true },\n  { message = \"^chore\\\\(pull\\\\)\", skip = true },\n  { message = \"^chore|^ci\", group = \"<!-- 7 -->⚙️ Miscellaneous Tasks\" },\n  { body = \".*security\", group = \"<!-- 8 -->🛡️ Security\" },\n  { message = \"^revert\", group = \"<!-- 9 -->◀️ Revert\" },\n  { message = \".*\", group = \"<!-- 10 -->💼 Other\" },\n]\n# filter out the commits that are not matched by commit parsers\nfilter_commits = false\n# sort the tags topologically\ntopo_order = false\n# sort the commits inside sections by oldest/newest order\nsort_commits = \"oldest\"\n"
  },
  {
    "path": "clippy.toml",
    "content": "too-many-arguments-threshold = 20\ntype-complexity-threshold = 500\nallow-expect-in-tests = true\nallow-unwrap-in-tests = true\n"
  },
  {
    "path": "docs/about.md",
    "content": "# About Kand\n\n## The Motivation\n\nTALib has long been a cornerstone for financial indicator calculations, valued for its comprehensive feature set. However, as modern workflows demand higher performance and flexibility, its limitations have become apparent:\n\n- **Performance Bottlenecks**: TALib’s C-based core, while fast, is constrained by Python’s Global Interpreter Lock (GIL), limiting multi-threaded potential in today’s multi-core world. This issue has been a persistent challenge, as noted in discussions around the [Python bindings](https://github.com/TA-Lib/ta-lib-python/issues/675).\n- **Complex Setup**: Installing TALib often involves wrangling C library dependencies, a hurdle for users seeking quick deployment. The fact that installation issues dominate their [GitHub issues](https://github.com/TA-Lib/ta-lib-python/issues) speaks volumes about this challenge.\n- **Batch-Only Design**: TALib focuses on full-batch computations, lacking efficient incremental updates needed for real-time systems. While its Python bindings offer a stream feature for incremental calculations, it still relies on batch processing underneath, resulting in slower performance. Even attempts to address parallelism in the [native C library](https://github.com/TA-Lib/ta-lib/issues/49) highlight its multi-threading constraints.\n\nThese pain points inspired us to rethink how financial tools should work in a modern, high-performance context.\n\n## Why We Built Kand\n\n`kand` was created to address TALib’s shortcomings and deliver a next-generation solution for financial developers. Leveraging Rust’s speed and safety, we set out to build a library that’s not just an alternative, but a leap forward:\n\n- **Elite Performance**: Written in Rust, `kand` matches or exceeds TALib’s speed while adding GIL-free multi-threading for true parallelism.\n- **Seamless Integration**: Powered by `rust-numpy`, `kand` shares array memory addresses directly between Python and Rust, enabling true zero-copy data access without any overhead in cross-language operations.\n- **Real-Time Ready**: True O(1) complexity with near-zero overhead—each update is just a pure variable computation without loops or batching, making it ideal for real-time streaming data processing.\n- **Frictionless Setup**: A single `pip install` command replaces TALib’s cumbersome C setup, with precompiled wheels for all major platforms.\n- **Cross-Platform Power**: Runs effortlessly on Linux, macOS, and Windows—musl Linux included.\n\n## Our Vision\n\n`kand` isn’t just about fixing what’s broken—it’s about enabling what’s possible. Whether you’re a quant trader, data scientist, or developer, we aim to provide a tool that’s fast, reliable, and effortless to use, so you can focus on building, not battling your tools.\n\nTo see `kand` in action, check out our [Installation Guide](install.md) or dive into the [API Documentation](api.md).\n"
  },
  {
    "path": "docs/advance.md",
    "content": "# Advanced Configuration Guide\n\nThis guide explains how to customize `kand` for numerical precision, optimization, and cross-platform use. Custom configurations require building from source with `maturin`, as the default `pip install kand` provides a pre-built package with fixed settings (`f64`, `i64`, `check`).\n\n## Customization Options\n\n### Numerical Precision\n\nChoose the floating-point precision for your needs:\n\n- **`f32` (32-bit)**: Lower memory usage, great for large datasets or constrained systems.\n- **`f64` (64-bit)**: Default, higher precision for complex calculations.\n\n| Precision | Memory Usage | Precision | Use Case                  |\n|-----------|--------------|-----------|---------------------------|\n| `f32`     | Low          | Lower     | Embedded systems, big data |\n| `f64`     | Medium       | Higher    | Scientific computing      |\n\n### Integer Types\n\nSelect the integer type for indexing:\n\n- **`i32` (32-bit)**: Memory-efficient, suits smaller applications.\n- **`i64` (64-bit)**: Default, handles larger datasets.\n\n| Type  | Description     | Best For                  |\n|-------|-----------------|---------------------------|\n| `i32` | 32-bit integers | Small-scale applications  |\n| `i64` | 64-bit integers | Large datasets            |\n\n### Validation Levels\n\nAdjust validation for safety vs. performance:\n\n- **`check`**: Basic validation, ideal for production.\n- **`check-nan`**: Detailed checks, best for debugging.\n- **None**: No validation, fastest but risky.\n\n| Level        | Safety | Performance | Use Case            |\n|--------------|--------|-------------|---------------------|\n| `check`      | High   | Medium      | Production          |\n| `check-nan` | Highest| Slowest     | Debugging           |\n| None         | Low    | Fastest     | Tested environments |\n\n!!! warning \"Performance vs. Safety\"\n    Skipping validation boosts speed but may cause issues with invalid inputs. Use cautiously.\n\n---\n\n## Building from Source\n\nCustomizing `kand` requires a local build with `maturin`.\n\n### Prerequisites\n\n- **Python**: 3.8+\n- **Rust**: 1.80+\n- **maturin**: `pip install maturin`\n\n### Build Steps\n\n1. **Clone the Repository**:\n\n   ```bash\n   git clone https://github.com/kand-ta/kand.git\n   cd kand\n   ```\n\n2. **Build with Custom Features**:\n    For development (editable install):\n\n    ```bash\n    maturin develop --features f32,i64,check\n    ```\n\n!!! tip \"High-Performance Build Example\"\n    Use `--release` for an optimized build:\n    ```bash\n    maturin build --release --features f64,i64,check\n    ```\n\n---\n\n## Troubleshooting\n\n- **Feature Not Working**: Ensure `--features` matches your desired configuration (e.g., `f32,i64,check`).\n- **Build Fails**: Check Rust/Python versions or run `cargo build` to debug.\n- **Help**: Visit [GitHub Discussions](https://github.com/kand-ta/kand/discussions).\n\n---\n\n## Performance Trade-offs\n\n| Configuration         | Memory | Speed  | Precision |\n|-----------------------|--------|--------|-----------|\n| `f32, i32, no checks` | Lowest | Fastest| Lowest    |\n| `f32, i64, check`     | Low    | Fast   | Low       |\n| `f64, i64, check`     | Medium | Medium | High      |\n| `f64, i64, check-nan` | High   | Slowest| Highest   |\n\nSee the [Performance Guide](performance.md) for benchmarks.\n"
  },
  {
    "path": "docs/api.md",
    "content": "\n::: kand\n"
  },
  {
    "path": "docs/install.md",
    "content": "# Installation Guide\n\nGet started with `kand` through Python or Docker. This guide covers all installation methods and system compatibility details.\n\n## Python Installation\n\n### Requirements\n- Python 3.8+\n- `pip` (Python package installer)\n\n### Install from PyPI\nInstall `kand` with one command—precompiled wheels available for instant setup:\n\n```bash\npip install kand\n```\n\n!!! tip \"Supported Platforms & Python Versions\"\n    We provide precompiled packages on PyPI for major systems and Python versions:\n\n    | Platform     | Supported Python Versions         |\n    |--------------|-----------------------------------|\n    | **Linux**    | 3.8, 3.9, 3.10, 3.11, 3.12       |\n    | **musl Linux** | 3.8, 3.9, 3.10, 3.11, 3.12     |\n    | **Windows**  | 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 |\n    | **macOS**    | 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 |\n\n    No compilation needed—just `pip install` and go!\n\n## Docker Usage\n\n### Pull the Official Image\nGrab the latest `kand` container:\n\n```bash\ndocker pull ghcr.io/rust-ta/kand:latest\n```\n\n### Run with Docker\nLaunch it interactively:\n\n```bash\ndocker run -it --rm ghcr.io/rust-ta/kand:latest\n```\n\nOr build your own:\n\n```bash\ndocker build -t my-kand-app .\ndocker run -it --rm my-kand-app\n```\n\n## Troubleshooting\n\nEncounter issues? Try these steps:\n\n1. Update `pip` or `cargo` to the latest version.\n2. Verify Python (3.8+) or Rust (1.80+) compatibility.\n3. Ensure the Docker daemon is running.\n4. Check [GitHub Issues](https://github.com/rust-ta/kand/issues) for solutions.\n\n!!! note\n    Still stuck? Join our community or file an issue on GitHub!\n\n## Next Steps\n\n- Explore the [API Documentation](api.md) to dive into `kand`.\n- Join our community for help and updates.\n- Report bugs or suggestions on [GitHub](https://github.com/rust-ta/kand/issues).\n"
  },
  {
    "path": "docs/performance.md",
    "content": "# Performance Testing\n\n## Introduction\n\nThis document showcases the performance testing results for Exponential Moving Average (EMA) computations, comparing single-threaded and multi-threaded approaches. Tests were conducted on two platforms: a Windows laptop featuring an Intel Core i7-13700 processor and a Mac Mini powered by an Apple M4 chip. The analysis highlights how thread count impacts computational efficiency across diverse dataset sizes, with a special focus on the superior scalability of `kand` over `talib`.\n\n!!! info\n    **`kand` vs. `talib`**: While `talib` is constrained to single-threaded execution, `kand` leverages multi-threading to unlock significant performance gains, especially on modern multi-core hardware.\n\n---\n\n## Test Environment\n\n!!! abstract \"Hardware Specifications\"\n    === \"Windows Platform\"\n        - **Device**: Laptop\n        - **CPU**: Intel Core i7-13700\n            * 6 Performance cores\n        - **OS**: Windows 11 Pro\n\n    === \"Mac Platform\"\n        - **Device**: Mac Mini\n        - **CPU**: Apple M4\n            * 4 Performance cores\n        - **OS**: macOS Sonoma\n\n!!! info \"Software Stack\"\n    | Component     | Version | Description |\n    |--------------|---------|-------------|\n    | :snake: Python | 3.11    | Core runtime environment |\n    | :chart_with_upwards_trend: TA-Lib | 0.6.3   | Technical analysis baseline |\n    | :zap: Kand    | 0.0.11  | High-performance implementation |\n\n!!! tip \"Version Compatibility\"\n    All tests were conducted using the latest stable releases of each component. Performance characteristics may vary with different versions.\n\n---\n\n## Test Methodology\n\n!!! note \"Test Code Location\"\n    All benchmark code is available in the `python/benches` directory:\n    - `bench_ema.py`: Single-thread performance testing\n    - `bench_ema_mt.py`: Multi-thread performance testing\n\n!!! warning \"System Variability\"\n    Performance results may vary significantly across different systems due to:\n    - CPU architecture and clock speeds\n    - Memory configuration and speed\n    - Operating system scheduling\n    - System load and background processes\n    - Thermal conditions\n\n    We strongly recommend running the benchmarks on your specific system for the most accurate performance assessment.\n\n- **Single-threaded tests**: Executed using 1 thread with `talib.EMA`.\n- **Multi-threaded tests**: Conducted with `kand.ema`:\n  - Windows i7-13700: 2, 4, and 6 threads.\n  - Mac Mini M4: 2 and 4 threads.\n- **Dataset sizes**: 50K, 100K, 250K, 500K, 1M, 2.5M, 5M, 10M data points.\n- **EMA period**: 30.\n- **Runs per test**: 1000 iterations to calculate average execution time.\n- **Tools**: Python scripts leveraging `talib.EMA` (single-threaded) and `kand.ema` (multi-threaded).\n\n---\n\n## Results\n\n### Windows i7-13700 Mobile\n\n=== \"Single-threaded\"\n    ![Windows Single-threaded](assets/batch_ema_performance_win_i713700.png)\n    *Figure 1: EMA computation performance on Windows with 1 thread using `talib`.*\n\n=== \"2 Threads\"\n    ![Windows 2 Threads](assets/batch_ema_performance_mt_2_win_i713700.png)\n    *Figure 2: Performance with 2 threads using `kand`.*\n\n=== \"4 Threads\"\n    ![Windows 4 Threads](assets/batch_ema_performance_mt_4_win_i713700.png)\n    *Figure 3: Performance with 4 threads using `kand`.*\n\n=== \"6 Threads\"\n    ![Windows 6 Threads](assets/batch_ema_performance_mt_6_win_i713700.png)\n    *Figure 4: Performance with 6 threads using `kand`.*\n\n### Mac Mini M4\n\n=== \"Single-threaded\"\n    ![Mac Single-threaded](assets/batch_ema_performance_mac_m4.png)\n    *Figure 5: EMA computation performance on Mac Mini with 1 thread using `talib`.*\n\n=== \"2 Threads\"\n    ![Mac 2 Threads](assets/batch_ema_performance_mt_2_mac_m4.png)\n    *Figure 6: Performance with 2 threads using `kand`.*\n\n=== \"4 Threads\"\n    ![Mac 4 Threads](assets/batch_ema_performance_mt_4_mac_m4.png)\n    *Figure 7: Performance with 4 threads using `kand`.*\n\n---\n\n## Analysis\n\nThe results reveal compelling insights into performance trends:\n\n- **Multi-threading Advantage with `kand`**: Unlike `talib`, which is bottlenecked by single-threaded execution, `kand` harnesses multiple threads to drastically reduce EMA computation times. This advantage becomes increasingly evident with larger datasets (e.g., 5M and 10M points).\n\n- **Scalability**:\n\n  - On the Windows i7-13700, performance peaks at 6 threads, fully utilizing its 6 P-cores.\n  - On the Mac Mini M4, efficiency maxes out at 4 threads, aligned with its 4 P-cores.\n\n!!! tip \"Why `kand` Outshines `talib`\"\n    The single-threaded nature of `talib` limits its ability to exploit modern multi-core CPUs. In contrast, `kand`’s multi-threaded design scales seamlessly with core count, delivering superior performance on datasets of any size.\n\n!!! success \"Key Takeaway\"\n    By embracing multi-threading, `kand` unlocks the full potential of modern processors, leaving `talib`’s single-threaded approach in the dust—especially for high-performance financial computations.\n\n!!! note \"Key Takeaway\"\n    📌 Thread counts are capped by the number of performance cores (P-cores). The i7-13700 supports up to 6 threads with its 6 P-cores, while the M4 is limited to 4 threads due to its 4 P-cores.\n\n---\n\n## Conclusion\n\nMulti-threading capabilities in `kand` deliver exceptional performance gains across various technical analysis computations, consistently outperforming the single-threaded limitations of traditional libraries like `talib`. Our benchmarks on the Windows i7-13700 (6 P-cores) and Mac Mini M4 (4 P-cores) demonstrate impressive scalability, with the i7-13700 achieving superior throughput at higher thread counts. For applications dealing with large-scale financial data processing, `kand`'s sophisticated multi-core utilization architecture provides a significant competitive advantage. This foundation sets the stage for future optimizations across different hardware configurations and computational scenarios, making `kand` an ideal choice for high-performance financial analysis systems.\n"
  },
  {
    "path": "kand/Cargo.toml",
    "content": "[package]\nname = \"kand\"\nversion = \"0.2.2\"\nedition = \"2024\"\nauthors = [\"CtrlX <gitctrlx@gmail.com>\"]\nreadme = \"../README.md\"\ndescription = \"Kand: A Modern, High-Performance Technical Analysis Library.\"\nlicense = \"Apache-2.0 OR MIT\"\nrepository = \"https://github.com/rust-ta/kand\"\ndocumentation = \"https://docs.rs/kand\"\nkeywords = [\"technical-analysis\", \"finance\", \"rust\", \"talib\", \"indicators\"]\ncategories = [\"finance\", \"algorithms\", \"data-structures\", \"science\", \"mathematics\"]\n\n[dependencies]\nnum_enum = { workspace = true }\nthiserror = { workspace = true }\n\n[dev-dependencies]\napprox = { workspace = true }\ncriterion = { workspace = true }\ncsv = { workspace = true }\nndarray = { workspace = true }\nrand = { workspace = true }\nrayon = { workspace = true }\n\n[[bench]]\nname = \"bench_main\"\nharness = false\n\n[features]\ndefault = [\"f64\", \"i64\", \"check\"]     # Default: extended precision with basic checks\nf32 = []                              # 32-bit floating point\nf64 = []                              # 64-bit floating point\ni32 = []                              # 32-bit integer\ni64 = []                              # 64-bit integer\ncheck = []                            # Basic validation checks\ncheck-nan = []                        # Check for NaN values in input data\nallow-nan = []                        # Allow NaN values in output data\n"
  },
  {
    "path": "kand/benches/bench_main.rs",
    "content": "use criterion::criterion_main;\n\nmod benchmarks;\nmod helper;\n\ncriterion_main! {\n    // OHLCV benchmarks\n    benchmarks::ohlcv::ad_bench::ohlcv,\n    benchmarks::ohlcv::adosc_bench::ohlcv,\n    benchmarks::ohlcv::adr_bench::ohlcv,\n    benchmarks::ohlcv::adx_bench::ohlcv,\n    benchmarks::ohlcv::adxr_bench::ohlcv,\n    benchmarks::ohlcv::aroon_bench::ohlcv,\n    benchmarks::ohlcv::aroonosc_bench::ohlcv,\n    benchmarks::ohlcv::atr_bench::ohlcv,\n    benchmarks::ohlcv::bbands_bench::ohlcv,\n    benchmarks::ohlcv::bop_bench::ohlcv,\n    benchmarks::ohlcv::cci_bench::ohlcv,\n    benchmarks::ohlcv::cdl_doji_bench::ohlcv,\n    benchmarks::ohlcv::cdl_dragonfly_doji_bench::ohlcv,\n    benchmarks::ohlcv::cdl_gravestone_doji_bench::ohlcv,\n    benchmarks::ohlcv::cdl_hammer_bench::ohlcv,\n    benchmarks::ohlcv::cdl_inverted_hammer_bench::ohlcv,\n    benchmarks::ohlcv::cdl_long_shadow_bench::ohlcv,\n    benchmarks::ohlcv::cdl_marubozu_bench::ohlcv,\n    benchmarks::ohlcv::dema_bench::ohlcv,\n    benchmarks::ohlcv::dx_bench::ohlcv,\n    benchmarks::ohlcv::ecl_bench::ohlcv,\n    benchmarks::ohlcv::ema_bench::ohlcv,\n    benchmarks::ohlcv::macd_bench::ohlcv,\n    benchmarks::ohlcv::medprice_bench::ohlcv,\n    benchmarks::ohlcv::mfi_bench::ohlcv,\n    benchmarks::ohlcv::midpoint_bench::ohlcv,\n    benchmarks::ohlcv::midprice_bench::ohlcv,\n    benchmarks::ohlcv::minus_di_bench::ohlcv,\n    benchmarks::ohlcv::minus_dm_bench::ohlcv,\n    benchmarks::ohlcv::mom_bench::ohlcv,\n    benchmarks::ohlcv::natr_bench::ohlcv,\n    benchmarks::ohlcv::obv_bench::ohlcv,\n    benchmarks::ohlcv::plus_di_bench::ohlcv,\n    benchmarks::ohlcv::plus_dm_bench::ohlcv,\n    benchmarks::ohlcv::rma_bench::ohlcv,\n    benchmarks::ohlcv::roc_bench::ohlcv,\n    benchmarks::ohlcv::rocp_bench::ohlcv,\n    benchmarks::ohlcv::rocr_bench::ohlcv,\n    benchmarks::ohlcv::rocr100_bench::ohlcv,\n    benchmarks::ohlcv::rsi_bench::ohlcv,\n    benchmarks::ohlcv::sar_bench::ohlcv,\n    benchmarks::ohlcv::sma_bench::ohlcv,\n    benchmarks::ohlcv::stoch_bench::ohlcv,\n    benchmarks::ohlcv::supertrend_bench::ohlcv,\n    benchmarks::ohlcv::t3_bench::ohlcv,\n    benchmarks::ohlcv::tema_bench::ohlcv,\n    benchmarks::ohlcv::trix_bench::ohlcv,\n    benchmarks::ohlcv::trange_bench::ohlcv,\n    benchmarks::ohlcv::trima_bench::ohlcv,\n    benchmarks::ohlcv::trix_bench::ohlcv,\n    benchmarks::ohlcv::typprice_bench::ohlcv,\n    benchmarks::ohlcv::vegas_bench::ohlcv,\n    benchmarks::ohlcv::wclprice_bench::ohlcv,\n    benchmarks::ohlcv::willr_bench::ohlcv,\n    benchmarks::ohlcv::wma_bench::ohlcv,\n    benchmarks::ohlcv::vwap_bench::ohlcv,\n\n    // Stats benchmarks\n    benchmarks::stats::max_bench::stats,\n    benchmarks::stats::min_bench::stats,\n    benchmarks::stats::stddev_bench::stats,\n    benchmarks::stats::sum_bench::stats,\n    benchmarks::stats::var_bench::stats,\n\n    // Helper benchmarks\n    // benchmarks::helper::helper,\n}\n"
  },
  {
    "path": "kand/benches/benchmarks/helper.rs",
    "content": ""
  },
  {
    "path": "kand/benches/benchmarks/mod.rs",
    "content": "pub mod ohlcv;\npub mod stats;\n\n// pub mod helper;\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/ad_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::ad::ad;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_ad(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"AD\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n\n    for size in sizes {\n        let high = generate_test_data(size);\n        let low = generate_test_data(size);\n        let close = generate_test_data(size);\n        let volume = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        group.bench_with_input(BenchmarkId::new(\"size\", size), &size, |b, _| {\n            b.iter(|| {\n                // Use black_box on the result to prevent LLVM from optimizing away the computation\n                let _ = ad(\n                    black_box(&high),\n                    black_box(&low),\n                    black_box(&close),\n                    black_box(&volume),\n                    black_box(&mut output),\n                );\n            });\n        });\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_ad);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/adosc_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::adosc::adosc;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_adosc(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"adosc\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let fast_periods = [3, 5, 10];\n    let slow_periods = [10, 20, 30];\n\n    for size in sizes {\n        let high = generate_test_data(size);\n        let low = generate_test_data(size);\n        let close = generate_test_data(size);\n        let volume = generate_test_data(size);\n        let mut output_adosc = vec![0.0; size];\n        let mut output_ad = vec![0.0; size];\n        let mut output_fast_ema = vec![0.0; size];\n        let mut output_slow_ema = vec![0.0; size];\n\n        for (fast_period, slow_period) in fast_periods.iter().zip(slow_periods.iter()) {\n            group.bench_with_input(\n                BenchmarkId::new(\n                    format!(\"size_{size}_fast_{fast_period}_slow_{slow_period}\"),\n                    format!(\"{fast_period}-{slow_period}\"),\n                ),\n                &(fast_period, slow_period),\n                |b, &(fast_period, slow_period)| {\n                    b.iter(|| {\n                        let _ = adosc(\n                            black_box(&high),\n                            black_box(&low),\n                            black_box(&close),\n                            black_box(&volume),\n                            black_box(*fast_period),\n                            black_box(*slow_period),\n                            black_box(&mut output_adosc),\n                            black_box(&mut output_ad),\n                            black_box(&mut output_fast_ema),\n                            black_box(&mut output_slow_ema),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_adosc);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/adr_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::adr::adr;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_adr(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"adr\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = adr(\n                            black_box(&input_high),\n                            black_box(&input_low),\n                            black_box(period),\n                            black_box(&mut output),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_adr);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/adx_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::adx::adx;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_adx(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"adx\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output_adx = vec![0.0; size];\n        let mut output_smoothed_plus_dm = vec![0.0; size];\n        let mut output_smoothed_minus_dm = vec![0.0; size];\n        let mut output_smoothed_tr = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = adx(\n                            black_box(&input_high),\n                            black_box(&input_low),\n                            black_box(&input_close),\n                            black_box(period),\n                            black_box(&mut output_adx),\n                            black_box(&mut output_smoothed_plus_dm),\n                            black_box(&mut output_smoothed_minus_dm),\n                            black_box(&mut output_smoothed_tr),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_adx);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/adxr_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::adxr::adxr;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_adxr(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"adxr\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output_adxr = vec![0.0; size];\n        let mut output_adx = vec![0.0; size];\n        let mut output_smoothed_plus_dm = vec![0.0; size];\n        let mut output_smoothed_minus_dm = vec![0.0; size];\n        let mut output_smoothed_tr = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = adxr(\n                            black_box(&input_high),\n                            black_box(&input_low),\n                            black_box(&input_close),\n                            black_box(period),\n                            black_box(&mut output_adxr),\n                            black_box(&mut output_adx),\n                            black_box(&mut output_smoothed_plus_dm),\n                            black_box(&mut output_smoothed_minus_dm),\n                            black_box(&mut output_smoothed_tr),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_adxr);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/aroon_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ta::ohlcv::aroon::aroon;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_aroon(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"aroon\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let mut output_aroon_up = vec![0.0; size];\n        let mut output_aroon_down = vec![0.0; size];\n        let mut output_prev_high = vec![0.0; size];\n        let mut output_prev_low = vec![0.0; size];\n        let mut output_days_since_high = vec![0; size];\n        let mut output_days_since_low = vec![0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = aroon(\n                            black_box(&input_high),\n                            black_box(&input_low),\n                            black_box(period),\n                            black_box(&mut output_aroon_up),\n                            black_box(&mut output_aroon_down),\n                            black_box(&mut output_prev_high),\n                            black_box(&mut output_prev_low),\n                            black_box(&mut output_days_since_high),\n                            black_box(&mut output_days_since_low),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_aroon);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/aroonosc_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::aroonosc::aroonosc;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_aroonosc(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"aroonosc\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let mut output_aroonosc = vec![0.0; size];\n        let mut output_prev_high = vec![0.0; size];\n        let mut output_prev_low = vec![0.0; size];\n        let mut output_days_since_high = vec![0; size];\n        let mut output_days_since_low = vec![0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = aroonosc(\n                            black_box(&input_high),\n                            black_box(&input_low),\n                            black_box(period),\n                            black_box(&mut output_aroonosc),\n                            black_box(&mut output_prev_high),\n                            black_box(&mut output_prev_low),\n                            black_box(&mut output_days_since_high),\n                            black_box(&mut output_days_since_low),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_aroonosc);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/atr_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::atr::atr;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_atr(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"atr\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let high = generate_test_data(size);\n        let low = generate_test_data(size);\n        let close = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = atr(\n                            black_box(&high),\n                            black_box(&low),\n                            black_box(&close),\n                            black_box(period),\n                            black_box(&mut output),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_atr);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/bbands_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::bbands::bbands;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_bbands(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"bbands\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_price = generate_test_data(size);\n        let mut output_upper = vec![0.0; size];\n        let mut output_middle = vec![0.0; size];\n        let mut output_lower = vec![0.0; size];\n        let mut output_sma = vec![0.0; size];\n        let mut output_var = vec![0.0; size];\n        let mut output_sum = vec![0.0; size];\n        let mut output_sum_sq = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = bbands(\n                            black_box(&input_price),\n                            black_box(period),\n                            black_box(2.0),\n                            black_box(2.0),\n                            black_box(&mut output_upper),\n                            black_box(&mut output_middle),\n                            black_box(&mut output_lower),\n                            black_box(&mut output_sma),\n                            black_box(&mut output_var),\n                            black_box(&mut output_sum),\n                            black_box(&mut output_sum_sq),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_bbands);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/bop_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::bop::bop;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_bop(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"bop\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n\n    for size in sizes {\n        let input_open = generate_test_data(size);\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output_bop = vec![0.0; size];\n\n        group.bench_with_input(BenchmarkId::new(\"size\", size), &size, |b, &_size| {\n            b.iter(|| {\n                let _ = bop(\n                    black_box(&input_open),\n                    black_box(&input_high),\n                    black_box(&input_low),\n                    black_box(&input_close),\n                    black_box(&mut output_bop),\n                );\n            });\n        });\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_bop);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/cci_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::cci::cci;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_cci(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"cci\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output_cci = vec![0.0; size];\n        let mut output_tp = vec![0.0; size];\n        let mut output_sma_tp = vec![0.0; size];\n        let mut output_mean_dev = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = cci(\n                            black_box(&input_high),\n                            black_box(&input_low),\n                            black_box(&input_close),\n                            black_box(period),\n                            black_box(&mut output_cci),\n                            black_box(&mut output_tp),\n                            black_box(&mut output_sma_tp),\n                            black_box(&mut output_mean_dev),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_cci);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/cdl_doji_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::cdl_doji::cdl_doji;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_cdl_doji(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"cdl_doji\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n\n    for size in sizes {\n        let input_open = generate_test_data(size);\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output_signals = vec![0; size];\n\n        group.bench_with_input(BenchmarkId::new(\"size\", size), &size, |b, &_size| {\n            b.iter(|| {\n                let _ = cdl_doji(\n                    black_box(&input_open),\n                    black_box(&input_high),\n                    black_box(&input_low),\n                    black_box(&input_close),\n                    black_box(0.1),\n                    black_box(0.1),\n                    black_box(&mut output_signals),\n                );\n            });\n        });\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_cdl_doji);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/cdl_dragonfly_doji_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::cdl_dragonfly_doji::cdl_dragonfly_doji;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_cdl_dragonfly_doji(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"cdl_dragonfly_doji\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n\n    for size in sizes {\n        let input_open = generate_test_data(size);\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output_signals = vec![0; size];\n\n        group.bench_with_input(BenchmarkId::new(\"size\", size), &size, |b, &_size| {\n            b.iter(|| {\n                let _ = cdl_dragonfly_doji(\n                    black_box(&input_open),\n                    black_box(&input_high),\n                    black_box(&input_low),\n                    black_box(&input_close),\n                    black_box(0.1),\n                    black_box(&mut output_signals),\n                );\n            });\n        });\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_cdl_dragonfly_doji);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/cdl_gravestone_doji_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::cdl_gravestone_doji::cdl_gravestone_doji;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_cdl_gravestone_doji(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"cdl_gravestone_doji\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n\n    for size in sizes {\n        let input_open = generate_test_data(size);\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output_signals = vec![0; size];\n\n        group.bench_with_input(BenchmarkId::new(\"size\", size), &size, |b, &_size| {\n            b.iter(|| {\n                let _ = cdl_gravestone_doji(\n                    black_box(&input_open),\n                    black_box(&input_high),\n                    black_box(&input_low),\n                    black_box(&input_close),\n                    black_box(0.1),\n                    black_box(&mut output_signals),\n                );\n            });\n        });\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_cdl_gravestone_doji);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/cdl_hammer_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::cdl_hammer::cdl_hammer;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n#[allow(dead_code)]\nfn bench_cdl_hammer(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"cdl_hammer\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n\n    for size in sizes {\n        let input_open = generate_test_data(size);\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output_signals = vec![0; size];\n        let mut output_body_avg = vec![0.0; size];\n\n        group.bench_with_input(BenchmarkId::new(\"size\", size), &size, |b, &_size| {\n            b.iter(|| {\n                let _ = cdl_hammer(\n                    black_box(&input_open),\n                    black_box(&input_high),\n                    black_box(&input_low),\n                    black_box(&input_close),\n                    black_box(5),\n                    black_box(0.1),\n                    black_box(&mut output_signals),\n                    black_box(&mut output_body_avg),\n                );\n            });\n        });\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_cdl_hammer);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/cdl_inverted_hammer_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::cdl_inverted_hammer::cdl_inverted_hammer;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n#[allow(dead_code)]\nfn bench_cdl_inverted_hammer(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"cdl_inverted_hammer\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_open = generate_test_data(size);\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output_signals = vec![0; size];\n        let mut output_body_avg = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = cdl_inverted_hammer(\n                            black_box(&input_open),\n                            black_box(&input_high),\n                            black_box(&input_low),\n                            black_box(&input_close),\n                            black_box(period),\n                            black_box(0.1),\n                            black_box(&mut output_signals),\n                            black_box(&mut output_body_avg),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_cdl_inverted_hammer);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/cdl_long_shadow_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::cdl_long_shadow::cdl_long_shadow;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n#[allow(dead_code)]\nfn bench_cdl_long_shadow(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"cdl_long_shadow\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_open = generate_test_data(size);\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output_signals = vec![0; size];\n        let mut output_body_avg = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = cdl_long_shadow(\n                            black_box(&input_open),\n                            black_box(&input_high),\n                            black_box(&input_low),\n                            black_box(&input_close),\n                            black_box(period),\n                            black_box(0.5),\n                            black_box(&mut output_signals),\n                            black_box(&mut output_body_avg),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_cdl_long_shadow);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/cdl_marubozu_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::cdl_marubozu::cdl_marubozu;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_cdl_marubozu(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"cdl_marubozu\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_open = generate_test_data(size);\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output_signals = vec![0; size];\n        let mut output_body_avg = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = cdl_marubozu(\n                            black_box(&input_open),\n                            black_box(&input_high),\n                            black_box(&input_low),\n                            black_box(&input_close),\n                            black_box(period),\n                            black_box(0.1),\n                            black_box(&mut output_signals),\n                            black_box(&mut output_body_avg),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_cdl_marubozu);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/dema_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::dema::dema;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n#[allow(dead_code)]\nfn bench_dema(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"dema\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output_dema = vec![0.0; size];\n        let mut output_ema1 = vec![0.0; size];\n        let mut output_ema2 = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = dema(\n                            black_box(&input),\n                            black_box(period),\n                            black_box(&mut output_dema),\n                            black_box(&mut output_ema1),\n                            black_box(&mut output_ema2),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_dema);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/dx_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::dx::dx;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_dx(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"dx\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output_dx = vec![0.0; size];\n        let mut output_smoothed_plus_dm = vec![0.0; size];\n        let mut output_smoothed_minus_dm = vec![0.0; size];\n        let mut output_smoothed_tr = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = dx(\n                            black_box(&input_high),\n                            black_box(&input_low),\n                            black_box(&input_close),\n                            black_box(period),\n                            black_box(&mut output_dx),\n                            black_box(&mut output_smoothed_plus_dm),\n                            black_box(&mut output_smoothed_minus_dm),\n                            black_box(&mut output_smoothed_tr),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_dx);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/ecl_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::ecl::ecl;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\n#[allow(clippy::similar_names)]\nfn bench_ecl(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"ecl\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output_h5 = vec![0.0; size];\n        let mut output_h4 = vec![0.0; size];\n        let mut output_h3 = vec![0.0; size];\n        let mut output_h2 = vec![0.0; size];\n        let mut output_h1 = vec![0.0; size];\n        let mut output_l1 = vec![0.0; size];\n        let mut output_l2 = vec![0.0; size];\n        let mut output_l3 = vec![0.0; size];\n        let mut output_l4 = vec![0.0; size];\n        let mut output_l5 = vec![0.0; size];\n\n        group.bench_with_input(BenchmarkId::new(\"size\", size), &size, |b, &_size| {\n            b.iter(|| {\n                let _ = ecl(\n                    black_box(&input_high),\n                    black_box(&input_low),\n                    black_box(&input_close),\n                    black_box(&mut output_h5),\n                    black_box(&mut output_h4),\n                    black_box(&mut output_h3),\n                    black_box(&mut output_h2),\n                    black_box(&mut output_h1),\n                    black_box(&mut output_l1),\n                    black_box(&mut output_l2),\n                    black_box(&mut output_l3),\n                    black_box(&mut output_l4),\n                    black_box(&mut output_l5),\n                );\n            });\n        });\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_ecl);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/ema_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::ema::ema;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_ema(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"ema\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = ema(\n                            black_box(&input),\n                            black_box(period),\n                            black_box(None),\n                            black_box(&mut output),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_ema);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/macd_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::macd::macd;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_macd(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"macd\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let fast_periods = [12, 26, 9]; // Standard MACD periods\n    let slow_periods = vec![26, 52, 18];\n    let signal_periods = vec![9, 18, 6];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut macd_line = vec![0.0; size];\n        let mut signal_line = vec![0.0; size];\n        let mut histogram = vec![0.0; size];\n        let mut fast_ema = vec![0.0; size];\n        let mut slow_ema = vec![0.0; size];\n\n        for ((fast_period, slow_period), signal_period) in\n            fast_periods.iter().zip(&slow_periods).zip(&signal_periods)\n        {\n            group.bench_with_input(\n                BenchmarkId::new(\n                    format!(\n                        \"size_{size}_fast_{fast_period}_slow_{slow_period}_signal_{signal_period}\"\n                    ),\n                    format!(\"{fast_period}-{slow_period}-{signal_period}\"),\n                ),\n                &(*fast_period, *slow_period, *signal_period),\n                |b, &(fast_period, slow_period, signal_period)| {\n                    b.iter(|| {\n                        let _ = macd(\n                            black_box(&input),\n                            black_box(fast_period),\n                            black_box(slow_period),\n                            black_box(signal_period),\n                            black_box(&mut macd_line),\n                            black_box(&mut signal_line),\n                            black_box(&mut histogram),\n                            black_box(&mut fast_ema),\n                            black_box(&mut slow_ema),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_macd);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/medprice_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::medprice::medprice;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_medprice(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"medprice\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let mut output_medprice = vec![0.0; size];\n\n        group.bench_with_input(BenchmarkId::new(\"size\", size), &size, |b, &_size| {\n            b.iter(|| {\n                let _ = medprice(\n                    black_box(&input_high),\n                    black_box(&input_low),\n                    black_box(&mut output_medprice),\n                );\n            });\n        });\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_medprice);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/mfi_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::mfi::mfi;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_mfi(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"mfi\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let input_volume = generate_test_data(size);\n        let mut output_mfi = vec![0.0; size];\n        let mut output_typ_prices = vec![0.0; size];\n        let mut output_money_flows = vec![0.0; size];\n        let mut output_pos_flows = vec![0.0; size];\n        let mut output_neg_flows = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = mfi(\n                            black_box(&input_high),\n                            black_box(&input_low),\n                            black_box(&input_close),\n                            black_box(&input_volume),\n                            black_box(period),\n                            black_box(&mut output_mfi),\n                            black_box(&mut output_typ_prices),\n                            black_box(&mut output_money_flows),\n                            black_box(&mut output_pos_flows),\n                            black_box(&mut output_neg_flows),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_mfi);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/midpoint_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::midpoint::midpoint;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_midpoint(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"midpoint\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_price = generate_test_data(size);\n        let mut output_midpoint = vec![0.0; size];\n        let mut output_highest = vec![0.0; size];\n        let mut output_lowest = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = midpoint(\n                            black_box(&input_price),\n                            black_box(period),\n                            black_box(&mut output_midpoint),\n                            black_box(&mut output_highest),\n                            black_box(&mut output_lowest),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_midpoint);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/midprice_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::midprice::midprice;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_midprice(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"midprice\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let mut output_midprice = vec![0.0; size];\n        let mut output_highest_high = vec![0.0; size];\n        let mut output_lowest_low = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = midprice(\n                            black_box(&input_high),\n                            black_box(&input_low),\n                            black_box(period),\n                            black_box(&mut output_midprice),\n                            black_box(&mut output_highest_high),\n                            black_box(&mut output_lowest_low),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_midprice);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/minus_di_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::minus_di::minus_di;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_minus_di(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"minus_di\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let high = generate_test_data(size);\n        let low = generate_test_data(size);\n        let close = generate_test_data(size);\n        let mut output_minus_di = vec![0.0; size];\n        let mut output_smoothed_minus_dm = vec![0.0; size];\n        let mut output_smoothed_tr = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = minus_di(\n                            black_box(&high),\n                            black_box(&low),\n                            black_box(&close),\n                            black_box(period),\n                            black_box(&mut output_minus_di),\n                            black_box(&mut output_smoothed_minus_dm),\n                            black_box(&mut output_smoothed_tr),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_minus_di);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/minus_dm_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::minus_dm::minus_dm;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_minus_dm(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"minus_dm\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = minus_dm(\n                            black_box(&input_high),\n                            black_box(&input_low),\n                            black_box(period),\n                            black_box(&mut output),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_minus_dm);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/mod.rs",
    "content": "pub mod ad_bench;\npub mod adosc_bench;\npub mod adr_bench;\npub mod adx_bench;\npub mod adxr_bench;\npub mod aroon_bench;\npub mod aroonosc_bench;\npub mod atr_bench;\npub mod bbands_bench;\npub mod bop_bench;\npub mod cci_bench;\npub mod cdl_doji_bench;\npub mod cdl_dragonfly_doji_bench;\npub mod cdl_gravestone_doji_bench;\npub mod cdl_hammer_bench;\npub mod cdl_inverted_hammer_bench;\npub mod cdl_long_shadow_bench;\npub mod cdl_marubozu_bench;\npub mod dema_bench;\npub mod dx_bench;\npub mod ecl_bench;\npub mod ema_bench;\npub mod macd_bench;\npub mod medprice_bench;\npub mod mfi_bench;\npub mod midpoint_bench;\npub mod midprice_bench;\npub mod minus_di_bench;\npub mod minus_dm_bench;\npub mod mom_bench;\npub mod natr_bench;\npub mod obv_bench;\npub mod plus_di_bench;\npub mod plus_dm_bench;\npub mod rma_bench;\npub mod roc_bench;\npub mod rocp_bench;\npub mod rocr100_bench;\npub mod rocr_bench;\npub mod rsi_bench;\npub mod sar_bench;\npub mod sma_bench;\npub mod stoch_bench;\npub mod supertrend_bench;\npub mod t3_bench;\npub mod tema_bench;\npub mod trange_bench;\npub mod trima_bench;\npub mod trix_bench;\npub mod typprice_bench;\npub mod vegas_bench;\npub mod vwap_bench;\npub mod wclprice_bench;\npub mod willr_bench;\npub mod wma_bench;\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/mom_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::mom::mom;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_mom(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"mom\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = mom(black_box(&input), black_box(period), black_box(&mut output));\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_mom);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/natr_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::natr::natr;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_natr(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"natr\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let high = generate_test_data(size);\n        let low = generate_test_data(size);\n        let close = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = natr(\n                            black_box(&high),\n                            black_box(&low),\n                            black_box(&close),\n                            black_box(period),\n                            black_box(&mut output),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_natr);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/obv_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::obv::obv;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_obv(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"obv\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n\n    for size in sizes {\n        let close = generate_test_data(size);\n        let volume = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        group.bench_with_input(BenchmarkId::new(\"size\", size), &size, |b, &_size| {\n            b.iter(|| {\n                let _ = obv(\n                    black_box(&close),\n                    black_box(&volume),\n                    black_box(&mut output),\n                );\n            });\n        });\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_obv);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/plus_di_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::plus_di::plus_di;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_plus_di(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"plus_di\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let high = generate_test_data(size);\n        let low = generate_test_data(size);\n        let close = generate_test_data(size);\n        let mut output_plus_di = vec![0.0; size];\n        let mut output_smoothed_plus_dm = vec![0.0; size];\n        let mut output_smoothed_tr = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = plus_di(\n                            black_box(&high),\n                            black_box(&low),\n                            black_box(&close),\n                            black_box(period),\n                            black_box(&mut output_plus_di),\n                            black_box(&mut output_smoothed_plus_dm),\n                            black_box(&mut output_smoothed_tr),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_plus_di);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/plus_dm_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::plus_dm::plus_dm;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_plus_dm(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"plus_dm\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = plus_dm(\n                            black_box(&input_high),\n                            black_box(&input_low),\n                            black_box(period),\n                            black_box(&mut output),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_plus_dm);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/rma_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::rma::rma;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_rma(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"rma\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = rma(black_box(&input), black_box(period), black_box(&mut output));\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_rma);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/roc_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::roc::roc;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_roc(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"roc\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = roc(black_box(&input), black_box(period), black_box(&mut output));\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_roc);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/rocp_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::rocp::rocp;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_rocp(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"rocp\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = rocp(black_box(&input), black_box(period), black_box(&mut output));\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_rocp);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/rocr100_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::rocr100::rocr100;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_rocr100(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"rocr100\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ =\n                            rocr100(black_box(&input), black_box(period), black_box(&mut output));\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_rocr100);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/rocr_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::rocr::rocr;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_rocr(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"rocr\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = rocr(black_box(&input), black_box(period), black_box(&mut output));\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_rocr);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/rsi_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::rsi::rsi;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_rsi(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"rsi\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n        let mut output_avg_gain = vec![0.0; size];\n        let mut output_avg_loss = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = rsi(\n                            black_box(&input),\n                            black_box(period),\n                            black_box(&mut output),\n                            black_box(&mut output_avg_gain),\n                            black_box(&mut output_avg_loss),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_rsi);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/sar_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::sar::sar;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n#[allow(dead_code)]\nfn bench_sar(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"sar\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let mut output_sar = vec![0.0; size];\n        let mut output_is_long = vec![false; size];\n        let mut output_af = vec![0.0; size];\n        let mut output_ep = vec![0.0; size];\n\n        group.bench_with_input(BenchmarkId::new(\"size\", size), &size, |b, &_size| {\n            b.iter(|| {\n                let _ = sar(\n                    black_box(&input_high),\n                    black_box(&input_low),\n                    black_box(0.02),\n                    black_box(0.2),\n                    black_box(&mut output_sar),\n                    black_box(&mut output_is_long),\n                    black_box(&mut output_af),\n                    black_box(&mut output_ep),\n                );\n            });\n        });\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_sar);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/sma_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::sma::sma;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_sma(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"SMA\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = sma(black_box(&input), black_box(period), black_box(&mut output));\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_sma);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/stoch_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::stoch::stoch;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_stoch(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"stoch\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let k_periods = [5, 14, 30];\n    let k_slow_periods = vec![3, 5, 9];\n    let d_periods = vec![3, 5, 9];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output_fast_k = vec![0.0; size];\n        let mut output_k = vec![0.0; size];\n        let mut output_d = vec![0.0; size];\n\n        for (&k_period, &k_slow_period, &d_period) in k_periods\n            .iter()\n            .zip(&k_slow_periods)\n            .zip(&d_periods)\n            .map(|((a, b), c)| (a, b, c))\n        {\n            group.bench_with_input(\n                BenchmarkId::new(\n                    format!(\"size_{size}_k{k_period}_ks{k_slow_period}_d{d_period}\"),\n                    format!(\"{k_period}-{k_slow_period}-{d_period}\"),\n                ),\n                &(k_period, k_slow_period, d_period),\n                |b, &(k_period, k_slow_period, d_period)| {\n                    b.iter(|| {\n                        let _ = stoch(\n                            black_box(&input_high),\n                            black_box(&input_low),\n                            black_box(&input_close),\n                            black_box(k_period),\n                            black_box(k_slow_period),\n                            black_box(d_period),\n                            black_box(&mut output_fast_k),\n                            black_box(&mut output_k),\n                            black_box(&mut output_d),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_stoch);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/supertrend_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::supertrend::supertrend;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_supertrend(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"supertrend\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n    let multiplier = 3.0;\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut trend = vec![0; size];\n        let mut supertrend_output = vec![0.0; size];\n        let mut atr = vec![0.0; size];\n        let mut upper = vec![0.0; size];\n        let mut lower = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = supertrend(\n                            black_box(&input), // high\n                            black_box(&input), // low\n                            black_box(&input), // close\n                            black_box(period),\n                            black_box(multiplier),\n                            black_box(&mut trend),\n                            black_box(&mut supertrend_output),\n                            black_box(&mut atr),\n                            black_box(&mut upper),\n                            black_box(&mut lower),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_supertrend);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/t3_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::t3::t3;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n#[allow(dead_code)]\nfn bench_t3(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"t3\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n    let vfactors = vec![0.5, 0.7, 0.9];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n        let mut ema1 = vec![0.0; size];\n        let mut ema2 = vec![0.0; size];\n        let mut ema3 = vec![0.0; size];\n        let mut ema4 = vec![0.0; size];\n        let mut ema5 = vec![0.0; size];\n        let mut ema6 = vec![0.0; size];\n\n        for period in &periods {\n            for vfactor in &vfactors {\n                group.bench_with_input(\n                    BenchmarkId::new(format!(\"size_{size}_vfactor_{vfactor:.1}\"), period),\n                    &(*period, *vfactor),\n                    |b, &(period, vfactor)| {\n                        b.iter(|| {\n                            let _ = t3(\n                                black_box(&input),\n                                black_box(period),\n                                black_box(vfactor),\n                                black_box(&mut output),\n                                black_box(&mut ema1),\n                                black_box(&mut ema2),\n                                black_box(&mut ema3),\n                                black_box(&mut ema4),\n                                black_box(&mut ema5),\n                                black_box(&mut ema6),\n                            );\n                        });\n                    },\n                );\n            }\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_t3);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/tema_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::tema::tema;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_tema(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"tema\");\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n        let mut output_ema1 = vec![0.0; size];\n        let mut output_ema2 = vec![0.0; size];\n        let mut output_ema3 = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = tema(\n                            black_box(&input),\n                            black_box(period),\n                            black_box(&mut output),\n                            black_box(&mut output_ema1),\n                            black_box(&mut output_ema2),\n                            black_box(&mut output_ema3),\n                        );\n                    });\n                },\n            );\n        }\n    }\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_tema);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/trange_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::trange::trange;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_trange(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"trange\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n\n    for size in sizes {\n        let high = generate_test_data(size);\n        let low = generate_test_data(size);\n        let close = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        group.bench_with_input(BenchmarkId::new(\"size\", size), &size, |b, &_size| {\n            b.iter(|| {\n                let _ = trange(\n                    black_box(&high),\n                    black_box(&low),\n                    black_box(&close),\n                    black_box(&mut output),\n                );\n            });\n        });\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_trange);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/trima_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::trima::trima;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_trima(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"trima\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output_sma1 = vec![0.0; size];\n        let mut output_sma2 = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = trima(\n                            black_box(&input),\n                            black_box(period),\n                            black_box(&mut output_sma1),\n                            black_box(&mut output_sma2),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_trima);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/trix_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::trix::trix;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_trix(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"trix\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n        let mut ema1_output = vec![0.0; size];\n        let mut ema2_output = vec![0.0; size];\n        let mut ema3_output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = trix(\n                            black_box(&input),\n                            black_box(period),\n                            black_box(&mut output),\n                            black_box(&mut ema1_output),\n                            black_box(&mut ema2_output),\n                            black_box(&mut ema3_output),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_trix);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/typprice_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::typprice::typprice;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_typprice(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"typprice\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        group.bench_with_input(BenchmarkId::new(\"size\", size), &size, |b, &_size| {\n            b.iter(|| {\n                let _ = typprice(\n                    black_box(&input_high),\n                    black_box(&input_low),\n                    black_box(&input_close),\n                    black_box(&mut output),\n                );\n            });\n        });\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_typprice);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/vegas_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::vegas::vegas;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_vegas(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"vegas\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output_channel_upper = vec![0.0; size];\n        let mut output_channel_lower = vec![0.0; size];\n        let mut output_boundary_upper = vec![0.0; size];\n        let mut output_boundary_lower = vec![0.0; size];\n\n        group.bench_with_input(BenchmarkId::new(\"size\", size), &size, |b, &_size| {\n            b.iter(|| {\n                let _ = vegas(\n                    black_box(&input),\n                    black_box(&mut output_channel_upper),\n                    black_box(&mut output_channel_lower),\n                    black_box(&mut output_boundary_upper),\n                    black_box(&mut output_boundary_lower),\n                );\n            });\n        });\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_vegas);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/vwap_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::vwap::vwap;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_vwap(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"vwap\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n\n    for size in sizes {\n        let high = generate_test_data(size);\n        let low = generate_test_data(size);\n        let close = generate_test_data(size);\n        let volume = generate_test_data(size);\n        let mut output_vwap = vec![0.0; size];\n        let mut output_cum_pv = vec![0.0; size];\n        let mut output_cum_vol = vec![0.0; size];\n\n        group.bench_with_input(BenchmarkId::new(\"size\", size), &size, |b, &_size| {\n            b.iter(|| {\n                let _ = vwap(\n                    black_box(&high),\n                    black_box(&low),\n                    black_box(&close),\n                    black_box(&volume),\n                    black_box(&mut output_vwap),\n                    black_box(&mut output_cum_pv),\n                    black_box(&mut output_cum_vol),\n                );\n            });\n        });\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_vwap);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/wclprice_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::wclprice::wclprice;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_wclprice(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"wclprice\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        group.bench_with_input(BenchmarkId::new(\"size\", size), &size, |b, &_size| {\n            b.iter(|| {\n                let _ = wclprice(\n                    black_box(&input_high),\n                    black_box(&input_low),\n                    black_box(&input_close),\n                    black_box(&mut output),\n                );\n            });\n        });\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_wclprice);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/willr_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::willr::willr;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_willr(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"willr\");\n\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input_high = generate_test_data(size);\n        let input_low = generate_test_data(size);\n        let input_close = generate_test_data(size);\n        let mut output = vec![0.0; size];\n        let mut output_highest_high = vec![0.0; size];\n        let mut output_lowest_low = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = willr(\n                            black_box(&input_high),\n                            black_box(&input_low),\n                            black_box(&input_close),\n                            black_box(period),\n                            black_box(&mut output),\n                            black_box(&mut output_highest_high),\n                            black_box(&mut output_lowest_low),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_willr);\n"
  },
  {
    "path": "kand/benches/benchmarks/ohlcv/wma_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::ohlcv::wma::wma;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_wma(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"wma\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = wma(black_box(&input), black_box(period), black_box(&mut output));\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_wma);\n"
  },
  {
    "path": "kand/benches/benchmarks/other/mod.rs",
    "content": ""
  },
  {
    "path": "kand/benches/benchmarks/stats/beta_bench.rs",
    "content": "use criterion::{black_box, criterion_group, BenchmarkId, Criterion};\nuse kand::ohlcv::beta::beta;\n\nuse crate::helpers::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_beta(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"beta\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{}\", size), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = beta(black_box(&input), black_box(period), black_box(&mut output));\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_beta);\n"
  },
  {
    "path": "kand/benches/benchmarks/stats/correl_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse std::hint::black_box;\nuse kand::stats::correl::correl;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_correl(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"correl\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{}\", size), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = correl(black_box(&input), black_box(period), black_box(&mut output));\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(ohlcv, bench_correl);\n"
  },
  {
    "path": "kand/benches/benchmarks/stats/max_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::stats::max::max;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_max(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"max\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = max(black_box(&input), black_box(period), black_box(&mut output));\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(stats, bench_max);\n"
  },
  {
    "path": "kand/benches/benchmarks/stats/min_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::stats::min::min;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_min(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"min\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = min(black_box(&input), black_box(period), black_box(&mut output));\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(stats, bench_min);\n"
  },
  {
    "path": "kand/benches/benchmarks/stats/mod.rs",
    "content": "pub mod max_bench;\npub mod min_bench;\npub mod stddev_bench;\npub mod sum_bench;\npub mod var_bench;\n"
  },
  {
    "path": "kand/benches/benchmarks/stats/stddev_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::stats::stddev::stddev;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_stddev(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"stddev\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n        let mut output_sum = vec![0.0; size];\n        let mut output_sum_sq = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = stddev(\n                            black_box(&input),\n                            black_box(period),\n                            black_box(&mut output),\n                            black_box(&mut output_sum),\n                            black_box(&mut output_sum_sq),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(stats, bench_stddev);\n"
  },
  {
    "path": "kand/benches/benchmarks/stats/sum_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::stats::sum::sum;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_sum(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"sum\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = sum(black_box(&input), black_box(period), black_box(&mut output));\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(stats, bench_sum);\n"
  },
  {
    "path": "kand/benches/benchmarks/stats/var_bench.rs",
    "content": "use criterion::{BenchmarkId, Criterion, criterion_group};\nuse kand::stats::var::var;\nuse std::hint::black_box;\n\nuse crate::helper::generate_test_data;\n\n#[allow(dead_code)]\nfn bench_var(c: &mut Criterion) {\n    let mut group = c.benchmark_group(\"var\");\n\n    // Test different data sizes\n    let sizes = vec![100_000, 1_000_000, 10_000_000];\n    let periods = vec![5, 50, 200];\n\n    for size in sizes {\n        let input = generate_test_data(size);\n        let mut output = vec![0.0; size];\n        let mut output_sum = vec![0.0; size];\n        let mut output_sum_sq = vec![0.0; size];\n\n        for period in &periods {\n            group.bench_with_input(\n                BenchmarkId::new(format!(\"size_{size}\"), period),\n                period,\n                |b, &period| {\n                    b.iter(|| {\n                        let _ = var(\n                            black_box(&input),\n                            black_box(period),\n                            black_box(&mut output),\n                            black_box(&mut output_sum),\n                            black_box(&mut output_sum_sq),\n                        );\n                    });\n                },\n            );\n        }\n    }\n\n    group.finish();\n}\n\ncriterion_group!(stats, bench_var);\n"
  },
  {
    "path": "kand/benches/helper.rs",
    "content": "use kand::TAFloat;\nuse rand::Rng;\n\n/// Generate a vector of floating point values simulating price movements\n///\n/// # Panics\n///\n/// This function will panic if:\n/// - Type conversion from `usize` to `TAFloat` fails\n#[must_use]\n#[allow(clippy::expect_used)]\npub fn generate_test_data(size: usize) -> Vec<TAFloat> {\n    let mut rng = rand::rng();\n    let mut data = Vec::with_capacity(size);\n\n    // These constants are used frequently\n    let price_init: TAFloat = 100.0;\n    let increment: TAFloat = 0.001;\n    let half: TAFloat = 0.5;\n    let two: TAFloat = 2.0;\n\n    let mut price = price_init;\n\n    for i in 0..size {\n        // Simulate small random price movements\n        let random_factor: TAFloat = rng.random_range(0.0..1.0);\n        price = (random_factor - half).mul_add(two, price);\n        let idx = i as TAFloat;\n        data.push(idx.mul_add(increment, price));\n    }\n    data\n}\n"
  },
  {
    "path": "kand/src/error.rs",
    "content": "#[derive(thiserror::Error, Debug)]\npub enum KandError {\n    #[error(\"Invalid parameter value provided to the function\")]\n    InvalidParameter,\n\n    #[error(\"Insufficient data points for the requested calculation\")]\n    InsufficientData,\n\n    #[error(\"Input data contains NaN (Not a Number) values\")]\n    NaNDetected,\n\n    #[error(\"Input arrays have mismatched lengths\")]\n    LengthMismatch,\n\n    #[error(\"Input data is invalid (out of range or empty)\")]\n    InvalidData,\n\n    #[error(\"File operation error occurred\")]\n    FileError,\n\n    #[error(\"Failed to convert between numeric types\")]\n    ConversionError,\n\n    #[error(\"Invalid input: {0}\")]\n    InvalidInput(String),\n\n    #[error(\"Calculation error: {0}\")]\n    CalculationError(String),\n}\n\npub type Result<T> = std::result::Result<T, KandError>;\n"
  },
  {
    "path": "kand/src/helper.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Find the number of bars back to the lowest value in a lookback period\n///\n/// # Arguments\n/// * `array` - Array of values to analyze\n/// * `start_idx` - Starting index for analysis\n/// * `lookback` - Number of bars to look back\n///\n/// # Visual Example\n/// ```text\n/// Given array: [5, 2, 4, 1, 3], start_idx = 4, lookback = 3\n///\n/// Lookback window:        [4, 1, 3]\n///                          ^  ^  ^\n/// Indices:                 2  3  4\n/// Values:                  4  1  3\n/// i values:                2  1  0\n///                             ↑\n///                      Lowest value (1)\n///\n/// Returns: 1 (number of bars back from start_idx to lowest value)\n/// ```\n///\n/// # Returns\n/// * `Result<usize, KandError>` - Number of bars back to lowest value if successful, error otherwise\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if:\n///   - The input array is empty\n///   - `start_idx` is out of bounds\n///   - `lookback` is 0\n///   - `start_idx` is less than `lookback - 1`\npub fn lowest_bars(\n    array: &[TAFloat],\n    start_idx: usize,\n    lookback: usize,\n) -> Result<usize, KandError> {\n    if array.is_empty() || start_idx >= array.len() || lookback == 0 || start_idx < lookback - 1 {\n        return Err(KandError::InvalidParameter);\n    }\n\n    let mut lowest = array[start_idx];\n    let mut lowest_idx = 0;\n\n    for i in 1..lookback {\n        if array[start_idx - i] < lowest {\n            lowest = array[start_idx - i];\n            lowest_idx = i;\n        }\n    }\n    Ok(lowest_idx)\n}\n\n/// Find the number of bars back to the highest value in a lookback period\n///\n/// # Arguments\n/// * `array` - Array of values to analyze\n/// * `start_idx` - Starting index for analysis\n/// * `lookback` - Number of bars to look back\n///\n/// # Visual Example\n/// ```text\n/// Given array: [1, 4, 2, 5, 3], start_idx = 4, lookback = 3\n///\n/// Lookback window:        [2, 5, 3]\n///                          ^  ^  ^\n/// Indices:                 2  3  4\n/// Values:                  2  5  3\n/// i values:                2  1  0\n///                             ↑\n///                      Highest value (5)\n///\n/// Returns: 1 (number of bars back from start_idx to highest value)\n/// ```\n///\n/// # Returns\n/// * `Result<usize, KandError>` - Number of bars back to highest value if successful, error otherwise\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if:\n///   - The input array is empty\n///   - `start_idx` is out of bounds\n///   - `lookback` is 0\n///   - `start_idx` is less than `lookback - 1`\npub fn highest_bars(\n    array: &[TAFloat],\n    start_idx: usize,\n    lookback: usize,\n) -> Result<usize, KandError> {\n    if array.is_empty() || start_idx >= array.len() || lookback == 0 || start_idx < lookback - 1 {\n        return Err(KandError::InvalidParameter);\n    }\n\n    let mut highest = array[start_idx];\n    let mut highest_idx = 0;\n\n    for i in 1..lookback {\n        if array[start_idx - i] > highest {\n            highest = array[start_idx - i];\n            highest_idx = i;\n        }\n    }\n    Ok(highest_idx)\n}\n\n/// Calculate k factor from period value (k = 2 / (period + 1))\n///\n/// # Arguments\n/// * `period` - The period value to convert\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The calculated k factor if successful, error otherwise\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if `period` is 0\npub fn period_to_k(period: usize) -> Result<TAFloat, KandError> {\n    if period == 0 {\n        return Err(KandError::InvalidParameter);\n    }\n    Ok(2.0 / ((period + 1) as TAFloat))\n}\n\n/// Calculate candlestick real body length\n///\n/// # Arguments\n/// * `open` - Opening price\n/// * `close` - Closing price\n///\n/// # Returns\n/// * `TAFloat` - Absolute difference between open and close prices\n#[must_use]\npub fn real_body_length(open: TAFloat, close: TAFloat) -> TAFloat {\n    (close - open).abs()\n}\n\n/// Calculate candlestick upper shadow length\n///\n/// # Arguments\n/// * `high` - High price\n/// * `open` - Opening price\n/// * `close` - Closing price\n///\n/// # Returns\n/// * `TAFloat` - Length of upper shadow\n#[must_use]\npub fn upper_shadow_length(high: TAFloat, open: TAFloat, close: TAFloat) -> TAFloat {\n    high - if close >= open { close } else { open }\n}\n\n/// Calculate candlestick lower shadow length\n///\n/// # Arguments\n/// * `low` - Low price\n/// * `open` - Opening price\n/// * `close` - Closing price\n///\n/// # Returns\n/// * `TAFloat` - Length of lower shadow\n#[must_use]\npub fn lower_shadow_length(low: TAFloat, open: TAFloat, close: TAFloat) -> TAFloat {\n    if close >= open {\n        open - low\n    } else {\n        close - low\n    }\n}\n\n/// Check if there is a gap up between real bodies of two candlesticks\n///\n/// # Arguments\n/// * `open2` - Opening price of second candle\n/// * `close2` - Closing price of second candle\n/// * `open1` - Opening price of first candle\n/// * `close1` - Closing price of first candle\n///\n/// # Returns\n/// * `bool` - True if gap up exists, false otherwise\n#[must_use]\npub fn has_real_body_gap_up(\n    open2: TAFloat,\n    close2: TAFloat,\n    open1: TAFloat,\n    close1: TAFloat,\n) -> bool {\n    open2.min(close2) > open1.max(close1)\n}\n\n/// Check if there is a gap down between real bodies of two candlesticks\n///\n/// # Arguments\n/// * `open2` - Opening price of second candle\n/// * `close2` - Closing price of second candle\n/// * `open1` - Opening price of first candle\n/// * `close1` - Closing price of first candle\n///\n/// # Returns\n/// * `bool` - True if gap down exists, false otherwise\n#[must_use]\npub fn has_real_body_gap_down(\n    open2: TAFloat,\n    close2: TAFloat,\n    open1: TAFloat,\n    close1: TAFloat,\n) -> bool {\n    open2.max(close2) < open1.min(close1)\n}\n"
  },
  {
    "path": "kand/src/lib.rs",
    "content": "//! # Kand\n//!\n//! A high-performance technical analysis library for Rust, inspired by TA-Lib.\n//!\n//! ## Overview\n//!\n//! Kand provides a comprehensive suite of technical analysis tools for financial market data analysis.\n//! Built with Rust's safety and performance in mind, it offers a modern alternative to traditional\n//! technical analysis libraries.\n//!\n//! ## Features\n//!\n//! - **High Performance**: Written in pure Rust with zero dependencies on external C libraries\n//! - **Type Safety**: Leverages Rust's type system to prevent common errors at compile time\n//! - **Flexible Types**: Supports both standard (32-bit) and extended (64-bit) precision modes\n//! - **Comprehensive Indicators**: Implements popular technical indicators including:\n//!   - Moving Averages (SMA, EMA, WMA)\n//!   - Momentum Indicators (RSI, MACD)\n//!   - Volume Indicators (OBV)\n//!   - And more...\n//!\n//! ## Quick Start\n//!\n//! ```rust\n//! use kand::ohlcv::sma;\n//!\n//! // Input price data\n//! let prices = vec![2.0, 4.0, 6.0, 8.0, 10.0];\n//! let period = 3;\n//! let mut sma_values = vec![0.0; prices.len()];\n//!\n//! // Calculate SMA\n//! sma::sma(&prices, period, &mut sma_values).unwrap();\n//! // First (period-1) values will be NaN, then: [NaN, NaN, 4.0, 6.0, 8.0]\n//!\n//! // Calculate next SMA value incrementally\n//! let prev_sma = 8.0; // Last SMA value\n//! let new_price = 12.0; // New price to include\n//! let old_price = 6.0; // Oldest price to remove\n//!\n//! let next_sma = sma::sma_inc(prev_sma, new_price, old_price, period).unwrap();\n//! // next_sma = 10.0 ((8.0 + 10.0 + 12.0) / 3)\n//! ```\n//!\n//! ## Feature Flags\n//!\n//! Kand can be configured through feature flags:\n//!\n//! ### Precision Modes\n//! - `default = [\"extended\", \"check\"]`: 64-bit precision with basic validation checks\n//! - `standard = [\"f32\", \"i32\"]`: Standard precision mode using 32-bit types\n//! - `extended = [\"f64\", \"i64\"]`: Extended precision mode using 64-bit types\n//!\n//! ### Type Selection\n//! - `f32`: Use 32-bit floating point numbers\n//! - `f64`: Use 64-bit floating point numbers\n//! - `i32`: Use 32-bit integers\n//! - `i64`: Use 64-bit integers\n//!\n//! ### Validation\n//! - `check`: Enable basic validation checks\n//! - `check-nan = [\"check\"]`: Enable extended validation (includes basic checks)\n//!\n//! ## Safety and Error Handling\n//!\n//! All functions in Kand return a `Result` type, properly handling edge cases and\n//! invalid inputs. Common error cases include:\n//!\n//! - `InvalidParameter`: Invalid input parameters (e.g., period < 2)\n//! - `InvalidData`: Empty or invalid input data\n//! - `LengthMismatch`: Input and output slice lengths don't match\n//! - `InsufficientData`: Not enough data points for calculation\n//! - `NaNDetected`: NaN values in input data (with `check-nan` feature)\n//!\n//! ## Performance Considerations\n//!\n//! The library is optimized for both speed and memory usage:\n//!\n//! - Incremental calculation support for real-time updates\n//! - Configurable precision modes (standard/extended)\n//! - In-place calculations to minimize memory allocations\n//! - Optional validation checks that can be disabled for maximum performance\n#![allow(clippy::similar_names, clippy::too_many_lines)]\n\npub mod ta;\npub use ta::*;\n\npub mod helper;\n\npub mod error;\npub use error::{KandError, Result};\n\n/// Default floating-point precision type used across the library.\n///\n/// This type is determined by the enabled features:\n/// - With feature \"f64\": Uses f64 (recommended for most cases)\n/// - With feature \"f32\": Uses f32 (for memory-constrained environments)\n/// - With no features enabled: Defaults to f64\n///\n/// The default configuration (feature \"extended\") provides f64 for:\n/// - Higher precision calculations (15-17 decimal digits)\n/// - Better handling of large price values\n/// - More accurate technical indicator results\n#[cfg(all(feature = \"f32\", not(feature = \"f64\")))]\npub type TAFloat = f32;\n\n#[cfg(not(all(feature = \"f32\", not(feature = \"f64\"))))]\npub type TAFloat = f64; // Default to f64 when no features are enabled\n\n/// Default integer type used for indicator outputs.\n///\n/// This type is determined by the enabled features:\n/// - With feature \"i64\": Uses i64 (recommended for most cases)\n/// - With feature \"i32\": Uses i32 (for memory-constrained environments)\n/// - With no features enabled: Defaults to i32\n///\n/// The default configuration (feature \"extended\") provides i64 for:\n/// - Larger value range (-2^63 to 2^63-1)\n/// - Better precision in accumulation operations\n/// - Future-proof for high-frequency data analysis\n#[cfg(all(feature = \"i32\", not(feature = \"i64\")))]\npub type TAInt = i32;\n\n#[cfg(not(all(feature = \"i32\", not(feature = \"i64\"))))]\npub type TAInt = i64; // Default to i64 when no features are enabled\n\n/// Default type for indicator periods.\n///\n/// This is consistently `usize` across the library to represent time periods,\n/// lookback windows, and other count-based parameters.\npub type TAPeriod = usize;\n\n/// Global EPSILON value used for floating-point comparisons\n/// to account for rounding errors in calculations.\n///\n/// This value is automatically set based on the floating-point type:\n/// - f32: Uses f32::EPSILON (≈ 1.19e-7)\n/// - f64: Uses f64::EPSILON (≈ 2.22e-16)\n#[cfg(all(feature = \"f32\", not(feature = \"f64\")))]\npub const EPSILON: TAFloat = f32::EPSILON;\n\n#[cfg(not(all(feature = \"f32\", not(feature = \"f64\"))))]\npub const EPSILON: TAFloat = f64::EPSILON;\n"
  },
  {
    "path": "kand/src/ta/mod.rs",
    "content": "pub mod ohlcv;\npub mod stats;\n\npub mod types;\n"
  },
  {
    "path": "kand/src/ta/ohlcv/ad.rs",
    "content": "use crate::{KandError, TAFloat, TAPeriod};\n\n/// Returns the lookback period required for A/D calculation.\n///\n/// The A/D indicator requires no lookback period, as it can be calculated starting from the first data point.\n///\n/// # Errors\n///\n/// This function always returns `Ok(0)`.\n///\n/// # Examples\n///\n/// ```\n/// use kand::ohlcv::ad;\n/// let lookback = ad::lookback().unwrap();\n/// assert_eq!(lookback, 0);\n/// ```\n#[must_use]\npub const fn lookback() -> Result<TAPeriod, KandError> {\n    Ok(0)\n}\n\n/// Core calculation for Accumulation/Distribution (A/D) without error checking.\n///\n/// This is a high-performance version for advanced users, assuming valid inputs.\n/// It computes the A/D by accumulating money flow volume.\n///\n/// # Parameters\n///\n/// - `input_high`: Slice of high prices.\n/// - `input_low`: Slice of low prices.\n/// - `input_close`: Slice of close prices.\n/// - `input_volume`: Slice of volumes.\n/// - `output_ad`: Mutable slice to store the A/D values.\n/// - `lookback`: The lookback period (0 for A/D).\n///\n/// # Notes\n///\n/// - No error checking is performed; ensure inputs are valid.\n/// - All values in `output_ad` are set since lookback is 0.\n/// - Assumes all input slices have the same length.\npub fn ad_raw(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    input_volume: &[TAFloat],\n    output_ad: &mut [TAFloat],\n    lookback: TAPeriod,\n) {\n    let len = input_high.len();\n    let mut ad = 0.0;\n    for i in lookback..len {\n        let high_low_diff = input_high[i] - input_low[i];\n        let mfm = if high_low_diff == 0.0 {\n            0.0\n        } else {\n            ((input_close[i] - input_low[i]) - (input_high[i] - input_close[i])) / high_low_diff\n        };\n        ad = mfm.mul_add(input_volume[i], ad);\n        output_ad[i] = ad;\n    }\n}\n\n/// Calculates the Accumulation/Distribution (A/D) indicator for the entire price series.\n///\n/// The A/D indicator measures the cumulative flow of money into and out of a security by analyzing\n/// price and volume data. It starts from 0 and accumulates the money flow volume over time.\n///\n/// # Formula\n///\n/// ```text\n/// Money Flow Multiplier (MFM) = ((Close - Low) - (High - Close)) / (High - Low)\n/// Money Flow Volume (MFV) = MFM * Volume\n/// A/D = Previous A/D + MFV\n/// ```\n///\n/// # Calculation\n///\n/// 1. Compute the Money Flow Multiplier (MFM), ranging from -1 to +1:\n///    - Positive values indicate buying pressure (close near high).\n///    - Negative values indicate selling pressure (close near low).\n/// 2. Multiply MFM by volume to get Money Flow Volume (MFV).\n/// 3. Accumulate MFV to form the A/D line, starting from 0.\n///\n/// If High - Low is zero, MFM is set to 0 to avoid division by zero.\n///\n/// # Errors\n///\n/// - [`KandError::InvalidData`] if input arrays are empty (enabled by \"check\" feature).\n/// - [`KandError::LengthMismatch`] if input arrays have different lengths (enabled by \"check\" feature).\n/// - [`KandError::NaNDetected`] if any input contains NaN values (enabled by \"check-nan\" feature).\n///\n/// # Examples\n///\n/// ```\n/// use kand::ohlcv::ad;\n/// let input_high = vec![10.0, 12.0, 15.0];\n/// let input_low = vec![8.0, 9.0, 11.0];\n/// let input_close = vec![9.0, 11.0, 13.0];\n/// let input_volume = vec![100.0, 150.0, 200.0];\n/// let mut output_ad = vec![0.0; 3];\n///\n/// ad::ad(\n///     &input_high,\n///     &input_low,\n///     &input_close,\n///     &input_volume,\n///     &mut output_ad,\n/// )\n/// .unwrap();\n/// ```\npub fn ad(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    input_volume: &[TAFloat],\n    output_ad: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback()?;\n\n    #[cfg(feature = \"check\")]\n    {\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        if len != input_low.len()\n            || len != input_close.len()\n            || len != input_volume.len()\n            || len != output_ad.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in lookback..len {\n            if input_high[i].is_nan()\n                || input_low[i].is_nan()\n                || input_close[i].is_nan()\n                || input_volume[i].is_nan()\n            {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    ad_raw(\n        input_high,\n        input_low,\n        input_close,\n        input_volume,\n        output_ad,\n        lookback,\n    );\n\n    Ok(())\n}\n\n/// Core calculation for incremental A/D without error checking.\n///\n/// This is a high-performance version for advanced users, assuming valid inputs.\n/// It computes the next A/D value using the previous A/D and new data.\n///\n/// # Formula\n///\n/// ```text\n/// Money Flow Multiplier (MFM) = ((Close - Low) - (High - Close)) / (High - Low)\n/// Money Flow Volume (MFV) = MFM * Volume\n/// Latest A/D = Previous A/D + MFV\n/// ```\n///\n/// # Parameters\n///\n/// - `input_high`: High price.\n/// - `input_low`: Low price.\n/// - `input_close`: Close price.\n/// - `input_volume`: Volume.\n/// - `prev_ad`: The previous A/D value.\n///\n/// # Returns\n///\n/// The next A/D value as a `TAFloat`.\n///\n/// # Notes\n///\n/// - No error checking is performed; ensure inputs are valid.\n/// - If High - Low is zero, MFM is set to 0.\npub fn ad_inc_raw(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    input_close: TAFloat,\n    input_volume: TAFloat,\n    prev_ad: TAFloat,\n) -> TAFloat {\n    let high_low_diff = input_high - input_low;\n    let mfm = if high_low_diff == 0.0 {\n        0.0\n    } else {\n        ((input_close - input_low) - (input_high - input_close)) / high_low_diff\n    };\n    mfm.mul_add(input_volume, prev_ad)\n}\n\n/// Calculates the latest A/D value incrementally using the previous A/D value.\n///\n/// This is an optimized version that computes only the latest A/D value, avoiding recalculation of the entire series.\n///\n/// # Formula\n///\n/// ```text\n/// Money Flow Multiplier (MFM) = ((Close - Low) - (High - Close)) / (High - Low)\n/// Money Flow Volume (MFV) = MFM * Volume\n/// Latest A/D = Previous A/D + MFV\n/// ```\n///\n/// If High - Low is zero, MFM is set to 0 to avoid division by zero.\n///\n/// # Errors\n///\n/// - [`KandError::NaNDetected`] if any input contains NaN values (enabled by \"check-nan\" feature).\n///\n/// # Examples\n///\n/// ```\n/// use kand::ohlcv::ad;\n/// let input_high = 15.0;\n/// let input_low = 11.0;\n/// let input_close = 13.0;\n/// let input_volume = 200.0;\n/// let prev_ad = 25.0;\n///\n/// let output_ad = ad::ad_inc(input_high, input_low, input_close, input_volume, prev_ad).unwrap();\n/// ```\n#[must_use]\npub fn ad_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    input_close: TAFloat,\n    input_volume: TAFloat,\n    prev_ad: TAFloat,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_high.is_nan()\n            || input_low.is_nan()\n            || input_close.is_nan()\n            || input_volume.is_nan()\n            || prev_ad.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    Ok(ad_inc_raw(\n        input_high,\n        input_low,\n        input_close,\n        input_volume,\n        prev_ad,\n    ))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n    use crate::EPSILON;\n\n    const INPUT_HIGH: [f64; 25] = [\n        35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0, 35210.0,\n        35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5, 35078.8, 35085.0,\n        35034.1, 34984.4, 35010.8, 35047.1, 35091.4,\n    ];\n\n    const INPUT_LOW: [f64; 25] = [\n        35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0, 35166.0,\n        35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0, 35012.3, 35022.2,\n        34931.6, 34911.0, 34952.5, 34977.9, 35039.0,\n    ];\n\n    const INPUT_CLOSE: [f64; 25] = [\n        35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6, 35184.7,\n        35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4, 35069.0, 35024.6,\n        34939.5, 34952.6, 35000.0, 35041.8, 35080.0,\n    ];\n\n    const INPUT_VOLUME: [f64; 25] = [\n        1055.365, 756.488, 682.152, 1197.747, 425.97, 859.638, 741.925, 888.477, 1043.333, 467.901,\n        387.47, 566.099, 672.296, 834.915, 1854.024, 3670.795, 3761.198, 1605.442, 1726.574,\n        934.713, 2199.061, 2349.823, 837.218, 1000.638, 1218.202,\n    ];\n\n    const EXPECTED_VALUES: [f64; 25] = [\n        -1_055.365,\n        -1_262.015_380_487_751_1,\n        -1_682.083_847_274_164,\n        -1_313.393_007_007_976_8,\n        -902.421_950_669_947_9,\n        -112.958_481_282_220_36,\n        -849.961_922_409_807_9,\n        -635.816_183_948_236_4,\n        -1_096.943_608_639_632,\n        -1_167.128_758_639_693_8,\n        -1_330.133_379_329_504_6,\n        -765.526_076_299_179_7,\n        -789.973_203_571_907,\n        -1_246.813_486_590_858_2,\n        -2_815.387_753_760_547,\n        -5_117.296_306_636_379,\n        -5_086.466_814_832_707,\n        -3_847.125_607_355_984_4,\n        -2_629.436_575_777_188,\n        -3_492.706_543_930_014,\n        -5_352.790_336_125_074,\n        -5_039.053_750_294_157,\n        -4_512.022_865_217_038,\n        -3_664.661_784_292_062_7,\n        -2_976.517_143_070_741_4,\n    ];\n\n    /// Tests A/D calculation with `allow-nan` feature enabled.\n    #[test]\n    #[cfg(feature = \"allow-nan\")]\n    fn test_ad_with_nan() {\n        let mut output_ad = vec![f64::NAN; INPUT_HIGH.len()];\n        ad(\n            &INPUT_HIGH,\n            &INPUT_LOW,\n            &INPUT_CLOSE,\n            &INPUT_VOLUME,\n            &mut output_ad,\n        )\n        .unwrap();\n\n        // Verify full series calculation\n        for (i, &expected) in EXPECTED_VALUES.iter().enumerate() {\n            assert_relative_eq!(output_ad[i], expected, epsilon = EPSILON);\n        }\n\n        // Verify incremental calculation matches full series\n        let mut prev_ad = output_ad[0];\n        for i in 1..INPUT_HIGH.len() {\n            let result = ad_inc(\n                INPUT_HIGH[i],\n                INPUT_LOW[i],\n                INPUT_CLOSE[i],\n                INPUT_VOLUME[i],\n                prev_ad,\n            )\n            .unwrap();\n            if result.is_nan() {\n                assert!(output_ad[i].is_nan());\n            } else {\n                assert_relative_eq!(result, output_ad[i], epsilon = EPSILON);\n            }\n            prev_ad = result;\n        }\n    }\n\n    /// Tests A/D calculation without `allow-nan` feature.\n    #[test]\n    #[cfg(not(feature = \"allow-nan\"))]\n    fn test_ad_without_nan() {\n        let mut output_ad = vec![0.0; INPUT_HIGH.len()];\n        ad(\n            &INPUT_HIGH,\n            &INPUT_LOW,\n            &INPUT_CLOSE,\n            &INPUT_VOLUME,\n            &mut output_ad,\n        )\n        .unwrap();\n\n        // Verify full series calculation\n        for (i, &expected) in EXPECTED_VALUES.iter().enumerate() {\n            assert_relative_eq!(output_ad[i], expected, epsilon = EPSILON);\n        }\n\n        // Verify incremental calculation matches full series\n        let mut prev_ad = output_ad[0];\n        for i in 1..INPUT_HIGH.len() {\n            let result = ad_inc(\n                INPUT_HIGH[i],\n                INPUT_LOW[i],\n                INPUT_CLOSE[i],\n                INPUT_VOLUME[i],\n                prev_ad,\n            )\n            .unwrap();\n            assert_relative_eq!(result, output_ad[i], epsilon = EPSILON);\n            prev_ad = result;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/adosc.rs",
    "content": "use super::{ad, ema};\nuse crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for A/D Oscillator calculation.\n///\n/// The A/D Oscillator requires a lookback equal to the slow EMA period minus one.\n///\n/// # Errors\n///\n/// - [`KandError::InvalidParameter`] if fast or slow period is less than 2, or if fast period is not less than slow period (enabled by \"check\" feature).\n///\n/// # Examples\n///\n/// ```\n/// use kand::ohlcv::adosc;\n/// let lookback = adosc::lookback(3, 10).unwrap();\n/// assert_eq!(lookback, 9);\n/// ```\n#[must_use]\npub const fn lookback(opt_fast_period: usize, opt_slow_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_fast_period < 2 || opt_slow_period < 2 || opt_fast_period >= opt_slow_period {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    ema::lookback(opt_slow_period)\n}\n\n/// Calculates the Accumulation/Distribution Oscillator (A/D Oscillator or ADOSC) for the entire price series.\n///\n/// The A/D Oscillator is the difference between fast and slow EMAs of the Accumulation/Distribution (A/D) line.\n/// It helps identify trend strength and potential reversals by measuring momentum in money flow.\n///\n/// # Formula\n///\n/// ```text\n/// Money Flow Multiplier (MFM) = ((Close - Low) - (High - Close)) / (High - Low)\n/// Money Flow Volume (MFV) = MFM * Volume\n/// A/D = Previous A/D + MFV\n/// ADOSC = EMA(A/D, fast_period) - EMA(A/D, slow_period)\n/// ```\n///\n/// # Calculation\n///\n/// 1. Compute the A/D line as the cumulative sum of MFV (see A/D documentation for details).\n/// 2. Calculate the fast EMA of the A/D line.\n/// 3. Calculate the slow EMA of the A/D line.\n/// 4. Subtract the slow EMA from the fast EMA to get ADOSC.\n///\n/// If High - Low is zero, MFM is set to 0 to avoid division by zero.\n/// Outputs for the first `lookback` periods are set to NaN.\n///\n/// # Errors\n///\n/// - [`KandError::InvalidData`] if input arrays are empty (enabled by \"check\" feature).\n/// - [`KandError::InsufficientData`] if input length is less than or equal to lookback (enabled by \"check\" feature).\n/// - [`KandError::LengthMismatch`] if input or output arrays have different lengths (enabled by \"check\" feature).\n/// - [`KandError::InvalidParameter`] if periods are invalid (propagated from lookback).\n/// - [`KandError::NaNDetected`] if any input contains NaN values (enabled by \"check-nan\" feature).\n///\n/// # Examples\n///\n/// ```\n/// use kand::ohlcv::adosc;\n/// let high = vec![10.0, 11.0, 12.0, 11.5, 10.5];\n/// let low = vec![8.0, 9.0, 10.0, 9.5, 8.5];\n/// let close = vec![9.0, 10.0, 11.0, 10.0, 9.0];\n/// let volume = vec![100.0, 150.0, 200.0, 150.0, 100.0];\n/// let mut adosc_out = vec![0.0; 5];\n/// let mut ad_out = vec![0.0; 5];\n/// let mut ad_fast_ema = vec![0.0; 5];\n/// let mut ad_slow_ema = vec![0.0; 5];\n///\n/// adosc::adosc(\n///     &high,\n///     &low,\n///     &close,\n///     &volume,\n///     3,\n///     5,\n///     &mut adosc_out,\n///     &mut ad_out,\n///     &mut ad_fast_ema,\n///     &mut ad_slow_ema,\n/// )\n/// .unwrap();\n/// ```\npub fn adosc(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    input_volume: &[TAFloat],\n    opt_fast_period: usize,\n    opt_slow_period: usize,\n    output_adosc: &mut [TAFloat],\n    output_ad: &mut [TAFloat],\n    output_ad_fast_ema: &mut [TAFloat],\n    output_ad_slow_ema: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_fast_period, opt_slow_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        if len != input_low.len()\n            || len != input_close.len()\n            || len != input_volume.len()\n            || len != output_adosc.len()\n            || len != output_ad.len()\n            || len != output_ad_fast_ema.len()\n            || len != output_ad_slow_ema.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            if input_high[i].is_nan()\n                || input_low[i].is_nan()\n                || input_close[i].is_nan()\n                || input_volume[i].is_nan()\n            {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    ad::ad(input_high, input_low, input_close, input_volume, output_ad)?;\n\n    ema::ema(output_ad, opt_fast_period, None, output_ad_fast_ema)?;\n    ema::ema(output_ad, opt_slow_period, None, output_ad_slow_ema)?;\n\n    for i in lookback..len {\n        output_adosc[i] = output_ad_fast_ema[i] - output_ad_slow_ema[i];\n    }\n\n    Ok(())\n}\n\n/// Calculates the latest A/D Oscillator value incrementally using previous values.\n///\n/// This is an optimized version that computes only the latest ADOSC value, avoiding recalculation of the entire series.\n///\n/// # Formula\n///\n/// ```text\n/// Money Flow Multiplier (MFM) = ((Close - Low) - (High - Close)) / (High - Low)\n/// Money Flow Volume (MFV) = MFM * Volume\n/// Latest A/D = Previous A/D + MFV\n/// Latest Fast EMA = (Latest A/D - Previous Fast EMA) * (2 / (fast_period + 1)) + Previous Fast EMA\n/// Latest Slow EMA = (Latest A/D - Previous Slow EMA) * (2 / (slow_period + 1)) + Previous Slow EMA\n/// Latest ADOSC = Latest Fast EMA - Latest Slow EMA\n/// ```\n///\n/// If High - Low is zero, MFM is set to 0 to avoid division by zero.\n///\n/// # Errors\n///\n/// - [`KandError::InvalidParameter`] if fast or slow period is 0, or if fast period is not less than slow period (enabled by \"check\" feature).\n/// - [`KandError::NaNDetected`] if any input contains NaN values (enabled by \"check-nan\" feature).\n///\n/// # Examples\n///\n/// ```\n/// use kand::ohlcv::adosc;\n/// let (adosc, ad, ad_fast_ema, ad_slow_ema) = adosc::adosc_inc(\n///     10.5,\n///     9.5,\n///     10.0,\n///     150.0,\n///     100.0,\n///     95.0,\n///     90.0,\n///     3,\n///     10,\n/// )\n/// .unwrap();\n/// ```\n#[must_use]\npub fn adosc_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    input_close: TAFloat,\n    input_volume: TAFloat,\n    prev_ad: TAFloat,\n    prev_ad_fast_ema: TAFloat,\n    prev_ad_slow_ema: TAFloat,\n    opt_fast_period: usize,\n    opt_slow_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_fast_period == 0 || opt_slow_period == 0 || opt_fast_period >= opt_slow_period {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_high.is_nan()\n            || input_low.is_nan()\n            || input_close.is_nan()\n            || input_volume.is_nan()\n            || prev_ad.is_nan()\n            || prev_ad_fast_ema.is_nan()\n            || prev_ad_slow_ema.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let output_ad = ad::ad_inc(input_high, input_low, input_close, input_volume, prev_ad)?;\n    let output_ad_fast_ema = ema::ema_inc(output_ad, prev_ad_fast_ema, opt_fast_period, None)?;\n    let output_ad_slow_ema = ema::ema_inc(output_ad, prev_ad_slow_ema, opt_slow_period, None)?;\n    let output_adosc = output_ad_fast_ema - output_ad_slow_ema;\n\n    Ok((\n        output_adosc,\n        output_ad,\n        output_ad_fast_ema,\n        output_ad_slow_ema,\n    ))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use crate::EPSILON;\n\n    use super::*;\n\n    /// Tests the calculation of A/D Oscillator for a full series and verifies incremental calculations match.\n    #[test]\n    fn test_adosc_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4, 35150.4, 35123.9,\n            35110.0, 35092.1, 35179.2, 35244.9, 35150.2, 35136.0, 35133.6, 35188.0, 35215.3,\n            35221.9, 35219.2, 35234.0, 35216.7, 35197.9, 35178.4, 35183.4, 35129.7, 35149.1,\n            35129.3, 35125.5, 35114.5, 35120.1, 35129.4, 35105.4, 35054.1, 35034.6, 35032.9,\n            35070.8, 35086.0, 35086.9, 35048.9, 34988.6, 35004.3, 34985.0, 35004.2, 35010.0,\n            35041.8, 35024.7, 34982.0, 35018.0, 34978.2, 34959.5, 34965.0, 34985.3, 35002.4,\n            35018.0, 34989.0, 34943.0, 34900.0, 34932.1, 34930.0, 34920.3, 34929.9, 34940.0,\n            35019.7, 35009.1, 34980.2, 34977.3, 34976.1, 34969.4, 35000.0, 35010.0, 35015.9,\n            35062.9, 35084.8, 35085.1, 35077.9, 35118.0, 35104.0, 35086.2, 35041.7, 35009.2,\n            34994.2,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0, 35073.0, 35055.0,\n            35084.0, 35060.0, 35073.1, 35090.0, 35072.0, 35078.0, 35088.0, 35124.8, 35169.4,\n            35138.0, 35141.0, 35182.0, 35151.1, 35158.4, 35140.0, 35087.0, 35085.8, 35114.7,\n            35086.0, 35090.6, 35074.1, 35078.4, 35100.0, 35030.2, 34986.3, 34988.1, 34973.1,\n            35012.3, 35048.3, 35038.9, 34937.3, 34937.0, 34958.7, 34925.0, 34910.0, 34981.6,\n            34980.2, 34982.0, 34940.9, 34970.0, 34924.7, 34922.1, 34914.0, 34955.8, 34975.0,\n            34975.0, 34926.0, 34865.1, 34821.0, 34830.4, 34883.5, 34888.5, 34904.6, 34880.6,\n            34934.0, 34978.5, 34965.9, 34936.4, 34942.5, 34945.0, 34969.3, 34983.8, 35003.9,\n            35001.1, 35032.1, 35027.3, 35062.3, 35067.8, 35070.7, 35030.2, 34981.0, 34970.5,\n            34974.5,\n        ];\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0, 35114.5, 35097.2,\n            35092.0, 35073.2, 35139.3, 35092.0, 35126.7, 35106.3, 35124.8, 35170.1, 35215.3,\n            35154.0, 35216.3, 35211.8, 35158.4, 35172.0, 35176.7, 35113.3, 35114.7, 35129.3,\n            35094.6, 35114.4, 35094.5, 35116.0, 35105.4, 35050.7, 35031.3, 35008.1, 35021.4,\n            35048.4, 35080.1, 35043.6, 34962.7, 34970.1, 34980.1, 34930.6, 35000.0, 34998.0,\n            35024.7, 34982.1, 34972.3, 34971.6, 34953.0, 34937.0, 34964.3, 34975.1, 34995.1,\n            34989.0, 34942.9, 34895.2, 34830.4, 34925.1, 34888.6, 34910.3, 34917.6, 34940.0,\n            35005.4, 34980.1, 34966.8, 34976.1, 34948.6, 34969.3, 34996.5, 35004.0, 35011.0,\n            35059.2, 35036.1, 35062.3, 35067.7, 35087.9, 35076.7, 35041.6, 34993.3, 34974.5,\n            34990.2,\n        ];\n        let input_volume = vec![\n            1055.365, 756.488, 682.152, 1197.747, 425.97, 859.638, 741.925, 888.477, 1043.333,\n            467.901, 387.47, 566.099, 672.296, 834.915, 1854.024, 3670.795, 3761.198, 1605.442,\n            1726.574, 934.713, 2199.061, 2349.823, 837.218, 1000.638, 1218.202, 2573.668, 1098.409,\n            609.582, 670.489, 1637.998, 2682.922, 923.588, 554.766, 510.261, 882.672, 1087.53,\n            1164.362, 991.265, 1042.659, 748.721, 469.772, 419.244, 896.583, 736.185, 510.968,\n            503.042, 376.2, 592.877, 580.915, 333.615, 1106.869, 1761.343, 506.403, 1181.917,\n            817.219, 727.725, 723.652, 1702.198, 769.212, 414.213, 702.499, 1083.179, 411.098,\n            971.148, 774.147, 376.625, 333.361, 666.541, 418.598, 836.645, 506.807, 418.69,\n            606.013, 658.819, 1776.331, 1757.305, 985.24, 607.588, 350.444, 402.724, 476.235,\n            1899.96, 546.185, 233.707, 612.487, 313.292, 167.004, 298.175, 397.43, 194.525,\n            685.384, 737.572, 576.129, 264.406, 577.913, 314.803, 694.229, 1253.468, 466.235,\n            248.839,\n        ];\n        let opt_fast_period = 3;\n        let opt_slow_period = 10;\n        let mut output_adosc = vec![0.0; input_high.len()];\n        let mut output_ad = vec![0.0; input_high.len()];\n        let mut output_ad_fast_ema = vec![0.0; input_high.len()];\n        let mut output_ad_slow_ema = vec![0.0; input_high.len()];\n\n        adosc(\n            &input_high,\n            &input_low,\n            &input_close,\n            &input_volume,\n            opt_fast_period,\n            opt_slow_period,\n            &mut output_adosc,\n            &mut output_ad,\n            &mut output_ad_fast_ema,\n            &mut output_ad_slow_ema,\n        )\n        .unwrap();\n\n        let expected_values = [\n            0.0,\n            0.0,\n            0.0,\n            0.0,\n            0.0,\n            0.0,\n            0.0,\n            0.0,\n            0.0,\n            -20.897_560_400_954_944,\n            -113.006_596_430_246_87,\n            39.233_539_470_168_466,\n            90.168_278_254_455_34,\n            -42.550_171_226_589_67,\n            -592.067_697_274_955_4,\n            -1495.471_434_093_873_8,\n            -1719.284_357_755_546_8,\n            -1260.209_773_442_224_6,\n            -570.395_499_498_773_5,\n            -511.021_422_797_917_7,\n            -1032.120_377_773_458_1,\n            -1051.642_820_428_754_5,\n            -796.333_736_224_223_7,\n            -349.880_245_462_221_4,\n            83.522_227_098_310_85,\n            312.478_479_876_569_96,\n            456.358_684_600_642_4,\n            399.132_419_401_690_64,\n            301.554_565_477_041_25,\n            363.412_358_422_480_8,\n            -475.932_587_904_782_4,\n            -658.787_445_656_951_6,\n            -677.962_679_865_731_4,\n            -524.481_981_814_993_1,\n            -292.253_246_808_773_84,\n            175.350_167_221_840_37,\n            121.525_736_229_688_85,\n            380.468_332_324_480_8,\n            500.298_653_312_819_17,\n            318.629_613_122_204_8,\n            168.799_403_197_662_74,\n            213.744_556_233_260_03,\n            83.082_596_846_209_07,\n            96.244_336_691_993_8,\n            68.303_043_208_752_11,\n            -45.815_725_437_386_39,\n            -44.777_157_576_240_825,\n            -38.413_888_892_083_83,\n            116.171_265_425_306_05,\n            101.693_356_373_634_74,\n            -73.643_785_940_669_2,\n            44.825_022_944_714_42,\n            66.691_232_998_159_65,\n            300.997_969_076_142_45,\n            430.381_835_636_462_7,\n            603.260_661_313_950_1,\n            433.980_224_577_686_84,\n            30.206_550_097_492_254,\n            -68.468_876_257_924_42,\n            -110.704_331_064_092_46,\n            -299.716_667_630_496_9,\n            -35.877_993_320_214_046,\n            95.583_039_929_305_87,\n            278.118_719_322_890_7,\n            82.343_107_720_553_23,\n            58.038_015_167_665_89,\n            -56.179_275_588_915_516,\n            -85.508_505_250_133_11,\n            -116.798_729_011_984_05,\n            139.916_423_976_043_46,\n            281.960_199_020_270_9,\n            376.670_218_959_854_54,\n            313.908_880_573_037_55,\n            162.537_450_978_871_22,\n            -42.584_065_563_915_83,\n            -548.706_875_059_190_2,\n            -435.543_557_433_369_64,\n            -500.571_623_447_350_7,\n            -440.291_467_418_113_37,\n            -372.059_546_147_532_3,\n            -158.793_574_159_791_66,\n            345.674_058_599_960_06,\n            365.009_843_917_410_76,\n            274.736_072_570_696_25,\n            396.276_482_140_549_9,\n            346.483_325_557_857_37,\n            347.316_995_690_533_53,\n            389.325_156_813_214_1,\n            439.653_796_951_506_8,\n            431.621_550_770_050_36,\n            581.061_289_074_445_3,\n            390.315_324_963_115_7,\n            315.492_200_162_527_07,\n            230.315_728_813_188_12,\n            137.903_252_420_144_1,\n            23.492_246_296_503_254,\n            -156.404_749_053_747_76,\n            -452.976_229_976_808_8,\n            -650.802_619_576_638_8,\n            -625.544_385_992_726_3,\n        ];\n        for (i, &expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_adosc[i], expected, epsilon = EPSILON);\n        }\n\n        let mut prev_ad = output_ad[9];\n        let mut prev_ad_fast_ema = output_ad_fast_ema[9];\n        let mut prev_ad_slow_ema = output_ad_slow_ema[9];\n\n        for i in 10..input_high.len() {\n            let (output_adosc_inc, output_ad_inc, output_ad_fast_ema_inc, output_ad_slow_ema_inc) =\n                adosc_inc(\n                    input_high[i],\n                    input_low[i],\n                    input_close[i],\n                    input_volume[i],\n                    prev_ad,\n                    prev_ad_fast_ema,\n                    prev_ad_slow_ema,\n                    opt_fast_period,\n                    opt_slow_period,\n                )\n                .unwrap();\n            assert_relative_eq!(output_adosc_inc, output_adosc[i], epsilon = EPSILON);\n            assert_relative_eq!(output_ad_inc, output_ad[i], epsilon = EPSILON);\n            assert_relative_eq!(\n                output_ad_fast_ema_inc,\n                output_ad_fast_ema[i],\n                epsilon = EPSILON\n            );\n            assert_relative_eq!(\n                output_ad_slow_ema_inc,\n                output_ad_slow_ema[i],\n                epsilon = EPSILON\n            );\n            prev_ad = output_ad_inc;\n            prev_ad_fast_ema = output_ad_fast_ema_inc;\n            prev_ad_slow_ema = output_ad_slow_ema_inc;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/adr.rs",
    "content": "use super::sma;\nuse crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for Average Daily Range (ADR) calculation.\n///\n/// The lookback period is the number of data points needed before the first valid ADR value, equal to the period minus one.\n///\n/// # Errors\n///\n/// - [`KandError::InvalidParameter`] if period is less than 2 (enabled by \"check\" feature).\n///\n/// # Examples\n///\n/// ```\n/// use kand::ohlcv::adr;\n/// let lookback = adr::lookback(14).unwrap();\n/// assert_eq!(lookback, 13);\n/// ```\n#[must_use]\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    sma::lookback(opt_period)\n}\n\n/// Calculates the Average Daily Range (ADR) for the entire price series.\n///\n/// The ADR measures the average price range (High - Low) over a specified period, indicating market volatility.\n///\n/// # Formula\n///\n/// ```text\n/// Daily Range = High - Low\n/// ADR = SMA(Daily Range, period)\n/// ```\n///\n/// # Calculation\n///\n/// 1. Compute the daily range (High - Low) for each period.\n/// 2. Apply a Simple Moving Average (SMA) to the daily ranges.\n/// 3. Set the first `period - 1` values to NaN, as they lack sufficient data.\n///\n/// # Errors\n///\n/// - [`KandError::InvalidData`] if input arrays are empty (enabled by \"check\" feature).\n/// - [`KandError::InsufficientData`] if input length is less than or equal to lookback (enabled by \"check\" feature).\n/// - [`KandError::LengthMismatch`] if input or output arrays have different lengths (enabled by \"check\" feature).\n/// - [`KandError::InvalidParameter`] if period is less than 2 (propagated from SMA).\n/// - [`KandError::NaNDetected`] if any input contains NaN values (enabled by \"check-nan\" feature).\n///\n/// # Examples\n///\n/// ```\n/// use kand::ohlcv::adr;\n/// let input_high = vec![10.0, 12.0, 15.0, 14.0, 13.0];\n/// let input_low = vec![8.0, 9.0, 11.0, 10.0, 9.0];\n/// let opt_period = 3;\n/// let mut output_adr = vec![0.0; 5];\n///\n/// adr::adr(&input_high, &input_low, opt_period, &mut output_adr).unwrap();\n/// ```\npub fn adr(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    opt_period: usize,\n    output_adr: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        if len != input_low.len() || len != output_adr.len() {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            if input_high[i].is_nan() || input_low[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    let mut ranges = Vec::with_capacity(len);\n    ranges.extend(\n        input_high\n            .iter()\n            .zip(input_low.iter())\n            .map(|(&h, &l)| h - l),\n    );\n\n    sma::sma(&ranges, opt_period, output_adr)\n}\n\n/// Calculates the latest Average Daily Range (ADR) value incrementally using the previous ADR value.\n///\n/// This optimized version computes only the latest ADR value, avoiding recalculation of the entire series.\n///\n/// # Formula\n///\n/// ```text\n/// New Range = New High - New Low\n/// Old Range = Old High - Old Low\n/// Latest ADR = SMA_inc(Previous ADR, New Range, Old Range, period)\n/// ```\n///\n/// # Errors\n///\n/// - [`KandError::InvalidParameter`] if period is less than 2 (enabled by \"check\" feature).\n/// - [`KandError::NaNDetected`] if any input contains NaN values (enabled by \"check-nan\" feature).\n///\n/// # Examples\n///\n/// ```\n/// use kand::ohlcv::adr;\n/// let prev_adr = 3.0;\n/// let new_high = 15.0;\n/// let new_low = 12.0;\n/// let old_high = 10.0;\n/// let old_low = 8.0;\n/// let period = 14;\n///\n/// let next_adr = adr::adr_inc(prev_adr, new_high, new_low, old_high, old_low, period).unwrap();\n/// ```\n#[must_use]\npub fn adr_inc(\n    prev_adr: TAFloat,\n    input_new_high: TAFloat,\n    input_new_low: TAFloat,\n    input_old_high: TAFloat,\n    input_old_low: TAFloat,\n    opt_period: usize,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_new_high.is_nan()\n            || input_new_low.is_nan()\n            || input_old_high.is_nan()\n            || input_old_low.is_nan()\n            || prev_adr.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let new_range = input_new_high - input_new_low;\n    let old_range = input_old_high - input_old_low;\n\n    sma::sma_inc(prev_adr, new_range, old_range, opt_period)\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    /// Tests the ADR calculation for a full series and verifies incremental calculations match.\n    #[test]\n    fn test_adr_calculation() {\n        const EPSILON: f64 = 1e-9; // Local epsilon for this test\n\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4, 35150.4, 35123.9,\n            35110.0, 35092.1, 35179.2, 35244.9, 35150.2, 35136.0, 35133.6, 35188.0, 35215.3,\n            35221.9, 35219.2, 35234.0, 35216.7, 35197.9, 35178.4, 35183.4, 35129.7, 35149.1,\n            35129.3, 35125.5, 35114.5, 35120.1, 35129.4, 35105.4, 35054.1, 35034.6, 35032.9,\n            35070.8, 35086.0, 35086.9, 35048.9, 34988.6, 35004.3, 34985.0, 35004.2, 35010.0,\n            35041.8, 35024.7, 34982.0, 35018.0, 34978.2, 34959.5, 34965.0, 34985.3, 35002.4,\n            35018.0, 34989.0, 34943.0, 34900.0, 34932.1, 34930.0, 34920.3, 34929.9, 34940.0,\n            35019.7, 35009.1, 34980.2, 34977.3, 34976.1, 34969.4, 35000.0, 35010.0, 35015.9,\n            35062.9, 35084.8, 35085.1, 35077.9, 35118.0, 35104.0, 35086.2, 35041.7, 35009.2,\n            34994.2,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0, 35073.0, 35055.0,\n            35084.0, 35060.0, 35073.1, 35090.0, 35072.0, 35078.0, 35088.0, 35124.8, 35169.4,\n            35138.0, 35141.0, 35182.0, 35151.1, 35158.4, 35140.0, 35087.0, 35085.8, 35114.7,\n            35086.0, 35090.6, 35074.1, 35078.4, 35100.0, 35030.2, 34986.3, 34988.1, 34973.1,\n            35012.3, 35048.3, 35038.9, 34937.3, 34937.0, 34958.7, 34925.0, 34910.0, 34981.6,\n            34980.2, 34982.0, 34940.9, 34970.0, 34924.7, 34922.1, 34914.0, 34955.8, 34975.0,\n            34975.0, 34926.0, 34865.1, 34821.0, 34830.4, 34883.5, 34888.5, 34904.6, 34880.6,\n            34934.0, 34978.5, 34965.9, 34936.4, 34942.5, 34945.0, 34969.3, 34983.8, 35003.9,\n            35001.1, 35032.1, 35027.3, 35062.3, 35067.8, 35070.7, 35030.2, 34981.0, 34970.5,\n            34974.5,\n        ];\n        let mut output_adr = vec![0.0; input_high.len()];\n        let period = 3;\n\n        adr(&input_high, &input_low, period, &mut output_adr).unwrap();\n\n        let expected_values = [\n            std::f64::NAN,\n            std::f64::NAN,\n            48.866_666_666_666_183_971_30,\n            52.266_666_666_667_639_162_82,\n            48.066_666_666_668_119_489_71,\n            57.266_666_666_670_062_113_55,\n            57.333_333_333_335_758_652_54,\n            73.866_666_666_668_606_922_03,\n            73.100_000_000_000_974_864_63,\n            67.666_666_666_666_671_403_62,\n            46.500_000_000_000_000_000_00,\n            44.800_000_000_000_487_432_32,\n            48.466_666_666_667_151_730_51,\n            68.366_666_666_666_176_865_87,\n            87.233_333_333_332_367_942_60,\n            104.833_333_333_333_328_596_38,\n            112.633_333_333_333_823_134_13,\n            86.300_000_000_000_480_326_89,\n            72.533_333_333_332_848_269_49,\n            60.933_333_333_334_303_461_02,\n            77.266_666_666_667_632_057_40,\n            79.566_666_666_668_126_595_14,\n            78.066_666_666_668_126_595_14,\n            66.966_666_666_667_151_730_51,\n            59.966_666_666_667_151_730_51,\n            66.333_333_333_333_328_596_38,\n            66.233_333_333_334_783_787_90,\n            57.433_333_333_334_303_461_02,\n            42.333_333_333_333_335_701_81,\n            54.733_333_333_332_360_837_18,\n            97.699_999_999_999_519_673_11,\n            113.066_666_666_665_696_538_98,\n            97.033_333_333_332_848_269_49,\n            60.599_999_999_998_544_808_48,\n            55.599_999_999_998_544_808_48,\n            51.566_666_666_665_696_538_98,\n            64.333_333_333_333_328_596_38,\n            69.333_333_333_333_328_596_38,\n            71.366_666_666_666_176_865_87,\n            65.266_666_666_665_216_212_10,\n            52.366_666_666_666_183_971_30,\n            47.833_333_333_333_335_701_81,\n            58.100_000_000_000_967_759_21,\n            59.566_666_666_665_696_538_98,\n            58.233_333_333_332_360_837_18,\n            40.533_333_333_332_848_269_49,\n            37.533_333_333_335_271_220_22,\n            39.533_333_333_335_271_220_22,\n            39.000_000_000_000_000_000_00,\n            37.166_666_666_666_664_298_19,\n            48.766_666_666_667_639_162_82,\n            57.466_666_666_667_151_730_51,\n            63.166_666_666_666_664_298_19,\n            58.033_333_333_332_848_269_49,\n            54.933_333_333_334_303_461_02,\n            52.000_000_000_000_000_000_00,\n            48.066_666_666_665_696_538_98,\n            65.766_666_666_665_216_212_10,\n            70.399_999_999_999_025_135_37,\n            69.600_000_000_000_974_864_63,\n            52.400_000_000_001_455_191_52,\n            66.600_000_000_000_974_864_63,\n            60.866_666_666_666_183_971_30,\n            61.400_000_000_001_455_191_52,\n            44.233_333_333_334_790_893_33,\n            48.466_666_666_667_151_730_51,\n            43.933_333_333_331_880_510_29,\n            47.533_333_333_332_848_269_49,\n            46.300_000_000_000_487_432_32,\n            47.300_000_000_000_487_432_32,\n            39.300_000_000_000_487_432_32,\n            35.966_666_666_667_151_730_51,\n            33.300_000_000_000_487_432_32,\n            44.466_666_666_667_151_730_51,\n            61.300_000_000_000_487_432_32,\n            73.300_000_000_000_480_326_89,\n            86.199_999_999_999_519_673_11,\n            75.733_333_333_332_367_942_60,\n            60.000_000_000_000_000_000_00,\n            34.533_333_333_335_271_220_22,\n            38.833_333_333_335_758_652_54,\n            56.800_000_000_000_487_432_32,\n            58.566_666_666_665_696_538_98,\n            43.533_333_333_330_425_318_76,\n            28.599_999_999_998_544_808_48,\n            29.599_999_999_998_544_808_48,\n            32.966_666_666_667_151_730_51,\n            29.566_666_666_665_696_538_98,\n            27.099_999_999_998_544_808_48,\n            22.966_666_666_664_725_227_06,\n            33.333_333_333_333_335_701_81,\n            42.166_666_666_669_094_354_35,\n            57.433_333_333_334_303_461_02,\n            42.033_333_333_332_848_269_49,\n            41.199_999_999_997_089_616_95,\n            33.033_333_333_332_848_269_49,\n            46.500_000_000_000_000_000_00,\n            50.000_000_000_000_000_000_00,\n            51.799_999_999_998_057_376_16,\n            39.699_999_999_997_089_616_95,\n        ];\n\n        for (i, &expected) in expected_values.iter().enumerate() {\n            if expected.is_nan() {\n                assert!(output_adr[i].is_nan());\n            } else {\n                assert_relative_eq!(output_adr[i], expected, epsilon = EPSILON);\n            }\n        }\n\n        let lookback = lookback(period).unwrap();\n        let mut prev_adr = output_adr[lookback];\n\n        for i in (lookback + 1)..input_high.len() {\n            let next_adr = adr_inc(\n                prev_adr,\n                input_high[i],\n                input_low[i],\n                input_high[i - period],\n                input_low[i - period],\n                period,\n            )\n            .unwrap();\n\n            assert_relative_eq!(next_adr, output_adr[i], epsilon = EPSILON);\n\n            prev_adr = next_adr;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/adx.rs",
    "content": "use super::dx;\nuse crate::{KandError, TAFloat};\n\n/// Calculate the lookback period required for ADX calculation\n///\n/// Returns the number of data points needed before the first valid ADX value can be calculated.\n///\n/// # Arguments\n/// * `opt_period` - The period parameter used for ADX calculation (typically 14)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period if successful, or error if invalid parameters\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::adx::lookback;\n///\n/// let period = 14;\n/// let lookback_period = lookback(period).unwrap();\n/// assert_eq!(lookback_period, 27); // 2 * period - 1\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period * 2 - 1)\n}\n\n/// Calculate Average Directional Index (ADX) for the entire input array\n///\n/// ADX measures the strength of a trend, regardless of its direction. Values range from 0 to 100,\n/// with higher values indicating stronger trends.\n///\n/// # Calculation Steps\n/// 1. Calculate +DM, -DM and TR for each period\n/// 2. Apply Wilder's smoothing to +DM, -DM and TR\n/// 3. Calculate +DI and -DI\n/// 4. Calculate DX using +DI and -DI\n/// 5. Apply Wilder's smoothing to DX to get ADX\n///\n/// # Mathematical Formula\n/// ```text\n/// TR = max(high - low, |high - prev_close|, |low - prev_close|)\n/// +DM = if(high - prev_high > prev_low - low) then max(high - prev_high, 0) else 0\n/// -DM = if(prev_low - low > high - prev_high) then max(prev_low - low, 0) else 0\n///\n/// Smoothed TR = ((prev_TR * (period-1)) + TR) / period\n/// Smoothed +DM = ((prev_+DM * (period-1)) + +DM) / period\n/// Smoothed -DM = ((prev_-DM * (period-1)) + -DM) / period\n///\n/// +DI = 100 * Smoothed +DM / Smoothed TR\n/// -DI = 100 * Smoothed -DM / Smoothed TR\n/// DX = 100 * |+DI - -DI| / (+DI + -DI)\n/// ADX = Wilder's Smoothing of DX\n/// ```\n///\n/// # Arguments\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of closing prices\n/// * `opt_period` - The period parameter (typically 14)\n/// * `output_adx` - Output array for ADX values\n/// * `output_smoothed_plus_dm` - Output array for smoothed +DM values\n/// * `output_smoothed_minus_dm` - Output array for smoothed -DM values\n/// * `output_smoothed_tr` - Output array for smoothed TR values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok if calculation succeeds, Err otherwise\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input/output arrays have different lengths\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n/// * `KandError::InsufficientData` - If input length <= lookback period\n/// * `KandError::NaNDetected` - If any input contains NaN values\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::adx::adx;\n///\n/// let high = vec![24.20, 24.07, 24.04, 23.87, 23.67];\n/// let low = vec![23.85, 23.72, 23.64, 23.37, 23.46];\n/// let close = vec![23.89, 23.95, 23.67, 23.78, 23.50];\n/// let period = 2;\n///\n/// let mut output_adx = vec![0.0; 5];\n/// let mut smoothed_plus_dm = vec![0.0; 5];\n/// let mut smoothed_minus_dm = vec![0.0; 5];\n/// let mut smoothed_tr = vec![0.0; 5];\n///\n/// adx(\n///     &high,\n///     &low,\n///     &close,\n///     period,\n///     &mut output_adx,\n///     &mut smoothed_plus_dm,\n///     &mut smoothed_minus_dm,\n///     &mut smoothed_tr,\n/// )\n/// .unwrap();\n/// ```\npub fn adx(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_period: usize,\n    output_adx: &mut [TAFloat],\n    output_smoothed_plus_dm: &mut [TAFloat],\n    output_smoothed_minus_dm: &mut [TAFloat],\n    output_smoothed_tr: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_low.len()\n            || len != input_close.len()\n            || len != output_adx.len()\n            || len != output_smoothed_plus_dm.len()\n            || len != output_smoothed_minus_dm.len()\n            || len != output_smoothed_tr.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_high[i].is_nan() || input_low[i].is_nan() || input_close[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    let mut dx_values = vec![0.0; len];\n\n    // Calculate DX values\n    dx::dx(\n        input_high,\n        input_low,\n        input_close,\n        opt_period,\n        &mut dx_values,\n        output_smoothed_plus_dm,\n        output_smoothed_minus_dm,\n        output_smoothed_tr,\n    )?;\n\n    // Calculate initial ADX as simple average of first period DX values\n    let mut sum = 0.0;\n    for value in dx_values\n        .iter()\n        .take(lookback + 1)\n        .skip(lookback + 1 - opt_period)\n    {\n        sum += *value;\n    }\n    output_adx[lookback] = sum / opt_period as TAFloat;\n\n    // Calculate remaining ADX values using Wilder's smoothing\n    let period_t = opt_period as TAFloat;\n    for i in (lookback + 1)..len {\n        output_adx[i] = output_adx[i - 1].mul_add(period_t - 1.0, dx_values[i]) / period_t;\n    }\n\n    // Fill initial values with NAN\n    for item in output_adx.iter_mut().take(opt_period * 2 - 1) {\n        *item = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculate the latest ADX value incrementally\n///\n/// This function calculates only the most recent ADX value using the previous values and the latest prices.\n/// It is optimized for real-time calculations where only the latest value needs to be updated.\n///\n/// # Arguments\n/// * `input_high` - Current period's high price\n/// * `input_low` - Current period's low price\n/// * `prev_high` - Previous period's high price\n/// * `prev_low` - Previous period's low price\n/// * `prev_close` - Previous period's close price\n/// * `prev_adx` - Previous period's ADX value\n/// * `prev_smoothed_plus_dm` - Previous period's smoothed +DM\n/// * `prev_smoothed_minus_dm` - Previous period's smoothed -DM\n/// * `prev_smoothed_tr` - Previous period's smoothed TR\n/// * `opt_period` - The period parameter (typically 14)\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat, TAFloat), KandError>` - Tuple containing:\n///   - Latest ADX value\n///   - New smoothed +DM\n///   - New smoothed -DM\n///   - New smoothed TR\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n/// * `KandError::NaNDetected` - If any input contains NaN values\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::adx::adx_inc;\n///\n/// let (adx, plus_dm, minus_dm, tr) = adx_inc(\n///     24.20, // current high\n///     23.85, // current low\n///     24.07, // previous high\n///     23.72, // previous low\n///     23.95, // previous close\n///     25.0,  // previous ADX\n///     0.5,   // previous smoothed +DM\n///     0.3,   // previous smoothed -DM\n///     1.2,   // previous smoothed TR\n///     14,    // period\n/// )\n/// .unwrap();\n/// ```\npub fn adx_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    prev_close: TAFloat,\n    prev_adx: TAFloat,\n    prev_smoothed_plus_dm: TAFloat,\n    prev_smoothed_minus_dm: TAFloat,\n    prev_smoothed_tr: TAFloat,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_high.is_nan()\n            || input_low.is_nan()\n            || prev_high.is_nan()\n            || prev_low.is_nan()\n            || prev_close.is_nan()\n            || prev_adx.is_nan()\n            || prev_smoothed_plus_dm.is_nan()\n            || prev_smoothed_minus_dm.is_nan()\n            || prev_smoothed_tr.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let (dx, output_smoothed_plus_dm, output_smoothed_minus_dm, output_smoothed_tr) = dx::dx_inc(\n        input_high,\n        input_low,\n        prev_high,\n        prev_low,\n        prev_close,\n        prev_smoothed_plus_dm,\n        prev_smoothed_minus_dm,\n        prev_smoothed_tr,\n        opt_period,\n    )?;\n\n    let period_t = opt_period as TAFloat;\n    let output_adx = prev_adx.mul_add(period_t - 1.0, dx) / period_t;\n\n    Ok((\n        output_adx,\n        output_smoothed_plus_dm,\n        output_smoothed_minus_dm,\n        output_smoothed_tr,\n    ))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    /// Basic functionality tests\n    #[test]\n    fn test_adx_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4, 35150.4, 35123.9,\n            35110.0, 35092.1, 35179.2, 35244.9, 35150.2, 35136.0, 35133.6, 35188.0, 35215.3,\n            35221.9, 35219.2, 35234.0, 35216.7, 35197.9, 35178.4, 35183.4, 35129.7, 35149.1,\n            35129.3, 35125.5, 35114.5, 35120.1, 35129.4,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0, 35073.0, 35055.0,\n            35084.0, 35060.0, 35073.1, 35090.0, 35072.0, 35078.0, 35088.0, 35124.8, 35169.4,\n            35138.0, 35141.0, 35182.0, 35151.1, 35158.4, 35140.0, 35087.0, 35085.8, 35114.7,\n            35086.0, 35090.6, 35074.1, 35078.4, 35100.0,\n        ];\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0, 35114.5, 35097.2,\n            35092.0, 35073.2, 35139.3, 35092.0, 35126.7, 35106.3, 35124.8, 35170.1, 35215.3,\n            35154.0, 35216.3, 35211.8, 35158.4, 35172.0, 35176.7, 35113.3, 35114.7, 35129.3,\n            35094.6, 35114.4, 35094.5, 35116.0, 35105.4,\n        ];\n        let opt_period = 14;\n        let mut output_adx = vec![0.0; input_high.len()];\n        let mut output_smoothed_plus_dm = vec![0.0; input_high.len()];\n        let mut output_smoothed_minus_dm = vec![0.0; input_high.len()];\n        let mut output_smoothed_tr = vec![0.0; input_high.len()];\n\n        adx(\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_period,\n            &mut output_adx,\n            &mut output_smoothed_plus_dm,\n            &mut output_smoothed_minus_dm,\n            &mut output_smoothed_tr,\n        )\n        .unwrap();\n\n        // First (2*period-1) values should be NaN\n        for value in output_adx.iter().take(2 * opt_period - 1) {\n            assert!(value.is_nan());\n        }\n\n        // Test against known values\n        let expected_values = [\n            23.383_153_393_338_485,\n            22.136_715_365_358_942,\n            21.473_716_847_015_616,\n            21.641_935_163_874_564,\n            21.481_280_068_978_72,\n            21.332_100_338_004_008,\n            21.193_576_302_098_915,\n            21.750_776_176_581_077,\n            22.574_928_816_977_09,\n            22.676_673_819_631_873,\n            22.771_151_322_097_033,\n            23.058_109_702_865_06,\n            22.634_231_810_989_206,\n            22.240_630_911_390_202,\n            21.456_760_075_706_324,\n            20.186_735_391_919_566,\n            19.029_883_367_053_312,\n            17.784_948_566_988_245,\n            16.972_889_325_368_367,\n            16.218_834_315_292_767,\n            15.852_748_058_319_879,\n            15.370_446_543_964_162,\n            14.680_323_641_503_575,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(\n                output_adx[i + 27],\n                *expected,\n                epsilon = 0.00001,\n                max_relative = 0.00001\n            );\n        }\n\n        // Calculate and verify incremental values starting from index 28\n        for i in 28..input_high.len() {\n            let (result, new_smoothed_plus_dm, new_smoothed_minus_dm, new_smoothed_tr) = adx_inc(\n                input_high[i],\n                input_low[i],\n                input_high[i - 1],\n                input_low[i - 1],\n                input_close[i - 1],\n                output_adx[i - 1],\n                output_smoothed_plus_dm[i - 1],\n                output_smoothed_minus_dm[i - 1],\n                output_smoothed_tr[i - 1],\n                opt_period,\n            )\n            .unwrap();\n\n            // Compare with full calculation\n            assert_relative_eq!(result, output_adx[i], epsilon = 0.00001);\n            assert_relative_eq!(\n                new_smoothed_plus_dm,\n                output_smoothed_plus_dm[i],\n                epsilon = 0.00001\n            );\n            assert_relative_eq!(\n                new_smoothed_minus_dm,\n                output_smoothed_minus_dm[i],\n                epsilon = 0.00001\n            );\n            assert_relative_eq!(new_smoothed_tr, output_smoothed_tr[i], epsilon = 0.00001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/adxr.rs",
    "content": "use super::adx;\nuse crate::{KandError, TAFloat};\n\n/// Calculates the lookback period required for ADXR calculation\n///\n/// # Arguments\n/// * `opt_period` - The period parameter for ADX calculation (typically 14)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The number of data points needed before first valid ADXR value\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If period is less than 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::adxr::lookback;\n/// let period = 14;\n/// let lookback_period = lookback(period).unwrap();\n/// assert_eq!(lookback_period, 40); // 14 * 3 - 2 = 40\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period * 3 - 2)\n}\n\n/// Calculates the Average Directional Index Rating (ADXR) for the entire input array\n///\n/// # Mathematical Formula\n/// ```text\n/// ADXR = (Current ADX + ADX period days ago) / 2\n/// ```\n///\n/// # Calculation Principle\n/// 1. Calculate ADX for the entire dataset\n/// 2. For each point starting from position (3*period-2):\n///    - Take current ADX value\n///    - Take ADX value from `period` days ago\n///    - Calculate average of these two values\n///\n/// # Arguments\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of closing prices\n/// * `opt_period` - Period for ADX calculation\n/// * `output_adxr` - Output array for ADXR values\n/// * `output_adx` - Output array for ADX values\n/// * `output_smoothed_plus_dm` - Output array for smoothed +DM values\n/// * `output_smoothed_minus_dm` - Output array for smoothed -DM values\n/// * `output_smoothed_tr` - Output array for smoothed TR values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok if calculation succeeds\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input/output arrays have different lengths\n/// * `KandError::InvalidParameter` - If period is less than 2\n/// * `KandError::InsufficientData` - If input length is less than required lookback period\n/// * `KandError::NaNDetected` - If any input value is NaN (when \"`check-nan`\" feature enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::adxr::adxr;\n///\n/// let input_high = vec![24.20, 24.07, 24.04, 23.87, 23.67];\n/// let input_low = vec![23.85, 23.72, 23.64, 23.37, 23.46];\n/// let input_close = vec![23.89, 23.95, 23.67, 23.78, 23.50];\n/// let period = 2;\n/// let mut output_adxr = vec![0.0; 5];\n/// let mut output_adx = vec![0.0; 5];\n/// let mut output_smoothed_plus_dm = vec![0.0; 5];\n/// let mut output_smoothed_minus_dm = vec![0.0; 5];\n/// let mut output_smoothed_tr = vec![0.0; 5];\n///\n/// adxr(\n///     &input_high,\n///     &input_low,\n///     &input_close,\n///     period,\n///     &mut output_adxr,\n///     &mut output_adx,\n///     &mut output_smoothed_plus_dm,\n///     &mut output_smoothed_minus_dm,\n///     &mut output_smoothed_tr,\n/// )\n/// .unwrap();\n/// ```\npub fn adxr(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_period: usize,\n    output_adxr: &mut [TAFloat],\n    output_adx: &mut [TAFloat],\n    output_smoothed_plus_dm: &mut [TAFloat],\n    output_smoothed_minus_dm: &mut [TAFloat],\n    output_smoothed_tr: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_low.len()\n            || len != input_close.len()\n            || len != output_adxr.len()\n            || len != output_adx.len()\n            || len != output_smoothed_plus_dm.len()\n            || len != output_smoothed_minus_dm.len()\n            || len != output_smoothed_tr.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_high[i].is_nan() || input_low[i].is_nan() || input_close[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate ADX first\n    adx::adx(\n        input_high,\n        input_low,\n        input_close,\n        opt_period,\n        output_adx,\n        output_smoothed_plus_dm,\n        output_smoothed_minus_dm,\n        output_smoothed_tr,\n    )?;\n\n    // Calculate ADXR = (Current ADX + ADX period days ago) / 2\n    // First valid value should be at index lookback (period * 3 - 2)\n    for i in lookback..len {\n        output_adxr[i] = f64::midpoint(output_adx[i], output_adx[i - opt_period + 1]);\n    }\n\n    // Fill initial values with NAN\n    for i in 0..lookback {\n        output_adxr[i] = TAFloat::NAN;\n        output_adx[i] = TAFloat::NAN;\n        output_smoothed_plus_dm[i] = TAFloat::NAN;\n        output_smoothed_minus_dm[i] = TAFloat::NAN;\n        output_smoothed_tr[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the latest ADXR value incrementally\n///\n/// # Mathematical Formula\n/// ```text\n/// Latest ADXR = (Latest ADX + ADX period days ago) / 2\n/// ```\n///\n/// # Arguments\n/// * `input_high` - Current high price\n/// * `input_low` - Current low price\n/// * `prev_high` - Previous high price\n/// * `prev_low` - Previous low price\n/// * `prev_close` - Previous close price\n/// * `prev_adx` - Previous ADX value\n/// * `prev_adx_period_ago` - ADX value from period days ago\n/// * `prev_smoothed_plus_dm` - Previous smoothed +DM value\n/// * `prev_smoothed_minus_dm` - Previous smoothed -DM value\n/// * `prev_smoothed_tr` - Previous smoothed TR value\n/// * `opt_period` - Period for ADX calculation\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat, TAFloat, TAFloat), KandError>` - Tuple containing:\n///   - Latest ADXR value\n///   - Latest ADX value\n///   - New smoothed +DM\n///   - New smoothed -DM\n///   - New smoothed TR\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If period is less than 2\n/// * `KandError::NaNDetected` - If any input value is NaN (when \"`check-nan`\" feature enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::adxr::adxr_inc;\n///\n/// let (adxr, adx, plus_dm, minus_dm, tr) = adxr_inc(\n///     24.20, // input_high\n///     23.85, // input_low\n///     24.07, // prev_high\n///     23.72, // prev_low\n///     23.95, // prev_close\n///     25.0,  // prev_adx\n///     20.0,  // prev_adx_period_ago\n///     0.5,   // prev_smoothed_plus_dm\n///     0.3,   // prev_smoothed_minus_dm\n///     1.2,   // prev_smoothed_tr\n///     14,    // opt_period\n/// )\n/// .unwrap();\n/// ```\npub fn adxr_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    prev_close: TAFloat,\n    prev_adx: TAFloat,\n    prev_adx_period_ago: TAFloat,\n    prev_smoothed_plus_dm: TAFloat,\n    prev_smoothed_minus_dm: TAFloat,\n    prev_smoothed_tr: TAFloat,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_high.is_nan()\n            || input_low.is_nan()\n            || prev_high.is_nan()\n            || prev_low.is_nan()\n            || prev_close.is_nan()\n            || prev_adx.is_nan()\n            || prev_adx_period_ago.is_nan()\n            || prev_smoothed_plus_dm.is_nan()\n            || prev_smoothed_minus_dm.is_nan()\n            || prev_smoothed_tr.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let (output_adx, output_smoothed_plus_dm, output_smoothed_minus_dm, output_smoothed_tr) =\n        adx::adx_inc(\n            input_high,\n            input_low,\n            prev_high,\n            prev_low,\n            prev_close,\n            prev_adx,\n            prev_smoothed_plus_dm,\n            prev_smoothed_minus_dm,\n            prev_smoothed_tr,\n            opt_period,\n        )?;\n\n    let output_adxr = f64::midpoint(output_adx, prev_adx_period_ago);\n\n    Ok((\n        output_adxr,\n        output_adx,\n        output_smoothed_plus_dm,\n        output_smoothed_minus_dm,\n        output_smoothed_tr,\n    ))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    // Basic functionality tests\n    #[test]\n    fn test_adxr_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4, 35150.4, 35123.9,\n            35110.0, 35092.1, 35179.2, 35244.9, 35150.2, 35136.0, 35133.6, 35188.0, 35215.3,\n            35221.9, 35219.2, 35234.0, 35216.7, 35197.9, 35178.4, 35183.4, 35129.7, 35149.1,\n            35129.3, 35125.5, 35114.5, 35120.1, 35129.4, 35105.4, 35054.1, 35034.6, 35032.9,\n            35070.8, 35086.0, 35086.9, 35048.9, 34988.6, 35004.3,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0, 35073.0, 35055.0,\n            35084.0, 35060.0, 35073.1, 35090.0, 35072.0, 35078.0, 35088.0, 35124.8, 35169.4,\n            35138.0, 35141.0, 35182.0, 35151.1, 35158.4, 35140.0, 35087.0, 35085.8, 35114.7,\n            35086.0, 35090.6, 35074.1, 35078.4, 35100.0, 35030.2, 34986.3, 34988.1, 34973.1,\n            35012.3, 35048.3, 35038.9, 34937.3, 34937.0, 34958.7,\n        ];\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0, 35114.5, 35097.2,\n            35092.0, 35073.2, 35139.3, 35092.0, 35126.7, 35106.3, 35124.8, 35170.1, 35215.3,\n            35154.0, 35216.3, 35211.8, 35158.4, 35172.0, 35176.7, 35113.3, 35114.7, 35129.3,\n            35094.6, 35114.4, 35094.5, 35116.0, 35105.4, 35050.7, 35031.3, 35008.1, 35021.4,\n            35048.4, 35080.1, 35043.6, 34962.7, 34970.1, 34980.1,\n        ];\n        let opt_period = 14;\n        let mut output_adxr = vec![0.0; input_high.len()];\n        let mut output_adx = vec![0.0; input_high.len()];\n        let mut output_smoothed_plus_dm = vec![0.0; input_high.len()];\n        let mut output_smoothed_minus_dm = vec![0.0; input_high.len()];\n        let mut output_smoothed_tr = vec![0.0; input_high.len()];\n\n        adxr(\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_period,\n            &mut output_adxr,\n            &mut output_adx,\n            &mut output_smoothed_plus_dm,\n            &mut output_smoothed_minus_dm,\n            &mut output_smoothed_tr,\n        )\n        .unwrap();\n\n        // First (3*period-2) values should be NaN\n        for value in output_adxr.iter().take(3 * opt_period - 2) {\n            assert!(value.is_nan());\n        }\n\n        // Test specific values from the dataset\n        let expected_values = [\n            22.811_892_152_364_344,\n            21.796_737_720_532_633,\n            20.830_226_119_467_59,\n            20.335_909_265_463_94,\n            19.633_114_317_983_484,\n            19.152_494_831_686_187,\n            18.706_205_308_695_843,\n            18.801_762_117_450_48,\n            18.972_687_680_470_624,\n            18.678_498_730_567_725,\n            19.081_591_663_045_668,\n            19.877_741_529_748_13,\n            20.271_853_926_105_9,\n            20.745_128_023_359_108,\n            20.439_791_113_055_39,\n            19.688_560_813_255_584,\n            19.089_803_821_272_966,\n            19.186_732_053_592_11,\n            19.450_462_541_153_087,\n            19.478_837_411_178_546,\n        ];\n\n        let first_valid_idx = 3 * opt_period - 2;\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(\n                output_adxr[i + first_valid_idx],\n                *expected,\n                epsilon = 0.00001\n            );\n        }\n\n        // Calculate and verify incremental values starting from index period * 4 - 3\n        // This starting index is required because:\n        // 1. First period * 3 - 2 values are NaN (base ADXR calculation requirement)\n        // 2. Need additional period - 1 values for i - opt_period + 1 lookback\n        // Total: (period * 3 - 2) - ( - period + 1) = period * 4 - 3\n        for i in (opt_period * 4 - 3)..input_high.len() {\n            let result = adxr_inc(\n                input_high[i],\n                input_low[i],\n                input_high[i - 1],\n                input_low[i - 1],\n                input_close[i - 1],\n                output_adx[i - 1],\n                output_adx[i - opt_period + 1], // ADX value from period days ago\n                output_smoothed_plus_dm[i - 1],\n                output_smoothed_minus_dm[i - 1],\n                output_smoothed_tr[i - 1],\n                opt_period,\n            )\n            .unwrap();\n\n            // Compare with full calculation\n            assert_relative_eq!(result.0, output_adxr[i], epsilon = 0.00001); // ADXR value\n            assert_relative_eq!(result.1, output_adx[i], epsilon = 0.00001); // ADX value\n            assert_relative_eq!(result.2, output_smoothed_plus_dm[i], epsilon = 0.00001); // +DM\n            assert_relative_eq!(result.3, output_smoothed_minus_dm[i], epsilon = 0.00001); // -DM\n            assert_relative_eq!(result.4, output_smoothed_tr[i], epsilon = 0.00001);\n            // TR\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/aroon.rs",
    "content": "use crate::{\n    TAFloat,\n    error::KandError,\n    helper::{highest_bars, lowest_bars},\n};\n\n/// Returns the lookback period required for Aroon indicator calculation.\n///\n/// # Description\n/// The lookback period determines how many historical data points are needed\n/// to start calculating valid Aroon values.\n///\n/// # Arguments\n/// * `opt_period` - The time period used for Aroon calculations (e.g. 14, 25)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - Returns the lookback period equal to `opt_period` on success\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if `opt_period` is less than 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::aroon;\n/// let lookback = aroon::lookback(14).unwrap();\n/// assert_eq!(lookback, 14);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period)\n}\n\n/// Calculates the Aroon indicator for a price series.\n///\n/// # Description\n/// The Aroon indicator consists of two lines that measure the time since the last high/low\n/// relative to a lookback period. It helps identify the start of new trends and trend reversals.\n///\n/// # Calculation Principle\n/// 1. Find the number of periods since the highest high and lowest low\n/// 2. Convert these periods into percentage values between 0-100\n/// 3. Higher values (closer to 100) indicate more recent highs/lows\n/// 4. Lower values (closer to 0) indicate older highs/lows\n///\n/// # Formula\n/// ```text\n/// Aroon Up = ((Period - Days Since High) / Period) × 100\n/// Aroon Down = ((Period - Days Since Low) / Period) × 100\n/// ```\n///\n/// # Arguments\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `opt_period` - The lookback period for calculations\n/// * `output_aroon_up` - Buffer to store Aroon Up values\n/// * `output_aroon_down` - Buffer to store Aroon Down values\n/// * `output_prev_high` - Buffer to store highest prices in period\n/// * `output_prev_low` - Buffer to store lowest prices in period\n/// * `output_days_since_high` - Buffer to store days since highest price\n/// * `output_days_since_low` - Buffer to store days since lowest price\n///\n/// # Returns\n/// * `Result<(), KandError>` - `Ok(())` on success, or error on failure\n///\n/// # Errors\n/// * Returns `KandError::InvalidData` if input arrays are empty\n/// * Returns `KandError::LengthMismatch` if input/output array lengths don't match\n/// * Returns `KandError::InvalidParameter` if period < 2\n/// * Returns `KandError::InsufficientData` if input length <= lookback period\n/// * Returns `KandError::NaNDetected` if any input contains NaN (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::aroon;\n///\n/// let input_high = vec![10.0, 12.0, 15.0, 14.0, 13.0];\n/// let input_low = vec![8.0, 9.0, 11.0, 10.0, 9.0];\n/// let opt_period = 3;\n/// let mut output_aroon_up = vec![0.0; 5];\n/// let mut output_aroon_down = vec![0.0; 5];\n/// let mut output_prev_high = vec![0.0; 5];\n/// let mut output_prev_low = vec![0.0; 5];\n/// let mut output_days_since_high = vec![0; 5];\n/// let mut output_days_since_low = vec![0; 5];\n///\n/// aroon::aroon(\n///     &input_high,\n///     &input_low,\n///     opt_period,\n///     &mut output_aroon_up,\n///     &mut output_aroon_down,\n///     &mut output_prev_high,\n///     &mut output_prev_low,\n///     &mut output_days_since_high,\n///     &mut output_days_since_low,\n/// )\n/// .unwrap();\n/// ```\npub fn aroon(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    opt_period: usize,\n    output_aroon_up: &mut [TAFloat],\n    output_aroon_down: &mut [TAFloat],\n    output_prev_high: &mut [TAFloat],\n    output_prev_low: &mut [TAFloat],\n    output_days_since_high: &mut [usize],\n    output_days_since_low: &mut [usize],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_low.len()\n            || len != output_aroon_up.len()\n            || len != output_aroon_down.len()\n            || len != output_prev_high.len()\n            || len != output_prev_low.len()\n            || len != output_days_since_high.len()\n            || len != output_days_since_low.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_high[i].is_nan() || input_low[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    let opt_period_t = opt_period as TAFloat;\n    let hundred_t = 100.0;\n\n    // Calculate Aroon Up and Down values for each index starting from lookback\n    // Note: We use opt_period + 1 in highest_bars/lowest_bars because:\n    //\n    // Visual example with opt_period = 3:\n    // Array:     [1, 4, 2, 5, 3]\n    //             0  1  2  3  4  <- indices\n    //\n    // For i = 4 (current index):\n    // With opt_period:     [2, 5, 3]  <- only 3 values\n    //                         2  3  4\n    // With opt_period+1:   [4, 2, 5, 3]  <- 4 values (includes previous value)\n    //                         1  2  3  4\n    //\n    // Using opt_period + 1 ensures we look at opt_period previous values PLUS\n    // the current value, allowing days_since_high/low to range from 0 to opt_period:\n    // - If current value is highest/lowest: days_since = 0\n    // - If earliest value is highest/lowest: days_since = opt_period\n    for i in lookback..len {\n        let days_since_high = highest_bars(input_high, i, opt_period + 1)?;\n        let days_since_low = lowest_bars(input_low, i, opt_period + 1)?;\n\n        // Store intermediate values\n        output_days_since_high[i] = days_since_high;\n        output_days_since_low[i] = days_since_low;\n\n        // Get highest high and lowest low from the indices we already calculated\n        output_prev_high[i] = input_high[i - days_since_high];\n        output_prev_low[i] = input_low[i - days_since_low];\n\n        // Calculate Aroon Up and Down values\n        let days_since_high_t = days_since_high as TAFloat;\n        let days_since_low_t = days_since_low as TAFloat;\n\n        output_aroon_up[i] = hundred_t - (hundred_t * days_since_high_t / opt_period_t);\n        output_aroon_down[i] = hundred_t - (hundred_t * days_since_low_t / opt_period_t);\n    }\n\n    // Fill NaN values for lookback period\n    for i in 0..lookback {\n        output_aroon_up[i] = TAFloat::NAN;\n        output_aroon_down[i] = TAFloat::NAN;\n        output_prev_high[i] = TAFloat::NAN;\n        output_prev_low[i] = TAFloat::NAN;\n        output_days_since_high[i] = 0;\n        output_days_since_low[i] = 0;\n    }\n\n    Ok(())\n}\n\n/// Calculates the next Aroon values incrementally.\n///\n/// # Description\n/// This function provides an optimized way to calculate the next Aroon values\n/// when processing streaming data, without recalculating the entire series.\n///\n/// # Calculation Principle\n/// 1. Update days since last high/low by incrementing counters\n/// 2. Check if current high/low prices create new extremes\n/// 3. Calculate Aroon values using updated information\n///\n/// # Arguments\n/// * `input_high` - Current period's high price\n/// * `input_low` - Current period's low price\n/// * `prev_high` - Previous highest price in period\n/// * `prev_low` - Previous lowest price in period\n/// * `input_days_since_high` - Days since previous highest price\n/// * `input_days_since_low` - Days since previous lowest price\n/// * `opt_period` - The lookback period\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat, TAFloat, usize, usize), KandError>` - Returns tuple containing:\n///   - Aroon Up value\n///   - Aroon Down value\n///   - New highest price\n///   - New lowest price\n///   - Updated days since high\n///   - Updated days since low\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if period < 2\n/// * Returns `KandError::NaNDetected` if any input is NaN (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::aroon;\n///\n/// let (aroon_up, aroon_down, new_high, new_low, days_high, days_low) = aroon::aroon_inc(\n///     15.0, // Current high\n///     12.0, // Current low\n///     14.0, // Previous high\n///     11.0, // Previous low\n///     2,    // Days since high\n///     1,    // Days since low\n///     14,   // Period\n/// )\n/// .unwrap();\n/// ```\npub fn aroon_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    input_days_since_high: usize,\n    input_days_since_low: usize,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat, TAFloat, usize, usize), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_high.is_nan() || input_low.is_nan() || prev_high.is_nan() || prev_low.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let mut new_high = prev_high;\n    let mut new_low = prev_low;\n    let mut days_since_high = input_days_since_high;\n    let mut days_since_low = input_days_since_low;\n\n    // Update days since high/low\n    if days_since_high < opt_period {\n        days_since_high += 1;\n    }\n    if days_since_low < opt_period {\n        days_since_low += 1;\n    }\n\n    // Check if current values are new high/low\n    if input_high >= prev_high {\n        new_high = input_high;\n        days_since_high = 0;\n    }\n    if input_low <= prev_low {\n        new_low = input_low;\n        days_since_low = 0;\n    }\n\n    let opt_period_t = opt_period as TAFloat;\n    let hundred_t = 100.0;\n    let days_since_high_t = days_since_high as TAFloat;\n    let days_since_low_t = days_since_low as TAFloat;\n\n    let aroon_up = hundred_t - (hundred_t * days_since_high_t / opt_period_t);\n    let aroon_down = hundred_t - (hundred_t * days_since_low_t / opt_period_t);\n\n    Ok((\n        aroon_up,\n        aroon_down,\n        new_high,\n        new_low,\n        days_since_high,\n        days_since_low,\n    ))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_aroon_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4, 35150.4, 35123.9,\n            35110.0, 35092.1, 35179.2, 35244.9, 35150.2, 35136.0, 35133.6, 35188.0, 35215.3,\n            35221.9, 35219.2, 35234.0, 35216.7, 35197.9, 35178.4, 35183.4, 35129.7, 35149.1,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0, 35073.0, 35055.0,\n            35084.0, 35060.0, 35073.1, 35090.0, 35072.0, 35078.0, 35088.0, 35124.8, 35169.4,\n            35138.0, 35141.0, 35182.0, 35151.1, 35158.4, 35140.0, 35087.0, 35085.8, 35114.7,\n        ];\n        let opt_period = 14;\n        let mut output_aroon_up = vec![0.0; input_high.len()];\n        let mut output_aroon_down = vec![0.0; input_high.len()];\n        let mut output_prev_high = vec![0.0; input_high.len()];\n        let mut output_prev_low = vec![0.0; input_high.len()];\n        let mut output_days_since_high = vec![0; input_high.len()];\n        let mut output_days_since_low = vec![0; input_high.len()];\n\n        aroon(\n            &input_high,\n            &input_low,\n            opt_period,\n            &mut output_aroon_up,\n            &mut output_aroon_down,\n            &mut output_prev_high,\n            &mut output_prev_low,\n            &mut output_days_since_high,\n            &mut output_days_since_low,\n        )\n        .unwrap();\n\n        // First 13 values should be NaN\n        for i in 0..14 {\n            assert!(output_aroon_up[i].is_nan());\n            assert!(output_aroon_down[i].is_nan());\n        }\n\n        // Compare with known values\n        let expected_up = [\n            50.0,\n            42.857_142_857_142_86,\n            35.714_285_714_285_715,\n            28.571_428_571_428_573,\n            21.428_571_428_571_43,\n            14.285_714_285_714_286,\n            7.142_857_142_857_143,\n            0.0,\n            0.0,\n            21.428_571_428_571_43,\n            14.285_714_285_714_286,\n            7.142_857_142_857_143,\n            0.0,\n            0.0,\n            0.0,\n            100.0,\n            100.0,\n            92.857_142_857_142_86,\n            85.714_285_714_285_72,\n            78.571_428_571_428_57,\n            71.428_571_428_571_43,\n            64.285_714_285_714_29,\n            57.142_857_142_857_146,\n            50.0,\n            42.857_142_857_142_86,\n            35.714_285_714_285_715,\n            28.571_428_571_428_573,\n            21.428_571_428_571_43,\n            14.285_714_285_714_286,\n            7.142_857_142_857_143,\n            0.0,\n        ];\n\n        let expected_down = [\n            100.0,\n            100.0,\n            100.0,\n            92.857_142_857_142_86,\n            85.714_285_714_285_72,\n            78.571_428_571_428_57,\n            100.0,\n            100.0,\n            92.857_142_857_142_86,\n            85.714_285_714_285_72,\n            78.571_428_571_428_57,\n            71.428_571_428_571_43,\n            64.285_714_285_714_29,\n            57.142_857_142_857_146,\n            50.0,\n            42.857_142_857_142_86,\n            35.714_285_714_285_715,\n            28.571_428_571_428_573,\n            21.428_571_428_571_43,\n            14.285_714_285_714_286,\n            7.142_857_142_857_143,\n            0.0,\n            0.0,\n            0.0,\n            0.0,\n            7.142_857_142_857_143,\n            0.0,\n            7.142_857_142_857_143,\n            0.0,\n            14.285_714_285_714_286,\n            7.142_857_142_857_143,\n        ];\n\n        for (i, (&exp_up, &exp_down)) in expected_up.iter().zip(expected_down.iter()).enumerate() {\n            assert_relative_eq!(output_aroon_up[i + 14], exp_up, epsilon = 0.0001);\n            assert_relative_eq!(output_aroon_down[i + 14], exp_down, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation\n        let mut prev_high = output_prev_high[14];\n        let mut prev_low = output_prev_low[14];\n        let mut days_since_high = output_days_since_high[14];\n        let mut days_since_low = output_days_since_low[14];\n\n        for i in 15..20 {\n            let result = aroon_inc(\n                input_high[i],\n                input_low[i],\n                prev_high,\n                prev_low,\n                days_since_high,\n                days_since_low,\n                opt_period,\n            )\n            .unwrap();\n\n            assert_relative_eq!(result.0, output_aroon_up[i], epsilon = 0.0001);\n            assert_relative_eq!(result.1, output_aroon_down[i], epsilon = 0.0001);\n\n            prev_high = result.2;\n            prev_low = result.3;\n            days_since_high = result.4;\n            days_since_low = result.5;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/aroonosc.rs",
    "content": "use crate::{\n    TAFloat,\n    error::KandError,\n    helper::{highest_bars, lowest_bars},\n};\n\n/// Returns the lookback period required for Aroon Oscillator calculation.\n///\n/// # Description\n/// The lookback period represents the minimum number of data points needed before the first valid output\n/// can be calculated. For the Aroon Oscillator, this equals the specified period parameter.\n///\n/// # Arguments\n/// * `opt_period` - The time period for Aroon calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period on success\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2 (when \"check\" feature enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::aroonosc::lookback;\n/// let period = 14;\n/// let lookback_period = lookback(period).unwrap();\n/// assert_eq!(lookback_period, 14);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period)\n}\n\n/// Calculates Aroon Oscillator values for an entire price series.\n///\n/// # Description\n/// The Aroon Oscillator is a trend-following indicator that measures the strength of a trend and the likelihood\n/// that the trend will continue. It oscillates between -100 and +100.\n///\n/// # Mathematical Formula\n/// ```text\n/// Aroon Up = ((Period - Days Since Period High) / Period) * 100\n/// Aroon Down = ((Period - Days Since Period Low) / Period) * 100\n/// Aroon Oscillator = Aroon Up - Aroon Down\n/// ```\n///\n/// # Calculation Principle\n/// 1. Calculate days since highest price within period\n/// 2. Calculate days since lowest price within period\n/// 3. Calculate Aroon Up and Aroon Down using above values\n/// 4. Subtract Aroon Down from Aroon Up to get oscillator value\n///\n/// # Arguments\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `opt_period` - The time period for Aroon calculation (must be >= 2)\n/// * `output_aroonosc` - Array to store the calculated Aroon Oscillator values\n/// * `output_prev_high` - Array to store highest prices within the period\n/// * `output_prev_low` - Array to store lowest prices within the period\n/// * `output_days_since_high` - Array to store number of days since highest price\n/// * `output_days_since_low` - Array to store number of days since lowest price\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok(()) on success\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input and output arrays have different lengths\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n/// * `KandError::InsufficientData` - If input length <= lookback period\n/// * `KandError::NaNDetected` - If any input contains NaN (when \"`check-nan`\" enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::aroonosc;\n///\n/// let high = vec![10.0, 11.0, 12.0, 11.0, 10.0];\n/// let low = vec![9.0, 10.0, 11.0, 10.0, 9.0];\n/// let period = 2;\n/// let mut aroon_osc = vec![0.0; 5];\n/// let mut prev_high = vec![0.0; 5];\n/// let mut prev_low = vec![0.0; 5];\n/// let mut days_high = vec![0; 5];\n/// let mut days_low = vec![0; 5];\n///\n/// aroonosc::aroonosc(\n///     &high,\n///     &low,\n///     period,\n///     &mut aroon_osc,\n///     &mut prev_high,\n///     &mut prev_low,\n///     &mut days_high,\n///     &mut days_low,\n/// )\n/// .unwrap();\n/// ```\npub fn aroonosc(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    opt_period: usize,\n    output_aroonosc: &mut [TAFloat],\n    output_prev_high: &mut [TAFloat],\n    output_prev_low: &mut [TAFloat],\n    output_days_since_high: &mut [usize],\n    output_days_since_low: &mut [usize],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_low.len()\n            || len != output_aroonosc.len()\n            || len != output_prev_high.len()\n            || len != output_prev_low.len()\n            || len != output_days_since_high.len()\n            || len != output_days_since_low.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            if input_high[i].is_nan() || input_low[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    let opt_period_t = opt_period as TAFloat;\n    let hundred_t = 100.0;\n\n    for i in lookback..len {\n        let days_since_high = highest_bars(input_high, i, opt_period + 1)?;\n        let days_since_low = lowest_bars(input_low, i, opt_period + 1)?;\n\n        output_days_since_high[i] = days_since_high;\n        output_days_since_low[i] = days_since_low;\n\n        output_prev_high[i] = input_high[i - days_since_high];\n        output_prev_low[i] = input_low[i - days_since_low];\n\n        // Calculate Aroon Up and Down values\n        let days_since_high_t = days_since_high as TAFloat;\n        let days_since_low_t = days_since_low as TAFloat;\n\n        let aroon_up = hundred_t - (hundred_t * days_since_high_t / opt_period_t);\n        let aroon_down = hundred_t - (hundred_t * days_since_low_t / opt_period_t);\n\n        output_aroonosc[i] = aroon_up - aroon_down;\n    }\n\n    for i in 0..lookback {\n        output_aroonosc[i] = TAFloat::NAN;\n        output_prev_high[i] = TAFloat::NAN;\n        output_prev_low[i] = TAFloat::NAN;\n        output_days_since_high[i] = 0;\n        output_days_since_low[i] = 0;\n    }\n\n    Ok(())\n}\n\n/// Calculates the next Aroon Oscillator value incrementally.\n///\n/// # Description\n/// This function provides an efficient way to calculate the next Aroon Oscillator value\n/// when new price data becomes available, without recalculating the entire series.\n///\n/// # Mathematical Formula\n/// ```text\n/// Aroon Up = ((Period - Days Since Period High) / Period) * 100\n/// Aroon Down = ((Period - Days Since Period Low) / Period) * 100\n/// Aroon Oscillator = Aroon Up - Aroon Down\n/// ```\n///\n/// # Calculation Principle\n/// 1. Update days since high/low values\n/// 2. Check if new high/low prices are set\n/// 3. Calculate new Aroon Up and Down values\n/// 4. Calculate oscillator as difference between Up and Down\n///\n/// # Arguments\n/// * `input_high` - Current period's high price\n/// * `input_low` - Current period's low price\n/// * `prev_high` - Previous highest price within the period\n/// * `prev_low` - Previous lowest price within the period\n/// * `input_days_since_high` - Days since previous highest price\n/// * `input_days_since_low` - Days since previous lowest price\n/// * `opt_period` - The time period for Aroon calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat, usize, usize), KandError>` - Returns tuple containing:\n///   - Aroon Oscillator value\n///   - New highest price\n///   - New lowest price\n///   - Updated days since highest price\n///   - Updated days since lowest price\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2 (when \"check\" enabled)\n/// * `KandError::NaNDetected` - If any input is NaN (when \"`check-nan`\" enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::aroonosc::aroonosc_inc;\n///\n/// let (aroonosc, high, low, days_high, days_low) = aroonosc_inc(\n///     10.0, // current high\n///     9.0,  // current low\n///     11.0, // previous high\n///     10.0, // previous low\n///     1,    // days since high\n///     2,    // days since low\n///     14,   // period\n/// )\n/// .unwrap();\n/// ```\npub fn aroonosc_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    input_days_since_high: usize,\n    input_days_since_low: usize,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat, usize, usize), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_high.is_nan() || input_low.is_nan() || prev_high.is_nan() || prev_low.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let mut new_high = prev_high;\n    let mut new_low = prev_low;\n    let mut days_since_high = input_days_since_high;\n    let mut days_since_low = input_days_since_low;\n\n    if days_since_high < opt_period {\n        days_since_high += 1;\n    }\n    if days_since_low < opt_period {\n        days_since_low += 1;\n    }\n\n    if input_high >= prev_high {\n        new_high = input_high;\n        days_since_high = 0;\n    }\n    if input_low <= prev_low {\n        new_low = input_low;\n        days_since_low = 0;\n    }\n\n    let opt_period_t = opt_period as TAFloat;\n    let hundred_t = 100.0;\n    let days_since_high_t = days_since_high as TAFloat;\n    let days_since_low_t = days_since_low as TAFloat;\n\n    let aroon_up = hundred_t - (hundred_t * days_since_high_t / opt_period_t);\n    let aroon_down = hundred_t - (hundred_t * days_since_low_t / opt_period_t);\n    let aroon_osc = aroon_up - aroon_down;\n\n    Ok((\n        aroon_osc,\n        new_high,\n        new_low,\n        days_since_high,\n        days_since_low,\n    ))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_aroonosc_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0,\n        ];\n        let opt_period = 14;\n        let mut output_aroonosc = vec![0.0; input_high.len()];\n        let mut output_prev_high = vec![0.0; input_high.len()];\n        let mut output_prev_low = vec![0.0; input_high.len()];\n        let mut output_days_since_high = vec![0; input_high.len()];\n        let mut output_days_since_low = vec![0; input_high.len()];\n\n        aroonosc(\n            &input_high,\n            &input_low,\n            opt_period,\n            &mut output_aroonosc,\n            &mut output_prev_high,\n            &mut output_prev_low,\n            &mut output_days_since_high,\n            &mut output_days_since_low,\n        )\n        .unwrap();\n\n        // First 13 values should be NaN\n        for value in output_aroonosc.iter().take(14) {\n            assert!(value.is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            -50.0,\n            -57.142_857_142_857_146,\n            -64.285_714_285_714_29,\n            -64.285_714_285_714_29,\n            -64.285_714_285_714_29,\n            -64.285_714_285_714_29,\n            -92.857_142_857_142_86,\n            -100.0,\n            -92.857_142_857_142_86,\n            -64.285_714_285_714_29,\n            -64.285_714_285_714_29,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_aroonosc[i + 14], *expected, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation\n        let mut prev_high = output_prev_high[14];\n        let mut prev_low = output_prev_low[14];\n        let mut days_since_high = output_days_since_high[14];\n        let mut days_since_low = output_days_since_low[14];\n\n        for i in 15..20 {\n            let (aroon_osc, new_high, new_low, new_days_high, new_days_low) = aroonosc_inc(\n                input_high[i],\n                input_low[i],\n                prev_high,\n                prev_low,\n                days_since_high,\n                days_since_low,\n                opt_period,\n            )\n            .unwrap();\n\n            assert_relative_eq!(aroon_osc, output_aroonosc[i], epsilon = 0.0001);\n\n            prev_high = new_high;\n            prev_low = new_low;\n            days_since_high = new_days_high;\n            days_since_low = new_days_low;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/atr.rs",
    "content": "use super::trange;\nuse crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for ATR calculation.\n///\n/// # Description\n/// The lookback period represents the number of data points needed before the first valid output\n/// can be calculated. For ATR, this equals the specified period.\n///\n/// # Arguments\n/// * `opt_period` - The time period used for ATR calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period on success\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if `opt_period` is less than 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::atr;\n/// let period = 14;\n/// let lookback = atr::lookback(period).unwrap();\n/// assert_eq!(lookback, 14); // lookback equals period\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period)\n}\n\n/// Calculates Average True Range (ATR) for an entire price series.\n///\n/// # Description\n/// The Average True Range (ATR) is a technical analysis indicator that measures market volatility\n/// by decomposing the entire range of an asset price for a given period.\n///\n/// # Mathematical Formula\n/// ```text\n/// TR = max(high - low, |high - prev_close|, |low - prev_close|)\n/// First ATR = SMA(TR, period)\n/// Subsequent ATR = ((period-1) * prev_ATR + TR) / period\n/// ```\n///\n/// # Calculation Steps\n/// 1. Calculate True Range (TR) for each period\n/// 2. First ATR value is the Simple Moving Average (SMA) of TR over the specified period\n/// 3. Subsequent ATR values use Wilder's RMA (Running Moving Average) formula\n/// 4. First (period) values will be NaN as they require full period data\n///\n/// # Arguments\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of close prices\n/// * `opt_period` - The time period for ATR calculation (must be >= 2)\n/// * `output_atr` - Array to store calculated ATR values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty result on success, error otherwise\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input arrays have different lengths\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n/// * `KandError::InsufficientData` - If input length <= lookback period\n/// * `KandError::NaNDetected` - If any input value is NaN\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::atr;\n///\n/// let input_high = vec![10.0, 12.0, 15.0, 14.0, 13.0];\n/// let input_low = vec![8.0, 9.0, 11.0, 10.0, 9.0];\n/// let input_close = vec![9.0, 11.0, 14.0, 12.0, 11.0];\n/// let opt_period = 3;\n/// let mut output_atr = vec![0.0; 5];\n///\n/// atr::atr(\n///     &input_high,\n///     &input_low,\n///     &input_close,\n///     opt_period,\n///     &mut output_atr,\n/// )\n/// .unwrap();\n/// ```\npub fn atr(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_period: usize,\n    output_atr: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_low.len() || len != input_close.len() || len != output_atr.len() {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_high[i].is_nan() || input_low[i].is_nan() || input_close[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate first TR values and initial ATR (SMA of TR)\n    let mut tr_sum = 0.0;\n    let mut prev_close = input_close[0];\n\n    for i in 1..=lookback {\n        let tr = trange::trange_inc(input_high[i], input_low[i], prev_close)?;\n        tr_sum += tr;\n        prev_close = input_close[i];\n    }\n    output_atr[lookback] = tr_sum / (opt_period as TAFloat);\n\n    // Calculate remaining ATR values using RMA\n    for i in (lookback + 1)..len {\n        let tr = trange::trange_inc(input_high[i], input_low[i], input_close[i - 1])?;\n        output_atr[i] =\n            output_atr[i - 1].mul_add((opt_period - 1) as TAFloat, tr) / (opt_period as TAFloat);\n    }\n\n    // Fill initial values with NAN\n    for value in output_atr.iter_mut().take(lookback) {\n        *value = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the next ATR value using the previous ATR value and current price data.\n///\n/// # Description\n/// This function provides an efficient way to calculate the next ATR value incrementally,\n/// using the previous ATR value and current price data. It uses Wilder's RMA formula.\n///\n/// # Mathematical Formula\n/// ```text\n/// TR = max(high - low, |high - prev_close|, |low - prev_close|)\n/// ATR = ((prev_ATR * (period-1)) + TR) / period\n/// ```\n///\n/// # Arguments\n/// * `input_high` - Current period's high price\n/// * `input_low` - Current period's low price\n/// * `prev_close` - Previous period's close price\n/// * `prev_atr` - Previous period's ATR value\n/// * `opt_period` - The time period for ATR calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The calculated ATR value on success\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n/// * `KandError::NaNDetected` - If any input value is NaN\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::atr::atr_inc;\n///\n/// let input_high = 15.0;\n/// let input_low = 11.0;\n/// let prev_close = 12.0;\n/// let prev_atr = 3.0;\n/// let opt_period = 3;\n///\n/// let output_atr = atr_inc(input_high, input_low, prev_close, prev_atr, opt_period).unwrap();\n/// ```\npub fn atr_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    prev_close: TAFloat,\n    prev_atr: TAFloat,\n    opt_period: usize,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_high.is_nan() || input_low.is_nan() || prev_close.is_nan() || prev_atr.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let tr = trange::trange_inc(input_high, input_low, prev_close)?;\n    Ok(prev_atr.mul_add((opt_period - 1) as TAFloat, tr) / (opt_period as TAFloat))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    // Basic functionality tests\n    #[test]\n    fn test_atr_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0,\n        ];\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0,\n        ];\n        let opt_period = 14;\n        let mut output_atr = vec![0.0; input_high.len()];\n\n        atr(\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_period,\n            &mut output_atr,\n        )\n        .unwrap();\n\n        // First 13 values should be NaN\n        for value in output_atr.iter().take(13) {\n            assert!(value.is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            63.185_714_285_714_7,\n            66.372_448_979_592_43,\n            68.602_988_338_192_87,\n            67.524_203_456_893_39,\n            67.451_046_067_115_29,\n            67.118_828_490_892_98,\n            69.646_055_027_257_76,\n            69.914_193_953_882_3,\n            69.084_608_671_462_35,\n            69.092_850_909_214_83,\n            67.900_504_415_699_59,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_atr[i + 14], *expected, epsilon = 0.0001);\n        }\n\n        // Now test incremental calculation matches regular calculation\n        let mut prev_atr = output_atr[14]; // First valid ATR value\n\n        // Test each incremental step\n        for i in 15..19 {\n            let result = atr_inc(\n                input_high[i],\n                input_low[i],\n                input_close[i - 1],\n                prev_atr,\n                opt_period,\n            )\n            .unwrap();\n            assert_relative_eq!(result, output_atr[i], epsilon = 0.0001);\n            prev_atr = result;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/bbands.rs",
    "content": "use crate::{\n    KandError, TAFloat,\n    ta::{ohlcv::sma, stats::var},\n};\n\n/// Returns the lookback period required for Bollinger Bands calculation.\n///\n/// # Description\n/// The lookback period represents the minimum number of data points needed before\n/// the first valid output can be calculated. For Bollinger Bands, this equals\n/// the specified period parameter.\n///\n/// # Arguments\n/// * `opt_period` - The time period used for calculations (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period on success, or error on failure\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If period is less than 2\n///\n/// # Example\n/// ```\n/// use kand::ta::ohlcv::bbands;\n/// let period = 20;\n/// let lookback = bbands::lookback(period).unwrap();\n/// assert_eq!(lookback, 19);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    sma::lookback(opt_period)\n}\n\n/// Calculates Bollinger Bands for a price series.\n///\n/// # Description\n/// Bollinger Bands are volatility bands placed above and below a moving average.\n/// They consist of:\n/// - A middle band (N-period simple moving average)\n/// - An upper band (K standard deviations above middle band)\n/// - A lower band (K standard deviations below middle band)\n///\n/// # Mathematical Formula\n/// ```text\n/// Middle Band = SMA(price, N)\n/// Standard Deviation = sqrt(sum((price - SMA)^2) / N)\n/// Upper Band = Middle Band + (K × Standard Deviation)\n/// Lower Band = Middle Band - (K × Standard Deviation)\n/// ```\n/// where:\n/// - N is the period\n/// - K is the number of standard deviations\n///\n/// # Calculation Steps\n/// 1. Calculate N-period SMA as middle band\n/// 2. Calculate N-period standard deviation\n/// 3. Add/subtract K standard deviations to get upper/lower bands\n///\n/// # Arguments\n/// * `input_price` - Slice of input price values\n/// * `opt_period` - The time period for calculations (must be >= 2)\n/// * `opt_dev_up` - Number of standard deviations for upper band\n/// * `opt_dev_down` - Number of standard deviations for lower band\n/// * `output_upper` - Buffer to store upper band values\n/// * `output_middle` - Buffer to store middle band values\n/// * `output_lower` - Buffer to store lower band values\n/// * `output_sma` - Buffer to store SMA values\n/// * `output_var` - Buffer to store variance values\n/// * `output_sum` - Buffer to store running sum values\n/// * `output_sum_sq` - Buffer to store running sum of squares values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty result on success, or error on failure\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input slice is empty\n/// * `KandError::LengthMismatch` - If input and output slices have different lengths\n/// * `KandError::InvalidParameter` - If period is less than 2\n/// * `KandError::InsufficientData` - If input length is less than required period\n/// * `KandError::NaNDetected` - If any input contains NaN values\n///\n/// # Example\n/// ```\n/// use kand::ta::ohlcv::bbands;\n/// let prices = vec![10.0, 11.0, 12.0, 13.0, 14.0];\n/// let period = 3;\n/// let mut upper = vec![0.0; 5];\n/// let mut middle = vec![0.0; 5];\n/// let mut lower = vec![0.0; 5];\n/// let mut sma = vec![0.0; 5];\n/// let mut var = vec![0.0; 5];\n/// let mut sum = vec![0.0; 5];\n/// let mut sum_sq = vec![0.0; 5];\n///\n/// bbands::bbands(\n///     &prices,\n///     period,\n///     2.0,\n///     2.0,\n///     &mut upper,\n///     &mut middle,\n///     &mut lower,\n///     &mut sma,\n///     &mut var,\n///     &mut sum,\n///     &mut sum_sq,\n/// )\n/// .unwrap();\n/// ```\npub fn bbands(\n    input_price: &[TAFloat],\n    opt_period: usize,\n    opt_dev_up: TAFloat,\n    opt_dev_down: TAFloat,\n    output_upper: &mut [TAFloat],\n    output_middle: &mut [TAFloat],\n    output_lower: &mut [TAFloat],\n    output_sma: &mut [TAFloat],\n    output_var: &mut [TAFloat],\n    output_sum: &mut [TAFloat],\n    output_sum_sq: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_price.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Data sufficiency check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length check\n        if len != output_upper.len()\n            || len != output_middle.len()\n            || len != output_lower.len()\n            || len != output_sma.len()\n            || len != output_var.len()\n            || len != output_sum.len()\n            || len != output_sum_sq.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for price in input_price {\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate SMA first\n    sma::sma(input_price, opt_period, output_sma)?;\n\n    // Calculate variance\n    var::var(\n        input_price,\n        opt_period,\n        output_var,\n        output_sum,\n        output_sum_sq,\n    )?;\n\n    for i in lookback..len {\n        output_middle[i] = output_sma[i];\n        let std_dev = output_var[i].sqrt();\n\n        // Calculate upper and lower bands using standard deviations\n        output_upper[i] = opt_dev_up.mul_add(std_dev, output_sma[i]);\n        output_lower[i] = opt_dev_down.mul_add(-std_dev, output_sma[i]);\n    }\n\n    // Fill initial values with NAN\n    for i in 0..lookback {\n        output_upper[i] = TAFloat::NAN;\n        output_middle[i] = TAFloat::NAN;\n        output_lower[i] = TAFloat::NAN;\n        output_sma[i] = TAFloat::NAN;\n        output_var[i] = TAFloat::NAN;\n        output_sum[i] = TAFloat::NAN;\n        output_sum_sq[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the next Bollinger Bands values using an incremental approach.\n///\n/// # Description\n/// This function provides an optimized way to calculate the next set of Bollinger Bands values\n/// when new data arrives, without recalculating the entire series. It uses the previous values\n/// to compute the new bands efficiently.\n///\n/// # Calculation Steps\n/// 1. Calculate new SMA using incremental approach\n/// 2. Calculate new variance using incremental approach\n/// 3. Compute standard deviation and bands\n///\n/// # Arguments\n/// * `input_price` - The current price value\n/// * `prev_sma` - The previous SMA value\n/// * `prev_sum` - The previous sum for variance calculation\n/// * `prev_sum_sq` - The previous sum of squares for variance calculation\n/// * `input_old_price` - The oldest price value to be removed from the period\n/// * `opt_period` - The time period for calculations (must be >= 2)\n/// * `opt_dev_up` - Number of standard deviations for upper band\n/// * `opt_dev_down` - Number of standard deviations for lower band\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat, TAFloat, TAFloat, TAFloat), KandError>` - A tuple containing:\n///   - Upper Band value\n///   - Middle Band value\n///   - Lower Band value\n///   - New SMA value\n///   - New Sum value\n///   - New Sum of Squares value\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If period is less than 2\n/// * `KandError::NaNDetected` - If any input contains NaN values\n///\n/// # Example\n/// ```\n/// use kand::ta::ohlcv::bbands;\n/// let (upper, middle, lower, sma, sum, sum_sq) = bbands::bbands_inc(\n///     10.0,   // new price\n///     9.5,    // previous SMA\n///     28.5,   // previous sum\n///     272.25, // previous sum of squares\n///     9.0,    // oldest price\n///     3,      // period\n///     2.0,    // upper deviation\n///     2.0,    // lower deviation\n/// )\n/// .unwrap();\n/// ```\npub fn bbands_inc(\n    input_price: TAFloat,\n    prev_sma: TAFloat,\n    prev_sum: TAFloat,\n    prev_sum_sq: TAFloat,\n    input_old_price: TAFloat,\n    opt_period: usize,\n    opt_dev_up: TAFloat,\n    opt_dev_down: TAFloat,\n) -> Result<(TAFloat, TAFloat, TAFloat, TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_price.is_nan()\n            || prev_sma.is_nan()\n            || prev_sum.is_nan()\n            || prev_sum_sq.is_nan()\n            || input_old_price.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n        if opt_dev_up.is_nan() || opt_dev_down.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    // Calculate new SMA using incremental SMA\n    let new_sma = sma::sma_inc(prev_sma, input_price, input_old_price, opt_period)?;\n\n    // Calculate new variance using incremental variance\n    let (new_variance, new_sum, new_sum_sq) = var::var_inc(\n        input_price,\n        prev_sum,\n        prev_sum_sq,\n        input_old_price,\n        opt_period,\n    )?;\n\n    let std_dev = new_variance.sqrt();\n    let upper = opt_dev_up.mul_add(std_dev, new_sma);\n    let lower = opt_dev_down.mul_add(-std_dev, new_sma);\n\n    Ok((upper, new_sma, lower, new_sma, new_sum, new_sum_sq))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_bbands_calculation() {\n        let input_price = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0, 35114.5, 35097.2,\n            35092.0, 35073.2, 35139.3, 35092.0, 35126.7, 35106.3, 35124.8, 35170.1, 35215.3,\n            35154.0, 35216.3, 35211.8, 35158.4, 35172.0, 35176.7, 35113.3, 35114.7, 35129.3,\n        ];\n\n        let opt_period = 20;\n        let opt_dev_up = 2.0;\n        let opt_dev_down = 2.0;\n        let mut output_upper = vec![0.0; input_price.len()];\n        let mut output_middle = vec![0.0; input_price.len()];\n        let mut output_lower = vec![0.0; input_price.len()];\n        let mut output_sma = vec![0.0; input_price.len()];\n        let mut output_var = vec![0.0; input_price.len()];\n        let mut output_sum = vec![0.0; input_price.len()];\n        let mut output_sum_sq = vec![0.0; input_price.len()];\n\n        bbands(\n            &input_price,\n            opt_period,\n            opt_dev_up,\n            opt_dev_down,\n            &mut output_upper,\n            &mut output_middle,\n            &mut output_lower,\n            &mut output_sma,\n            &mut output_var,\n            &mut output_sum,\n            &mut output_sum_sq,\n        )\n        .unwrap();\n\n        // First 19 values should be NaN\n        for i in 0..19 {\n            assert!(output_upper[i].is_nan());\n            assert!(output_middle[i].is_nan());\n            assert!(output_lower[i].is_nan());\n            assert!(output_sma[i].is_nan());\n            assert!(output_var[i].is_nan());\n            assert!(output_sum[i].is_nan());\n            assert!(output_sum_sq[i].is_nan());\n        }\n\n        // Compare with known values\n        let expected_upper = vec![\n            35_315.492_158_169_03,\n            35_324.023_520_348_93,\n            35_323.822_186_479_93,\n            35_319.449_647_081_79,\n            35_314.110_592_229_015,\n            35_306.809_201_120_76,\n            35_288.014_966_586_7,\n            35_276.648_971_890_07,\n            35_253.671_769_987_47,\n            35_239.448_423_376_95,\n            35_232.360_028_616_255,\n            35_221.822_196_455_76,\n            35_200.867_114_660_425,\n            35_179.557_912_368_81,\n            35_172.678_349_978_625,\n            35_185.898_169_265_74,\n            35_210.535_957_882_84,\n            35_218.090_674_365_98,\n            35_236.434_486_030_11,\n            35_252.252_647_217_1,\n            35_257.112_658_379_57,\n            35_250.714_615_459_73,\n            35_240.881_227_372_27,\n            35_230.468_530_636_03,\n            35_225.037_992_782_84,\n            35_223.587_496_067_295,\n        ];\n        let expected_middle = vec![\n            35_154.365_000_000_005,\n            35140.535,\n            35127.095,\n            35_117.560_000_000_005,\n            35_111.150_000_000_01,\n            35_106.075_000_000_004,\n            35_099.070_000_000_01,\n            35093.79,\n            35085.795,\n            35079.575,\n            35_077.305_000_000_01,\n            35_073.150_000_000_01,\n            35_067.990_000_000_005,\n            35_062.680_000_000_01,\n            35_060.885_000_000_01,\n            35_064.875_000_000_01,\n            35_073.580_000_000_01,\n            35_081.315_000_000_01,\n            35_091.460_000_000_01,\n            35_098.600_000_000_01,\n            35_105.290_000_000_015,\n            35_116.915_000_000_015,\n            35_128.120_000_000_01,\n            35_133.785_000_000_02,\n            35_137.430_000_000_01,\n            35_139.895_000_000_01,\n        ];\n        let expected_lower = vec![\n            34_993.237_841_830_98,\n            34_957.046_479_651_08,\n            34_930.367_813_520_075,\n            34_915.670_352_918_22,\n            34_908.189_407_771,\n            34_905.340_798_879_246,\n            34_910.125_033_413_315,\n            34_910.931_028_109_93,\n            34_917.918_230_012_525,\n            34_919.701_576_623_05,\n            34_922.249_971_383_76,\n            34_924.477_803_544_26,\n            34_935.112_885_339_586,\n            34_945.802_087_631_21,\n            34_949.091_650_021_39,\n            34_943.851_830_734_275,\n            34_936.624_042_117_175,\n            34_944.539_325_634_04,\n            34_946.485_513_969_9,\n            34_944.947_352_782_925,\n            34_953.467_341_620_46,\n            34_983.115_384_540_3,\n            35_015.358_772_627_75,\n            35_037.101_469_364,\n            35_049.822_007_217_175,\n            35_056.202_503_932_73,\n        ];\n\n        for i in 0..expected_upper.len() {\n            assert_relative_eq!(output_upper[i + 19], expected_upper[i], epsilon = 0.0001);\n            assert_relative_eq!(output_middle[i + 19], expected_middle[i], epsilon = 0.0001);\n            assert_relative_eq!(output_lower[i + 19], expected_lower[i], epsilon = 0.0001);\n        }\n\n        // Test incremental calculation\n        let mut prev_sma = output_sma[19];\n        let mut prev_sum = output_sum[19];\n        let mut prev_sum_sq = output_sum_sq[19];\n\n        for i in 20..45 {\n            let (upper, middle, lower, new_sma, new_sum, new_sum_sq) = bbands_inc(\n                input_price[i],\n                prev_sma,\n                prev_sum,\n                prev_sum_sq,\n                input_price[i - opt_period],\n                opt_period,\n                opt_dev_up,\n                opt_dev_down,\n            )\n            .unwrap();\n\n            assert_relative_eq!(upper, output_upper[i], epsilon = 0.0001);\n            assert_relative_eq!(middle, output_middle[i], epsilon = 0.0001);\n            assert_relative_eq!(lower, output_lower[i], epsilon = 0.0001);\n\n            prev_sma = new_sma;\n            prev_sum = new_sum;\n            prev_sum_sq = new_sum_sq;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/bop.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for Balance of Power (BOP) calculation.\n///\n/// # Description\n/// The BOP indicator has no lookback period as it is calculated point-by-point\n/// using only current period's price data.\n///\n/// # Arguments\n/// * None\n///\n/// # Returns\n/// * `Result<usize, KandError>` - Always returns 0 as no lookback is needed\n///\n/// # Errors\n/// This function does not return any errors. It always returns `Ok(0)`.\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::bop;\n/// let lookback = bop::lookback().unwrap();\n/// assert_eq!(lookback, 0);\n/// ```\npub const fn lookback() -> Result<usize, KandError> {\n    Ok(0)\n}\n\n/// Calculates the Balance of Power (BOP) indicator for a price series.\n///\n/// # Description\n/// The Balance of Power (BOP) is a momentum oscillator that measures the relative strength\n/// between buyers and sellers by comparing the closing price to the opening price and\n/// normalizing it by the trading range (high - low).\n///\n/// # Mathematical Formula\n/// ```text\n/// BOP = (Close - Open) / (High - Low)\n/// ```\n///\n/// # Calculation Steps\n/// 1. Calculate the price range (High - Low) for each period\n/// 2. Calculate the close-open difference (Close - Open) for each period\n/// 3. Divide the close-open difference by the price range\n/// 4. If price range is zero, return zero to avoid division by zero\n///\n/// # Arguments\n/// * `input_open` - Array of opening prices\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of closing prices\n/// * `output_bop` - Array to store calculated BOP values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty Ok value on success\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input arrays have different lengths\n/// * `KandError::NaNDetected` - If any input contains NaN values (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::bop;\n///\n/// let input_open = vec![10.0, 11.0, 12.0, 13.0];\n/// let input_high = vec![12.0, 13.0, 14.0, 15.0];\n/// let input_low = vec![8.0, 9.0, 10.0, 11.0];\n/// let input_close = vec![11.0, 12.0, 13.0, 14.0];\n/// let mut output_bop = vec![0.0; 4];\n///\n/// bop::bop(\n///     &input_open,\n///     &input_high,\n///     &input_low,\n///     &input_close,\n///     &mut output_bop,\n/// )\n/// .unwrap();\n/// ```\npub fn bop(\n    input_open: &[TAFloat],\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    output_bop: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_open.len();\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Length consistency check\n        if len != input_high.len()\n            || len != input_low.len()\n            || len != input_close.len()\n            || len != output_bop.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_open[i].is_nan()\n                || input_high[i].is_nan()\n                || input_low[i].is_nan()\n                || input_close[i].is_nan()\n            {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    for i in 0..len {\n        let range = input_high[i] - input_low[i];\n        if range == 0.0 {\n            output_bop[i] = 0.0;\n        } else {\n            output_bop[i] = (input_close[i] - input_open[i]) / range;\n        }\n    }\n\n    Ok(())\n}\n\n/// Calculates a single Balance of Power (BOP) value for the latest price data.\n///\n/// # Description\n/// This function provides an efficient way to calculate the BOP value incrementally\n/// for new price data, without recalculating the entire series.\n///\n/// # Mathematical Formula\n/// ```text\n/// BOP = (Close - Open) / (High - Low)\n/// ```\n///\n/// # Arguments\n/// * `input_open` - Current period's opening price\n/// * `input_high` - Current period's high price\n/// * `input_low` - Current period's low price\n/// * `input_close` - Current period's closing price\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - Calculated BOP value if successful\n///\n/// # Errors\n/// * `KandError::NaNDetected` - If any input value is NaN (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::bop::bop_inc;\n///\n/// let input_open = 10.0;\n/// let input_high = 12.0;\n/// let input_low = 8.0;\n/// let input_close = 11.0;\n///\n/// let output_bop = bop_inc(input_open, input_high, input_low, input_close).unwrap();\n/// ```\npub fn bop_inc(\n    input_open: TAFloat,\n    input_high: TAFloat,\n    input_low: TAFloat,\n    input_close: TAFloat,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_open.is_nan() || input_high.is_nan() || input_low.is_nan() || input_close.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let range = input_high - input_low;\n    if range == 0.0 {\n        Ok(0.0)\n    } else {\n        Ok((input_close - input_open) / range)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_bop_calculation() {\n        let input_open = vec![\n            35253.1, 35216.2, 35221.4, 35190.7, 35169.9, 35181.5, 35254.6, 35203.5, 35251.8,\n            35198.0, 35184.6, 35175.0, 35229.8, 35212.6, 35160.7, 35090.3, 35041.2, 34999.2,\n            35013.4, 35069.0,\n        ];\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2,\n        ];\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6,\n        ];\n\n        let mut output_bop = vec![0.0; input_open.len()];\n        bop(\n            &input_open,\n            &input_high,\n            &input_low,\n            &input_close,\n            &mut output_bop,\n        )\n        .unwrap();\n\n        let expected_values = [\n            -0.741_482_965_931_842_2,\n            0.126_829_268_292_789_4,\n            -0.551_166_965_888_796_5,\n            -0.344_425_956_738_686_9,\n            0.408_450_704_225_279_96,\n            0.877_551_020_408_115_2,\n            -0.859_038_142_620_118_4,\n            0.620_512_820_512_839_2,\n            -0.669_135_802_469_189_7,\n            -0.302_272_727_272_793_4,\n            -0.655_172_413_793_103_4,\n            0.723_320_158_102_772_1,\n            -0.314_545_454_545_507_5,\n            -0.699_460_916_442_095_5,\n            -0.531_320_754_716_937_2,\n            -0.455_473_098_330_282_9,\n            -0.429_303_278_688_471_35,\n            0.265_420_560_747_745_17,\n            0.836_090_225_563_887_9,\n            -0.707_006_369_426_742,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_bop[i], *expected, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation matches regular calculation\n        for i in 0..input_open.len() {\n            let result =\n                bop_inc(input_open[i], input_high[i], input_low[i], input_close[i]).unwrap();\n            assert_relative_eq!(result, output_bop[i], epsilon = 0.0001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/cci.rs",
    "content": "use super::{sma, typprice};\nuse crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for CCI calculation.\n///\n/// # Description\n/// The lookback period represents the minimum number of data points needed before\n/// the first valid output can be calculated. For CCI, this equals the specified\n/// period parameter minus 1.\n///\n/// # Arguments\n/// * `opt_period` - The time period used for calculations (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period on success, or error on failure\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If period is less than 2\n///\n/// # Example\n/// ```\n/// use kand::ta::ohlcv::cci;\n/// let period = 14;\n/// let lookback = cci::lookback(period).unwrap();\n/// assert_eq!(lookback, 13);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Calculates the Commodity Channel Index (CCI) for a price series.\n///\n/// # Description\n/// The CCI is a momentum-based oscillator that helps identify overbought and oversold\n/// conditions by measuring the deviation of an asset's price from its statistical mean.\n///\n/// # Mathematical Formula\n/// ```text\n/// Typical Price (TP) = (High + Low + Close) / 3\n/// Mean Deviation = Σ|TP - SMA(TP)| / n\n/// CCI = (TP - SMA(TP)) / (0.015 * Mean Deviation)\n/// ```\n/// where:\n/// - n is the period\n/// - SMA(TP) is the Simple Moving Average of Typical Price over n periods\n/// - 0.015 is a constant scaling factor\n///\n/// # Calculation Steps\n/// 1. Calculate Typical Price for each period\n/// 2. Calculate SMA of Typical Prices\n/// 3. Calculate Mean Deviation\n/// 4. Apply CCI formula using constant factor 0.015\n///\n/// # Arguments\n/// * `input_high` - High prices array\n/// * `input_low` - Low prices array\n/// * `input_close` - Close prices array\n/// * `opt_period` - The time period for calculations (must be >= 2)\n/// * `output_cci` - Buffer to store CCI values\n/// * `output_tp` - Buffer to store typical price values\n/// * `output_tp_sma` - Buffer to store SMA of typical price values\n/// * `output_mean_dev` - Buffer to store mean deviation values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty result on success, or error on failure\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input slice is empty\n/// * `KandError::LengthMismatch` - If input and output slices have different lengths\n/// * `KandError::InvalidParameter` - If period is less than 2\n/// * `KandError::InsufficientData` - If input length is less than required period\n/// * `KandError::NaNDetected` - If any input contains NaN values\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::cci;\n///\n/// let input_high = vec![24.20, 24.07, 24.04, 23.87, 23.67];\n/// let input_low = vec![23.85, 23.72, 23.64, 23.37, 23.46];\n/// let input_close = vec![23.89, 23.95, 23.67, 23.78, 23.50];\n/// let period = 3;\n/// let mut output_cci = vec![0.0; 5];\n/// let mut output_tp = vec![0.0; 5];\n/// let mut output_tp_sma = vec![0.0; 5];\n/// let mut output_mean_dev = vec![0.0; 5];\n///\n/// cci::cci(\n///     &input_high,\n///     &input_low,\n///     &input_close,\n///     period,\n///     &mut output_cci,\n///     &mut output_tp,\n///     &mut output_tp_sma,\n///     &mut output_mean_dev,\n/// )\n/// .unwrap();\n/// ```\npub fn cci(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_period: usize,\n    output_cci: &mut [TAFloat],\n    output_tp: &mut [TAFloat],\n    output_tp_sma: &mut [TAFloat],\n    output_mean_dev: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_low.len()\n            || len != input_close.len()\n            || len != output_cci.len()\n            || len != output_tp.len()\n            || len != output_tp_sma.len()\n            || len != output_mean_dev.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_high[i].is_nan() || input_low[i].is_nan() || input_close[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n    // Calculate typical prices\n    typprice::typprice(input_high, input_low, input_close, output_tp)?;\n\n    // Calculate SMA of typical prices\n    sma::sma(output_tp, opt_period, output_tp_sma)?;\n\n    // Calculate mean deviation\n    let factor = 0.015;\n    for i in lookback..len {\n        let mut mean_dev = 0.0;\n        for j in 0..opt_period {\n            mean_dev += (output_tp[i - j] - output_tp_sma[i]).abs();\n        }\n        mean_dev /= opt_period as TAFloat;\n        output_mean_dev[i] = mean_dev;\n\n        // Calculate CCI\n        output_cci[i] = if mean_dev == 0.0 {\n            0.0\n        } else {\n            (output_tp[i] - output_tp_sma[i]) / (factor * mean_dev)\n        };\n    }\n\n    // Fill all output arrays with NAN initially\n    for i in 0..lookback {\n        output_cci[i] = TAFloat::NAN;\n        output_tp[i] = TAFloat::NAN;\n        output_tp_sma[i] = TAFloat::NAN;\n        output_mean_dev[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the next CCI value using an incremental approach.\n///\n/// # Description\n/// This function provides an optimized way to calculate the next CCI value when new data\n/// arrives, without recalculating the entire series. It maintains a circular buffer of\n/// typical prices to ensure exact match with batch calculation.\n///\n/// # Mathematical Formula\n/// ```text\n/// Typical Price (TP) = (High + Low + Close) / 3\n/// Next SMA(TP) = Previous SMA(TP) + (New TP - Old TP) / n\n/// Mean Deviation = Σ|TP - SMA(TP)| / n\n/// CCI = (TP - SMA(TP)) / (0.015 * Mean Deviation)\n/// ```\n///\n/// # Calculation Steps\n/// 1. Calculate new and old typical prices\n/// 2. Update SMA using incremental formula\n/// 3. Update circular buffer and recalculate mean deviation\n/// 4. Apply CCI formula with constant factor 0.015\n///\n/// # Arguments\n/// * `prev_sma_tp` - Previous SMA value of typical prices\n/// * `prev_mean_dev` - Previous mean deviation value\n/// * `input_new_high` - New high price\n/// * `input_new_low` - New low price\n/// * `input_new_close` - New close price\n/// * `input_old_high` - Old high price to be removed\n/// * `input_old_low` - Old low price to be removed\n/// * `input_old_close` - Old close price to be removed\n/// * `opt_period` - The time period for calculations (must be >= 2)\n/// * `tp_buffer` - Circular buffer containing last `opt_period` typical prices\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The next CCI value on success, or error on failure\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If period is less than 2\n/// * `KandError::NaNDetected` - If any input contains NaN values\n///\n/// # Example\n/// ```\n/// use kand::ta::ohlcv::cci;\n///\n/// let prev_sma_tp = 100.0;\n/// let new_high = 105.0;\n/// let new_low = 95.0;\n/// let new_close = 100.0;\n/// let old_high = 102.0;\n/// let old_low = 98.0;\n/// let old_close = 100.0;\n/// let period = 14;\n/// let mut tp_buffer = vec![100.0; period];\n///\n/// let next_cci = cci::cci_inc(\n///     prev_sma_tp,\n///     new_high,\n///     new_low,\n///     new_close,\n///     old_high,\n///     old_low,\n///     old_close,\n///     period,\n///     &mut tp_buffer,\n/// )\n/// .unwrap();\n/// ```\npub fn cci_inc(\n    prev_sma_tp: TAFloat,\n    input_new_high: TAFloat,\n    input_new_low: TAFloat,\n    input_new_close: TAFloat,\n    input_old_high: TAFloat,\n    input_old_low: TAFloat,\n    input_old_close: TAFloat,\n    opt_period: usize,\n    tp_buffer: &mut Vec<TAFloat>,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if prev_sma_tp.is_nan()\n            || input_new_high.is_nan()\n            || input_new_low.is_nan()\n            || input_new_close.is_nan()\n            || input_old_high.is_nan()\n            || input_old_low.is_nan()\n            || input_old_close.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    // Calculate new and old typical prices\n    let new_tp = (input_new_high + input_new_low + input_new_close) / 3.0;\n    let old_tp = (input_old_high + input_old_low + input_old_close) / 3.0;\n\n    // Calculate new SMA of typical prices\n    let sma_tp = sma::sma_inc(prev_sma_tp, new_tp, old_tp, opt_period)?;\n\n    // Update circular buffer - remove oldest and add newest TP\n    if tp_buffer.len() == opt_period {\n        tp_buffer.remove(0);\n    }\n    tp_buffer.push(new_tp);\n\n    // Recalculate mean deviation using all points in buffer against new SMA\n    let mut mean_dev = 0.0;\n    for &tp in tp_buffer.iter() {\n        mean_dev += (tp - sma_tp).abs();\n    }\n    mean_dev /= opt_period as TAFloat;\n\n    // Calculate CCI using constant factor 0.015\n    let factor = 0.015;\n    Ok(if mean_dev.abs() <= TAFloat::EPSILON {\n        0.0\n    } else {\n        (new_tp - sma_tp) / (factor * mean_dev)\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    // Basic functionality tests\n    #[test]\n    fn test_cci_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0,\n        ];\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0,\n        ];\n        let opt_period = 14;\n        let mut output_cci = vec![0.0; input_high.len()];\n        let mut output_tp = vec![0.0; input_high.len()];\n        let mut output_tp_sma = vec![0.0; input_high.len()];\n        let mut output_mean_dev = vec![0.0; input_high.len()];\n\n        cci(\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_period,\n            &mut output_cci,\n            &mut output_tp,\n            &mut output_tp_sma,\n            &mut output_mean_dev,\n        )\n        .unwrap();\n\n        // First 13 values should be NaN\n        for i in 0..13 {\n            assert!(output_cci[i].is_nan());\n            assert!(output_tp[i].is_nan());\n            assert!(output_tp_sma[i].is_nan());\n            assert!(output_mean_dev[i].is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            -94.082_890_723_346_37,\n            -180.802_792_321_114_62,\n            -244.063_557_150_198_87,\n            -243.848_383_823_747_3,\n            -166.790_215_765_872_72,\n            -89.041_824_371_64,\n            -81.225_924_313_890_73,\n            -119.920_356_473_813_2,\n            -114.051_248_309_390_3,\n            -74.418_873_070_067_66,\n            -41.113_546_460_345_28,\n            7.295_737_949_004_944,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_cci[i + 13], *expected, epsilon = 0.0001);\n        }\n\n        // Initialize circular buffer for incremental calculation\n        let mut tp_buffer = Vec::with_capacity(opt_period);\n        for i in 0..opt_period {\n            let tp = (input_high[i] + input_low[i] + input_close[i]) / 3.0;\n            tp_buffer.push(tp);\n        }\n\n        // Calculate and verify incremental values\n        for i in opt_period..input_high.len() {\n            // Calculate incremental CCI\n            let result = cci_inc(\n                output_tp_sma[i - 1],\n                input_high[i],\n                input_low[i],\n                input_close[i],\n                input_high[i - opt_period],\n                input_low[i - opt_period],\n                input_close[i - opt_period],\n                opt_period,\n                &mut tp_buffer,\n            )\n            .unwrap();\n\n            // Compare with full calculation\n            assert_relative_eq!(result, output_cci[i], epsilon = 0.00001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/cdl_doji.rs",
    "content": "use crate::{\n    KandError, TAFloat, TAInt,\n    helper::{lower_shadow_length, real_body_length, upper_shadow_length},\n    types::Signal,\n};\n\n/// Returns the lookback period for Doji pattern detection.\n///\n/// # Description\n/// The lookback period represents the minimum number of data points needed before\n/// valid pattern detection can begin. For Doji patterns, no lookback is required\n/// since each candle is evaluated independently.\n///\n/// # Returns\n/// * `Result<usize, KandError>` - Returns 0 as no lookback is needed\n///\n/// # Errors\n/// This function does not return any errors. It always returns `Ok(0)`.\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::cdl_doji;\n/// let lookback = cdl_doji::lookback().unwrap();\n/// assert_eq!(lookback, 0);\n/// ```\npub const fn lookback() -> Result<usize, KandError> {\n    Ok(0)\n}\n\n/// Detects Doji candlestick patterns in price data.\n///\n/// # Description\n/// A Doji pattern indicates market indecision, occurring when opening and closing prices\n/// are nearly equal with upper and lower shadows of similar length.\n///\n/// # Mathematical Formula\n/// ```text\n/// Body = |Close - Open|\n/// Range = High - Low\n/// UpperShadow = High - max(Open, Close)\n/// LowerShadow = min(Open, Close) - Low\n///\n/// IsDoji = Body <= Range * BodyPercent/100\n/// ShadowDiff = min(|Upper-Lower|/Lower, |Lower-Upper|/Upper) * 100\n/// ShadowsEqual = ShadowDiff < ShadowEqualPercent\n/// ```\n///\n/// # Calculation Steps\n/// 1. Calculate real body length and total range\n/// 2. Check if body is small relative to range\n/// 3. Calculate upper and lower shadow lengths\n/// 4. Compare shadow lengths for equality\n/// 5. Generate signal if both conditions met\n///\n/// # Arguments\n/// * `input_open` - Array of opening prices\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of closing prices\n/// * `opt_body_percent` - Maximum body size as percentage of range (e.g. 5.0 for 5%)\n/// * `opt_shadow_equal_percent` - Maximum shadow length difference percentage (e.g. 100.0)\n/// * `output_signals` - Output array for pattern signals\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty result on success, or error on failure\n///\n/// # Errors\n/// * `KandError::LengthMismatch` - If input arrays have different lengths\n/// * `KandError::InvalidParameter` - If any parameter is invalid (e.g. <= 0)\n/// * `KandError::NaNDetected` - If any input contains NaN values (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ta::ohlcv::cdl_doji;\n///\n/// let input_open = vec![10.0, 10.5, 10.2];\n/// let input_high = vec![11.0, 11.2, 10.8];\n/// let input_low = vec![9.8, 10.1, 9.9];\n/// let input_close = vec![10.3, 10.4, 10.25];\n/// let mut output_signals = vec![0i64; 3];\n///\n/// cdl_doji::cdl_doji(\n///     &input_open,\n///     &input_high,\n///     &input_low,\n///     &input_close,\n///     5.0,   // opt_body_percent\n///     100.0, // opt_shadow_equal_percent\n///     &mut output_signals,\n/// )\n/// .unwrap();\n/// ```\npub fn cdl_doji(\n    input_open: &[TAFloat],\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_body_percent: TAFloat,\n    opt_shadow_equal_percent: TAFloat,\n    output_signals: &mut [TAInt],\n) -> Result<(), KandError> {\n    let len = input_open.len();\n\n    #[cfg(feature = \"check\")]\n    {\n        // Check array lengths\n        if len != input_high.len()\n            || len != input_low.len()\n            || len != input_close.len()\n            || len != output_signals.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n\n        // Check parameters\n        if opt_body_percent <= 0.0 || opt_shadow_equal_percent <= 0.0 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            if input_open[i].is_nan()\n                || input_high[i].is_nan()\n                || input_low[i].is_nan()\n                || input_close[i].is_nan()\n            {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Process each candle\n    for i in 0..len {\n        output_signals[i] = cdl_doji_inc(\n            input_open[i],\n            input_high[i],\n            input_low[i],\n            input_close[i],\n            opt_body_percent,\n            opt_shadow_equal_percent,\n        )?;\n    }\n\n    Ok(())\n}\n\n/// Processes a single candlestick to detect a Doji pattern.\n///\n/// # Description\n/// This function provides an optimized way to detect Doji patterns by analyzing\n/// individual candlesticks. It evaluates the body size relative to range and\n/// compares upper/lower shadow lengths.\n///\n/// # Mathematical Formula\n/// ```text\n/// Body = |Close - Open|\n/// Range = High - Low\n/// UpperShadow = High - max(Open, Close)\n/// LowerShadow = min(Open, Close) - Low\n///\n/// IsDoji = Body <= Range * BodyPercent/100\n/// ShadowDiff = min(|Upper-Lower|/Lower, |Lower-Upper|/Upper) * 100\n/// ShadowsEqual = ShadowDiff < ShadowEqualPercent\n/// ```\n///\n/// # Arguments\n/// * `input_open` - Opening price of the candlestick\n/// * `input_high` - High price of the candlestick\n/// * `input_low` - Low price of the candlestick\n/// * `input_close` - Closing price of the candlestick\n/// * `opt_body_percent` - Maximum body size as percentage of range\n/// * `opt_shadow_equal_percent` - Maximum allowed difference between shadow lengths\n///\n/// # Returns\n/// * `Result<TAInt, KandError>` - Signal value (Pattern for Doji, Neutral for no pattern)\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If any parameter is invalid (e.g. <= 0)\n/// * `KandError::NaNDetected` - If any input value is NaN (when `check-nan` enabled)\n/// * `KandError::ConversionError` - If numeric conversion fails\npub fn cdl_doji_inc(\n    input_open: TAFloat,\n    input_high: TAFloat,\n    input_low: TAFloat,\n    input_close: TAFloat,\n    opt_body_percent: TAFloat,\n    opt_shadow_equal_percent: TAFloat,\n) -> Result<TAInt, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Check parameters\n        if opt_body_percent <= 0.0 || opt_shadow_equal_percent <= 0.0 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_open.is_nan() || input_high.is_nan() || input_low.is_nan() || input_close.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let body = real_body_length(input_open, input_close);\n    let range = input_high - input_low;\n    let up_shadow = upper_shadow_length(input_high, input_open, input_close);\n    let dn_shadow = lower_shadow_length(input_low, input_open, input_close);\n\n    // Check for Doji pattern\n    let is_doji_body = range > 0.0 && body <= range * opt_body_percent / 100.0;\n\n    // Calculates the percentage difference between upper and lower shadows.\n    // Returns the minimum relative difference to provide a more balanced comparison.\n    //\n    // The calculation is performed in two ways:\n    // 1. (|upper - lower| / lower) * 100: difference relative to lower shadow\n    // 2. (|lower - upper| / upper) * 100: difference relative to upper shadow\n    //\n    // The minimum of these two percentages is used because:\n    // - It provides the most conservative estimate of shadow inequality\n    // - It prevents bias from choice of baseline in relative calculations\n    // - It better handles cases where shadows have significant size differences\n    //\n    // Returns 100% if either shadow length is zero, indicating maximum inequality.\n    let shadow_diff_percent = if dn_shadow > 0.0 && up_shadow > 0.0 {\n        let up_diff = (up_shadow - dn_shadow).abs() / dn_shadow * 100.0;\n        let dn_diff = (dn_shadow - up_shadow).abs() / up_shadow * 100.0;\n        up_diff.min(dn_diff)\n    } else {\n        100.0\n    };\n\n    let shadows_equal = shadow_diff_percent < opt_shadow_equal_percent;\n\n    Ok(if is_doji_body && shadows_equal {\n        Signal::Pattern.into()\n    } else {\n        Signal::Neutral.into()\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_cdl_doji() {\n        let input_open = vec![\n            98826.3, 98554.6, 98610.4, 98508.0, 98314.1, 98245.0, 98214.2, 98486.1, 98563.5,\n            98419.9, 98249.6, 98074.7, 97797.1, 97925.6, 97544.2, 97140.3, 97285.7, 97486.5,\n            97009.3, 96554.9, 96542.5, 96450.1, 96772.8, 96797.0, 96662.7, 96252.2, 96131.1,\n            96364.3, 96274.1, 96448.0, 96408.3, 95960.1, 95946.0, 96238.8, 96358.5, 96770.6,\n            96884.2, 96613.9, 96489.0, 96710.1, 96779.9, 96149.6, 96548.1, 96560.0, 96923.0,\n            96567.1, 96571.7, 96341.5, 96515.0, 96720.2, 96746.1, 96461.1, 96460.9, 96735.0,\n            96679.9, 96759.9, 97350.8, 97216.6, 97346.4, 97419.9, 97534.2, 97521.6,\n        ];\n        let input_high = vec![\n            99019.9, 98680.0, 98618.7, 98590.0, 98443.9, 98366.8, 98745.5, 98745.6, 98711.4,\n            98454.3, 98922.1, 98356.2, 98025.4, 97952.6, 97554.0, 97285.7, 97500.0, 97500.0,\n            97076.1, 96754.1, 96826.5, 96795.0, 97154.2, 96936.7, 96797.1, 96415.7, 96430.0,\n            96539.7, 96530.5, 96883.1, 96412.7, 96161.9, 96327.2, 96408.3, 96781.0, 97041.4,\n            96913.2, 96696.8, 96730.7, 96827.7, 96794.7, 96577.5, 96560.0, 96923.0, 96923.0,\n            96638.4, 96634.5, 96576.4, 96896.7, 96896.5, 96788.3, 96563.4, 96815.0, 96822.3,\n            96835.0, 97805.8, 97561.9, 97473.4, 97480.0, 97586.0, 97727.7, 97639.8,\n        ];\n        let input_low = vec![\n            98550.0, 98465.0, 98300.0, 98252.3, 98135.6, 98122.0, 98214.2, 98350.0, 98223.3,\n            98067.6, 97712.7, 97743.3, 97593.6, 97468.1, 96963.3, 96866.9, 97147.7, 96845.8,\n            96536.0, 96337.2, 96330.0, 96440.0, 96592.4, 96662.7, 96220.0, 96111.0, 95811.1,\n            96161.5, 95880.1, 96390.5, 95860.0, 95613.5, 95736.0, 96093.4, 96337.3, 96650.8,\n            96609.1, 96313.0, 96050.4, 96522.0, 96036.0, 96130.0, 96313.1, 96410.4, 96548.1,\n            96439.6, 96161.1, 96311.8, 96488.5, 96611.9, 96446.1, 96358.3, 96456.2, 96600.0,\n            96508.0, 96700.0, 97150.0, 97021.3, 97290.0, 97333.5, 97411.4, 97355.0,\n        ];\n        let input_close = vec![\n            98554.6, 98610.4, 98507.9, 98314.1, 98245.0, 98214.1, 98485.8, 98563.5, 98419.9,\n            98249.5, 98074.7, 97797.1, 97925.6, 97546.1, 97140.3, 97285.6, 97486.5, 97009.3,\n            96555.0, 96542.5, 96450.1, 96772.8, 96796.9, 96662.7, 96252.3, 96131.1, 96364.4,\n            96274.1, 96447.8, 96408.3, 95960.1, 95946.1, 96238.8, 96359.0, 96770.6, 96884.2,\n            96613.9, 96489.1, 96710.0, 96780.0, 96149.6, 96548.1, 96560.0, 96923.0, 96567.2,\n            96571.7, 96341.5, 96515.1, 96720.2, 96746.1, 96461.0, 96460.9, 96735.0, 96679.9,\n            96759.9, 97350.9, 97216.7, 97346.3, 97419.9, 97534.2, 97521.5, 97384.1,\n        ];\n\n        let opt_body_percent = 5.0;\n        let opt_shadow_equal_percent = 100.0;\n        let mut output_signals = vec![0i64; input_open.len()];\n\n        cdl_doji(\n            &input_open,\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_body_percent,\n            opt_shadow_equal_percent,\n            &mut output_signals,\n        )\n        .unwrap();\n\n        println!(\"output_signals: {output_signals:?}\");\n\n        // Verify specific doji signals\n        let doji_indices = [19, 22, 31, 45, 51, 60]; // Indices for TV BTCUSDT.P 15m 2025-02-07 00:45, 01:30, 03:45, 07:15, 08:45, 11:00\n        for &idx in &doji_indices {\n            assert_eq!(\n                output_signals[idx],\n                Signal::Pattern.into(),\n                \"Expected doji signal at index {idx}\"\n            );\n        }\n\n        // Test incremental calculation matches regular calculation\n        for i in 0..input_open.len() {\n            let signal = cdl_doji_inc(\n                input_open[i],\n                input_high[i],\n                input_low[i],\n                input_close[i],\n                opt_body_percent,\n                opt_shadow_equal_percent,\n            )\n            .unwrap();\n            assert_eq!(signal, output_signals[i], \"Mismatch at index {i}\");\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/cdl_dragonfly_doji.rs",
    "content": "use crate::{\n    KandError, TAFloat, TAInt,\n    helper::{real_body_length, upper_shadow_length},\n    types::Signal,\n};\n\n/// Returns the lookback period required for Dragonfly Doji pattern detection.\n///\n/// # Description\n/// The lookback period is the minimum number of data points needed before the indicator\n/// can produce valid output values. For Dragonfly Doji pattern, only a single candlestick\n/// is required.\n///\n/// # Returns\n/// * `Result<usize, KandError>` - Always returns `Ok(0)` as the pattern requires a single candlestick\n///\n/// # Errors\n/// This function does not return any errors. It always returns `Ok(0)`.\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_dragonfly_doji;\n/// let lookback = cdl_dragonfly_doji::lookback().unwrap();\n/// assert_eq!(lookback, 0);\n/// ```\npub const fn lookback() -> Result<usize, KandError> {\n    Ok(0)\n}\n\n/// Detects Dragonfly Doji candlestick patterns in price data.\n///\n/// # Description\n/// A Dragonfly Doji is a candlestick pattern that suggests a potential bullish reversal.\n/// The pattern is characterized by:\n/// - Opening and closing prices that are very close or equal and near the high\n/// - Little to no upper shadow (the part above the body)\n/// - Long lower shadow (the part below the body)\n///\n/// # Calculation\n/// 1. Calculate the real body length (absolute difference between open and close)\n/// 2. Calculate the total range (high - low)\n/// 3. Calculate the upper shadow length\n/// 4. Check if:\n///    - Body is small relative to range (using `opt_body_percent`)\n///    - Upper shadow is minimal (less than or equal to body length)\n///\n/// # Arguments\n/// * `input_open` - Array of opening prices for each period\n/// * `input_high` - Array of high prices for each period\n/// * `input_low` - Array of low prices for each period\n/// * `input_close` - Array of closing prices for each period\n/// * `opt_body_percent` - Maximum body size as percentage of total range (typically 5%)\n/// * `output_signals` - Output array that will contain the pattern signals:\n///   - 100: Bullish Dragonfly Doji pattern detected\n///   - 0: No pattern detected\n///\n/// # Returns\n/// * `Ok(())` - Calculation completed successfully\n///\n/// # Errors\n/// * [`KandError::LengthMismatch`] - If input arrays have different lengths\n/// * [`KandError::InvalidParameter`] - If `opt_body_percent` is less than or equal to zero\n/// * [`KandError::NaNDetected`] - If any input contains NaN values (when `check-nan` feature enabled)\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_dragonfly_doji;\n///\n/// let input_open = vec![100.0, 101.0, 102.0];\n/// let input_high = vec![102.0, 103.0, 104.0];\n/// let input_low = vec![98.0, 99.0, 100.0];\n/// let input_close = vec![101.0, 102.0, 103.0];\n/// let mut output_signals = vec![0i64; 3];\n///\n/// cdl_dragonfly_doji::cdl_dragonfly_doji(\n///     &input_open,\n///     &input_high,\n///     &input_low,\n///     &input_close,\n///     5.0,\n///     &mut output_signals,\n/// )\n/// .unwrap();\n/// ```\npub fn cdl_dragonfly_doji(\n    input_open: &[TAFloat],\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_body_percent: TAFloat,\n    output_signals: &mut [TAInt],\n) -> Result<(), KandError> {\n    let len = input_open.len();\n\n    #[cfg(feature = \"check\")]\n    {\n        // Check array lengths\n        if len != input_high.len()\n            || len != input_low.len()\n            || len != input_close.len()\n            || len != output_signals.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n\n        // Check parameters\n        if opt_body_percent <= 0.0 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            if input_open[i].is_nan()\n                || input_high[i].is_nan()\n                || input_low[i].is_nan()\n                || input_close[i].is_nan()\n            {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Process each candle\n    for i in 0..len {\n        output_signals[i] = cdl_dragonfly_doji_inc(\n            input_open[i],\n            input_high[i],\n            input_low[i],\n            input_close[i],\n            opt_body_percent,\n        )?;\n    }\n\n    Ok(())\n}\n\n/// Processes a single candlestick to detect a Dragonfly Doji pattern.\n///\n/// # Description\n/// This function analyzes an individual candlestick to determine if it forms a Dragonfly Doji pattern.\n/// It's useful for real-time analysis or when processing candlesticks one at a time.\n///\n/// # Calculation\n/// 1. Calculate real body length and total range\n/// 2. Check if body is small relative to range (using `opt_body_percent`)\n/// 3. Verify upper shadow is minimal\n/// 4. Return appropriate signal value\n///\n/// # Arguments\n/// * `input_open` - Opening price of the candlestick\n/// * `input_high` - High price of the candlestick\n/// * `input_low` - Low price of the candlestick\n/// * `input_close` - Closing price of the candlestick\n/// * `opt_body_percent` - Maximum body size as percentage of total range\n///\n/// # Returns\n/// * `Ok(TAInt)` - Signal value where:\n///   - 100: Bullish Dragonfly Doji pattern detected\n///   - 0: No pattern detected\n///\n/// # Errors\n/// * [`KandError::InvalidParameter`] - If `opt_body_percent` is less than or equal to zero\n/// * [`KandError::NaNDetected`] - If any input value is NaN (when `check-nan` feature enabled)\n/// * [`KandError::ConversionError`] - If numeric conversion fails\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_dragonfly_doji;\n///\n/// let signal = cdl_dragonfly_doji::cdl_dragonfly_doji_inc(\n///     100.0, // input_open\n///     102.0, // input_high\n///     98.0,  // input_low\n///     100.1, // input_close\n///     5.0,   // opt_body_percent\n/// )\n/// .unwrap();\n/// ```\npub fn cdl_dragonfly_doji_inc(\n    input_open: TAFloat,\n    input_high: TAFloat,\n    input_low: TAFloat,\n    input_close: TAFloat,\n    opt_body_percent: TAFloat,\n) -> Result<TAInt, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Check parameters\n        if opt_body_percent <= 0.0 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_open.is_nan() || input_high.is_nan() || input_low.is_nan() || input_close.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let body = real_body_length(input_open, input_close);\n    let range = input_high - input_low;\n    let up_shadow = upper_shadow_length(input_high, input_open, input_close);\n\n    // Check for Dragonfly Doji pattern\n    let is_doji_body = range > 0.0 && body <= range * opt_body_percent / 100.0;\n    let has_minimal_upper_shadow = up_shadow <= body;\n\n    let signal = if is_doji_body && has_minimal_upper_shadow {\n        Signal::Bullish.into()\n    } else {\n        Signal::Neutral.into()\n    };\n\n    Ok(signal)\n}\n\n#[cfg(test)]\nmod tests {\n\n    use super::*;\n\n    #[test]\n    fn test_cdl_dragonfly_doji() {\n        let input_open = vec![\n            97285.7, 97486.5, 97009.3, 96554.9, 96542.5, 96450.1, 96772.8, 96797.0, 96662.7,\n            96252.2, 96131.1, 96364.3, 96274.1, 96448.0, 96408.3, 95960.1, 95946.0, 96238.8,\n            96358.5, 96770.6, 96884.2, 96613.9, 96489.0, 96710.1, 96779.9, 96149.6, 96548.1,\n            96560.0, 96923.0, 96567.1, 96571.7, 96341.5, 96515.0, 96720.2, 96746.1, 96461.1,\n            96460.9, 96735.0, 96679.9, 96759.9, 97350.8, 97216.6, 97346.4, 97419.9, 97534.2,\n            97521.6,\n        ];\n        let input_high = vec![\n            97500.0, 97500.0, 97076.1, 96754.1, 96826.5, 96795.0, 97154.2, 96936.7, 96797.1,\n            96415.7, 96430.0, 96539.7, 96530.5, 96883.1, 96412.7, 96161.9, 96327.2, 96408.3,\n            96781.0, 97041.4, 96913.2, 96696.8, 96730.7, 96827.7, 96794.7, 96577.5, 96560.0,\n            96923.0, 96923.0, 96638.4, 96634.5, 96576.4, 96896.7, 96896.5, 96788.3, 96563.4,\n            96815.0, 96822.3, 96835.0, 97805.8, 97561.9, 97473.4, 97480.0, 97586.0, 97727.7,\n            97639.8,\n        ];\n        let input_low = vec![\n            97147.7, 96845.8, 96536.0, 96337.2, 96330.0, 96440.0, 96592.4, 96662.7, 96220.0,\n            96111.0, 95811.1, 96161.5, 95880.1, 96390.5, 95860.0, 95613.5, 95736.0, 96093.4,\n            96337.3, 96650.8, 96609.1, 96313.0, 96050.4, 96522.0, 96036.0, 96130.0, 96313.1,\n            96410.4, 96548.1, 96439.6, 96161.1, 96311.8, 96488.5, 96611.9, 96446.1, 96358.3,\n            96456.2, 96600.0, 96508.0, 96700.0, 97150.0, 97021.3, 97290.0, 97333.5, 97411.4,\n            97355.0,\n        ];\n        let input_close = vec![\n            97486.5, 97009.3, 96555.0, 96542.5, 96450.1, 96772.8, 96796.9, 96662.7, 96252.3,\n            96131.1, 96364.4, 96274.1, 96447.8, 96408.3, 95960.1, 95946.1, 96238.8, 96359.0,\n            96770.6, 96884.2, 96613.9, 96489.1, 96710.0, 96780.0, 96149.6, 96548.1, 96560.0,\n            96923.0, 96567.2, 96571.7, 96341.5, 96515.1, 96720.2, 96746.1, 96461.0, 96460.9,\n            96735.0, 96679.9, 96759.9, 97350.9, 97216.7, 97346.3, 97419.9, 97534.2, 97521.5,\n            97384.1,\n        ];\n\n        let opt_body_percent = 5.0;\n        let mut output_signals = vec![0i64; input_open.len()];\n\n        cdl_dragonfly_doji(\n            &input_open,\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_body_percent,\n            &mut output_signals,\n        )\n        .unwrap();\n\n        println!(\"output_signals: {output_signals:?}\");\n\n        // Test specific signals\n        assert_eq!(output_signals[26], Signal::Bullish.into()); // TV BTCUSDT.P 5m 2025-02-07 06:30\n\n        // Test incremental calculation matches regular calculation\n        for i in 0..18 {\n            let signal: i64 = cdl_dragonfly_doji_inc(\n                input_open[i],\n                input_high[i],\n                input_low[i],\n                input_close[i],\n                opt_body_percent,\n            )\n            .unwrap();\n            assert_eq!(signal, output_signals[i]);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/cdl_gravestone_doji.rs",
    "content": "use crate::{\n    KandError, TAFloat, TAInt,\n    helper::{lower_shadow_length, real_body_length},\n    types::Signal,\n};\n\n/// Returns the lookback period required for Gravestone Doji pattern detection.\n///\n/// # Description\n/// The lookback period represents the minimum number of historical data points needed\n/// for the indicator to generate valid signals. For Gravestone Doji pattern, only a single\n/// candlestick is required.\n///\n/// # Returns\n/// * `Result<usize, KandError>` - Returns `Ok(0)` as the Gravestone Doji pattern only requires a single candlestick\n///\n/// # Errors\n/// This function does not return any errors.\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_gravestone_doji;\n/// let lookback = cdl_gravestone_doji::lookback().unwrap();\n/// assert_eq!(lookback, 0);\n/// ```\npub const fn lookback() -> Result<usize, KandError> {\n    Ok(0)\n}\n\n/// Detects Gravestone Doji candlestick patterns in price data.\n///\n/// # Description\n/// A Gravestone Doji is a bearish reversal pattern that forms when the opening and closing prices\n/// are at or near the low of the day, with a long upper shadow and minimal lower shadow.\n///\n/// # Calculation\n/// 1. Calculate real body length = |close - open|\n/// 2. Calculate total range = high - low\n/// 3. Calculate lower shadow length = min(open,close) - low\n/// 4. Check if body is small relative to range (doji condition)\n/// 5. Check if lower shadow is minimal\n///\n/// # Arguments\n/// * `input_open` - Array of opening prices for each period\n/// * `input_high` - Array of high prices for each period\n/// * `input_low` - Array of low prices for each period\n/// * `input_close` - Array of closing prices for each period\n/// * `opt_body_percent` - Maximum body size as percentage of total range to qualify as doji (e.g. 5.0 for 5%)\n/// * `output_signals` - Output array that will contain the pattern signals:\n///   - -100: Bearish Gravestone Doji pattern detected\n///   - 0: No pattern detected\n/// # Returns\n/// * `Ok(())` - Calculation completed successfully\n///\n/// # Errors\n/// * [`KandError::LengthMismatch`] - If input arrays have different lengths\n/// * [`KandError::NaNDetected`] - If any input contains NaN values (when `check-nan` feature enabled)\n/// * [`KandError::InvalidParameter`] - If `opt_body_percent` is less than or equal to zero\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_gravestone_doji;\n///\n/// let input_open = vec![100.0, 101.0];\n/// let input_high = vec![102.0, 103.0];\n/// let input_low = vec![99.0, 98.0];\n/// let input_close = vec![99.5, 98.5];\n/// let mut output_signals = vec![0i64; 2];\n///\n/// cdl_gravestone_doji::cdl_gravestone_doji(\n///     &input_open,\n///     &input_high,\n///     &input_low,\n///     &input_close,\n///     5.0,\n///     &mut output_signals,\n/// )\n/// .unwrap();\n/// ```\npub fn cdl_gravestone_doji(\n    input_open: &[TAFloat],\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_body_percent: TAFloat,\n    output_signals: &mut [TAInt],\n) -> Result<(), KandError> {\n    let len = input_open.len();\n\n    #[cfg(feature = \"check\")]\n    {\n        // Check array lengths\n        if len != input_high.len()\n            || len != input_low.len()\n            || len != input_close.len()\n            || len != output_signals.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            if input_open[i].is_nan()\n                || input_high[i].is_nan()\n                || input_low[i].is_nan()\n                || input_close[i].is_nan()\n            {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Process each candle\n    for i in 0..len {\n        output_signals[i] = cdl_gravestone_doji_inc(\n            input_open[i],\n            input_high[i],\n            input_low[i],\n            input_close[i],\n            opt_body_percent,\n        )?;\n    }\n\n    Ok(())\n}\n\n/// Processes a single candlestick to detect a Gravestone Doji pattern.\n///\n/// # Description\n/// This function performs incremental Gravestone Doji pattern detection on individual candlesticks.\n/// A Gravestone Doji forms when the open and close prices are at or near the low, with a long\n/// upper shadow and minimal lower shadow.\n///\n/// # Calculation\n/// 1. Calculate real body = |close - open|\n/// 2. Calculate total range = high - low\n/// 3. Calculate lower shadow = min(open,close) - low\n/// 4. Check if:\n///    - Body is small relative to range (doji condition)\n///    - Lower shadow is minimal\n///\n/// # Arguments\n/// * `input_open` - Opening price of the candlestick\n/// * `input_high` - High price of the candlestick\n/// * `input_low` - Low price of the candlestick\n/// * `input_close` - Closing price of the candlestick\n/// * `opt_body_percent` - Maximum body size as percentage of total range to qualify as doji\n///\n/// # Returns\n/// * `Ok(TAInt)` - Signal value where:\n///   - -100: Bearish Gravestone Doji pattern detected\n///   - 0: No pattern detected\n///\n/// # Errors\n/// * [`KandError::InvalidParameter`] - If `opt_body_percent` is less than or equal to zero\n/// * [`KandError::NaNDetected`] - If any input value is NaN (when `check-nan` feature enabled)\n/// * [`KandError::ConversionError`] - If numeric conversion fails\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_gravestone_doji;\n///\n/// let signal = cdl_gravestone_doji::cdl_gravestone_doji_inc(\n///     100.0, // open\n///     102.0, // high\n///     99.8,  // low\n///     99.9,  // close\n///     5.0,   // body_percent\n/// )\n/// .unwrap();\n/// ```\npub fn cdl_gravestone_doji_inc(\n    input_open: TAFloat,\n    input_high: TAFloat,\n    input_low: TAFloat,\n    input_close: TAFloat,\n    opt_body_percent: TAFloat,\n) -> Result<TAInt, KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_open.is_nan() || input_high.is_nan() || input_low.is_nan() || input_close.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let body = real_body_length(input_open, input_close);\n    let range = input_high - input_low;\n    let dn_shadow = lower_shadow_length(input_low, input_open, input_close);\n\n    // Check for Gravestone Doji pattern\n    let is_doji_body = range > 0.0 && body <= range * opt_body_percent / 100.0;\n    let has_minimal_lower_shadow = dn_shadow <= body;\n\n    let output_signal = if is_doji_body && has_minimal_lower_shadow {\n        Signal::Bearish.into()\n    } else {\n        Signal::Neutral.into()\n    };\n\n    Ok(output_signal)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_cdl_gravestone_doji() {\n        let input_open = vec![\n            102_730.6, 102_233.5, 102_003.4, 102_330.6, 102_211.8, 102_994.9, 102_817.5, 102_407.9,\n            102_525.3, 103_002.3, 102_826.2, 102_499.1, 102_161.0, 102_033.9, 102_191.6, 102_358.0,\n            102_368.7, 102_354.8, 101_928.3, 101_923.5, 101_226.3,\n        ];\n        let input_high = vec![\n            102_870.3, 102_421.8, 102_374.7, 102_543.5, 103_065.9, 103_059.4, 102_899.3, 102_713.6,\n            103_089.9, 103_083.5, 102_914.6, 102_510.8, 102_204.8, 102_366.8, 102_358.1, 102_624.0,\n            102_495.0, 102_354.9, 102_115.4, 101_933.7, 101_449.1,\n        ];\n        let input_low = vec![\n            102_205.0, 101_850.0, 101_984.1, 101_921.2, 102_170.3, 102_700.0, 102_301.0, 102_308.3,\n            102_336.7, 102_733.2, 102_435.0, 102_123.9, 101_778.0, 101_929.2, 101_994.0, 102_357.9,\n            102_241.4, 101_921.8, 101_852.7, 101_195.2, 101_056.1,\n        ];\n        let input_close = vec![\n            102_233.4, 102_003.3, 102_330.6, 102_211.8, 102_994.8, 102_817.5, 102_407.8, 102_525.2,\n            103_002.3, 102_826.3, 102_499.2, 102_161.1, 102_033.8, 102_191.6, 102_358.0, 102_368.7,\n            102_354.8, 101_928.4, 101_923.6, 101_226.3, 101_260.0,\n        ];\n\n        let opt_body_percent = 5.0;\n        let mut output_signals = vec![0i64; input_open.len()];\n\n        cdl_gravestone_doji(\n            &input_open,\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_body_percent,\n            &mut output_signals,\n        )\n        .unwrap();\n\n        println!(\"output_signals: {output_signals:?}\");\n\n        // Test specific signals\n        assert_eq!(output_signals[15], Signal::Bearish.into()); // TV BTCUSDT.P 5m 2025-01-29 03:45\n\n        // Test incremental calculation matches regular calculation\n        for i in 0..18 {\n            let output_signal: i64 = cdl_gravestone_doji_inc(\n                input_open[i],\n                input_high[i],\n                input_low[i],\n                input_close[i],\n                opt_body_percent,\n            )\n            .unwrap();\n            assert_eq!(output_signal, output_signals[i]);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/cdl_hammer.rs",
    "content": "use crate::{\n    KandError, TAFloat, TAInt,\n    helper::{lower_shadow_length, period_to_k, real_body_length, upper_shadow_length},\n    types::Signal,\n};\n\n/// Returns the lookback period for Hammer pattern detection.\n///\n/// # Description\n/// Calculates the minimum number of historical data points needed to generate valid signals.\n/// For Hammer pattern, this equals `opt_period - 1` to ensure proper EMA calculation of candle body sizes.\n///\n/// # Arguments\n/// * `opt_period` - The period used for EMA calculation of candle body sizes\n///\n/// # Returns\n/// * `Ok(usize)` - The required lookback period\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if `opt_period` is less than 2\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_hammer;\n/// let lookback = cdl_hammer::lookback(14).unwrap();\n/// assert_eq!(lookback, 13);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Detects Hammer candlestick patterns in price data.\n///\n/// # Description\n/// A Hammer is a bullish reversal pattern that forms during downtrends, characterized by a small real body\n/// near the high and a long lower shadow, suggesting buyers regained control after initial selling pressure.\n///\n/// # Mathematical Formula\n/// ```text\n/// Body = |Close - Open|\n/// UpperShadow = High - max(Open, Close)\n/// LowerShadow = min(Open, Close) - Low\n/// BodyAvg[i] = BodyAvg[i-1] + k * (Body[i] - BodyAvg[i-1])\n/// where k = 2/(period + 1)\n///\n/// Conditions for Hammer:\n/// 1. Body <= BodyAvg && Body > 0\n/// 2. LowerShadow >= opt_factor * Body\n/// 3. UpperShadow <= Body\n/// 4. min(Open, Close) > (High + Low)/2\n/// ```\n///\n/// # Arguments\n/// * `input_open` - Array of opening prices\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of closing prices\n/// * `opt_period` - Period for EMA calculation of body sizes\n/// * `opt_factor` - Minimum ratio of lower shadow to body length\n/// * `output_signals` - Output array for pattern signals:\n///   - 1: Bullish Hammer detected\n///   - 0: No pattern detected\n/// * `output_body_avg` - Output array storing EMA values of candle body sizes\n///\n/// # Returns\n/// * `Ok(())` - Calculation completed successfully\n///\n/// # Errors\n/// * [`KandError::LengthMismatch`] - If input arrays have different lengths\n/// * [`KandError::InvalidParameter`] - If parameter values are invalid\n/// * [`KandError::InsufficientData`] - If input length is less than required lookback\n/// * [`KandError::NaNDetected`] - If input contains NaN values (when `check-nan` enabled)\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_hammer;\n/// let input_open = vec![10.0, 11.0, 10.5];\n/// let input_high = vec![12.0, 11.5, 11.0];\n/// let input_low = vec![9.0, 10.0, 9.5];\n/// let input_close = vec![11.0, 10.5, 10.8];\n/// let mut output = vec![0i64; 3];\n/// let mut body_avg = vec![0.0; 3];\n/// cdl_hammer::cdl_hammer(\n///     &input_open,\n///     &input_high,\n///     &input_low,\n///     &input_close,\n///     2,\n///     2.0,\n///     &mut output,\n///     &mut body_avg,\n/// )\n/// .unwrap();\n/// ```\npub fn cdl_hammer(\n    input_open: &[TAFloat],\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_period: usize,\n    opt_factor: TAFloat,\n    output_signals: &mut [TAInt],\n    output_body_avg: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_open.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_high.len()\n            || len != input_low.len()\n            || len != input_close.len()\n            || len != output_signals.len()\n            || len != output_body_avg.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n        if opt_factor <= 0.0 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            if input_open[i].is_nan()\n                || input_high[i].is_nan()\n                || input_low[i].is_nan()\n                || input_close[i].is_nan()\n            {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate initial SMA\n    let mut sum = 0.0;\n    for i in 0..opt_period {\n        sum += real_body_length(input_open[i], input_close[i]);\n    }\n    let mut body_avg = sum / opt_period as TAFloat;\n    output_body_avg[lookback] = body_avg;\n\n    // Process remaining candles\n    for i in lookback..len {\n        let (signal, new_body_avg) = cdl_hammer_inc(\n            input_open[i],\n            input_high[i],\n            input_low[i],\n            input_close[i],\n            body_avg,\n            opt_period,\n            opt_factor,\n        )?;\n        output_signals[i] = signal;\n        output_body_avg[i] = new_body_avg;\n        body_avg = new_body_avg;\n    }\n\n    // Fill initial values with -1\n    for i in 0..lookback {\n        output_signals[i] = Signal::Invalid.into();\n        output_body_avg[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Incrementally processes a single candlestick for Hammer pattern detection.\n///\n/// # Description\n/// Calculates the pattern signal and updated EMA value for a single candlestick,\n/// using the previous EMA value of body sizes for comparison.\n///\n/// # Arguments\n/// * `input_open` - Opening price of current candlestick\n/// * `input_high` - High price of current candlestick\n/// * `input_low` - Low price of current candlestick\n/// * `input_close` - Closing price of current candlestick\n/// * `prev_body_avg` - Previous EMA value of body sizes\n/// * `opt_period` - Period for EMA calculation\n/// * `opt_factor` - Minimum ratio of lower shadow to body length\n///\n/// # Returns\n/// * `Ok((TAInt, TAFloat))` - Returns tuple containing:\n///   - First element: Pattern signal (100 for bullish hammer, 0 for no pattern)\n///   - Second element: Updated EMA value of body sizes\n///\n/// # Errors\n/// * [`KandError::InvalidParameter`] - If parameters are invalid:\n///   - `opt_period` is less than 2\n///   - `opt_factor` is less than or equal to zero\n/// * [`KandError::NaNDetected`] - If any input value is NaN (when `check-nan` enabled)\n/// * [`KandError::ConversionError`] - If numeric conversion fails\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_hammer;\n/// let (signal, body_avg) = cdl_hammer::cdl_hammer_inc(\n///     10.0, // open\n///     11.0, // high\n///     9.0,  // low\n///     10.5, // close\n///     0.5,  // prev_body_avg\n///     14,   // period\n///     2.0,  // factor\n/// )\n/// .unwrap();\n/// ```\npub fn cdl_hammer_inc(\n    input_open: TAFloat,\n    input_high: TAFloat,\n    input_low: TAFloat,\n    input_close: TAFloat,\n    prev_body_avg: TAFloat,\n    opt_period: usize,\n    opt_factor: TAFloat,\n) -> Result<(TAInt, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n        if opt_factor <= 0.0 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_open.is_nan()\n            || input_high.is_nan()\n            || input_low.is_nan()\n            || input_close.is_nan()\n            || prev_body_avg.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let body = real_body_length(input_open, input_close);\n    let up_shadow = upper_shadow_length(input_high, input_open, input_close);\n    let down_shadow = lower_shadow_length(input_low, input_open, input_close);\n    let k = period_to_k(opt_period)?;\n    let body_avg = (body - prev_body_avg).mul_add(k, prev_body_avg);\n\n    // Check for Hammer pattern\n    let is_small_body = body <= body_avg && body > 0.0;\n    let has_long_lower_shadow = down_shadow >= opt_factor * body;\n    let has_minimal_upper_shadow = up_shadow <= body;\n    let body_in_upper_half =\n        TAFloat::min(input_open, input_close) > f64::midpoint(input_high, input_low);\n\n    let signal =\n        if is_small_body && has_long_lower_shadow && has_minimal_upper_shadow && body_in_upper_half\n        {\n            Signal::Bullish.into()\n        } else {\n            Signal::Neutral.into()\n        };\n\n    Ok((signal, body_avg))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_cdl_hammer() {\n        let input_open = vec![\n            97798.1, 96982.9, 97050.5, 97281.3, 97480.7, 98310.4, 98232.0, 98473.2, 98136.9,\n            97912.7, 97759.0, 97516.4, 96913.4, 96738.1, 96999.0, 97472.5, 97368.3, 97140.0,\n            97971.6, 97684.9, 96985.2, 97298.6, 97664.5, 97286.7, 97041.2, 95591.8, 96464.5,\n            95750.1, 95132.6, 94132.8, 93408.4, 94009.0, 93876.7, 93847.5, 93539.2, 94308.4,\n            94403.9, 93820.9, 94001.6, 93880.0, 93317.1, 92969.9, 92765.2, 92874.4, 93562.1,\n            93583.3, 94171.9, 93910.0, 94387.7, 93965.3, 93872.7, 93974.7, 94162.4, 94518.5,\n            95271.1, 95354.0, 95340.9, 94978.8, 95281.6, 95742.5, 95829.2, 95680.3, 95227.2,\n        ];\n        let input_high = vec![\n            97833.9, 97420.1, 97562.1, 97550.0, 98371.8, 98667.2, 98594.9, 98523.7, 98216.5,\n            97912.7, 97947.4, 97582.8, 97294.2, 97051.5, 97683.0, 97700.0, 97368.3, 97999.0,\n            97985.8, 97897.9, 97608.8, 97755.5, 97748.0, 97570.0, 97167.0, 96869.7, 96875.7,\n            95825.8, 95430.9, 94150.6, 94282.2, 94983.2, 94159.7, 94052.2, 94577.2, 94645.4,\n            94473.2, 94217.7, 94461.0, 94102.9, 93525.7, 93480.0, 93168.0, 93650.0, 93931.3,\n            94249.5, 94204.0, 94421.0, 94578.5, 94237.2, 94162.8, 94303.4, 94662.1, 95373.1,\n            95354.1, 95525.7, 95582.9, 95380.6, 95830.0, 95891.9, 95877.8, 95713.6, 95380.0,\n        ];\n        let input_low = vec![\n            96750.1, 96760.0, 96759.1, 96985.1, 97469.9, 97982.8, 98161.2, 98043.2, 97780.9,\n            97618.2, 97481.4, 96880.4, 96520.0, 96576.3, 96948.1, 97131.8, 96029.6, 97023.7,\n            97130.0, 96500.0, 96716.2, 97273.0, 97226.8, 96006.0, 95325.6, 95539.0, 95740.3,\n            95006.0, 93750.0, 91800.1, 91130.3, 93547.0, 93170.9, 93290.4, 93400.0, 94239.3,\n            93732.0, 93748.6, 93725.6, 93118.8, 92777.9, 92595.7, 92739.1, 92843.5, 93430.5,\n            93520.1, 93700.9, 93781.1, 93924.6, 93605.1, 93805.7, 93974.7, 93964.4, 94404.9,\n            94852.4, 95011.0, 94793.8, 94914.3, 95211.9, 95225.3, 95476.5, 95180.0, 95081.1,\n        ];\n        let input_close = vec![\n            96977.5, 97050.5, 97281.2, 97480.7, 98310.3, 98232.0, 98473.2, 98136.8, 97912.7,\n            97759.1, 97516.4, 96913.4, 96738.0, 96998.8, 97472.6, 97368.3, 97140.0, 97971.6,\n            97684.9, 96985.3, 97298.6, 97664.5, 97287.8, 97041.2, 95591.8, 96464.6, 95750.2,\n            95132.6, 94132.8, 93408.4, 94009.0, 93876.7, 93847.5, 93539.1, 94308.4, 94403.8,\n            93820.9, 94001.6, 93879.9, 93317.1, 92969.8, 92765.2, 92874.4, 93562.1, 93583.4,\n            94171.9, 93909.9, 94387.6, 93965.3, 93872.7, 93974.6, 94162.3, 94518.6, 95271.1,\n            95354.1, 95340.9, 94978.9, 95281.5, 95742.6, 95829.2, 95680.2, 95227.2, 95223.8,\n        ];\n\n        let opt_period = 14;\n        let opt_factor = 2.0;\n        let mut output_signals = vec![0; input_open.len()];\n        let mut output_body_avg = vec![0.0; input_open.len()];\n\n        cdl_hammer(\n            &input_open,\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_period,\n            opt_factor,\n            &mut output_signals,\n            &mut output_body_avg,\n        )\n        .unwrap();\n\n        // First 13 values should be -1\n        for i in 0..13 {\n            assert_eq!(output_signals[i], Signal::Invalid.into());\n            assert!(output_body_avg[i].is_nan());\n        }\n\n        // Test specific signals\n        assert_eq!(output_signals[16], Signal::Bullish.into()); // TV BTCUSDT.P 5m 2025-02-03 06:30\n        assert_eq!(output_signals[54], Signal::Bullish.into()); // TV BTCUSDT.P 5m 2025-02-03 16:00\n        assert_eq!(output_signals[59], Signal::Bullish.into()); // TV BTCUSDT.P 5m 2025-02-03 17:15\n\n        // Test incremental calculation matches regular calculation\n        let mut prev_body_avg = output_body_avg[13]; // First valid body average\n\n        // Test each incremental step\n        for i in 14..18 {\n            let (signal, new_body_avg) = cdl_hammer_inc(\n                input_open[i],\n                input_high[i],\n                input_low[i],\n                input_close[i],\n                prev_body_avg,\n                opt_period,\n                opt_factor,\n            )\n            .unwrap();\n            assert_eq!(signal, output_signals[i]);\n            assert_relative_eq!(new_body_avg, output_body_avg[i], epsilon = 0.00001);\n            prev_body_avg = new_body_avg;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/cdl_inverted_hammer.rs",
    "content": "use crate::{\n    KandError, TAFloat, TAInt,\n    helper::{lower_shadow_length, period_to_k, real_body_length, upper_shadow_length},\n    types::Signal,\n};\n\n/// Returns the required lookback period for Inverted Hammer pattern detection.\n///\n/// # Description\n/// Calculates the minimum number of historical data points needed to generate valid signals.\n/// For Inverted Hammer pattern with EMA of body sizes, this is `opt_period` - 1.\n///\n/// # Arguments\n/// * `opt_period` - The period used for calculating the exponential moving average (EMA) of candle body sizes.\n///   Must be >= 2.\n///\n/// # Returns\n/// * `Ok(usize)` - The required lookback period (`opt_period` - 1)\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_inverted_hammer;\n///\n/// let opt_period = 14;\n/// let lookback = cdl_inverted_hammer::lookback(opt_period).unwrap();\n/// assert_eq!(lookback, 13);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Detects Inverted Hammer candlestick patterns in price data.\n///\n/// # Description\n/// An Inverted Hammer is a bullish reversal candlestick pattern that forms during a downtrend.\n/// The pattern has a small real body near the bottom of the trading range with a long upper shadow\n/// and little to no lower shadow.\n///\n/// # Calculation\n/// For each candlestick:\n/// 1. Calculate real body length = |close - open|\n/// 2. Calculate upper shadow = high - max(open, close)\n/// 3. Calculate lower shadow = min(open, close) - low\n/// 4. Calculate EMA of body sizes using `opt_period`\n/// 5. Check pattern conditions:\n///    - Small body: body <= `body_avg` && body > 0\n///    - Long upper shadow: `upper_shadow` >= `opt_factor` * body\n///    - Minimal lower shadow: `lower_shadow` <= body\n///    - Body in lower half: max(open, close) < (high + low)/2\n///\n/// # Arguments\n/// * `input_open` - Array of opening prices\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of closing prices\n/// * `opt_period` - Period for EMA calculation of body sizes (typically 14)\n/// * `opt_factor` - Minimum ratio of upper shadow to body length (typically 2.0)\n/// * `output_signals` - Output array for pattern signals:\n///   - 1: Bullish Inverted Hammer detected\n///   - 0: No pattern detected\n/// * `output_body_avg` - Output array storing the EMA values of candle body sizes\n///\n/// # Returns\n/// * `Ok(())` - Calculation completed successfully\n///\n/// # Errors\n/// * [`KandError::LengthMismatch`] - Input arrays have different lengths\n/// * [`KandError::InvalidParameter`] - Parameter values are invalid\n/// * [`KandError::InsufficientData`] - Input length is less than required lookback\n/// * [`KandError::NaNDetected`] - Input contains NaN values (when `check-nan` enabled)\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_inverted_hammer;\n///\n/// let input_open = vec![100.0, 101.0, 102.0];\n/// let input_high = vec![105.0, 106.0, 107.0];\n/// let input_low = vec![99.0, 100.0, 101.0];\n/// let input_close = vec![101.0, 102.0, 103.0];\n/// let mut output_signals = vec![0; 3];\n/// let mut output_body_avg = vec![0.0; 3];\n///\n/// cdl_inverted_hammer::cdl_inverted_hammer(\n///     &input_open,\n///     &input_high,\n///     &input_low,\n///     &input_close,\n///     2,\n///     2.0,\n///     &mut output_signals,\n///     &mut output_body_avg,\n/// )\n/// .unwrap();\n/// ```\npub fn cdl_inverted_hammer(\n    input_open: &[TAFloat],\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_period: usize,\n    opt_factor: TAFloat,\n    output_signals: &mut [TAInt],\n    output_body_avg: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_open.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        if len != input_high.len() || len != input_low.len() || len != input_close.len() {\n            return Err(KandError::LengthMismatch);\n        }\n        if len != output_signals.len() || len != output_body_avg.len() {\n            return Err(KandError::LengthMismatch);\n        }\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n        if opt_factor <= 0.0 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            if input_open[i].is_nan()\n                || input_high[i].is_nan()\n                || input_low[i].is_nan()\n                || input_close[i].is_nan()\n            {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate initial SMA\n    let mut sum = 0.0;\n    for i in 0..opt_period {\n        sum += real_body_length(input_open[i], input_close[i]);\n    }\n    let mut body_avg = sum / opt_period as TAFloat;\n    output_body_avg[lookback] = body_avg;\n\n    // Process remaining candles\n    for i in lookback..len {\n        let (signal, new_body_avg) = cdl_inverted_hammer_inc(\n            input_open[i],\n            input_high[i],\n            input_low[i],\n            input_close[i],\n            body_avg,\n            opt_period,\n            opt_factor,\n        )?;\n        output_signals[i] = signal;\n        output_body_avg[i] = new_body_avg;\n        body_avg = new_body_avg;\n    }\n\n    // Fill initial values with -1\n    for i in 0..lookback {\n        output_signals[i] = Signal::Invalid.into();\n        output_body_avg[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Processes a single candlestick to detect an Inverted Hammer pattern.\n///\n/// # Description\n/// Calculates the pattern signal and updated EMA of body sizes for a single candlestick.\n/// This incremental version is useful for real-time processing of streaming data.\n///\n/// # Calculation\n/// 1. Calculate real body, upper shadow and lower shadow lengths\n/// 2. Update EMA of body sizes using: EMA = (body - `prev_ema`) * k + `prev_ema`\n/// 3. Check pattern conditions:\n///    - Small body: body <= `body_avg` && body > 0\n///    - Long upper shadow: `upper_shadow` >= `opt_factor` * body\n///    - Minimal lower shadow: `lower_shadow` <= body\n///    - Body in lower half: max(open, close) < (high + low)/2\n///\n/// # Arguments\n/// * `input_open` - Opening price of the candlestick\n/// * `input_high` - High price of the candlestick\n/// * `input_low` - Low price of the candlestick\n/// * `input_close` - Closing price of the candlestick\n/// * `prev_body_avg` - Previous EMA value of body sizes\n/// * `opt_period` - Period for EMA calculation\n/// * `opt_factor` - Minimum ratio of upper shadow to body length\n///\n/// # Returns\n/// * `Ok((TAInt, TAFloat))` - Tuple containing:\n///   - First element: Pattern signal (100 for bullish inverted hammer, 0 for no pattern)\n///   - Second element: Updated EMA value of body sizes\n///\n/// # Errors\n/// * [`KandError::InvalidParameter`] - If parameters are invalid:\n///   - `opt_period` is less than 2\n///   - `opt_factor` is less than or equal to zero\n/// * [`KandError::NaNDetected`] - If any input value is NaN (when `check-nan` enabled)\n/// * [`KandError::ConversionError`] - If numeric conversion fails\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_inverted_hammer;\n///\n/// let (signal, new_body_avg) = cdl_inverted_hammer::cdl_inverted_hammer_inc(\n///     100.0, // open\n///     105.0, // high\n///     99.5,  // low\n///     100.5, // close\n///     1.2,   // previous body average\n///     14,    // period\n///     2.0,   // factor\n/// )\n/// .unwrap();\n/// ```\npub fn cdl_inverted_hammer_inc(\n    input_open: TAFloat,\n    input_high: TAFloat,\n    input_low: TAFloat,\n    input_close: TAFloat,\n    prev_body_avg: TAFloat,\n    opt_period: usize,\n    opt_factor: TAFloat,\n) -> Result<(TAInt, TAFloat), KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_open.is_nan()\n            || input_high.is_nan()\n            || input_low.is_nan()\n            || input_close.is_nan()\n            || prev_body_avg.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let body = real_body_length(input_open, input_close);\n    let up_shadow = upper_shadow_length(input_high, input_open, input_close);\n    let down_shadow = lower_shadow_length(input_low, input_open, input_close);\n    let k = period_to_k(opt_period)?;\n    let body_avg = (body - prev_body_avg).mul_add(k, prev_body_avg);\n\n    // Check for Inverted Hammer pattern\n    let is_small_body = body <= body_avg && body > 0.0;\n    let has_long_upper_shadow = up_shadow >= opt_factor * body;\n    let has_minimal_lower_shadow = down_shadow <= body;\n    let body_in_lower_half =\n        TAFloat::max(input_open, input_close) < f64::midpoint(input_high, input_low);\n\n    let signal =\n        if is_small_body && has_long_upper_shadow && has_minimal_lower_shadow && body_in_lower_half\n        {\n            Signal::Bullish.into()\n        } else {\n            Signal::Neutral.into()\n        };\n\n    Ok((signal, body_avg))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_cdl_inverted_hammer() {\n        let input_open = vec![\n            96470.4, 96415.2, 96386.1, 96259.3, 96290.1, 96307.4, 96266.7, 96200.0, 96078.4,\n            96175.2, 96123.1, 96242.9, 96149.3, 96104.0, 96191.9, 96236.3, 96278.1, 96200.6,\n            96164.9, 96113.6, 96095.8, 96051.1, 96085.9, 96074.0, 96092.5, 96052.5, 96067.5,\n            96100.0, 96067.1, 96054.1, 95951.3,\n        ];\n        let input_high = vec![\n            96470.5, 96450.0, 96386.1, 96344.7, 96374.1, 96312.5, 96300.0, 96244.8, 96183.1,\n            96198.5, 96242.9, 96275.6, 96156.9, 96211.7, 96240.5, 96323.0, 96300.0, 96200.7,\n            96165.0, 96162.2, 96118.1, 96107.6, 96086.0, 96183.8, 96095.7, 96102.0, 96125.7,\n            96120.5, 96110.5, 96054.1, 96043.7,\n        ];\n        let input_low = vec![\n            96388.0, 96370.4, 96168.8, 96217.2, 96286.2, 96261.5, 96111.0, 96061.2, 96078.4,\n            96070.4, 96079.1, 96138.3, 96008.7, 96052.1, 96169.4, 96236.2, 96200.6, 96137.0,\n            96050.0, 96095.7, 96050.0, 96045.9, 96006.1, 96074.0, 96032.1, 96047.0, 96050.0,\n            96060.3, 96054.0, 95835.8, 95900.0,\n        ];\n        let input_close = vec![\n            96415.2, 96386.1, 96259.3, 96290.2, 96307.3, 96266.8, 96200.0, 96078.3, 96175.3,\n            96123.2, 96242.9, 96149.3, 96104.1, 96192.0, 96236.3, 96278.1, 96200.6, 96165.0,\n            96113.6, 96095.8, 96051.2, 96086.0, 96074.0, 96092.6, 96052.5, 96067.6, 96100.0,\n            96067.0, 96054.0, 95951.4, 95951.5,\n        ];\n\n        let opt_period = 14;\n        let opt_factor = 2.0;\n        let mut output_signals = vec![0i64; input_open.len()];\n        let mut output_body_avg = vec![0.0; input_open.len()];\n\n        cdl_inverted_hammer(\n            &input_open,\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_period,\n            opt_factor,\n            &mut output_signals,\n            &mut output_body_avg,\n        )\n        .unwrap();\n\n        // First 13 values should be -1\n        for i in 0..13 {\n            assert_eq!(output_signals[i], -1);\n            assert!(output_body_avg[i].is_nan());\n        }\n\n        println!(\"output_signals: {output_signals:?}\");\n        println!(\"output_body_avg: {output_body_avg:?}\");\n\n        // Test specific signals\n        assert_eq!(output_signals[19], Signal::Bullish.into()); // TV BTCUSDT.P 5m 2025-02-08 14:05\n        assert_eq!(output_signals[23], Signal::Bullish.into()); // TV BTCUSDT.P 5m 2025-02-08 14:25\n        assert_eq!(output_signals[25], Signal::Bullish.into()); // TV BTCUSDT.P 5m 2025-02-08 14:35\n        assert_eq!(output_signals[28], Signal::Bullish.into()); // TV BTCUSDT.P 5m 2025-02-08 14:50\n\n        // Test incremental calculation matches regular calculation\n        let mut prev_body_avg = output_body_avg[13]; // First valid body average\n\n        // Test each incremental step\n        for i in 14..18 {\n            let (signal, new_body_avg) = cdl_inverted_hammer_inc(\n                input_open[i],\n                input_high[i],\n                input_low[i],\n                input_close[i],\n                prev_body_avg,\n                opt_period,\n                opt_factor,\n            )\n            .unwrap();\n            assert_eq!(signal, output_signals[i]);\n            assert_relative_eq!(new_body_avg, output_body_avg[i], epsilon = 0.00001);\n            prev_body_avg = new_body_avg;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/cdl_long_shadow.rs",
    "content": "use crate::{\n    KandError, TAFloat, TAInt,\n    helper::{lower_shadow_length, period_to_k, real_body_length, upper_shadow_length},\n    types::Signal,\n};\n\n/// Returns the required lookback period for Long Shadow pattern detection.\n///\n/// # Description\n/// Calculates the minimum number of historical data points needed to generate the first valid signal.\n/// For Long Shadow pattern detection, this equals `opt_period - 1` to ensure proper EMA calculation\n/// of candle body sizes.\n///\n/// # Arguments\n/// * `opt_period` - The period used for EMA calculation of candle body sizes (must be >= 2)\n///\n/// # Returns\n/// * `Ok(usize)` - The required lookback period\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_long_shadow;\n///\n/// let period = 14;\n/// let lookback = cdl_long_shadow::lookback(period).unwrap();\n/// assert_eq!(lookback, 13);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Detects Long Shadow candlestick patterns in price data.\n///\n/// # Description\n/// A Long Shadow candlestick pattern indicates potential trend reversals based on the relative lengths\n/// of the candlestick's shadows and body. The pattern is characterized by a small real body and a\n/// significantly long shadow on either the upper or lower side.\n///\n/// # Mathematical Formula\n/// ```text\n/// Body = |Close - Open|\n/// UpperShadow = High - max(Open, Close)\n/// LowerShadow = min(Open, Close) - Low\n/// TotalRange = High - Low\n/// BodyAvg = EMA(Body, Period)\n///\n/// Pattern detected when:\n/// 1. Body <= BodyAvg\n/// 2. UpperShadow or LowerShadow >= ShadowFactor * TotalRange\n/// ```\n///\n/// # Arguments\n/// * `input_open` - Array of opening prices\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of closing prices\n/// * `opt_period` - Period for EMA calculation of body sizes (typically 14)\n/// * `opt_shadow_factor` - Minimum percentage of total range that shadow must be (typically 75.0)\n/// * `output_signals` - Output array for pattern signals:\n///   - 1: Bullish Long Lower Shadow\n///   - -1: Bearish Long Upper Shadow\n///   - 0: No pattern\n///   - `i64::MIN`: Insufficient data\n/// * `output_body_avg` - Output array storing the EMA values of candle body sizes\n///\n/// # Returns\n/// * `Ok(())` - Calculation completed successfully\n///\n/// # Errors\n/// * [`KandError::LengthMismatch`] - Input arrays have different lengths\n/// * [`KandError::InvalidParameter`] - Parameter values are invalid\n/// * [`KandError::InsufficientData`] - Input length is less than required lookback\n/// * [`KandError::NaNDetected`] - Input contains NaN values (when `check-nan` enabled)\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_long_shadow;\n///\n/// let open = vec![10.0, 11.0, 10.5];\n/// let high = vec![12.0, 11.5, 11.0];\n/// let low = vec![9.0, 10.5, 9.5];\n/// let close = vec![11.0, 10.5, 10.0];\n/// let mut signals = vec![0; 3];\n/// let mut body_avg = vec![0.0; 3];\n///\n/// cdl_long_shadow::cdl_long_shadow(\n///     &open,\n///     &high,\n///     &low,\n///     &close,\n///     2,\n///     75.0,\n///     &mut signals,\n///     &mut body_avg,\n/// )\n/// .unwrap();\n/// ```\npub fn cdl_long_shadow(\n    input_open: &[TAFloat],\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_period: usize,\n    opt_shadow_factor: TAFloat,\n    output_signals: &mut [TAInt],\n    output_body_avg: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_open.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Length consistency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_high.len()\n            || len != input_low.len()\n            || len != input_close.len()\n            || len != output_signals.len()\n            || len != output_body_avg.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n        if opt_shadow_factor <= 0.0 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            if input_open[i].is_nan()\n                || input_high[i].is_nan()\n                || input_low[i].is_nan()\n                || input_close[i].is_nan()\n            {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate initial SMA\n    let mut sum = 0.0;\n    for i in 0..opt_period {\n        sum += real_body_length(input_open[i], input_close[i]);\n    }\n    let mut body_avg = sum / opt_period as TAFloat;\n    output_body_avg[lookback] = body_avg;\n\n    // Process remaining candles\n    for i in lookback..len {\n        let (signal, new_body_avg) = cdl_long_shadow_inc(\n            input_open[i],\n            input_high[i],\n            input_low[i],\n            input_close[i],\n            body_avg,\n            opt_period,\n            opt_shadow_factor,\n        )?;\n        output_signals[i] = signal;\n        output_body_avg[i] = new_body_avg;\n        body_avg = new_body_avg;\n    }\n\n    // Fill initial values\n    for i in 0..lookback {\n        output_signals[i] = Signal::Invalid.into();\n        output_body_avg[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Processes a single candlestick to detect a Long Shadow pattern.\n///\n/// # Description\n/// Implements incremental calculation for Long Shadow pattern detection by analyzing\n/// a single candlestick's components and comparing them against thresholds.\n///\n/// # Calculation\n/// 1. Calculate real body, upper shadow, and lower shadow lengths\n/// 2. Calculate total range = high - low\n/// 3. Update EMA of body sizes using: EMA = (body - `prev_ema`) * k + `prev_ema`\n/// 4. Check pattern conditions:\n///    - Small body: body <= `body_avg`\n///    - Shadow threshold = `shadow_factor` * `total_range` / 100\n///    - Long upper/lower shadow: shadow >= `shadow_threshold`\n///\n/// # Arguments\n/// * `input_open` - Opening price of the candlestick\n/// * `input_high` - High price of the candlestick\n/// * `input_low` - Low price of the candlestick\n/// * `input_close` - Closing price of the candlestick\n/// * `prev_body_avg` - Previous EMA value of body sizes\n/// * `opt_period` - Period for EMA calculation\n/// * `opt_shadow_factor` - Minimum percentage of total range that shadow must be\n///\n/// # Returns\n/// * `Ok((TAInt, TAFloat))` - Tuple containing:\n///   - First element: Pattern signal where:\n///     * 100: Bullish Long Lower Shadow\n///     * -100: Bearish Long Upper Shadow\n///     * 0: No pattern detected\n///   - Second element: Updated EMA value of body sizes\n///\n/// # Errors\n/// * [`KandError::InvalidParameter`] - If parameters are invalid:\n///   - `opt_period` is less than 2\n///   - `opt_shadow_factor` is less than or equal to zero\n/// * [`KandError::NaNDetected`] - If any input value is NaN (when `check-nan` enabled)\n/// * [`KandError::ConversionError`] - If numeric conversion fails\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_long_shadow;\n///\n/// let (signal, body_avg) = cdl_long_shadow::cdl_long_shadow_inc(\n///     10.0, // open\n///     11.0, // high\n///     9.0,  // low\n///     10.5, // close\n///     0.5,  // previous body average\n///     14,   // period\n///     75.0, // shadow factor\n/// )\n/// .unwrap();\n/// ```\npub fn cdl_long_shadow_inc(\n    input_open: TAFloat,\n    input_high: TAFloat,\n    input_low: TAFloat,\n    input_close: TAFloat,\n    prev_body_avg: TAFloat,\n    opt_period: usize,\n    opt_shadow_factor: TAFloat,\n) -> Result<(TAInt, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n        if opt_shadow_factor <= 0.0 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_open.is_nan()\n            || input_high.is_nan()\n            || input_low.is_nan()\n            || input_close.is_nan()\n            || prev_body_avg.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let body = real_body_length(input_open, input_close);\n    let up_shadow = upper_shadow_length(input_high, input_open, input_close);\n    let down_shadow = lower_shadow_length(input_low, input_open, input_close);\n    let total_range = input_high - input_low;\n    let k = period_to_k(opt_period)?;\n    let body_avg = (body - prev_body_avg).mul_add(k, prev_body_avg);\n\n    // Check for Long Shadow patterns\n    let is_small_body = body <= body_avg;\n    let shadow_threshold = (opt_shadow_factor / 100.0) * total_range;\n    let has_long_upper_shadow = up_shadow >= shadow_threshold;\n    let has_long_lower_shadow = down_shadow >= shadow_threshold;\n\n    let signal = if is_small_body {\n        if has_long_upper_shadow && !has_long_lower_shadow {\n            Signal::Bearish.into()\n        } else if has_long_lower_shadow && !has_long_upper_shadow {\n            Signal::Bullish.into()\n        } else {\n            Signal::Neutral.into()\n        }\n    } else {\n        Signal::Neutral.into()\n    };\n\n    Ok((signal, body_avg))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_cdl_long_shadow() {\n        let input_open = vec![\n            96674.3, 96814.9, 96667.3, 96747.9, 96743.4, 96712.4, 96677.7, 96556.3, 96500.0,\n            96442.8, 96229.7, 96152.2, 96145.7, 96233.1, 96140.1, 95505.0, 95575.1, 95585.2,\n            95544.0, 95450.0, 95592.4, 95456.0, 95664.2, 95674.1, 95546.9,\n        ];\n        let input_high = vec![\n            96911.0, 96831.2, 96754.9, 96875.7, 96793.9, 96725.7, 96802.4, 96646.6, 96526.0,\n            96470.9, 96229.8, 96245.3, 96341.5, 96233.1, 96140.1, 95582.6, 95856.1, 95585.2,\n            95702.4, 95729.2, 95633.3, 95720.6, 95698.2, 95682.9, 95794.6,\n        ];\n        let input_low = vec![\n            96567.2, 96646.7, 96648.0, 96730.0, 96694.7, 96660.4, 96556.2, 96500.1, 96400.0,\n            96200.0, 96100.0, 96073.6, 96130.4, 96045.6, 95400.0, 95200.1, 95544.8, 95400.7,\n            95427.6, 95359.5, 95442.3, 95427.0, 95545.4, 95473.1, 95475.8,\n        ];\n        let input_close = vec![\n            96814.9, 96667.4, 96747.9, 96743.4, 96712.5, 96677.7, 96556.2, 96500.1, 96442.7,\n            96229.7, 96152.2, 96145.7, 96233.1, 96140.0, 95505.2, 95575.1, 95585.3, 95544.0,\n            95450.1, 95592.5, 95456.0, 95664.2, 95674.1, 95547.0, 95679.4,\n        ];\n\n        let opt_period = 14;\n        let opt_shadow_factor = 75.0;\n        let mut output_signals = vec![0; input_open.len()];\n        let mut output_body_avg = vec![0.0; input_open.len()];\n\n        cdl_long_shadow(\n            &input_open,\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_period,\n            opt_shadow_factor,\n            &mut output_signals,\n            &mut output_body_avg,\n        )\n        .unwrap();\n\n        // First 13 values should be i64::MIN\n        for i in 0..13 {\n            assert_eq!(output_signals[i], Signal::Invalid.into());\n            assert!(output_body_avg[i].is_nan());\n        }\n\n        println!(\"output_signals: {output_signals:?}\");\n\n        // Test specific signals\n        assert_eq!(output_signals[15], Signal::Bullish.into()); // Example bullish signal\n        assert_eq!(output_signals[16], Signal::Bearish.into()); // Example bearish signal\n        assert_eq!(output_signals[17], Signal::Bullish.into()); // Example bullish signal\n        assert_eq!(output_signals[22], Signal::Bullish.into()); // Example bullish signal\n\n        // Test incremental calculation matches regular calculation\n        let mut prev_body_avg = output_body_avg[13]; // First valid body average\n\n        // Test each incremental step\n        for i in 14..18 {\n            let (signal, new_body_avg) = cdl_long_shadow_inc(\n                input_open[i],\n                input_high[i],\n                input_low[i],\n                input_close[i],\n                prev_body_avg,\n                opt_period,\n                opt_shadow_factor,\n            )\n            .unwrap();\n            assert_eq!(signal, output_signals[i]);\n            assert_relative_eq!(new_body_avg, output_body_avg[i], epsilon = 0.00001);\n            prev_body_avg = new_body_avg;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/cdl_marubozu.rs",
    "content": "use crate::{\n    KandError, TAFloat, TAInt,\n    helper::{lower_shadow_length, period_to_k, real_body_length, upper_shadow_length},\n    types::Signal,\n};\n\n/// Calculates the lookback period for Marubozu pattern detection.\n///\n/// # Description\n/// The lookback period represents the minimum number of historical data points needed\n/// before generating the first valid signal. For Marubozu pattern, this equals\n/// `opt_period - 1` to allow for proper EMA calculation of average body sizes.\n///\n/// # Parameters\n/// * `opt_period` - The period used for calculating the exponential moving average (EMA)\n///   of candle body sizes. Must be >= 2.\n///\n/// # Returns\n/// * `Ok(usize)` - The required lookback period\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_marubozu;\n///\n/// let opt_period = 14;\n/// let lookback_period = cdl_marubozu::lookback(opt_period).unwrap();\n/// assert_eq!(lookback_period, 13);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Identifies Marubozu candlestick patterns in price data.\n///\n/// # Description\n/// A Marubozu (\"bald head\" or \"close-cropped\" in Japanese) is a candlestick pattern with\n/// a long real body and very small or no shadows/wicks. It indicates strong market momentum:\n/// - Bullish Marubozu (green/white): Open is low, close is high, showing strong buying pressure\n/// - Bearish Marubozu (red/black): Open is high, close is low, showing strong selling pressure\n///\n/// # Calculation\n/// The pattern is identified by:\n/// 1. Body length = |close - open|\n/// 2. Upper shadow = high - max(open,close)\n/// 3. Lower shadow = min(open,close) - low\n/// 4. Compare body length to EMA of previous body lengths\n/// 5. Check if both shadows are smaller than threshold (`opt_shadow_percent`% of body)\n///\n/// # Parameters\n/// * `input_open` - Array of opening prices\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of closing prices\n/// * `opt_period` - Period for EMA calculation (>= 2)\n/// * `opt_shadow_percent` - Maximum shadow size as percentage of body (e.g. 5.0)\n/// * `output_signals` - Output array for pattern signals:\n///   - 1: Bullish Marubozu (strong upward trend)\n///   - 0: No pattern\n///   - -1: Bearish Marubozu (strong downward trend)\n/// * `output_body_avg` - Output array for EMA values of body sizes\n///\n/// # Returns\n/// * `Ok(())` - Calculation successful\n///\n/// # Errors\n/// * [`KandError::InvalidData`] - Empty input arrays\n/// * [`KandError::LengthMismatch`] - Input/output arrays have different lengths\n/// * [`KandError::InvalidParameter`] - Invalid `opt_period` (<2) or `opt_shadow_percent` (<=0)\n/// * [`KandError::InsufficientData`] - Input length less than required lookback period\n/// * [`KandError::NaNDetected`] - NaN values in input (when `check-nan` enabled)\n/// * [`KandError::ConversionError`] - Numeric conversion error\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_marubozu;\n///\n/// let input_open = vec![10.0, 10.5, 10.3, 10.2];\n/// let input_high = vec![11.0, 11.2, 10.8, 10.5];\n/// let input_low = vec![9.8, 10.3, 10.1, 10.0];\n/// let input_close = vec![10.5, 11.0, 10.5, 10.1];\n/// let mut output_signals = vec![0i64; 4];\n/// let mut output_body_avg = vec![0.0; 4];\n///\n/// cdl_marubozu::cdl_marubozu(\n///     &input_open,\n///     &input_high,\n///     &input_low,\n///     &input_close,\n///     2,\n///     5.0,\n///     &mut output_signals,\n///     &mut output_body_avg,\n/// )\n/// .unwrap();\n/// ```\npub fn cdl_marubozu(\n    input_open: &[TAFloat],\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_period: usize,\n    opt_shadow_percent: TAFloat,\n    output_signals: &mut [TAInt],\n    output_body_avg: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_open.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Check data sufficiency\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Check array lengths\n        if len != input_high.len()\n            || len != input_low.len()\n            || len != input_close.len()\n            || len != output_signals.len()\n            || len != output_body_avg.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n\n        // Check opt_period\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n        if opt_shadow_percent <= 0.0 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_open[i].is_nan()\n                || input_high[i].is_nan()\n                || input_low[i].is_nan()\n                || input_close[i].is_nan()\n            {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate initial SMA\n    let mut sum = 0.0;\n    for i in 0..opt_period {\n        sum += real_body_length(input_open[i], input_close[i]);\n    }\n    let mut body_avg = sum / opt_period as TAFloat;\n    output_body_avg[lookback] = body_avg;\n\n    // Process each candle\n    for i in opt_period..len {\n        let (signal, new_body_avg) = cdl_marubozu_inc(\n            input_open[i],\n            input_high[i],\n            input_low[i],\n            input_close[i],\n            body_avg,\n            opt_period,\n            opt_shadow_percent,\n        )?;\n        output_signals[i] = signal;\n        output_body_avg[i] = new_body_avg;\n        body_avg = new_body_avg;\n    }\n\n    // Fill initial values\n    for i in 0..lookback {\n        output_signals[i] = Signal::Neutral.into();\n        output_body_avg[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Incrementally processes a single candlestick for Marubozu pattern detection.\n///\n/// # Description\n/// This function efficiently updates Marubozu calculations for new data points without\n/// reprocessing the entire dataset. It:\n/// 1. Updates the EMA of body sizes using the latest candle\n/// 2. Checks if the current candle forms a Marubozu pattern\n///\n/// # Calculation\n/// 1. Body = |close - open|\n/// 2. Upper shadow = high - max(open,close)\n/// 3. Lower shadow = min(open,close) - low\n/// 4. Shadow threshold = body * `opt_shadow_percent` / 100\n/// 5. New EMA = (Current - Prev) * K + Prev, where K = 2/(period+1)\n/// 6. Pattern identified if:\n///    - Body > current EMA\n///    - Both shadows <= shadow threshold\n///    - Direction determined by close vs open\n///\n/// # Parameters\n/// * `input_open` - Opening price of current candle\n/// * `input_high` - High price of current candle\n/// * `input_low` - Low price of current candle\n/// * `input_close` - Closing price of current candle\n/// * `prev_body_avg` - Previous EMA value of body sizes\n/// * `opt_period` - Period for EMA calculation (>= 2)\n/// * `opt_shadow_percent` - Maximum shadow size as percentage of body\n///\n/// # Returns\n/// * `Ok((TAInt, T))` - Tuple containing:\n///   - Signal value (100: Bullish, 0: None, -100: Bearish)\n///   - Updated EMA of body sizes\n///\n/// # Errors\n/// * [`KandError::InvalidParameter`] - If `opt_period` < 2 or `opt_shadow_percent` <= 0\n/// * [`KandError::NaNDetected`] - If any input contains NaN (when `check-nan` feature is enabled)\n/// * [`KandError::ConversionError`] - If numeric conversion fails\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::cdl_marubozu;\n///\n/// let (signal, new_avg) = cdl_marubozu::cdl_marubozu_inc(\n///     10.0, // open\n///     10.5, // high\n///     9.8,  // low\n///     10.4, // close\n///     10.2, // prev_body_avg\n///     14,   // period\n///     5.0,  // shadow_percent\n/// )\n/// .unwrap();\n/// ```\npub fn cdl_marubozu_inc(\n    input_open: TAFloat,\n    input_high: TAFloat,\n    input_low: TAFloat,\n    input_close: TAFloat,\n    prev_body_avg: TAFloat,\n    opt_period: usize,\n    opt_shadow_percent: TAFloat,\n) -> Result<(TAInt, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n        if opt_shadow_percent <= 0.0 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_open.is_nan()\n            || input_high.is_nan()\n            || input_low.is_nan()\n            || input_close.is_nan()\n            || prev_body_avg.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let body = real_body_length(input_open, input_close);\n    let up_shadow = upper_shadow_length(input_high, input_open, input_close);\n    let dn_shadow = lower_shadow_length(input_low, input_open, input_close);\n    let shadow_threshold = body * opt_shadow_percent / 100.0;\n\n    // Calculate new body average using EMA formula\n    let multiplier = period_to_k(opt_period)?;\n    let new_body_avg = (body - prev_body_avg).mul_add(multiplier, prev_body_avg);\n\n    // Check for Marubozu pattern\n    let signal =\n        if body > prev_body_avg && up_shadow <= shadow_threshold && dn_shadow <= shadow_threshold {\n            // Bullish if close > open, Bearish if close < open\n            if input_close > input_open {\n                Signal::Bullish.into()\n            } else {\n                Signal::Bearish.into()\n            }\n        } else {\n            Signal::Neutral.into()\n        };\n\n    Ok((signal, new_body_avg))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_cdl_marubozu() {\n        let input_open = vec![\n            96105.4, 96156.3, 96166.5, 96171.2, 96225.4, 96183.7, 96069.0, 95991.2, 96005.3,\n            95930.5, 95902.0, 95931.7, 95979.0, 95950.1, 96045.6, 96139.0, 96146.5, 96139.2,\n            96216.2, 96460.1, 96519.4, 96511.7, 96405.0, 96316.9, 96232.7, 96277.0, 96196.6,\n            96300.0, 96200.0, 96007.3, 95958.1, 95909.6, 95800.1, 95437.2, 94963.6, 94820.8,\n            94789.1, 95121.0, 94925.0, 95136.7, 95376.3, 95399.9, 95575.1, 95679.0, 95723.1,\n            95779.9, 96350.1, 95906.8, 95747.8, 95779.8, 95869.8, 95984.8, 96040.7, 96040.1,\n            96062.5, 96009.7, 96087.9, 96230.0, 96275.6, 96350.2, 96477.6, 96423.6,\n        ];\n        let input_high = vec![\n            96205.1, 96180.8, 96182.7, 96240.2, 96230.6, 96183.8, 96069.0, 96020.0, 96084.6,\n            95975.0, 95945.7, 95991.2, 95993.3, 96016.3, 96142.3, 96177.5, 96210.0, 96247.7,\n            96640.7, 96591.7, 96750.0, 96530.7, 96413.9, 96319.9, 96305.2, 96291.8, 96300.0,\n            96300.0, 96225.5, 96043.9, 95973.0, 95952.3, 95822.4, 95484.7, 95155.8, 94930.3,\n            95237.6, 95121.0, 95136.7, 95463.1, 95471.3, 95671.7, 95679.0, 95742.6, 95788.0,\n            96417.5, 96366.4, 96000.0, 95940.7, 95940.6, 96034.7, 96100.1, 96120.0, 96062.5,\n            96062.5, 96115.1, 96236.6, 96333.2, 96481.8, 96547.0, 96522.1, 96763.3,\n        ];\n        let input_low = vec![\n            96105.4, 96123.9, 96081.6, 96157.3, 96162.4, 96050.2, 95980.1, 95974.0, 95893.0,\n            95828.0, 95829.0, 95849.9, 95932.8, 95950.0, 96041.0, 96097.7, 96082.0, 96123.0,\n            96204.1, 96346.9, 96442.5, 96405.2, 96302.2, 96182.6, 96206.5, 96145.0, 96190.3,\n            96200.0, 95972.1, 95921.7, 95888.2, 95777.3, 95180.0, 94848.0, 94727.4, 94650.1,\n            94744.4, 94718.4, 94828.2, 95095.9, 95217.6, 95399.9, 95428.4, 95420.0, 95654.7,\n            95747.1, 95902.0, 95462.4, 95615.5, 95674.7, 95780.0, 95984.8, 96000.0, 95948.2,\n            95935.0, 96008.2, 96062.5, 96229.9, 96225.5, 96350.2, 96407.5, 96414.0,\n        ];\n        let input_close = vec![\n            96156.3, 96166.4, 96171.1, 96225.4, 96183.7, 96069.0, 95991.3, 96005.3, 95930.5,\n            95902.0, 95931.7, 95979.1, 95950.1, 96011.4, 96139.0, 96146.5, 96139.1, 96216.3,\n            96460.2, 96519.3, 96511.8, 96405.2, 96316.9, 96232.7, 96277.0, 96196.6, 96300.0,\n            96200.0, 96007.4, 95958.1, 95909.6, 95800.0, 95437.3, 94963.6, 94820.3, 94789.2,\n            95120.9, 94925.0, 95136.7, 95376.3, 95399.9, 95575.0, 95678.9, 95723.1, 95779.9,\n            96350.0, 95906.8, 95747.8, 95779.9, 95869.9, 95984.8, 96040.7, 96040.1, 96062.4,\n            96009.7, 96087.9, 96230.0, 96275.6, 96350.2, 96477.6, 96423.6, 96749.1,\n        ];\n\n        let opt_period = 14;\n        let opt_shadow_percent = 5.0;\n        let mut output_signals = vec![0i64; input_open.len()];\n        let mut output_body_avg = vec![0.0; input_open.len()];\n\n        cdl_marubozu(\n            &input_open,\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_period,\n            opt_shadow_percent,\n            &mut output_signals,\n            &mut output_body_avg,\n        )\n        .unwrap();\n\n        // First 13 values should be 0\n        for i in 0..13 {\n            assert_eq!(output_signals[i], 0);\n            assert!(output_body_avg[i].is_nan());\n        }\n\n        // Test specific signals\n        assert_eq!(output_signals[14], Signal::Bullish.into()); // TV BTCUSDT.P 5m 2025-02-10 14:05\n        assert_eq!(output_signals[27], Signal::Bearish.into()); // TV BTCUSDT.P 5m 2025-02-10 05:10\n        assert_eq!(output_signals[46], Signal::Bearish.into()); // TV BTCUSDT.P 5m 2025-02-10 06:45\n        assert_eq!(output_signals[61], Signal::Bullish.into()); // TV BTCUSDT.P 5m 2025-02-10 08:00\n\n        // Now test incremental calculation matches regular calculation\n        let mut prev_body_avg = output_body_avg[13]; // First valid body average\n\n        // Test each incremental step\n        for i in 14..18 {\n            let (signal, new_body_avg) = cdl_marubozu_inc(\n                input_open[i],\n                input_high[i],\n                input_low[i],\n                input_close[i],\n                prev_body_avg,\n                opt_period,\n                opt_shadow_percent,\n            )\n            .unwrap();\n            assert_eq!(signal, output_signals[i]);\n            assert_relative_eq!(new_body_avg, output_body_avg[i], epsilon = 0.00001);\n            prev_body_avg = new_body_avg;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/dema.rs",
    "content": "use crate::{TAFloat, error::KandError, helper::period_to_k};\n\n/// Returns the lookback period required for DEMA calculation.\n///\n/// # Description\n/// Calculates the minimum number of historical data points needed before generating the first valid DEMA value.\n/// For DEMA, this equals 2 * (`opt_period` - 1) due to the double exponential smoothing process.\n///\n/// # Arguments\n/// * `opt_period` - The smoothing period used for EMA calculations. Must be >= 2.\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The required lookback period if successful\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::dema;\n/// let lookback = dema::lookback(5).unwrap();\n/// assert_eq!(lookback, 8); // 2 * (5-1) = 8\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(2 * (opt_period - 1))\n}\n\n/// Calculates Double Exponential Moving Average (DEMA) for a price series.\n///\n/// # Description\n/// DEMA is designed to reduce the lag in traditional moving averages by applying double exponential smoothing.\n/// It puts more weight on recent data while maintaining smoothness and reducing noise.\n///\n/// # Mathematical Formula\n/// ```text\n/// α = 2/(period + 1)\n/// EMA1(t) = α * price(t) + (1 - α) * EMA1(t-1)\n/// EMA2(t) = α * EMA1(t) + (1 - α) * EMA2(t-1)\n/// DEMA(t) = 2 * EMA1(t) - EMA2(t)\n/// ```\n///\n/// # Calculation Steps\n/// 1. Calculate first EMA (EMA1) of the price series\n/// 2. Calculate second EMA (EMA2) using the EMA1 values\n/// 3. Calculate DEMA as: 2 * EMA1 - EMA2\n///\n/// # Arguments\n/// * `input` - Array of price values to calculate DEMA\n/// * `opt_period` - The smoothing period for EMA calculations. Must be >= 2.\n/// * `output_dema` - Array to store calculated DEMA values\n/// * `output_ema1` - Array to store first EMA values\n/// * `output_ema2` - Array to store second EMA values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty result if calculation succeeds\n///\n/// # Errors\n/// * `KandError::InvalidData` - Input array is empty\n/// * `KandError::LengthMismatch` - Output arrays don't match input length\n/// * `KandError::InvalidParameter` - Period is less than 2\n/// * `KandError::InsufficientData` - Input length <= lookback period\n/// * `KandError::NaNDetected` - Input contains NaN (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::dema;\n/// let input = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];\n/// let mut output_dema = vec![0.0; input.len()];\n/// let mut output_ema1 = vec![0.0; input.len()];\n/// let mut output_ema2 = vec![0.0; input.len()];\n///\n/// dema::dema(\n///     &input,\n///     3,\n///     &mut output_dema,\n///     &mut output_ema1,\n///     &mut output_ema2,\n/// )\n/// .unwrap();\n/// ```\npub fn dema(\n    input: &[TAFloat],\n    opt_period: usize,\n    output_dema: &mut [TAFloat],\n    output_ema1: &mut [TAFloat],\n    output_ema2: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Insufficient data check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length mismatch check\n        if len != output_dema.len() || len != output_ema1.len() || len != output_ema2.len() {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for price in input {\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    let alpha = period_to_k(opt_period)?;\n\n    // Calculate initial SMA for first EMA\n    let mut sum = input[0];\n    for price in input.iter().take(opt_period).skip(1) {\n        sum += *price;\n    }\n    let mut prev_ema1 = sum / opt_period as TAFloat;\n    output_ema1[opt_period - 1] = prev_ema1;\n\n    // Calculate first EMA series\n    for i in opt_period..len {\n        prev_ema1 = input[i].mul_add(alpha, prev_ema1 * (1.0 - alpha));\n        output_ema1[i] = prev_ema1;\n    }\n    // Initialize second EMA with SMA of first EMA\n    let mut sum = output_ema1[opt_period - 1];\n    for value in output_ema1.iter().take(opt_period * 2 - 1).skip(opt_period) {\n        sum += *value;\n    }\n    let mut prev_ema2 = sum / opt_period as TAFloat;\n    output_ema2[opt_period * 2 - 2] = prev_ema2;\n\n    // Calculate second EMA series (EMA of EMA)\n    for i in opt_period * 2 - 1..len {\n        prev_ema2 = output_ema1[i].mul_add(alpha, prev_ema2 * (1.0 - alpha));\n        output_ema2[i] = prev_ema2;\n    }\n\n    // Calculate DEMA = 2 * EMA1 - EMA2\n    for i in 0..len {\n        output_dema[i] = 2.0f64.mul_add(output_ema1[i], -output_ema2[i]);\n    }\n\n    // Fill initial values with NAN\n    for i in 0..lookback {\n        output_dema[i] = TAFloat::NAN;\n        output_ema1[i] = TAFloat::NAN;\n        output_ema2[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates a single new DEMA value incrementally using previous EMAs.\n///\n/// # Description\n/// Enables real-time DEMA calculation by computing only the latest value based on the current price\n/// and previous EMA values. This is more efficient than recalculating the entire series for streaming data.\n///\n/// # Mathematical Formula\n/// ```text\n/// α = 2/(period + 1)\n/// new_ema1 = α * price + (1 - α) * prev_ema1\n/// new_ema2 = α * new_ema1 + (1 - α) * prev_ema2\n/// dema = 2 * new_ema1 - new_ema2\n/// ```\n///\n/// # Arguments\n/// * `input_price` - Current price value\n/// * `prev_ema1` - Previous value of first EMA\n/// * `prev_ema2` - Previous value of second EMA\n/// * `opt_period` - The smoothing period. Must be >= 2.\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat), KandError>` - Tuple containing (DEMA, `new_ema1`, `new_ema2`) if successful\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - Period is less than 2\n/// * `KandError::NaNDetected` - Any input is NaN (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::dema;\n/// let (dema_value, new_ema1, new_ema2) = dema::dema_inc(\n///     10.0, // current price\n///     9.5,  // previous EMA1\n///     9.0,  // previous EMA2\n///     3,    // period\n/// )\n/// .unwrap();\n/// ```\npub fn dema_inc(\n    input_price: TAFloat,\n    prev_ema1: TAFloat,\n    prev_ema2: TAFloat,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_price.is_nan() || prev_ema1.is_nan() || prev_ema2.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let alpha = period_to_k(opt_period)?;\n\n    let new_ema1 = input_price.mul_add(alpha, prev_ema1 * (1.0 - alpha));\n    let new_ema2 = new_ema1.mul_add(alpha, prev_ema2 * (1.0 - alpha));\n    let dema = 2.0f64.mul_add(new_ema1, -new_ema2);\n\n    Ok((dema, new_ema1, new_ema2))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_dema_calculation() {\n        let input = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6,\n        ];\n        let opt_period = 5;\n        let mut output_dema = vec![0.0; input.len()];\n        let mut output_ema1 = vec![0.0; input.len()];\n        let mut output_ema2 = vec![0.0; input.len()];\n\n        dema(\n            &input,\n            opt_period,\n            &mut output_dema,\n            &mut output_ema1,\n            &mut output_ema2,\n        )\n        .unwrap();\n\n        // First 8 values should be NaN (lookback = 2 * (period - 1) = 8)\n        for value in output_dema.iter().take(8) {\n            assert!(value.is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            35_218.829_037_037_04,\n            35_200.555_187_928_67,\n            35_185.338_456_332_87,\n            35_207.882_302_697_76,\n            35_210.681_534_115_734,\n            35_183.349_910_955_31,\n            35_129.574_755_000_09,\n            35_074.033_046_242_2,\n            35_022.421_948_322_895,\n            35_004.747_910_545_1,\n            35_028.743_014_805_514,\n            35_019.213_837_276_19,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_dema[i + 8], *expected, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation matches regular calculation\n        let mut prev_ema1 = output_ema1[9];\n        let mut prev_ema2 = output_ema2[9];\n\n        // Test each incremental step\n        for i in 10..15 {\n            let (dema_val, new_ema1, new_ema2) =\n                dema_inc(input[i], prev_ema1, prev_ema2, opt_period).unwrap();\n\n            assert_relative_eq!(dema_val, output_dema[i], epsilon = 0.0001);\n            assert_relative_eq!(new_ema1, output_ema1[i], epsilon = 0.0001);\n            assert_relative_eq!(new_ema2, output_ema2[i], epsilon = 0.0001);\n\n            prev_ema1 = new_ema1;\n            prev_ema2 = new_ema2;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/dx.rs",
    "content": "use super::{minus_di, plus_di};\nuse crate::{KandError, TAFloat};\n\n/// Calculate the lookback period required for DX calculation\n///\n/// # Description\n/// Returns the number of data points needed before the first valid DX value can be calculated.\n///\n/// # Arguments\n/// * `opt_period` - The period used for DX calculation (typically 14)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period if successful, or error if parameters are invalid\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::dx;\n///\n/// let period = 14;\n/// let lookback = dx::lookback(period).unwrap();\n/// assert_eq!(lookback, 14);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period)\n}\n\n/// Calculate Directional Movement Index (DX) for the entire input array\n///\n/// # Description\n/// The DX indicator measures the strength of a trend by comparing positive and negative directional movements.\n/// It is calculated using +DI and -DI values to determine the relative strength of the trend.\n///\n/// # Formula\n/// ```text\n/// DX = 100 * |+DI - -DI| / (+DI + -DI)\n/// ```\n///\n/// # Calculation Steps\n/// 1. Calculate +DI and -DI for the given period\n/// 2. Calculate DX using the formula above\n/// 3. First `opt_period` values are set to NaN\n///\n/// # Arguments\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of closing prices\n/// * `opt_period` - Period for DX calculation (typically 14)\n/// * `output_dx` - Output array to store DX values\n/// * `output_smoothed_plus_dm` - Output array for smoothed +DM values\n/// * `output_smoothed_minus_dm` - Output array for smoothed -DM values\n/// * `output_smoothed_tr` - Output array for smoothed TR values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok if calculation succeeds, Err otherwise\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input/output arrays have different lengths\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n/// * `KandError::InsufficientData` - If input length <= lookback period\n/// * `KandError::NaNDetected` - If any input value is NaN (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::dx;\n///\n/// let input_high = vec![24.20, 24.07, 24.04, 23.87, 23.67, 23.59];\n/// let input_low = vec![23.85, 23.72, 23.64, 23.37, 23.46, 23.18];\n/// let input_close = vec![23.89, 23.95, 23.67, 23.78, 23.50, 23.32];\n/// let opt_period = 3;\n/// let mut output_dx = vec![0.0; 6];\n/// let mut output_smoothed_plus_dm = vec![0.0; 6];\n/// let mut output_smoothed_minus_dm = vec![0.0; 6];\n/// let mut output_smoothed_tr = vec![0.0; 6];\n///\n/// dx::dx(\n///     &input_high,\n///     &input_low,\n///     &input_close,\n///     opt_period,\n///     &mut output_dx,\n///     &mut output_smoothed_plus_dm,\n///     &mut output_smoothed_minus_dm,\n///     &mut output_smoothed_tr,\n/// )\n/// .unwrap();\n/// ```\npub fn dx(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_period: usize,\n    output_dx: &mut [TAFloat],\n    output_smoothed_plus_dm: &mut [TAFloat],\n    output_smoothed_minus_dm: &mut [TAFloat],\n    output_smoothed_tr: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_low.len()\n            || len != input_close.len()\n            || len != output_dx.len()\n            || len != output_smoothed_plus_dm.len()\n            || len != output_smoothed_minus_dm.len()\n            || len != output_smoothed_tr.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_high[i].is_nan() || input_low[i].is_nan() || input_close[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    let mut plus_di_values = vec![0.0; len];\n    let mut minus_di_values = vec![0.0; len];\n\n    // Calculate +DI and -DI\n    plus_di::plus_di(\n        input_high,\n        input_low,\n        input_close,\n        opt_period,\n        &mut plus_di_values,\n        output_smoothed_plus_dm,\n        output_smoothed_tr,\n    )?;\n    minus_di::minus_di(\n        input_high,\n        input_low,\n        input_close,\n        opt_period,\n        &mut minus_di_values,\n        output_smoothed_minus_dm,\n        output_smoothed_tr,\n    )?;\n\n    // Calculate DX\n    for i in lookback..len {\n        let plus_di = plus_di_values[i];\n        let minus_di = minus_di_values[i];\n        output_dx[i] = 100.0 * (plus_di - minus_di).abs() / (plus_di + minus_di);\n    }\n\n    // Fill initial values with NAN\n    for item in output_dx.iter_mut().take(lookback) {\n        *item = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculate the latest DX value incrementally\n///\n/// # Description\n/// Calculates only the most recent DX value using previous smoothed values.\n/// This is optimized for real-time calculations where only the latest value is needed.\n///\n/// # Formula\n/// See the formula section in the [`dx`] function documentation.\n///\n/// # Arguments\n/// * `input_high` - Current high price\n/// * `input_low` - Current low price\n/// * `prev_high` - Previous period's high price\n/// * `prev_low` - Previous period's low price\n/// * `prev_close` - Previous period's close price\n/// * `prev_smoothed_plus_dm` - Previous smoothed +DM value\n/// * `prev_smoothed_minus_dm` - Previous smoothed -DM value\n/// * `prev_smoothed_tr` - Previous smoothed TR value\n/// * `opt_period` - Period for DX calculation (typically 14)\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat, TAFloat), KandError>` - Tuple containing:\n///   - Latest DX value\n///   - New smoothed +DM\n///   - New smoothed -DM\n///   - New smoothed TR\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n/// * `KandError::NaNDetected` - If any input value is NaN (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::dx;\n///\n/// let input_high = 24.20;\n/// let input_low = 23.85;\n/// let prev_high = 24.07;\n/// let prev_low = 23.72;\n/// let prev_close = 23.95;\n/// let prev_smoothed_plus_dm = 0.5;\n/// let prev_smoothed_minus_dm = 0.3;\n/// let prev_smoothed_tr = 1.2;\n/// let opt_period = 14;\n///\n/// let (output_dx, output_smoothed_plus_dm, output_smoothed_minus_dm, output_smoothed_tr) =\n///     dx::dx_inc(\n///         input_high,\n///         input_low,\n///         prev_high,\n///         prev_low,\n///         prev_close,\n///         prev_smoothed_plus_dm,\n///         prev_smoothed_minus_dm,\n///         prev_smoothed_tr,\n///         opt_period,\n///     )\n///     .unwrap();\n/// ```\npub fn dx_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    prev_close: TAFloat,\n    prev_smoothed_plus_dm: TAFloat,\n    prev_smoothed_minus_dm: TAFloat,\n    prev_smoothed_tr: TAFloat,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        // DX requires at least 2 periods:\n        // - One for initial DM and TR calculations (needs previous prices)\n        // - One for the current period\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_high.is_nan()\n            || input_low.is_nan()\n            || prev_high.is_nan()\n            || prev_low.is_nan()\n            || prev_close.is_nan()\n            || prev_smoothed_plus_dm.is_nan()\n            || prev_smoothed_minus_dm.is_nan()\n            || prev_smoothed_tr.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let (plus_di, output_smoothed_plus_dm, output_smoothed_tr) = plus_di::plus_di_inc(\n        input_high,\n        input_low,\n        prev_high,\n        prev_low,\n        prev_close,\n        prev_smoothed_plus_dm,\n        prev_smoothed_tr,\n        opt_period,\n    )?;\n\n    let (minus_di, output_smoothed_minus_dm, _) = minus_di::minus_di_inc(\n        input_high,\n        input_low,\n        prev_high,\n        prev_low,\n        prev_close,\n        prev_smoothed_minus_dm,\n        prev_smoothed_tr,\n        opt_period,\n    )?;\n\n    let output_dx = 100.0 * (plus_di - minus_di).abs() / (plus_di + minus_di);\n    Ok((\n        output_dx,\n        output_smoothed_plus_dm,\n        output_smoothed_minus_dm,\n        output_smoothed_tr,\n    ))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    // Basic functionality tests\n    #[test]\n    fn test_dx_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3,\n        ];\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0,\n        ];\n        let opt_period = 14;\n        let mut output_dx = vec![0.0; 19];\n        let mut output_smoothed_plus_dm = vec![0.0; 19];\n        let mut output_smoothed_minus_dm = vec![0.0; 19];\n        let mut output_smoothed_tr = vec![0.0; 19];\n\n        dx(\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_period,\n            &mut output_dx,\n            &mut output_smoothed_plus_dm,\n            &mut output_smoothed_minus_dm,\n            &mut output_smoothed_tr,\n        )\n        .unwrap();\n\n        // First opt_period values should be NaN\n        for value in output_dx.iter().take(opt_period) {\n            assert!(value.is_nan());\n        }\n\n        // Test specific values\n        let expected_values = [\n            20.217_627_856_366_71,\n            32.157_235_756_576_65,\n            43.177_552_482_915_67,\n            43.177_552_482_915_65,\n            23.711_947_860_846_085,\n        ];\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_dx[i + 14], *expected, epsilon = 0.00001);\n        }\n\n        // Calculate and verify incremental values\n        for i in opt_period + 1..input_high.len() {\n            let (result, new_smoothed_plus_dm, new_smoothed_minus_dm, new_smoothed_tr) = dx_inc(\n                input_high[i],\n                input_low[i],\n                input_high[i - 1],\n                input_low[i - 1],\n                input_close[i - 1],\n                output_smoothed_plus_dm[i - 1],\n                output_smoothed_minus_dm[i - 1],\n                output_smoothed_tr[i - 1],\n                opt_period,\n            )\n            .unwrap();\n\n            // Compare with full calculation\n            assert_relative_eq!(result, output_dx[i], epsilon = 0.00001);\n            assert_relative_eq!(\n                new_smoothed_plus_dm,\n                output_smoothed_plus_dm[i],\n                epsilon = 0.00001\n            );\n            assert_relative_eq!(\n                new_smoothed_minus_dm,\n                output_smoothed_minus_dm[i],\n                epsilon = 0.00001\n            );\n            assert_relative_eq!(new_smoothed_tr, output_smoothed_tr[i], epsilon = 0.00001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/ecl.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for Expanded Camarilla Levels (ECL) calculation.\n///\n/// # Description\n/// The lookback period represents the minimum number of historical data points needed\n/// before generating the first valid output. For ECL, this equals 1 since calculation\n/// requires the previous period's data.\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period (1)\n///\n/// # Errors\n/// This function does not return any errors.\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::ecl;\n/// let lookback = ecl::lookback().unwrap();\n/// assert_eq!(lookback, 1);\n/// ```\npub const fn lookback() -> Result<usize, KandError> {\n    Ok(1)\n}\n\n/// Calculates Expanded Camarilla Levels (ECL) for price data.\n///\n/// # Description\n/// ECL provides support and resistance levels based on the previous period's high, low and close prices.\n/// The levels are calculated using various ratios of the previous period's range.\n///\n/// # Mathematical Formula\n/// ```text\n/// Range = High[t-1] - Low[t-1]\n/// H5 = (High[t-1]/Low[t-1]) * Close[t-1]\n/// H4 = Close[t-1] + Range * 1.1/2\n/// H3 = Close[t-1] + Range * 1.1/4\n/// H2 = Close[t-1] + Range * 1.1/6\n/// H1 = Close[t-1] + Range * 1.1/12\n/// L1 = Close[t-1] - Range * 1.1/12\n/// L2 = Close[t-1] - Range * 1.1/6\n/// L3 = Close[t-1] - Range * 1.1/4\n/// L4 = Close[t-1] - Range * 1.1/2\n/// L5 = Close[t-1] - (H5 - Close[t-1])\n/// ```\n///\n/// # Arguments\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of closing prices\n/// * `output_h5` - Output array for H5 resistance levels\n/// * `output_h4` - Output array for H4 resistance levels\n/// * `output_h3` - Output array for H3 resistance levels\n/// * `output_h2` - Output array for H2 resistance levels\n/// * `output_h1` - Output array for H1 resistance levels\n/// * `output_l1` - Output array for L1 support levels\n/// * `output_l2` - Output array for L2 support levels\n/// * `output_l3` - Output array for L3 support levels\n/// * `output_l4` - Output array for L4 support levels\n/// * `output_l5` - Output array for L5 support levels\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok(()) if calculation succeeds\n///\n/// # Errors\n/// * `KandError::InvalidData` - Input array is empty or too short\n/// * `KandError::LengthMismatch` - Input/output arrays have different lengths\n/// * `KandError::InsufficientData` - Input length <= lookback period\n/// * `KandError::NaNDetected` - Input contains NaN (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::ecl;\n/// let high = vec![24.20, 24.07, 24.04];\n/// let low = vec![23.85, 23.72, 23.64];\n/// let close = vec![23.89, 23.95, 23.67];\n/// let mut h5 = vec![0.0; 3];\n/// let mut h4 = vec![0.0; 3];\n/// let mut h3 = vec![0.0; 3];\n/// let mut h2 = vec![0.0; 3];\n/// let mut h1 = vec![0.0; 3];\n/// let mut l1 = vec![0.0; 3];\n/// let mut l2 = vec![0.0; 3];\n/// let mut l3 = vec![0.0; 3];\n/// let mut l4 = vec![0.0; 3];\n/// let mut l5 = vec![0.0; 3];\n///\n/// ecl::ecl(\n///     &high, &low, &close, &mut h5, &mut h4, &mut h3, &mut h2, &mut h1, &mut l1, &mut l2,\n///     &mut l3, &mut l4, &mut l5,\n/// )\n/// .unwrap();\n/// ```\n#[allow(clippy::similar_names)]\npub fn ecl(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    output_h5: &mut [TAFloat],\n    output_h4: &mut [TAFloat],\n    output_h3: &mut [TAFloat],\n    output_h2: &mut [TAFloat],\n    output_h1: &mut [TAFloat],\n    output_l1: &mut [TAFloat],\n    output_l2: &mut [TAFloat],\n    output_l3: &mut [TAFloat],\n    output_l4: &mut [TAFloat],\n    output_l5: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback()?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        // ECL requires at least 2 periods:\n        // - One for initial range calculation (needs previous prices)\n        // - One for the current period\n        if len < 2 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Length consistency check\n        if len != input_low.len()\n            || len != input_close.len()\n            || len != output_h5.len()\n            || len != output_h4.len()\n            || len != output_h3.len()\n            || len != output_h2.len()\n            || len != output_h1.len()\n            || len != output_l1.len()\n            || len != output_l2.len()\n            || len != output_l3.len()\n            || len != output_l4.len()\n            || len != output_l5.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n\n        // Data sufficiency check\n        if len <= lookback + 1 {\n            return Err(KandError::InsufficientData);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_high[i].is_nan() || input_low[i].is_nan() || input_close[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    let opt_factor = 1.1;\n\n    for i in lookback..len {\n        let range = input_high[i - 1] - input_low[i - 1];\n        let h5_val = (input_high[i - 1] / input_low[i - 1]) * input_close[i - 1];\n\n        output_h5[i] = h5_val;\n        output_h4[i] = input_close[i - 1] + range * opt_factor / 2.0;\n        output_h3[i] = input_close[i - 1] + range * opt_factor / 4.0;\n        output_h2[i] = input_close[i - 1] + range * opt_factor / 6.0;\n        output_h1[i] = input_close[i - 1] + range * opt_factor / 12.0;\n        output_l1[i] = input_close[i - 1] - range * opt_factor / 12.0;\n        output_l2[i] = input_close[i - 1] - range * opt_factor / 6.0;\n        output_l3[i] = input_close[i - 1] - range * opt_factor / 4.0;\n        output_l4[i] = input_close[i - 1] - range * opt_factor / 2.0;\n        output_l5[i] = input_close[i - 1] - (h5_val - input_close[i - 1]);\n    }\n\n    // Fill initial values with NAN\n    for i in 0..lookback {\n        output_h5[i] = TAFloat::NAN;\n        output_h4[i] = TAFloat::NAN;\n        output_h3[i] = TAFloat::NAN;\n        output_h2[i] = TAFloat::NAN;\n        output_h1[i] = TAFloat::NAN;\n        output_l1[i] = TAFloat::NAN;\n        output_l2[i] = TAFloat::NAN;\n        output_l3[i] = TAFloat::NAN;\n        output_l4[i] = TAFloat::NAN;\n        output_l5[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Incrementally calculates Expanded Camarilla Levels (ECL) for a single period.\n///\n/// # Description\n/// Provides an efficient way to calculate ECL values for new data without reprocessing\n/// the entire dataset. Uses only the previous period's prices to generate new levels.\n///\n/// # Mathematical Formula\n/// ```text\n/// Range = prev_high - prev_low\n/// H5 = (prev_high/prev_low) * prev_close\n/// H4 = prev_close + Range * 1.1/2\n/// H3 = prev_close + Range * 1.1/4\n/// H2 = prev_close + Range * 1.1/6\n/// H1 = prev_close + Range * 1.1/12\n/// L1 = prev_close - Range * 1.1/12\n/// L2 = prev_close - Range * 1.1/6\n/// L3 = prev_close - Range * 1.1/4\n/// L4 = prev_close - Range * 1.1/2\n/// L5 = prev_close - (H5 - prev_close)\n/// ```\n///\n/// # Arguments\n/// * `prev_high` - Previous period's high price\n/// * `prev_low` - Previous period's low price\n/// * `prev_close` - Previous period's close price\n///\n/// # Returns\n/// * `Result<(TAFloat,TAFloat,TAFloat,TAFloat,TAFloat,TAFloat,TAFloat,TAFloat,TAFloat,TAFloat), KandError>` - Tuple containing (H5,H4,H3,H2,H1,L1,L2,L3,L4,L5)\n///\n/// # Errors\n/// * `KandError::NaNDetected` - Input contains NaN (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::ecl;\n/// let (h5, h4, h3, h2, h1, l1, l2, l3, l4, l5) = ecl::ecl_inc(\n///     24.20, // prev_high\n///     23.85, // prev_low\n///     23.89, // prev_close\n/// )\n/// .unwrap();\n/// ```\n#[allow(clippy::similar_names)]\npub fn ecl_inc(\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    prev_close: TAFloat,\n) -> Result<\n    (\n        TAFloat,\n        TAFloat,\n        TAFloat,\n        TAFloat,\n        TAFloat,\n        TAFloat,\n        TAFloat,\n        TAFloat,\n        TAFloat,\n        TAFloat,\n    ),\n    KandError,\n> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if prev_high.is_nan() || prev_low.is_nan() || prev_close.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let opt_factor = 1.1;\n    let range = prev_high - prev_low;\n    let h5_val = (prev_high / prev_low) * prev_close;\n\n    let h4 = prev_close + range * opt_factor / 2.0;\n    let h3 = prev_close + range * opt_factor / 4.0;\n    let h2 = prev_close + range * opt_factor / 6.0;\n    let h1 = prev_close + range * opt_factor / 12.0;\n    let l1 = prev_close - range * opt_factor / 12.0;\n    let l2 = prev_close - range * opt_factor / 6.0;\n    let l3 = prev_close - range * opt_factor / 4.0;\n    let l4 = prev_close - range * opt_factor / 2.0;\n    let l5 = prev_close - (h5_val - prev_close);\n\n    Ok((h5_val, h4, h3, h2, h1, l1, l2, l3, l4, l5))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    #[allow(clippy::similar_names)]\n    fn test_ecl_calculation() {\n        let input_high = vec![24.20, 24.07, 24.04, 23.87, 23.67];\n        let input_low = vec![23.85, 23.72, 23.64, 23.37, 23.46];\n        let input_close = vec![23.89, 23.95, 23.67, 23.78, 23.50];\n        let mut output_h5 = vec![0.0; 5];\n        let mut output_h4 = vec![0.0; 5];\n        let mut output_h3 = vec![0.0; 5];\n        let mut output_h2 = vec![0.0; 5];\n        let mut output_h1 = vec![0.0; 5];\n        let mut output_l1 = vec![0.0; 5];\n        let mut output_l2 = vec![0.0; 5];\n        let mut output_l3 = vec![0.0; 5];\n        let mut output_l4 = vec![0.0; 5];\n        let mut output_l5 = vec![0.0; 5];\n\n        ecl(\n            &input_high,\n            &input_low,\n            &input_close,\n            &mut output_h5,\n            &mut output_h4,\n            &mut output_h3,\n            &mut output_h2,\n            &mut output_h1,\n            &mut output_l1,\n            &mut output_l2,\n            &mut output_l3,\n            &mut output_l4,\n            &mut output_l5,\n        )\n        .unwrap();\n\n        // First value should be NaN\n        let outputs = [\n            &output_h5, &output_h4, &output_h3, &output_h2, &output_h1, &output_l1, &output_l2,\n            &output_l3, &output_l4, &output_l5,\n        ];\n        for output in outputs {\n            assert!(output[0].is_nan());\n        }\n\n        // Verify remaining values are calculated\n        let outputs = [\n            &output_h5, &output_h4, &output_h3, &output_h2, &output_h1, &output_l1, &output_l2,\n            &output_l3, &output_l4, &output_l5,\n        ];\n        for i in 1..5 {\n            for output in &outputs {\n                assert!(output[i].is_finite());\n            }\n        }\n\n        // Test incremental calculation matches\n        let i = input_high.len() - 1;\n        let (h5_inc, h4_inc, h3_inc, h2_inc, h1_inc, l1_inc, l2_inc, l3_inc, l4_inc, l5_inc) =\n            ecl_inc(input_high[i - 1], input_low[i - 1], input_close[i - 1]).unwrap();\n\n        assert_relative_eq!(h5_inc, output_h5[i], epsilon = TAFloat::EPSILON);\n        assert_relative_eq!(h4_inc, output_h4[i], epsilon = TAFloat::EPSILON);\n        assert_relative_eq!(h3_inc, output_h3[i], epsilon = TAFloat::EPSILON);\n        assert_relative_eq!(h2_inc, output_h2[i], epsilon = TAFloat::EPSILON);\n        assert_relative_eq!(h1_inc, output_h1[i], epsilon = TAFloat::EPSILON);\n        assert_relative_eq!(l1_inc, output_l1[i], epsilon = TAFloat::EPSILON);\n        assert_relative_eq!(l2_inc, output_l2[i], epsilon = TAFloat::EPSILON);\n        assert_relative_eq!(l3_inc, output_l3[i], epsilon = TAFloat::EPSILON);\n        assert_relative_eq!(l4_inc, output_l4[i], epsilon = TAFloat::EPSILON);\n        assert_relative_eq!(l5_inc, output_l5[i], epsilon = TAFloat::EPSILON);\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/ema.rs",
    "content": "use crate::{KandError, TAFloat, helper::period_to_k};\n\n/// Returns the lookback period required for EMA calculation.\n///\n/// # Description\n/// Calculates the minimum number of historical data points needed before generating the first valid EMA value.\n/// For EMA, this equals period - 1 since the first value requires a complete period for SMA calculation.\n///\n/// # Arguments\n/// * `opt_period` - The time period for EMA calculation. Must be >= 2.\n///\n/// # Returnss\n/// * `Result<usize, KandError>` - The lookback period on success, or error on failure.\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2.\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::ema;\n/// let period = 14;\n/// let lookback = ema::lookback(period).unwrap();\n/// assert_eq!(lookback, 13); // lookback is period - 1\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Calculates Exponential Moving Average (EMA) for a price series.\n///\n/// # Description\n/// EMA is a type of moving average that places a greater weight and significance on the most recent data points.\n/// The weighting given to each data point decreases exponentially with time.\n///\n/// # Mathematical Formula\n/// ```text\n/// Initial EMA = SMA(first n prices)\n/// EMA = Price * k + EMA(previous) * (1 - k)\n/// where:\n/// k = smoothing factor (default is 2/(period+1))\n/// ```\n///\n/// # Calculation Steps\n/// 1. Calculate Simple Moving Average (SMA) for initial period\n/// 2. Apply EMA formula using smoothing factor k for remaining periods\n/// 3. Fill initial values before lookback period with NaN\n///\n/// # Arguments\n/// * `input_prices` - Array of price values to calculate EMA\n/// * `opt_period` - The time period for EMA calculation (must be >= 2)\n/// * `opt_k` - Optional custom smoothing factor. If None, uses 2/(period+1)\n/// * `output_ema` - Array to store calculated EMA values. Must match input length\n///\n/// # Returns\n/// * `Result<(), KandError>` - Unit on success, or error on failure\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input array is empty\n/// * `KandError::LengthMismatch` - If output length doesn't match input\n/// * `KandError::InvalidParameter` - If period < 2\n/// * `KandError::InsufficientData` - If input length < period\n/// * `KandError::NaNDetected` - If any input price is NaN (with \"`check-nan`\")\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::ema;\n/// let prices = vec![10.0, 11.0, 12.0, 13.0, 14.0];\n/// let period = 3;\n/// let mut ema_values = vec![0.0; prices.len()];\n///\n/// // Calculate EMA with default smoothing\n/// ema::ema(&prices, period, None, &mut ema_values).unwrap();\n/// ```\npub fn ema(\n    input_prices: &[TAFloat],\n    opt_period: usize,\n    opt_k: Option<TAFloat>,\n    output_ema: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_prices.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if output_ema.len() != len {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for price in input_prices {\n            // NaN check\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate initial SMA\n    let mut sum = input_prices[0];\n    for value in input_prices.iter().take(opt_period).skip(1) {\n        sum += *value;\n    }\n    let mut prev_ma = sum / (opt_period as TAFloat);\n    output_ema[lookback] = prev_ma;\n\n    // Get multiplier - either custom or default\n    let multiplier = match opt_k {\n        Some(k) => k,\n        None => period_to_k(opt_period)?,\n    };\n\n    // Calculate EMA\n    for i in opt_period..len {\n        prev_ma = (input_prices[i] - prev_ma).mul_add(multiplier, prev_ma);\n        output_ema[i] = prev_ma;\n    }\n\n    // Fill initial values with NAN\n    for value in output_ema.iter_mut().take(lookback) {\n        *value = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates a single EMA value incrementally using the previous EMA.\n///\n/// # Description\n/// Provides an efficient way to update EMA calculations when new data arrives, without reprocessing\n/// the entire dataset. Uses the previous EMA value and current price to compute the new EMA.\n///\n/// # Mathematical Formula\n/// ```text\n/// EMA = Price * k + EMA(previous) * (1 - k)\n/// where:\n/// k = smoothing factor (default is 2/(period+1))\n/// ```\n///\n/// # Arguments\n/// * `input_price` - The current period's price value\n/// * `prev_ema` - The previous period's EMA value\n/// * `opt_period` - The time period for EMA calculation (must be >= 2)\n/// * `opt_k` - Optional custom smoothing factor. If None, uses 2/(period+1)\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The new EMA value on success, or error on failure\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If period < 2\n/// * `KandError::NaNDetected` - If price or previous EMA is NaN (with \"`check-nan`\")\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::ema;\n/// let current_price = 15.0;\n/// let prev_ema = 14.5;\n/// let period = 14;\n///\n/// // Calculate next EMA with default smoothing\n/// let new_ema = ema::ema_inc(current_price, prev_ema, period, None).unwrap();\n/// ```\npub fn ema_inc(\n    input_price: TAFloat,\n    prev_ema: TAFloat,\n    opt_period: usize,\n    opt_k: Option<TAFloat>,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_price.is_nan() || prev_ema.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let multiplier = match opt_k {\n        Some(k) => k,\n        None => period_to_k(opt_period)?,\n    };\n    Ok((input_price - prev_ema).mul_add(multiplier, prev_ema))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    // Basic functionality tests\n    #[test]\n    fn test_ema_calculation() {\n        let input_prices = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0, 35114.5, 35097.2,\n            35092.0, 35073.2, 35139.3, 35092.0, 35126.7, 35106.3, 35124.8, 35170.1, 35215.3,\n        ];\n        let opt_period = 14;\n        let mut output_ema = vec![0.0; input_prices.len()];\n\n        ema(&input_prices, opt_period, None, &mut output_ema).unwrap();\n\n        // First 13 values should be NaN\n        for value in output_ema.iter().take(13) {\n            assert!(value.is_nan());\n        }\n\n        // Test first valid value\n        let expected_values = [\n            35_203.535_714_285_72,\n            35_188.437_619_047_625,\n            35_168.805_936_507_94,\n            35_146.205_144_973_545,\n            35_128.497_792_310_41,\n            35_120.564_753_335_69,\n            35_107.769_452_890_934,\n            35_085.333_525_838_81,\n            35_067.635_722_393_636,\n            35_058.617_626_074_48,\n            35_056.375_275_931_22,\n            35_059.525_239_140_39,\n            35_066.855_207_255_01,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_ema[i + 13], *expected, epsilon = 0.00001);\n        }\n\n        let opt_period = 14;\n        let mut output_ema = vec![0.0; input_prices.len()];\n\n        ema(&input_prices, opt_period, None, &mut output_ema).unwrap();\n\n        // Now test incremental calculation matches regular calculation\n        let mut prev_ema = output_ema[13]; // First valid EMA value\n\n        // Test each incremental step\n        for i in 14..18 {\n            let result = ema_inc(input_prices[i], prev_ema, opt_period, None).unwrap();\n            assert_relative_eq!(result, output_ema[i], epsilon = 0.00001);\n            prev_ema = result;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/ha.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for Heikin-Ashi calculation\n///\n/// # Description\n/// Calculates the minimum number of data points needed before the first valid Heikin-Ashi value can be computed.\n/// The lookback period is 1 since we need the previous candle to calculate the first value.\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period (always 1)\n///\n/// # Errors\n/// This function does not return any errors.\npub const fn lookback() -> Result<usize, KandError> {\n    Ok(1)\n}\n\n/// Calculates Heikin-Ashi candlestick values from OHLC price data\n///\n/// # Description\n/// Heikin-Ashi is a Japanese candlestick charting technique that uses modified formulas\n/// for open, high, low and close values to filter out market noise and better identify trends.\n/// The resulting candles provide a clearer view of price action compared to traditional candlesticks.\n///\n/// # Mathematical Formula\n/// ```text\n/// HA_Close = (Open + High + Low + Close) / 4\n/// HA_Open = (Previous HA_Open + Previous HA_Close) / 2\n/// HA_High = max(High, HA_Open, HA_Close)\n/// HA_Low = min(Low, HA_Open, HA_Close)\n/// ```\n///\n/// # Parameters\n/// * `input_open` - Array of opening prices (slice of type `TAFloat`)\n/// * `input_high` - Array of high prices (slice of type `TAFloat`)\n/// * `input_low` - Array of low prices (slice of type `TAFloat`)\n/// * `input_close` - Array of closing prices (slice of type `TAFloat`)\n/// * `output_open` - Array to store calculated HA open values (mutable slice of type `TAFloat`)\n/// * `output_high` - Array to store calculated HA high values (mutable slice of type `TAFloat`)\n/// * `output_low` - Array to store calculated HA low values (mutable slice of type `TAFloat`)\n/// * `output_close` - Array to store calculated HA close values (mutable slice of type `TAFloat`)\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok(()) if calculation succeeds\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input and output arrays have different lengths\n/// * `KandError::NaNDetected` - If input contains NaN values (with \"`check-nan`\")\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::ha;\n///\n/// let input_open = vec![10.0, 10.5, 11.2];\n/// let input_high = vec![11.0, 11.5, 11.8];\n/// let input_low = vec![9.5, 10.2, 10.8];\n/// let input_close = vec![10.8, 11.3, 11.5];\n///\n/// let mut output_open = vec![0.0; 3];\n/// let mut output_high = vec![0.0; 3];\n/// let mut output_low = vec![0.0; 3];\n/// let mut output_close = vec![0.0; 3];\n///\n/// ha::ha(\n///     &input_open,\n///     &input_high,\n///     &input_low,\n///     &input_close,\n///     &mut output_open,\n///     &mut output_high,\n///     &mut output_low,\n///     &mut output_close,\n/// )\n/// .unwrap();\n/// ```\npub fn ha(\n    input_open: &[TAFloat],\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    output_open: &mut [TAFloat],\n    output_high: &mut [TAFloat],\n    output_low: &mut [TAFloat],\n    output_close: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_open.len();\n    let lookback = lookback()?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Length consistency check\n        if len != input_high.len()\n            || len != input_low.len()\n            || len != input_close.len()\n            || len != output_open.len()\n            || len != output_high.len()\n            || len != output_low.len()\n            || len != output_close.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        for i in 0..len {\n            if input_open[i].is_nan()\n                || input_high[i].is_nan()\n                || input_low[i].is_nan()\n                || input_close[i].is_nan()\n            {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate first candle\n    output_close[0] = (input_open[0] + input_high[0] + input_low[0] + input_close[0]) / 4.0;\n    output_open[0] = f64::midpoint(input_open[0], input_close[0]);\n    output_high[0] = input_high[0];\n    output_low[0] = input_low[0];\n\n    // Calculate remaining candles\n    for i in lookback..len {\n        let (o, h, l, c) = ha_inc(\n            input_open[i],\n            input_high[i],\n            input_low[i],\n            input_close[i],\n            output_open[i - 1],\n            output_close[i - 1],\n        )?;\n        output_open[i] = o;\n        output_high[i] = h;\n        output_low[i] = l;\n        output_close[i] = c;\n    }\n\n    Ok(())\n}\n\n/// Calculates a single Heikin-Ashi candle incrementally for streaming data\n///\n/// # Description\n/// Provides an optimized way to calculate the latest Heikin-Ashi candle when new data arrives,\n/// without recalculating the entire series. This is particularly useful for real-time\n/// data processing and streaming applications.\n///\n/// # Mathematical Formula\n/// ```text\n/// HA_Close = (Open + High + Low + Close) / 4\n/// HA_Open = (Previous HA_Open + Previous HA_Close) / 2\n/// HA_High = max(High, HA_Open, HA_Close)\n/// HA_Low = min(Low, HA_Open, HA_Close)\n/// ```\n///\n/// # Parameters\n/// * `curr_open` - Current candle's open price (type `TAFloat`)\n/// * `curr_high` - Current candle's high price (type `TAFloat`)\n/// * `curr_low` - Current candle's low price (type `TAFloat`)\n/// * `curr_close` - Current candle's close price (type `TAFloat`)\n/// * `prev_ha_open` - Previous Heikin-Ashi candle's open price (type `TAFloat`)\n/// * `prev_ha_close` - Previous Heikin-Ashi candle's close price (type `TAFloat`)\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat, TAFloat), KandError>` - Tuple of (`HA_Open`, `HA_High`, `HA_Low`, `HA_Close`) if successful\n///\n/// # Errors\n/// * `KandError::NaNDetected` - If any input is NaN (with \"`check-nan`\")\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::ha::ha_inc;\n///\n/// let (ha_open, ha_high, ha_low, ha_close) = ha_inc(\n///     11.2,    // Current open\n///     11.8,    // Current high\n///     10.8,    // Current low\n///     11.5,    // Current close\n///     10.3625, // Previous HA open\n///     10.875,  // Previous HA close\n/// )\n/// .unwrap();\n/// ```\npub fn ha_inc(\n    curr_open: TAFloat,\n    curr_high: TAFloat,\n    curr_low: TAFloat,\n    curr_close: TAFloat,\n    prev_ha_open: TAFloat,\n    prev_ha_close: TAFloat,\n) -> Result<(TAFloat, TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if curr_open.is_nan()\n            || curr_high.is_nan()\n            || curr_low.is_nan()\n            || curr_close.is_nan()\n            || prev_ha_open.is_nan()\n            || prev_ha_close.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let ha_close = (curr_open + curr_high + curr_low + curr_close) / 4.0;\n    let ha_open = f64::midpoint(prev_ha_open, prev_ha_close);\n    let ha_high = curr_high.max(ha_open).max(ha_close);\n    let ha_low = curr_low.min(ha_open).min(ha_close);\n\n    Ok((ha_open, ha_high, ha_low, ha_close))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_ha_calculation() {\n        let input_open = vec![10.0, 10.5, 11.2, 10.8, 11.5];\n        let input_high = vec![11.0, 11.5, 11.8, 11.3, 12.0];\n        let input_low = vec![9.5, 10.2, 10.8, 10.5, 11.3];\n        let input_close = vec![10.8, 11.3, 11.5, 11.0, 11.8];\n\n        let mut output_open = vec![0.0; 5];\n        let mut output_high = vec![0.0; 5];\n        let mut output_low = vec![0.0; 5];\n        let mut output_close = vec![0.0; 5];\n\n        ha(\n            &input_open,\n            &input_high,\n            &input_low,\n            &input_close,\n            &mut output_open,\n            &mut output_high,\n            &mut output_low,\n            &mut output_close,\n        )\n        .unwrap();\n\n        // Test first candle\n        assert_relative_eq!(output_open[0], 10.4, epsilon = 0.0001);\n        assert_relative_eq!(output_high[0], 11.0, epsilon = 0.0001);\n        assert_relative_eq!(output_low[0], 9.5, epsilon = 0.0001);\n        assert_relative_eq!(output_close[0], 10.325, epsilon = 0.0001);\n\n        // Test subsequent candles\n        assert_relative_eq!(output_open[1], 10.3625, epsilon = 0.0001);\n        assert_relative_eq!(output_close[1], 10.875, epsilon = 0.0001);\n        assert_relative_eq!(output_high[1], 11.5, epsilon = 0.0001);\n        assert_relative_eq!(output_low[1], 10.2, epsilon = 0.0001);\n\n        // Test incremental calculation matches regular calculation\n        for i in 1..5 {\n            let (ha_open, ha_high, ha_low, ha_close) = ha_inc(\n                input_open[i],\n                input_high[i],\n                input_low[i],\n                input_close[i],\n                output_open[i - 1],\n                output_close[i - 1],\n            )\n            .unwrap();\n\n            assert_relative_eq!(ha_open, output_open[i], epsilon = 0.0001);\n            assert_relative_eq!(ha_high, output_high[i], epsilon = 0.0001);\n            assert_relative_eq!(ha_low, output_low[i], epsilon = 0.0001);\n            assert_relative_eq!(ha_close, output_close[i], epsilon = 0.0001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/macd.rs",
    "content": "use super::ema;\nuse crate::{KandError, TAFloat};\n\n/// Calculate the lookback period required for MACD calculation\n///\n/// Returns the minimum number of data points needed before the first valid MACD output can be generated.\n///\n/// # Arguments\n/// * `opt_fast_period` - Fast EMA period, must be > 0 and < `slow_period`\n/// * `opt_slow_period` - Slow EMA period, must be > 0 and > `fast_period`\n/// * `opt_signal_period` - Signal line period, must be > 0\n///\n/// # Returns\n/// * `Result<usize, KandError>` - Lookback period if successful\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If any period is 0 or `fast_period` >= `slow_period`\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::macd;\n/// let lookback = macd::lookback(12, 26, 9).unwrap();\n/// assert_eq!(lookback, 33); // 25 (slow EMA) + 8 (signal)\n/// ```\npub fn lookback(\n    opt_fast_period: usize,\n    opt_slow_period: usize,\n    opt_signal_period: usize,\n) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_fast_period < 2 || opt_slow_period < 2 || opt_signal_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n\n        if opt_fast_period >= opt_slow_period {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    let slow_lookback = ema::lookback(opt_slow_period)?;\n    let signal_lookback = ema::lookback(opt_signal_period)?;\n    Ok(slow_lookback + signal_lookback)\n}\n\n/// Calculate Moving Average Convergence Divergence (MACD) for a price series\n///\n/// MACD is a trend-following momentum indicator that shows the relationship between two moving averages.\n/// It consists of the MACD line (difference between fast and slow EMAs), signal line (EMA of MACD line),\n/// and histogram (difference between MACD and signal lines).\n///\n/// # Mathematical Formula\n/// ```text\n/// Fast EMA = EMA(price, fast_period)\n/// Slow EMA = EMA(price, slow_period)\n/// MACD Line = Fast EMA - Slow EMA\n/// Signal Line = EMA(MACD Line, signal_period)\n/// Histogram = MACD Line - Signal Line\n/// ```\n///\n/// # Calculation Steps\n/// 1. Calculate fast EMA of price using `fast_period`\n/// 2. Calculate slow EMA of price using `slow_period`\n/// 3. Calculate MACD line as difference between fast and slow EMAs\n/// 4. Calculate signal line as EMA of MACD line\n/// 5. Calculate histogram as difference between MACD and signal lines\n///\n/// # Arguments\n/// * `input_price` - Array of price values\n/// * `opt_fast_period` - Fast EMA period (typically 12)\n/// * `opt_slow_period` - Slow EMA period (typically 26)\n/// * `opt_signal_period` - Signal line period (typically 9)\n/// * `output_macd_line` - Output buffer for MACD line values\n/// * `output_signal_line` - Output buffer for signal line values\n/// * `output_histogram` - Output buffer for histogram values\n/// * `output_fast_ema` - Output buffer for fast EMA values\n/// * `output_slow_ema` - Output buffer for slow EMA values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty Ok if successful\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input array is empty\n/// * `KandError::LengthMismatch` - If input/output arrays have different lengths\n/// * `KandError::InvalidParameter` - If any period is 0 or `fast_period` >= `slow_period`\n/// * `KandError::InsufficientData` - If input length < required lookback period\n/// * `KandError::NaNDetected` - If any input value is NaN (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::macd;\n///\n/// let prices = vec![10.0, 12.0, 15.0, 11.0, 9.0, 10.0, 12.0];\n/// let mut macd_line = vec![0.0; prices.len()];\n/// let mut signal_line = vec![0.0; prices.len()];\n/// let mut histogram = vec![0.0; prices.len()];\n/// let mut fast_ema = vec![0.0; prices.len()];\n/// let mut slow_ema = vec![0.0; prices.len()];\n///\n/// macd::macd(\n///     &prices,\n///     2, // fast period\n///     3, // slow period\n///     4, // signal period\n///     &mut macd_line,\n///     &mut signal_line,\n///     &mut histogram,\n///     &mut fast_ema,\n///     &mut slow_ema,\n/// )\n/// .unwrap();\n/// ```\npub fn macd(\n    input_price: &[TAFloat],\n    opt_fast_period: usize,\n    opt_slow_period: usize,\n    opt_signal_period: usize,\n    output_macd_line: &mut [TAFloat],\n    output_signal_line: &mut [TAFloat],\n    output_histogram: &mut [TAFloat],\n    output_fast_ema: &mut [TAFloat],\n    output_slow_ema: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_price.len();\n    let lookback = lookback(opt_fast_period, opt_slow_period, opt_signal_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != output_macd_line.len()\n            || len != output_signal_line.len()\n            || len != output_histogram.len()\n            || len != output_fast_ema.len()\n            || len != output_slow_ema.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n\n        // Check if remaining data after slow period is sufficient for signal calculation\n        if len.saturating_sub(opt_slow_period) < opt_signal_period {\n            return Err(KandError::InsufficientData);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for price in input_price {\n            // NaN check\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    ema::ema(input_price, opt_fast_period, None, output_fast_ema)?;\n    ema::ema(input_price, opt_slow_period, None, output_slow_ema)?;\n\n    // Calculate MACD line\n    for i in 0..len {\n        output_macd_line[i] = output_fast_ema[i] - output_slow_ema[i];\n    }\n\n    // Calculate signal line using non-NaN MACD values\n    ema::ema(\n        &output_macd_line[opt_slow_period - 1..],\n        opt_signal_period,\n        None,\n        &mut output_signal_line[opt_slow_period - 1..],\n    )?;\n\n    // Calculate histogram\n    for i in lookback..len {\n        output_histogram[i] = output_macd_line[i] - output_signal_line[i];\n    }\n\n    // Fill initial values with NAN\n    for i in 0..lookback {\n        output_macd_line[i] = TAFloat::NAN;\n        output_signal_line[i] = TAFloat::NAN;\n        output_histogram[i] = TAFloat::NAN;\n        output_fast_ema[i] = TAFloat::NAN;\n        output_slow_ema[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculate latest MACD values incrementally from previous state\n///\n/// This function provides an efficient way to calculate MACD for streaming data by using\n/// previous EMA values instead of recalculating the entire series.\n///\n/// # Mathematical Formula\n/// ```text\n/// Fast EMA = EMA(price, fast_period, prev_fast_ema)\n/// Slow EMA = EMA(price, slow_period, prev_slow_ema)\n/// MACD = Fast EMA - Slow EMA\n/// Signal = EMA(MACD, signal_period, prev_signal)\n/// Histogram = MACD - Signal\n/// ```\n///\n/// # Arguments\n/// * `input_price` - Current price value\n/// * `prev_fast_ema` - Previous fast EMA value\n/// * `prev_slow_ema` - Previous slow EMA value\n/// * `prev_signal` - Previous signal line value\n/// * `opt_fast_period` - Fast EMA period (typically 12)\n/// * `opt_slow_period` - Slow EMA period (typically 26)\n/// * `opt_signal_period` - Signal line period (typically 9)\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat), KandError>` - Tuple of (MACD, Signal, Histogram) if successful\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If any period is 0 or `fast_period` >= `slow_period`\n/// * `KandError::NaNDetected` - If any input value is NaN (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::macd;\n///\n/// let (macd, signal, hist) = macd::macd_inc(\n///     100.0, // current price\n///     95.0,  // previous fast EMA\n///     98.0,  // previous slow EMA\n///     -2.5,  // previous signal\n///     12,    // fast period\n///     26,    // slow period\n///     9,     // signal period\n/// )\n/// .unwrap();\n/// ```\npub fn macd_inc(\n    input_price: TAFloat,\n    prev_fast_ema: TAFloat,\n    prev_slow_ema: TAFloat,\n    prev_signal: TAFloat,\n    opt_fast_period: usize,\n    opt_slow_period: usize,\n    opt_signal_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_fast_period < 2 || opt_slow_period < 2 || opt_signal_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n        if opt_fast_period >= opt_slow_period {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_price.is_nan()\n            || prev_fast_ema.is_nan()\n            || prev_slow_ema.is_nan()\n            || prev_signal.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let fast_ema = ema::ema_inc(input_price, prev_fast_ema, opt_fast_period, None)?;\n    let slow_ema = ema::ema_inc(input_price, prev_slow_ema, opt_slow_period, None)?;\n    let macd = fast_ema - slow_ema;\n    let signal = ema::ema_inc(macd, prev_signal, opt_signal_period, None)?;\n    let histogram = macd - signal;\n\n    Ok((macd, signal, histogram))\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/medprice.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Calculates the lookback period required for MEDPRICE calculation.\n///\n/// # Description\n/// Returns the number of data points needed for calculating the Median Price indicator.\n/// Since MEDPRICE only requires current high and low prices, the lookback period is 0.\n///\n/// # Returns\n/// * `Result<usize, KandError>` - Returns 0 on success\n///\n/// # Errors\n/// No errors are returned by this function.\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::medprice;\n/// let lookback = medprice::lookback().unwrap();\n/// assert_eq!(lookback, 0);\n/// ```\npub const fn lookback() -> Result<usize, KandError> {\n    Ok(0)\n}\n\n/// Calculates Median Price (MEDPRICE) for a price series.\n///\n/// # Description\n/// The Median Price is a technical analysis indicator that represents the middle point between\n/// high and low prices for each period. It helps identify the overall price level and can be\n/// used as a basic trend indicator.\n///\n/// # Mathematical Formula\n/// ```text\n/// MEDPRICE = (High + Low) / 2\n/// ```\n///\n/// # Calculation Principles\n/// 1. For each period, take the high and low prices\n/// 2. Sum the high and low prices\n/// 3. Divide the sum by 2 to get the median price\n///\n/// # Arguments\n/// * `input_high` - Array of high prices for each period\n/// * `input_low` - Array of low prices for each period\n/// * `output_medprice` - Array to store calculated median price values. Must be same length as inputs.\n///\n/// # Returns\n/// * `Result<(), KandError>` - Returns Ok(()) on success\n///\n/// # Errors\n/// * `KandError::InvalidData` - Input arrays are empty\n/// * `KandError::LengthMismatch` - Input arrays have different lengths\n/// * `KandError::NaNDetected` - Input contains NaN values (when \"`check-nan`\" feature enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::medprice;\n///\n/// let high = vec![10.0, 11.0, 12.0];\n/// let low = vec![8.0, 9.0, 10.0];\n/// let mut output = vec![0.0; 3];\n///\n/// medprice::medprice(&high, &low, &mut output).unwrap();\n/// assert_eq!(output, vec![9.0, 10.0, 11.0]);\n/// ```\npub fn medprice(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    output_medprice: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Length consistency check\n        if len != input_low.len() || len != output_medprice.len() {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            if input_high[i].is_nan() || input_low[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    for i in 0..len {\n        output_medprice[i] = f64::midpoint(input_high[i], input_low[i]);\n    }\n\n    Ok(())\n}\n\n/// Calculates a single MEDPRICE value incrementally.\n///\n/// # Description\n/// This function provides an optimized way to calculate the median price for a single period,\n/// making it suitable for real-time calculations without processing the entire data series.\n///\n/// # Arguments\n/// * `input_high` - Current period's high price\n/// * `input_low` - Current period's low price\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - Returns calculated median price on success\n///\n/// # Errors\n/// * `KandError::NaNDetected` - Input contains NaN values (when \"`check-nan`\" feature enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::medprice;\n///\n/// let high = 10.0;\n/// let low = 8.0;\n/// let result = medprice::medprice_inc(high, low).unwrap();\n/// assert_eq!(result, 9.0);\n/// ```\npub const fn medprice_inc(input_high: TAFloat, input_low: TAFloat) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_high.is_nan() || input_low.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    Ok(f64::midpoint(input_high, input_low))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_medprice_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1,\n        ];\n        let mut output_medprice = vec![0.0; input_high.len()];\n\n        medprice(&input_high, &input_low, &mut output_medprice).unwrap();\n\n        // Compare with known values\n        let expected_values = [\n            35241.05, 35227.0, 35207.85, 35160.75, 35167.8, 35216.35, 35232.75, 35242.5, 35215.5,\n            35188.0, 35178.15, 35192.05, 35213.5, 35181.0, 35146.35,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_medprice[i], *expected, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation matches regular calculation\n        for i in 0..input_high.len() {\n            let result = medprice_inc(input_high[i], input_low[i]).unwrap();\n            assert_relative_eq!(result, output_medprice[i], epsilon = 0.0001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/mfi.rs",
    "content": "use crate::{KandError, TAFloat, ta::ohlcv::typprice};\n\n/// Calculates the lookback period required for Money Flow Index (MFI) calculation.\n///\n/// # Description\n/// The lookback period is equal to the input period parameter since MFI requires\n/// previous data points to calculate the money flow ratio.\n///\n/// # Arguments\n/// * `opt_period` - The time period for MFI calculation (e.g. 14 for a 14-period MFI)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The required lookback period on success\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if `opt_period` is less than 2\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::mfi;\n/// let period = 14;\n/// let lookback = mfi::lookback(period).unwrap();\n/// assert_eq!(lookback, 14);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period)\n}\n\n/// Calculates Money Flow Index (MFI) for a price series.\n///\n/// # Description\n/// Money Flow Index (MFI) is a technical oscillator that uses price and volume data to identify\n/// overbought or oversold conditions in an asset. It can also be used to spot divergences which\n/// may lead to price reversals.\n///\n/// # Mathematical Formula\n/// ```text\n/// Typical Price = (High + Low + Close) / 3\n/// Money Flow = Typical Price × Volume\n/// Positive Money Flow = Sum of Money Flow where Typical Price increases\n/// Negative Money Flow = Sum of Money Flow where Typical Price decreases\n/// Money Flow Ratio = Positive Money Flow / Negative Money Flow\n/// MFI = 100 - (100 / (1 + Money Flow Ratio))\n/// ```\n///\n/// # Arguments\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of close prices\n/// * `input_volume` - Array of volume data\n/// * `opt_period` - The time period for MFI calculation (typically 14)\n/// * `output_mfi` - Array to store the calculated MFI values (0-100)\n/// * `output_typ_prices` - Array to store the calculated typical prices\n/// * `output_money_flows` - Array to store the calculated money flows\n/// * `output_pos_flows` - Array to store the positive money flows\n/// * `output_neg_flows` - Array to store the negative money flows\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty result on success\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if `opt_period` is zero\n/// * Returns `KandError::InvalidData` if input arrays are empty\n/// * Returns `KandError::LengthMismatch` if input arrays have different lengths\n///\n/// # Examples\n/// ```\n/// use kand::ta::ohlcv::mfi;\n/// let high = vec![10.0, 11.0, 12.0, 11.0];\n/// let low = vec![8.0, 9.0, 10.0, 9.0];\n/// let close = vec![9.0, 10.0, 11.0, 10.0];\n/// let volume = vec![100.0, 150.0, 200.0, 150.0];\n/// let period = 2;\n/// let mut mfi = vec![0.0; 4];\n/// let mut typ_prices = vec![0.0; 4];\n/// let mut money_flows = vec![0.0; 4];\n/// let mut pos_flows = vec![0.0; 4];\n/// let mut neg_flows = vec![0.0; 4];\n///\n/// mfi::mfi(\n///     &high,\n///     &low,\n///     &close,\n///     &volume,\n///     period,\n///     &mut mfi,\n///     &mut typ_prices,\n///     &mut money_flows,\n///     &mut pos_flows,\n///     &mut neg_flows,\n/// )\n/// .unwrap();\n/// ```\npub fn mfi(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    input_volume: &[TAFloat],\n    opt_period: usize,\n    output_mfi: &mut [TAFloat],\n    output_typ_prices: &mut [TAFloat],\n    output_money_flows: &mut [TAFloat],\n    output_pos_flows: &mut [TAFloat],\n    output_neg_flows: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len < lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_low.len()\n            || len != input_close.len()\n            || len != input_volume.len()\n            || len != output_mfi.len()\n            || len != output_typ_prices.len()\n            || len != output_money_flows.len()\n            || len != output_pos_flows.len()\n            || len != output_neg_flows.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n\n        // Parameter validation\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    // Calculate typical prices\n    typprice::typprice(input_high, input_low, input_close, output_typ_prices)?;\n\n    // Initialize money flows\n    for i in 0..len {\n        output_money_flows[i] = output_typ_prices[i] * input_volume[i];\n    }\n\n    // Calculate MFI for each period\n    for i in opt_period..len {\n        let mut pos_flow = 0.0;\n        let mut neg_flow = 0.0;\n\n        // Calculate positive and negative money flows over the period\n        for j in (i - opt_period + 1)..=i {\n            if output_typ_prices[j] > output_typ_prices[j - 1] {\n                pos_flow += output_money_flows[j];\n            } else if output_typ_prices[j] < output_typ_prices[j - 1] {\n                neg_flow += output_money_flows[j];\n            }\n        }\n\n        output_pos_flows[i] = pos_flow;\n        output_neg_flows[i] = neg_flow;\n\n        // Calculate MFI using the optimized formula:\n        // MFI = 100 * (posSumMF/(posSumMF+negSumMF))\n        let total_flow = pos_flow + neg_flow;\n        if total_flow < 1.0 {\n            output_mfi[i] = 0.0;\n        } else {\n            output_mfi[i] = 100.0 * (pos_flow / total_flow);\n        }\n    }\n\n    // Set initial values to NaN\n    for i in 0..opt_period {\n        output_mfi[i] = TAFloat::NAN;\n        output_pos_flows[i] = TAFloat::NAN;\n        output_neg_flows[i] = TAFloat::NAN;\n        output_typ_prices[i] = TAFloat::NAN;\n        output_money_flows[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_mfi_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4, 35150.4, 35123.9,\n            35110.0, 35092.1, 35179.2,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0, 35073.0, 35055.0,\n            35084.0, 35060.0, 35073.1,\n        ];\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0, 35114.5, 35097.2,\n            35092.0, 35073.2, 35139.3,\n        ];\n        let input_volume = vec![\n            1055.365, 756.488, 682.152, 1197.747, 425.97, 859.638, 741.925, 888.477, 1043.333,\n            467.901, 387.47, 566.099, 672.296, 834.915, 1854.024, 3670.795, 3761.198, 1605.442,\n            1726.574, 934.713, 2199.061, 2349.823, 837.218, 1000.638, 1218.202, 2573.668, 1098.409,\n            609.582, 670.489, 1637.998,\n        ];\n        let opt_period = 14;\n        let mut output_mfi = vec![0.0; input_high.len()];\n        let mut output_typ_prices = vec![0.0; input_high.len()];\n        let mut output_money_flows = vec![0.0; input_high.len()];\n        let mut output_pos_flows = vec![0.0; input_high.len()];\n        let mut output_neg_flows = vec![0.0; input_high.len()];\n\n        mfi(\n            &input_high,\n            &input_low,\n            &input_close,\n            &input_volume,\n            opt_period,\n            &mut output_mfi,\n            &mut output_typ_prices,\n            &mut output_money_flows,\n            &mut output_pos_flows,\n            &mut output_neg_flows,\n        )\n        .unwrap();\n\n        // First 13 values should be NaN\n        for value in output_mfi.iter().take(14) {\n            assert!(value.is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            30.014_661_355_061_06,\n            23.918_092_608_653_822,\n            19.698_868_438_822_11,\n            28.256_916_470_899_76,\n            33.135_317_878_529_85,\n            28.508_181_473_309_836,\n            26.506_381_443_527_35,\n            20.718_757_833_025_176,\n            24.742_436_684_915_482,\n            28.621_541_030_827_412,\n            32.847_079_830_044_35,\n            38.195_318_060_163_59,\n            34.931_258_232_645_05,\n            37.643_920_900_904_62,\n            39.487_301_568_579_9,\n            50.487_923_737_718_95,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_mfi[i + 14], *expected, epsilon = 0.0001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/midpoint.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Calculates the lookback period required for Midpoint calculation.\n///\n/// # Description\n/// Returns the minimum number of data points needed before the first valid output can be calculated.\n/// For the Midpoint indicator, this equals `period - 1`.\n///\n/// # Arguments\n/// * `opt_period` - The time period used for calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period (period - 1) on success\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::midpoint;\n///\n/// let period = 14;\n/// let lookback = midpoint::lookback(period).unwrap();\n/// assert_eq!(lookback, 13);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Calculates Midpoint values for a price series.\n///\n/// # Description\n/// The Midpoint is a technical indicator that represents the arithmetic mean of the highest and lowest\n/// prices over a specified period. It helps identify average price levels and potential support/resistance areas.\n///\n/// # Mathematical Formula\n/// For each period:\n/// ```text\n/// MIDPOINT = (Highest Price + Lowest Price) / 2\n/// ```\n/// Where:\n/// - Highest Price = Maximum price within the period\n/// - Lowest Price = Minimum price within the period\n///\n/// # Calculation Steps\n/// 1. For each period window:\n///    - Find the highest and lowest prices\n///    - Calculate midpoint as their average\n/// 2. Fill initial values before lookback period with NaN\n///\n/// # Arguments\n/// * `input_price` - Array of price values\n/// * `opt_period` - Time period for calculation (must be >= 2)\n/// * `output_midpoint` - Array to store calculated Midpoint values\n/// * `output_highest` - Array to store highest values for each period\n/// * `output_lowest` - Array to store lowest values for each period\n///\n/// # Returns\n/// * `Result<(), KandError>` - Unit type on success\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input array is empty\n/// * `KandError::LengthMismatch` - If input/output array lengths don't match\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n/// * `KandError::InsufficientData` - If input length is less than lookback period\n/// * `KandError::NaNDetected` - If any input value is NaN (when `check-nan` enabled)\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::midpoint;\n///\n/// let input_price = vec![10.0, 12.0, 15.0, 14.0, 13.0];\n/// let opt_period = 3;\n/// let mut output_midpoint = vec![0.0; 5];\n/// let mut output_highest = vec![0.0; 5];\n/// let mut output_lowest = vec![0.0; 5];\n///\n/// midpoint::midpoint(\n///     &input_price,\n///     opt_period,\n///     &mut output_midpoint,\n///     &mut output_highest,\n///     &mut output_lowest,\n/// )\n/// .unwrap();\n/// ```\npub fn midpoint(\n    input_price: &[TAFloat],\n    opt_period: usize,\n    output_midpoint: &mut [TAFloat],\n    output_highest: &mut [TAFloat],\n    output_lowest: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_price.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != output_midpoint.len() || len != output_highest.len() || len != output_lowest.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for &price in input_price.iter().take(len) {\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate midpoint for each window\n    for i in lookback..len {\n        let start_idx = i + 1 - opt_period;\n        let mut highest = input_price[start_idx];\n        let mut lowest = input_price[start_idx];\n\n        // Find highest and lowest prices in the period\n        for &price in input_price.iter().take(i + 1).skip(start_idx + 1) {\n            highest = highest.max(price);\n            lowest = lowest.min(price);\n        }\n\n        output_highest[i] = highest;\n        output_lowest[i] = lowest;\n        output_midpoint[i] = f64::midpoint(highest, lowest);\n    }\n\n    // Fill initial values with NAN\n    for i in 0..lookback {\n        output_midpoint[i] = TAFloat::NAN;\n        output_highest[i] = TAFloat::NAN;\n        output_lowest[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the next Midpoint value incrementally.\n///\n/// # Description\n/// Provides an optimized way to calculate the next Midpoint value when new data arrives,\n/// without recalculating the entire series. Updates the highest and lowest values with new price.\n///\n/// # Mathematical Formula\n/// ```text\n/// MIDPOINT = (Highest Price + Lowest Price) / 2\n/// ```\n/// Where:\n/// - Highest Price = max(current price, previous highest)\n/// - Lowest Price = min(current price, previous lowest)\n///\n/// # Calculation Steps\n/// 1. Compare new price with previous highest/lowest\n/// 2. Update highest/lowest values\n/// 3. Calculate new midpoint\n///\n/// # Arguments\n/// * `input_price` - Current price value\n/// * `prev_highest` - Previous highest value\n/// * `prev_lowest` - Previous lowest value\n/// * `opt_period` - Time period for calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat), KandError>` - Tuple (midpoint, `new_highest`, `new_lowest`) on success\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n/// * `KandError::NaNDetected` - If any input value is NaN (when `check-nan` enabled)\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::midpoint;\n///\n/// let current_price = 15.0;\n/// let prev_highest = 16.0;\n/// let prev_lowest = 14.0;\n/// let period = 14;\n///\n/// let (midpoint, new_highest, new_lowest) =\n///     midpoint::midpoint_inc(current_price, prev_highest, prev_lowest, period).unwrap();\n/// ```\npub const fn midpoint_inc(\n    input_price: TAFloat,\n    prev_highest: TAFloat,\n    prev_lowest: TAFloat,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_price.is_nan() || prev_highest.is_nan() || prev_lowest.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let new_highest = input_price.max(prev_highest);\n    let new_lowest = input_price.min(prev_lowest);\n    let midpoint = f64::midpoint(new_highest, new_lowest);\n\n    Ok((midpoint, new_highest, new_lowest))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_midpoint_calculation() {\n        let input_price = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0,\n        ];\n        let opt_period = 14;\n        let mut output_midpoint = vec![0.0; input_price.len()];\n        let mut output_highest = vec![0.0; input_price.len()];\n        let mut output_lowest = vec![0.0; input_price.len()];\n\n        midpoint(\n            &input_price,\n            opt_period,\n            &mut output_midpoint,\n            &mut output_highest,\n            &mut output_lowest,\n        )\n        .unwrap();\n\n        // First 13 values should be NaN\n        for i in 0..13 {\n            assert!(output_midpoint[i].is_nan());\n            assert!(output_highest[i].is_nan());\n            assert!(output_lowest[i].is_nan());\n        }\n\n        // Compare with known values from the CSV data\n        let expected_values = [\n            35207.65, 35172.45, 35147.90, 35126.95, 35126.95, 35126.95, 35125.60, 35095.70,\n            35084.70, 35084.70, 35084.70, 35084.70,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_midpoint[i + 13], *expected, epsilon = 0.01);\n        }\n\n        // Test incremental calculation\n        let mut prev_highest = output_highest[13];\n        let mut prev_lowest = output_lowest[13];\n\n        for i in 14..19 {\n            let (midpoint, new_highest, new_lowest) =\n                midpoint_inc(input_price[i], prev_highest, prev_lowest, opt_period).unwrap();\n\n            assert_relative_eq!(midpoint, output_midpoint[i], epsilon = 0.01);\n            prev_highest = new_highest;\n            prev_lowest = new_lowest;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/midprice.rs",
    "content": "use crate::{\n    TAFloat,\n    error::KandError,\n    helper::{highest_bars, lowest_bars},\n};\n\n/// Calculates the lookback period required for Midpoint Price calculation.\n///\n/// # Arguments\n/// * `opt_period` - The time period used for calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - Returns `opt_period - 1` on success\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::midprice;\n/// let lookback = midprice::lookback(14).unwrap();\n/// assert_eq!(lookback, 13);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Calculates Midpoint Price for a price series.\n///\n/// The Midpoint Price is a technical indicator that represents the mean value between the highest high\n/// and lowest low prices over a specified period.\n///\n/// # Mathematical Formula\n/// ```text\n/// MIDPRICE[i] = (Highest High[i-n+1...i] + Lowest Low[i-n+1...i]) / 2\n/// ```\n/// Where:\n/// - n is the period\n/// - i is the current index\n///\n/// # Calculation Steps\n/// 1. Find highest high price in the period window\n/// 2. Find lowest low price in the period window\n/// 3. Calculate arithmetic mean of these values\n///\n/// # Arguments\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `opt_period` - Calculation period (must be >= 2)\n/// * `output_midprice` - Buffer to store calculated midpoint prices\n/// * `output_highest_high` - Buffer to store highest highs\n/// * `output_lowest_low` - Buffer to store lowest lows\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty Ok on success\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input/output array lengths differ\n/// * `KandError::InvalidParameter` - If period is less than 2\n/// * `KandError::InsufficientData` - If input length <= lookback period\n/// * `KandError::NaNDetected` - If any input contains NaN (with `check-nan`)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::midprice;\n///\n/// let high = vec![10.0, 12.0, 15.0, 14.0, 13.0];\n/// let low = vec![8.0, 9.0, 11.0, 10.0, 9.0];\n/// let mut midprice = vec![0.0; 5];\n/// let mut highest = vec![0.0; 5];\n/// let mut lowest = vec![0.0; 5];\n///\n/// midprice::midprice(&high, &low, 3, &mut midprice, &mut highest, &mut lowest).unwrap();\n/// ```\npub fn midprice(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    opt_period: usize,\n    output_midprice: &mut [TAFloat],\n    output_highest_high: &mut [TAFloat],\n    output_lowest_low: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_low.len()\n            || len != output_midprice.len()\n            || len != output_highest_high.len()\n            || len != output_lowest_low.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_high[i].is_nan() || input_low[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate midpoint price for each window\n    for i in lookback..len {\n        let highest_idx = highest_bars(input_high, i, opt_period)?;\n        let lowest_idx = lowest_bars(input_low, i, opt_period)?;\n\n        let highest_high = input_high[i - highest_idx];\n        let lowest_low = input_low[i - lowest_idx];\n\n        output_highest_high[i] = highest_high;\n        output_lowest_low[i] = lowest_low;\n        output_midprice[i] = f64::midpoint(highest_high, lowest_low);\n    }\n\n    // Fill initial values with NAN\n    for i in 0..lookback {\n        output_midprice[i] = TAFloat::NAN;\n        output_highest_high[i] = TAFloat::NAN;\n        output_lowest_low[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Incrementally calculates the next Midpoint Price value.\n///\n/// Provides optimized calculation of the next value when new data arrives, avoiding\n/// recalculation of the entire series.\n///\n/// # Arguments\n/// * `input_high` - Current high price\n/// * `input_low` - Current low price\n/// * `prev_highest_high` - Previous period's highest high\n/// * `prev_lowest_low` - Previous period's lowest low\n/// * `opt_period` - Calculation period (must be >= 2)\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat), KandError>` - Returns (midprice, `new_highest_high`, `new_lowest_low`)\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If period is less than 2\n/// * `KandError::NaNDetected` - If any input contains NaN (with `check-nan`)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::midprice;\n///\n/// let (midprice, highest, lowest) = midprice::midprice_inc(\n///     10.5, // current high\n///     9.8,  // current low\n///     10.2, // previous highest high\n///     9.5,  // previous lowest low\n///     14,   // period\n/// )\n/// .unwrap();\n/// ```\npub const fn midprice_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    prev_highest_high: TAFloat,\n    prev_lowest_low: TAFloat,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_high.is_nan()\n            || input_low.is_nan()\n            || prev_highest_high.is_nan()\n            || prev_lowest_low.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let new_highest_high = input_high.max(prev_highest_high);\n    let new_lowest_low = input_low.min(prev_lowest_low);\n    let midprice = f64::midpoint(new_highest_high, new_lowest_low);\n\n    Ok((midprice, new_highest_high, new_lowest_low))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_midprice_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0,\n        ];\n        let opt_period = 14;\n        let mut output_midprice = vec![0.0; input_high.len()];\n        let mut output_highest_high = vec![0.0; input_high.len()];\n        let mut output_lowest_low = vec![0.0; input_high.len()];\n\n        midprice(\n            &input_high,\n            &input_low,\n            opt_period,\n            &mut output_midprice,\n            &mut output_highest_high,\n            &mut output_lowest_low,\n        )\n        .unwrap();\n\n        // First 13 values should be NaN\n        for i in 0..13 {\n            assert!(output_midprice[i].is_nan());\n            assert!(output_highest_high[i].is_nan());\n            assert!(output_lowest_low[i].is_nan());\n        }\n\n        // Compare with known values from the CSV data\n        let expected_values = [\n            35206.1, 35180.8, 35151.3, 35115.8, 35115.8, 35115.8, 35115.8, 35106.55, 35083.5,\n            35076.0, 35076.0, 35076.0,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_midprice[i + 13], *expected, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation\n        let mut prev_highest_high = output_highest_high[13];\n        let mut prev_lowest_low = output_lowest_low[13];\n\n        for i in 14..19 {\n            let (midprice, new_highest_high, new_lowest_low) = midprice_inc(\n                input_high[i],\n                input_low[i],\n                prev_highest_high,\n                prev_lowest_low,\n                opt_period,\n            )\n            .unwrap();\n\n            assert_relative_eq!(midprice, output_midprice[i], epsilon = 0.0001);\n            prev_highest_high = new_highest_high;\n            prev_lowest_low = new_lowest_low;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/minus_di.rs",
    "content": "use super::trange;\nuse crate::{KandError, TAFloat};\n\n/// Calculates the lookback period required for -DI (Minus Directional Indicator) calculation.\n///\n/// # Arguments\n/// * `opt_period` - The time period used for calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - Returns `opt_period` on success\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::minus_di;\n/// let lookback = minus_di::lookback(14).unwrap();\n/// assert_eq!(lookback, 14);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period)\n}\n\n/// Calculates the Minus Directional Indicator (-DI) for the entire input array.\n///\n/// The -DI is a component of the Directional Movement System that measures the strength of downward price movement.\n/// It is commonly used with +DI and ADX to analyze trend direction and strength.\n///\n/// # Mathematical Formula\n/// ```text\n/// 1. Minus Directional Movement (-DM):\n///    -DM = prev_low - curr_low, if:\n///          - (prev_low - curr_low) > (curr_high - prev_high) AND\n///          - (prev_low - curr_low) > 0\n///    Otherwise, -DM = 0\n///\n/// 2. True Range (TR):\n///    TR = max(high - low, |high - prev_close|, |low - prev_close|)\n///\n/// 3. Initial Values:\n///    First -DI = 100 * SMA(-DM, period) / SMA(TR, period)\n///\n/// 4. Subsequent Values (Wilder's Smoothing):\n///    Smoothed -DM = ((prev_smoothed_-DM * (period-1)) + curr_-DM) / period\n///    Smoothed TR = ((prev_smoothed_TR * (period-1)) + curr_TR) / period\n///    -DI = 100 * Smoothed_-DM / Smoothed_TR\n/// ```\n///\n/// # Calculation Principles\n/// 1. Measures downward price movement strength (0 to 100)\n/// 2. Uses Wilder's smoothing for more weight on recent data\n/// 3. Higher values indicate stronger downward trends\n/// 4. Part of the complete Directional Movement System\n/// 5. First valid value appears after `opt_period` bars\n///\n/// # Arguments\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of closing prices\n/// * `opt_period` - Calculation period (>= 2)\n/// * `output_minus_di` - Output array for -DI values\n/// * `output_smoothed_minus_dm` - Output array for smoothed -DM values\n/// * `output_smoothed_tr` - Output array for smoothed TR values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok on success, Err otherwise\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input/output arrays have different lengths\n/// * `KandError::InsufficientData` - If input length <= `opt_period`\n/// * `KandError::NaNDetected` - If any input contains NaN\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::minus_di;\n///\n/// let high = vec![35.0, 36.0, 35.5, 35.8, 36.2];\n/// let low = vec![34.0, 35.0, 34.5, 34.8, 35.2];\n/// let close = vec![34.5, 35.5, 35.0, 35.3, 35.7];\n/// let period = 3;\n///\n/// let mut minus_di = vec![0.0; high.len()];\n/// let mut smoothed_minus_dm = vec![0.0; high.len()];\n/// let mut smoothed_tr = vec![0.0; high.len()];\n///\n/// minus_di::minus_di(\n///     &high,\n///     &low,\n///     &close,\n///     period,\n///     &mut minus_di,\n///     &mut smoothed_minus_dm,\n///     &mut smoothed_tr,\n/// )\n/// .unwrap();\n/// ```\npub fn minus_di(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_period: usize,\n    output_minus_di: &mut [TAFloat],\n    output_smoothed_minus_dm: &mut [TAFloat],\n    output_smoothed_tr: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_low.len()\n            || len != input_close.len()\n            || len != output_minus_di.len()\n            || len != output_smoothed_minus_dm.len()\n            || len != output_smoothed_tr.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_high[i].is_nan() || input_low[i].is_nan() || input_close[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate initial -DM and TR sums\n    let mut minus_dm_sum = 0.0;\n    let mut tr_sum = 0.0;\n    let mut prev_high = input_high[0];\n    let mut prev_low = input_low[0];\n    let mut prev_close = input_close[0];\n\n    // Calculate first period-1 -DM1 and TR1 values\n    for i in 1..opt_period {\n        let high_diff = input_high[i] - prev_high;\n        let low_diff = prev_low - input_low[i];\n\n        let minus_dm1 = if low_diff > high_diff && low_diff > 0.0 {\n            low_diff\n        } else {\n            0.0\n        };\n\n        minus_dm_sum += minus_dm1;\n\n        let tr1 = trange::trange_inc(input_high[i], input_low[i], prev_close)?;\n        tr_sum += tr1;\n\n        prev_high = input_high[i];\n        prev_low = input_low[i];\n        prev_close = input_close[i];\n    }\n\n    // Calculate first -DI value\n    let hundred = 100.0;\n    let period_t = opt_period as TAFloat;\n\n    // Initialize smoothed values\n    let mut curr_smoothed_minus_dm = minus_dm_sum;\n    let mut curr_smoothed_tr = tr_sum;\n\n    // Calculate remaining -DI values using Wilder's smoothing\n    for i in lookback..len {\n        let high_diff = input_high[i] - input_high[i - 1];\n        let low_diff = input_low[i - 1] - input_low[i];\n\n        let minus_dm1 = if low_diff > high_diff && low_diff > 0.0 {\n            low_diff\n        } else {\n            0.0\n        };\n\n        let tr1 = trange::trange_inc(input_high[i], input_low[i], input_close[i - 1])?;\n\n        // Apply Wilder's smoothing\n        curr_smoothed_minus_dm =\n            curr_smoothed_minus_dm - (curr_smoothed_minus_dm / period_t) + minus_dm1;\n        curr_smoothed_tr = curr_smoothed_tr - (curr_smoothed_tr / period_t) + tr1;\n\n        output_smoothed_minus_dm[i] = curr_smoothed_minus_dm;\n        output_smoothed_tr[i] = curr_smoothed_tr;\n\n        output_minus_di[i] = if curr_smoothed_tr == 0.0 {\n            0.0\n        } else {\n            hundred * curr_smoothed_minus_dm / curr_smoothed_tr\n        };\n    }\n\n    // Fill initial values with NAN\n    for i in 0..lookback {\n        output_minus_di[i] = TAFloat::NAN;\n        output_smoothed_minus_dm[i] = TAFloat::NAN;\n        output_smoothed_tr[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the latest -DI value incrementally using previous smoothed values.\n///\n/// This function provides an efficient way to update -DI with new price data without recalculating the entire series.\n/// It maintains the same mathematical properties as the full calculation.\n///\n/// # Mathematical Formula\n/// ```text\n/// 1. Current -DM:\n///    -DM = prev_low - curr_low, if:\n///          - (prev_low - curr_low) > (curr_high - prev_high) AND\n///          - (prev_low - curr_low) > 0\n///    Otherwise, -DM = 0\n///\n/// 2. Current TR:\n///    TR = max(high - low, |high - prev_close|, |low - prev_close|)\n///\n/// 3. Wilder's Smoothing:\n///    smoothed_-DM = ((prev_smoothed_-DM * (period-1)) + curr_-DM) / period\n///    smoothed_TR = ((prev_smoothed_TR * (period-1)) + curr_TR) / period\n///\n/// 4. -DI Value:\n///    -DI = 100 * smoothed_-DM / smoothed_TR\n/// ```\n///\n/// # Arguments\n/// * `input_high` - Current high price\n/// * `input_low` - Current low price\n/// * `prev_high` - Previous high price\n/// * `prev_low` - Previous low price\n/// * `prev_close` - Previous close price\n/// * `prev_smoothed_minus_dm` - Previous smoothed -DM\n/// * `prev_smoothed_tr` - Previous smoothed TR\n/// * `opt_period` - Calculation period (>= 2)\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat), KandError>` - Tuple of (new -DI, new smoothed -DM, new smoothed TR)\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n/// * `KandError::NaNDetected` - If any input contains NaN\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::minus_di;\n///\n/// let (minus_di, smoothed_minus_dm, smoothed_tr) = minus_di::minus_di_inc(\n///     36.2, // high\n///     35.2, // low\n///     35.8, // prev_high\n///     34.8, // prev_low\n///     35.3, // prev_close\n///     0.5,  // prev_smoothed_minus_dm\n///     1.5,  // prev_smoothed_tr\n///     14,   // period\n/// )\n/// .unwrap();\n/// ```\npub fn minus_di_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    prev_close: TAFloat,\n    prev_smoothed_minus_dm: TAFloat,\n    prev_smoothed_tr: TAFloat,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        // -DI requires at least 2 periods:\n        // - One for initial DM and TR calculations (needs previous prices)\n        // - One for the current period\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_high.is_nan()\n            || input_low.is_nan()\n            || prev_high.is_nan()\n            || prev_low.is_nan()\n            || prev_close.is_nan()\n            || prev_smoothed_minus_dm.is_nan()\n            || prev_smoothed_tr.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let high_diff = input_high - prev_high;\n    let low_diff = prev_low - input_low;\n\n    let minus_dm = if low_diff > high_diff && low_diff > 0.0 {\n        low_diff\n    } else {\n        0.0\n    };\n\n    let tr = trange::trange_inc(input_high, input_low, prev_close)?;\n    let period_t = opt_period as TAFloat;\n\n    let output_smoothed_minus_dm =\n        prev_smoothed_minus_dm - (prev_smoothed_minus_dm / period_t) + minus_dm;\n    let output_smoothed_tr = prev_smoothed_tr - (prev_smoothed_tr / period_t) + tr;\n\n    let output_minus_di = if output_smoothed_tr == 0.0 {\n        0.0\n    } else {\n        100.0 * output_smoothed_minus_dm / output_smoothed_tr\n    };\n\n    Ok((\n        output_minus_di,\n        output_smoothed_minus_dm,\n        output_smoothed_tr,\n    ))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_minus_di_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0,\n        ];\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0,\n        ];\n\n        let opt_period = 14;\n        let mut output_minus_di = vec![0.0; input_high.len()];\n        let mut output_smoothed_minus_dm = vec![0.0; input_high.len()];\n        let mut output_smoothed_tr = vec![0.0; input_high.len()];\n\n        minus_di(\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_period,\n            &mut output_minus_di,\n            &mut output_smoothed_minus_dm,\n            &mut output_smoothed_tr,\n        )\n        .unwrap();\n\n        // First opt_period values should be NaN\n        for value in output_minus_di.iter().take(opt_period) {\n            assert!(value.is_nan());\n        }\n\n        // Check first valid value\n        assert_relative_eq!(\n            output_minus_di[14],\n            26.118_652_373_133_61,\n            epsilon = 0.00001\n        );\n        assert_relative_eq!(\n            output_minus_di[15],\n            29.626_333_125_808_358,\n            epsilon = 0.00001\n        );\n        assert_relative_eq!(\n            output_minus_di[16],\n            34.230_177_437_536_13,\n            epsilon = 0.00001\n        );\n        assert_relative_eq!(\n            output_minus_di[17],\n            32.200_629_859_296_83,\n            epsilon = 0.00001\n        );\n        assert_relative_eq!(\n            output_minus_di[18],\n            29.832_869_923_860_61,\n            epsilon = 0.00001\n        );\n\n        // Now test incremental calculation matches regular calculation\n        let mut prev_smoothed_minus_dm = output_smoothed_minus_dm[14];\n        let mut prev_smoothed_tr = output_smoothed_tr[14];\n\n        // Test each incremental step\n        for i in 15..input_high.len() {\n            let (minus_di, new_smoothed_minus_dm, new_smoothed_tr) = minus_di_inc(\n                input_high[i],\n                input_low[i],\n                input_high[i - 1],\n                input_low[i - 1],\n                input_close[i - 1],\n                prev_smoothed_minus_dm,\n                prev_smoothed_tr,\n                opt_period,\n            )\n            .unwrap();\n\n            assert_relative_eq!(minus_di, output_minus_di[i], epsilon = 0.00001);\n            prev_smoothed_minus_dm = new_smoothed_minus_dm;\n            prev_smoothed_tr = new_smoothed_tr;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/minus_dm.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Calculates the lookback period required for Minus Directional Movement (-DM) calculation.\n///\n/// # Description\n/// Returns the minimum number of data points needed before the first valid -DM value can be calculated.\n/// For the -DM indicator, this equals `period - 1`.\n///\n/// # Arguments\n/// * `opt_period` - The time period used for calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period (period - 1) on success\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::minus_dm;\n/// let period = 14;\n/// let lookback = minus_dm::lookback(period).unwrap();\n/// assert_eq!(lookback, 13);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Calculates Minus Directional Movement (-DM) for a price series.\n///\n/// # Description\n/// Minus Directional Movement (-DM) is a component of the Directional Movement System developed by J. Welles Wilder.\n/// It measures the strength of downward price movement by comparing consecutive lows.\n///\n/// # Mathematical Formula\n/// ```text\n/// For each period:\n/// 1. Calculate -DM1 (one-period directional movement):\n///    If (Low[t-1] - Low[t]) > (High[t] - High[t-1]) AND (Low[t-1] - Low[t]) > 0:\n///        -DM1 = Low[t-1] - Low[t]\n///    Else:\n///        -DM1 = 0\n///\n/// 2. Initial -DM:\n///    First -DM = Sum(-DM1, period)\n///\n/// 3. Subsequent -DM using Wilder's smoothing:\n///    -DM[t] = -DM[t-1] - (-DM[t-1]/period) + -DM1[t]\n/// ```\n///\n/// # Calculation Steps\n/// 1. Calculate initial -DM1 values for the first period\n/// 2. Sum these values to get the first -DM\n/// 3. Apply Wilder's smoothing formula for subsequent periods\n/// 4. Fill initial values before lookback period with NaN\n///\n/// # Arguments\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `opt_period` - Time period for calculation (must be >= 2)\n/// * `output_dm` - Array to store calculated -DM values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty Ok on success\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input/output array lengths differ\n/// * `KandError::InvalidParameter` - If period is less than 2\n/// * `KandError::InsufficientData` - If input length <= lookback period\n/// * `KandError::NaNDetected` - If any input contains NaN (with `check-nan`)\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::minus_dm;\n///\n/// let high = vec![10.0, 12.0, 15.0, 14.0, 13.0];\n/// let low = vec![8.0, 9.0, 11.0, 10.0, 9.0];\n/// let mut minus_dm = vec![0.0; 5];\n///\n/// minus_dm::minus_dm(&high, &low, 3, &mut minus_dm).unwrap();\n/// ```\npub fn minus_dm(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    opt_period: usize,\n    output_dm: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_low.len() || len != output_dm.len() {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_high[i].is_nan() || input_low[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate first -DM values and initial -DM (sum of -DM1)\n    let mut dm_sum = 0.0;\n\n    for i in 1..opt_period {\n        let high_diff = input_high[i] - input_high[i - 1];\n        let low_diff = input_low[i - 1] - input_low[i];\n\n        let dm = if low_diff > high_diff && low_diff > 0.0 {\n            low_diff\n        } else {\n            0.0\n        };\n        dm_sum += dm;\n    }\n    output_dm[lookback] = dm_sum;\n\n    // Calculate remaining -DM values using Wilder's smoothing\n    for i in opt_period..len {\n        let high_diff = input_high[i] - input_high[i - 1];\n        let low_diff = input_low[i - 1] - input_low[i];\n\n        let dm = if low_diff > high_diff && low_diff > 0.0 {\n            low_diff\n        } else {\n            0.0\n        };\n\n        output_dm[i] = output_dm[i - 1] - (output_dm[i - 1] / opt_period as TAFloat) + dm;\n    }\n\n    // Fill initial values with NAN\n    for value in output_dm.iter_mut().take(lookback) {\n        *value = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the next Minus DM value incrementally.\n///\n/// # Description\n/// Provides an optimized way to calculate the next -DM value when new data arrives,\n/// without recalculating the entire series. Uses Wilder's smoothing formula.\n///\n/// # Mathematical Formula\n/// ```text\n/// -DM[t] = -DM[t-1] - (-DM[t-1]/period) + -DM1[t]\n///\n/// where -DM1[t] is:\n/// If (Low[t-1] - Low[t]) > (High[t] - High[t-1]) AND (Low[t-1] - Low[t]) > 0:\n///     -DM1[t] = Low[t-1] - Low[t]\n/// Else:\n///     -DM1[t] = 0\n/// ```\n///\n/// # Arguments\n/// * `input_high` - Current high price\n/// * `prev_high` - Previous high price\n/// * `input_low` - Current low price\n/// * `prev_low` - Previous low price\n/// * `prev_minus_dm` - Previous -DM value\n/// * `opt_period` - Time period for calculation (must be between 2 and 100)\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The next -DM value on success\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If period is not between 2 and 100\n/// * `KandError::NaNDetected` - If any input contains NaN (with `check-nan`)\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::minus_dm;\n///\n/// let current_high = 15.0;\n/// let prev_high = 14.0;\n/// let current_low = 13.0;\n/// let prev_low = 12.0;\n/// let prev_minus_dm = 2.5;\n/// let period = 14;\n///\n/// let next_minus_dm = minus_dm::minus_dm_inc(\n///     current_high,\n///     prev_high,\n///     current_low,\n///     prev_low,\n///     prev_minus_dm,\n///     period,\n/// )\n/// .unwrap();\n/// ```\npub fn minus_dm_inc(\n    input_high: TAFloat,\n    prev_high: TAFloat,\n    input_low: TAFloat,\n    prev_low: TAFloat,\n    prev_minus_dm: TAFloat,\n    opt_period: usize,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_high.is_nan()\n            || prev_high.is_nan()\n            || input_low.is_nan()\n            || prev_low.is_nan()\n            || prev_minus_dm.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let high_diff = input_high - prev_high;\n    let low_diff = prev_low - input_low;\n\n    let dm = if low_diff > high_diff && low_diff > 0.0 {\n        low_diff\n    } else {\n        0.0\n    };\n\n    Ok(prev_minus_dm - (prev_minus_dm / opt_period as TAFloat) + dm)\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    // Basic functionality tests\n    #[test]\n    fn test_minus_dm_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4, 35150.4, 35123.9,\n            35110.0, 35092.1,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0, 35073.0, 35055.0,\n            35084.0, 35060.0,\n        ];\n        let opt_period = 14;\n        let mut output_dm = vec![0.0; input_high.len()];\n\n        minus_dm(&input_high, &input_low, opt_period, &mut output_dm).unwrap();\n\n        // First period-1 values should be NaN\n        for value in output_dm.iter().take(opt_period - 1) {\n            assert!(value.is_nan());\n        }\n\n        // Test subsequent values\n        let expected_values = [\n            165.0,\n            217.014_285_714_288_63,\n            260.513_265_306_125_16,\n            312.905_174_927_116_26,\n            290.554_805_289_465_1,\n            269.800_890_625_931_86,\n            250.529_398_438_365_3,\n            323.234_441_407_052_03,\n            320.746_267_020_832_6,\n            297.835_819_376_487_4,\n            276.561_832_278_166_9,\n            256.807_415_686_869_26,\n            238.464_028_852_092_9,\n            239.430_883_934_086_27,\n            222.328_677_938_794_39,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_dm[i + 13], *expected, epsilon = 0.00001);\n        }\n\n        // Now test incremental calculation matches regular calculation\n        let mut prev_dm = output_dm[14]; // First valid -DM value\n\n        // Test each incremental step\n        for i in 15..19 {\n            let result = minus_dm_inc(\n                input_high[i],\n                input_high[i - 1],\n                input_low[i],\n                input_low[i - 1],\n                prev_dm,\n                opt_period,\n            )\n            .unwrap();\n            assert_relative_eq!(result, output_dm[i], epsilon = 0.0001);\n            prev_dm = result;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/mod.rs",
    "content": "pub mod ad;\npub mod adosc;\npub mod adr;\npub mod adx;\npub mod adxr;\npub mod aroon;\npub mod aroonosc;\npub mod atr;\npub mod bbands;\npub mod bop;\npub mod cci;\npub mod cdl_doji;\npub mod cdl_dragonfly_doji;\npub mod cdl_gravestone_doji;\npub mod cdl_hammer;\npub mod cdl_inverted_hammer;\npub mod cdl_long_shadow;\npub mod cdl_marubozu;\npub mod dema;\npub mod dx;\npub mod ecl;\npub mod ema;\npub mod ha;\npub mod macd;\npub mod medprice;\npub mod mfi;\npub mod midpoint;\npub mod midprice;\npub mod minus_di;\npub mod minus_dm;\npub mod mom;\npub mod natr;\npub mod obv;\npub mod plus_di;\npub mod plus_dm;\npub mod rma;\npub mod roc;\npub mod rocp;\npub mod rocr;\npub mod rocr100;\npub mod rsi;\npub mod sar;\npub mod sma;\npub mod stoch;\npub mod supertrend;\npub mod t3;\npub mod tema;\npub mod trange;\npub mod trima;\npub mod trix;\npub mod typprice;\npub mod vegas;\npub mod vwap;\npub mod wclprice;\npub mod willr;\npub mod wma;\n"
  },
  {
    "path": "kand/src/ta/ohlcv/mom.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for Momentum (MOM) calculation\n///\n/// # Description\n/// The lookback period determines how many data points are needed before the first valid output can be calculated.\n/// For momentum calculation, this equals the momentum period parameter.\n///\n/// # Arguments\n/// * `opt_period` - The number of periods to look back for momentum calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period on success, or error on failure\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2 (when \"check\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::mom;\n///\n/// let period = 14;\n/// let lookback = mom::lookback(period).unwrap();\n/// assert_eq!(lookback, 14);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period)\n}\n\n/// Calculates Momentum (MOM) for an array of prices\n///\n/// # Description\n/// Momentum is a technical indicator that measures the rate of change in price movement by comparing\n/// the current price with the price from n periods ago. It helps identify trend strength and potential\n/// reversals.\n///\n/// # Mathematical Formula\n/// ```text\n/// MOM[i] = Price[i] - Price[i - n]\n/// ```\n/// Where:\n/// * `i` is the current period\n/// * `n` is the momentum period\n///\n/// # Calculation Principles\n/// 1. For each period after the lookback period:\n///    - Subtract the price from n periods ago from the current price\n/// 2. The first n periods are filled with NaN values\n///\n/// # Arguments\n/// * `input_prices` - Array of input price values\n/// * `opt_period` - Number of periods to look back (n)\n/// * `output_mom` - Array to store calculated momentum values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok(()) on success, or error on failure\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input array is empty\n/// * `KandError::LengthMismatch` - If output array length != input array length\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n/// * `KandError::InsufficientData` - If input length < lookback period\n/// * `KandError::NaNDetected` - If any input price is NaN (when \"`check-nan`\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::mom;\n///\n/// let input_prices = vec![2.0, 4.0, 6.0, 8.0, 10.0];\n/// let period = 2;\n/// let mut output_mom = vec![0.0; 5];\n///\n/// mom::mom(&input_prices, period, &mut output_mom).unwrap();\n/// // output_mom = [NaN, NaN, 4.0, 4.0, 4.0]\n/// ```\npub fn mom(\n    input_prices: &[TAFloat],\n    opt_period: usize,\n    output_mom: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_prices.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if output_mom.len() != len {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        for price in input_prices {\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate momentum\n    for i in lookback..len {\n        output_mom[i] = input_prices[i] - input_prices[i - opt_period];\n    }\n\n    // Fill initial values with NAN\n    for item in output_mom.iter_mut().take(lookback) {\n        *item = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the latest Momentum (MOM) value incrementally\n///\n/// # Description\n/// This function provides an optimized way to calculate the latest momentum value\n/// when streaming data is available, without needing the full price history.\n///\n/// # Arguments\n/// * `input_current_price` - The current period's price value\n/// * `input_old_price` - The price value from n periods ago\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The calculated momentum value on success, or error on failure\n///\n/// # Errors\n/// * `KandError::NaNDetected` - If any input price is NaN (when \"`check-nan`\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::mom;\n///\n/// let current_price = 10.0;\n/// let old_price = 6.0;\n/// let momentum = mom::mom_inc(current_price, old_price).unwrap();\n/// assert_eq!(momentum, 4.0);\n/// ```\npub fn mom_inc(\n    input_current_price: TAFloat,\n    input_old_price: TAFloat,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_current_price.is_nan() || input_old_price.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    Ok(input_current_price - input_old_price)\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_mom_calculation() {\n        let input_prices = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0, 35114.5, 35097.2,\n            35092.0, 35073.2, 35139.3,\n        ];\n        let opt_period = 14;\n        let mut output_mom = vec![0.0; input_prices.len()];\n\n        mom(&input_prices, opt_period, &mut output_mom).unwrap();\n\n        // First 14 values should be NaN\n        for value in output_mom.iter().take(14) {\n            assert!(value.is_nan());\n        }\n\n        // Test expected values\n        let expected_values = [\n            -125.8, -180.2, -191.4, -156.6, -112.5, -230.0, -263.3, -299.3, -197.6, -142.9, -95.1,\n            -115.4, -115.3, -68.7, -17.1, 98.1,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_mom[i + 14], *expected, epsilon = 0.1);\n        }\n\n        // Test incremental calculation\n        for i in opt_period..input_prices.len() {\n            let result = mom_inc(input_prices[i], input_prices[i - opt_period]).unwrap();\n            assert_relative_eq!(result, output_mom[i], epsilon = 0.00001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/natr.rs",
    "content": "use super::atr;\nuse crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for NATR calculation\n///\n/// # Description\n/// The lookback period determines how many data points are needed before the first valid NATR value can be calculated.\n///\n/// # Arguments\n/// * `opt_period` - The period used for NATR calculation. Must be >= 2.\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The number of data points needed before first valid output\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::natr;\n/// let period = 14;\n/// let lookback = natr::lookback(period).unwrap();\n/// assert_eq!(lookback, 14);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period)\n}\n\n/// Calculates Normalized Average True Range (NATR) for the entire input array\n///\n/// # Description\n/// NATR is a volatility indicator that expresses ATR as a percentage of closing price,\n/// making it easier to compare volatility across different price levels.\n///\n/// # Mathematical Formula\n/// ```text\n/// TR = max(high - low, |high - prev_close|, |low - prev_close|)\n/// ATR = EMA(TR, period)\n/// NATR = (ATR / close_price) * 100\n/// ```\n///\n/// # Calculation Steps\n/// 1. Calculate True Range (TR) for each period\n/// 2. Calculate ATR using exponential moving average of TR\n/// 3. Normalize ATR by dividing by closing price and multiplying by 100\n///\n/// # Arguments\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of closing prices\n/// * `opt_period` - Period for NATR calculation (must be >= 2)\n/// * `output_natr` - Array to store calculated NATR values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok if calculation succeeds\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input arrays have different lengths\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n/// * `KandError::InsufficientData` - If input length <= lookback period\n/// * `KandError::NaNDetected` - If any input contains NaN values\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::natr;\n///\n/// let high = vec![10.0, 12.0, 15.0, 14.0, 13.0];\n/// let low = vec![8.0, 9.0, 11.0, 10.0, 9.0];\n/// let close = vec![9.0, 11.0, 14.0, 12.0, 11.0];\n/// let period = 3;\n/// let mut natr = vec![0.0; 5];\n///\n/// natr::natr(&high, &low, &close, period, &mut natr).unwrap();\n/// ```\npub fn natr(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_period: usize,\n    output_natr: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_low.len() || len != input_close.len() || len != output_natr.len() {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_high[i].is_nan() || input_low[i].is_nan() || input_close[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate ATR first\n    let mut atr_values = vec![0.0; len];\n    atr::atr(\n        input_high,\n        input_low,\n        input_close,\n        opt_period,\n        &mut atr_values,\n    )?;\n\n    // Calculate NATR = (ATR / Close) * 100\n    for i in lookback..len {\n        output_natr[i] = (atr_values[i] / input_close[i]) * 100.0;\n    }\n\n    // Fill initial values with NAN up to lookback period\n    for value in output_natr.iter_mut().take(lookback) {\n        *value = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the latest NATR value incrementally\n///\n/// # Description\n/// This function provides an optimized way to calculate a single new NATR value\n/// using the previous ATR value and current price data, without recalculating the entire series.\n///\n/// # Mathematical Formula\n/// ```text\n/// TR = max(high - low, |high - prev_close|, |low - prev_close|)\n/// New ATR = ((prev_ATR * (period-1)) + TR) / period\n/// NATR = (ATR / close_price) * 100\n/// ```\n///\n/// # Arguments\n/// * `input_high` - Current period's high price\n/// * `input_low` - Current period's low price\n/// * `input_close` - Current period's closing price\n/// * `prev_close` - Previous period's closing price\n/// * `prev_atr` - Previous period's ATR value\n/// * `opt_period` - Period for NATR calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The calculated NATR value\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n/// * `KandError::NaNDetected` - If any input contains NaN values\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::natr;\n///\n/// let high = 15.0;\n/// let low = 11.0;\n/// let close = 14.0;\n/// let prev_close = 12.0;\n/// let prev_atr = 3.0;\n/// let period = 3;\n///\n/// let natr = natr::natr_inc(high, low, close, prev_close, prev_atr, period).unwrap();\n/// ```\npub fn natr_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    input_close: TAFloat,\n    prev_close: TAFloat,\n    prev_atr: TAFloat,\n    opt_period: usize,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_high.is_nan()\n            || input_low.is_nan()\n            || input_close.is_nan()\n            || prev_close.is_nan()\n            || prev_atr.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let output_atr = atr::atr_inc(input_high, input_low, prev_close, prev_atr, opt_period)?;\n    Ok((output_atr / input_close) * 100.0)\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_natr_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0, 35114.5, 35097.2,\n            35092.0, 35073.2, 35139.3,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35013.4, 34950.1, 34900.0, 34920.0, 34952.6, 35000.0, 35041.8, 35080.0, 35080.0,\n            35070.0, 35050.0, 35092.0,\n        ];\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0, 35114.5, 35097.2,\n            35092.0, 35073.2, 35139.3,\n        ];\n        let opt_period = 14;\n        let mut output_natr = vec![0.0; input_high.len()];\n\n        natr(\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_period,\n            &mut output_natr,\n        )\n        .unwrap();\n\n        // First period values should be NaN\n        for value in output_natr.iter().take(opt_period) {\n            assert!(value.is_nan());\n        }\n\n        // Test expected values\n        let expected_values = [\n            0.180_066_041_856_908_33,\n            0.189_412_602_820_658_08,\n            0.196_012_458_358_289_65,\n            0.192_852_460_649_046_9,\n            0.192_338_093_664_248_45,\n            0.191_633_390_505_224_85,\n            0.199_333_290_479_994_7,\n            0.200_025_731_859_381_86,\n            0.197_384_596_204_178_14,\n            0.197_172_664_957_892_62,\n            0.193_559_020_569_269_05,\n            0.195_301_199_830_777_3,\n            0.195_462_787_749_759_28,\n            0.186_820_266_737_109_55,\n            0.180_106_296_880_325_17,\n            0.188_494_189_518_896_63,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_natr[i + opt_period], *expected, epsilon = 0.1);\n        }\n\n        // Test incremental calculation matches\n        for i in opt_period + 1..input_high.len() {\n            let output_natr_inc = natr_inc(\n                input_high[i],\n                input_low[i],\n                input_close[i],\n                input_close[i - 1],\n                output_natr[i - 1] * input_close[i - 1] / 100.0,\n                opt_period,\n            )\n            .unwrap();\n            assert_relative_eq!(output_natr_inc, output_natr[i], epsilon = 0.00001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/obv.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for On Balance Volume (OBV) calculation\n///\n/// # Description\n/// OBV has no lookback period as it can be calculated from the first data point.\n///\n/// # Returns\n/// * `Ok(usize)` - Returns 0 as OBV can be calculated from first data point\n///\n/// # Errors\n/// This function does not return any errors.\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::obv;\n/// let lookback = obv::lookback().unwrap();\n/// assert_eq!(lookback, 0);\n/// ```\npub const fn lookback() -> Result<usize, KandError> {\n    Ok(0)\n}\n\n/// Calculate On Balance Volume (OBV) for the entire input array\n///\n/// # Description\n/// On Balance Volume (OBV) is a momentum indicator that uses volume flow to predict changes in stock price.\n/// The indicator assumes that volume precedes price movements.\n///\n/// # Mathematical Formula\n/// ```text\n/// If Close[i] > Close[i-1]:\n///     OBV[i] = OBV[i-1] + Volume[i]\n/// If Close[i] < Close[i-1]:\n///     OBV[i] = OBV[i-1] - Volume[i]\n/// If Close[i] = Close[i-1]:\n///     OBV[i] = OBV[i-1]\n/// ```\n///\n/// # Calculation Principles\n/// 1. When volume increases without significant price change:\n///    - Price is expected to jump upward\n/// 2. When volume decreases without significant price change:\n///    - Price is expected to jump downward\n/// 3. First OBV value equals the first volume value\n///\n/// # Arguments\n/// * `input_close` - Array of closing prices\n/// * `input_volume` - Array of volume values\n/// * `output_obv` - Array to store calculated OBV values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok if calculation succeeds, Err otherwise\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input arrays have different lengths\n/// * `KandError::InsufficientData` - If input length <= lookback period\n/// * `KandError::NaNDetected` - If any input value is NaN (when \"`check-nan`\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::obv;\n///\n/// let input_close = vec![10.0, 12.0, 11.0, 13.0];\n/// let input_volume = vec![100.0, 150.0, 120.0, 200.0];\n/// let mut output_obv = vec![0.0; 4];\n///\n/// obv::obv(&input_close, &input_volume, &mut output_obv).unwrap();\n/// // output_obv = [100.0, 250.0, 130.0, 330.0]\n/// ```\npub fn obv(\n    input_close: &[TAFloat],\n    input_volume: &[TAFloat],\n    output_obv: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_close.len();\n    let lookback = lookback()?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_volume.len() || len != output_obv.len() {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_close[i].is_nan() || input_volume[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    let mut obv = input_volume[lookback];\n    output_obv[lookback] = obv;\n\n    for i in (lookback + 1)..len {\n        obv = if input_close[i] > input_close[i - 1] {\n            obv + input_volume[i]\n        } else if input_close[i] < input_close[i - 1] {\n            obv - input_volume[i]\n        } else {\n            obv\n        };\n        output_obv[i] = obv;\n    }\n\n    Ok(())\n}\n\n/// Calculate latest On Balance Volume (OBV) value incrementally\n///\n/// # Description\n/// Calculates a single OBV value using the previous OBV value and current price/volume data.\n///\n/// # Arguments\n/// * `input_curr_close` - Current closing price\n/// * `prev_close` - Previous closing price\n/// * `input_volume` - Current volume\n/// * `prev_obv` - Previous OBV value\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - Latest OBV value if calculation succeeds, Err otherwise\n///\n/// # Errors\n/// * `KandError::NaNDetected` - If any input value is NaN (when \"`check-nan`\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::obv;\n///\n/// let curr_close = 12.0;\n/// let prev_close = 10.0;\n/// let volume = 150.0;\n/// let prev_obv = 100.0;\n///\n/// let output_obv = obv::obv_inc(curr_close, prev_close, volume, prev_obv).unwrap();\n/// // output_obv = 250.0 (prev_obv + volume since price increased)\n/// ```\npub fn obv_inc(\n    input_curr_close: TAFloat,\n    prev_close: TAFloat,\n    input_volume: TAFloat,\n    prev_obv: TAFloat,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_curr_close.is_nan()\n            || prev_close.is_nan()\n            || input_volume.is_nan()\n            || prev_obv.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    Ok(if input_curr_close > prev_close {\n        prev_obv + input_volume\n    } else if input_curr_close < prev_close {\n        prev_obv - input_volume\n    } else {\n        prev_obv\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_obv_calculation() {\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6,\n        ];\n        let input_volume = vec![\n            1055.365, 756.488, 682.152, 1197.747, 425.97, 859.638, 741.925, 888.477, 1043.333,\n            467.901, 387.47, 566.099, 672.296, 834.915, 1854.024, 3670.795, 3761.198, 1605.442,\n            1726.574, 934.713,\n        ];\n        let mut output_obv = vec![0.0; input_close.len()];\n\n        obv(&input_close, &input_volume, &mut output_obv).unwrap();\n\n        let expected_values = [\n            1055.365, 1811.853, 1129.701, -68.046, 357.924, 1217.562, 475.637, 1364.114, 320.781,\n            -147.12, -534.59, 31.509, -640.787, -1475.702, -3329.726, -7000.521, -10761.719,\n            -9156.277, -7429.703, -8364.416,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_obv[i], *expected, epsilon = 0.00001);\n        }\n\n        let mut prev_obv = output_obv[0];\n\n        for i in 1..input_close.len() {\n            let result = obv_inc(\n                input_close[i],\n                input_close[i - 1],\n                input_volume[i],\n                prev_obv,\n            )\n            .unwrap();\n            assert_relative_eq!(result, output_obv[i], epsilon = 0.00001);\n            prev_obv = result;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/plus_di.rs",
    "content": "use super::trange;\nuse crate::{KandError, TAFloat};\n\n/// Returns the lookback period needed for +DI calculation\n///\n/// # Arguments\n/// * `opt_period` - The period parameter for +DI calculation. Must be >= 2.\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The number of data points needed before first valid output\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If period is less than 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::plus_di;\n/// let period = 14;\n/// let lookback = plus_di::lookback(period).unwrap();\n/// assert_eq!(lookback, 14);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period)\n}\n\n/// Calculates Plus Directional Indicator (+DI) values for the entire input array\n///\n/// The Plus Directional Indicator (+DI) measures the strength of upward price movement.\n/// It is a key component of the Directional Movement System developed by J. Welles Wilder.\n///\n/// # Calculation Principles\n/// 1. Identifies upward price movement by comparing consecutive highs and lows\n/// 2. Uses Wilder's smoothing method to reduce noise\n/// 3. Normalizes the indicator to a percentage scale (0-100)\n///\n/// # Mathematical Formula\n/// ```text\n/// 1. Plus Directional Movement (+DM):\n///    +DM = high[i] - high[i-1] if:\n///          - high[i] - high[i-1] > low[i-1] - low[i] AND\n///          - high[i] - high[i-1] > 0\n///          Otherwise, +DM = 0\n///\n/// 2. True Range (TR):\n///    TR = max(high - low, |high - prev_close|, |low - prev_close|)\n///\n/// 3. Initial Values:\n///    First +DI = 100 * SMA(+DM, period) / SMA(TR, period)\n///\n/// 4. Subsequent Values (Wilder's Smoothing):\n///    Smoothed +DM = ((prev_smoothed_+DM * (period-1)) + current_+DM) / period\n///    Smoothed TR = ((prev_smoothed_TR * (period-1)) + current_TR) / period\n///    +DI = 100 * Smoothed_+DM / Smoothed_TR\n/// ```\n///\n/// # Arguments\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of closing prices\n/// * `opt_period` - Smoothing period (>= 2)\n/// * `output_plus_di` - Output array for +DI values\n/// * `output_smoothed_plus_dm` - Output array for smoothed +DM values\n/// * `output_smoothed_tr` - Output array for smoothed TR values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok if calculation succeeds\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input/output arrays have different lengths\n/// * `KandError::InvalidParameter` - If period < 2\n/// * `KandError::InsufficientData` - If input length <= lookback period\n/// * `KandError::NaNDetected` - If any input contains NaN (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::plus_di;\n///\n/// let high = vec![10.0, 12.0, 11.5, 11.0];\n/// let low = vec![9.0, 10.0, 10.0, 9.5];\n/// let close = vec![9.5, 11.0, 10.5, 10.0];\n/// let period = 2;\n/// let mut plus_di = vec![0.0; 4];\n/// let mut smoothed_plus_dm = vec![0.0; 4];\n/// let mut smoothed_tr = vec![0.0; 4];\n///\n/// plus_di::plus_di(\n///     &high,\n///     &low,\n///     &close,\n///     period,\n///     &mut plus_di,\n///     &mut smoothed_plus_dm,\n///     &mut smoothed_tr,\n/// )\n/// .unwrap();\n/// ```\npub fn plus_di(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_period: usize,\n    output_plus_di: &mut [TAFloat],\n    output_smoothed_plus_dm: &mut [TAFloat],\n    output_smoothed_tr: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_low.len()\n            || len != input_close.len()\n            || len != output_plus_di.len()\n            || len != output_smoothed_plus_dm.len()\n            || len != output_smoothed_tr.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_high[i].is_nan() || input_low[i].is_nan() || input_close[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate initial +DM and TR sums\n    let mut plus_dm_sum = 0.0;\n    let mut tr_sum = 0.0;\n    let mut prev_high = input_high[0];\n    let mut prev_low = input_low[0];\n    let mut prev_close = input_close[0];\n\n    // Calculate first period-1 +DM1 and TR1 values\n    for i in 1..opt_period {\n        let high_diff = input_high[i] - prev_high;\n        let low_diff = prev_low - input_low[i];\n\n        let plus_dm1 = if high_diff > low_diff && high_diff > 0.0 {\n            high_diff\n        } else {\n            0.0\n        };\n\n        plus_dm_sum += plus_dm1;\n\n        let tr1 = trange::trange_inc(input_high[i], input_low[i], prev_close)?;\n        tr_sum += tr1;\n\n        prev_high = input_high[i];\n        prev_low = input_low[i];\n        prev_close = input_close[i];\n    }\n\n    // Calculate first +DI value\n    let hundred = 100.0;\n    let period_t = opt_period as TAFloat;\n\n    // Initialize smoothed values\n    let mut curr_smoothed_plus_dm = plus_dm_sum;\n    let mut curr_smoothed_tr = tr_sum;\n\n    // Calculate remaining +DI values using Wilder's smoothing\n    for i in opt_period..len {\n        let high_diff = input_high[i] - input_high[i - 1];\n        let low_diff = input_low[i - 1] - input_low[i];\n\n        let plus_dm1 = if high_diff > low_diff && high_diff > 0.0 {\n            high_diff\n        } else {\n            0.0\n        };\n\n        let tr1 = trange::trange_inc(input_high[i], input_low[i], input_close[i - 1])?;\n\n        // Apply Wilder's smoothing\n        curr_smoothed_plus_dm =\n            curr_smoothed_plus_dm - (curr_smoothed_plus_dm / period_t) + plus_dm1;\n        curr_smoothed_tr = curr_smoothed_tr - (curr_smoothed_tr / period_t) + tr1;\n\n        output_smoothed_plus_dm[i] = curr_smoothed_plus_dm;\n        output_smoothed_tr[i] = curr_smoothed_tr;\n\n        output_plus_di[i] = if curr_smoothed_tr == 0.0 {\n            0.0\n        } else {\n            hundred * curr_smoothed_plus_dm / curr_smoothed_tr\n        };\n    }\n\n    // Fill initial values with NAN\n    for i in 0..lookback {\n        output_plus_di[i] = TAFloat::NAN;\n        output_smoothed_plus_dm[i] = TAFloat::NAN;\n        output_smoothed_tr[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the latest +DI value incrementally using previous smoothed values\n///\n/// This function enables real-time calculation of +DI by using the previous smoothed values\n/// and current price data, avoiding the need to recalculate the entire series.\n///\n/// # Mathematical Formula\n/// ```text\n/// 1. Current +DM:\n///    +DM = high - prev_high if:\n///          - (high - prev_high) > (prev_low - low) AND\n///          - (high - prev_high) > 0\n///          Otherwise, +DM = 0\n///\n/// 2. Current TR:\n///    TR = max(high - low, |high - prev_close|, |low - prev_close|)\n///\n/// 3. Wilder's Smoothing:\n///    smoothed_+DM = ((prev_smoothed_+DM * (period-1)) + current_+DM) / period\n///    smoothed_TR = ((prev_smoothed_TR * (period-1)) + current_TR) / period\n///\n/// 4. +DI:\n///    +DI = 100 * smoothed_+DM / smoothed_TR\n/// ```\n///\n/// # Arguments\n/// * `input_high` - Current high price\n/// * `input_low` - Current low price\n/// * `prev_high` - Previous high price\n/// * `prev_low` - Previous low price\n/// * `prev_close` - Previous close price\n/// * `prev_smoothed_plus_dm` - Previous smoothed +DM value\n/// * `prev_smoothed_tr` - Previous smoothed TR value\n/// * `opt_period` - Smoothing period (>= 2)\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat), KandError>` - Tuple of (latest +DI, new smoothed +DM, new smoothed TR)\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If period < 2\n/// * `KandError::NaNDetected` - If any input contains NaN (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::plus_di;\n///\n/// let (plus_di, smoothed_plus_dm, smoothed_tr) = plus_di::plus_di_inc(\n///     10.5, // high\n///     9.5,  // low\n///     10.0, // prev_high\n///     9.0,  // prev_low\n///     9.5,  // prev_close\n///     15.0, // prev_smoothed_plus_dm\n///     20.0, // prev_smoothed_tr\n///     14,   // period\n/// )\n/// .unwrap();\n/// ```\npub fn plus_di_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    prev_close: TAFloat,\n    prev_smoothed_plus_dm: TAFloat,\n    prev_smoothed_tr: TAFloat,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_high.is_nan()\n            || input_low.is_nan()\n            || prev_high.is_nan()\n            || prev_low.is_nan()\n            || prev_close.is_nan()\n            || prev_smoothed_plus_dm.is_nan()\n            || prev_smoothed_tr.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let high_diff = input_high - prev_high;\n    let low_diff = prev_low - input_low;\n\n    let plus_dm = if high_diff > low_diff && high_diff > 0.0 {\n        high_diff\n    } else {\n        0.0\n    };\n\n    let tr = trange::trange_inc(input_high, input_low, prev_close)?;\n    let period_t = opt_period as TAFloat;\n\n    let output_smoothed_plus_dm =\n        prev_smoothed_plus_dm - (prev_smoothed_plus_dm / period_t) + plus_dm;\n    let output_smoothed_tr = prev_smoothed_tr - (prev_smoothed_tr / period_t) + tr;\n\n    let output_plus_di = if output_smoothed_tr == 0.0 {\n        0.0\n    } else {\n        100.0 * output_smoothed_plus_dm / output_smoothed_tr\n    };\n\n    Ok((output_plus_di, output_smoothed_plus_dm, output_smoothed_tr))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    // Basic functionality tests\n    #[test]\n    fn test_plus_di_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0,\n        ];\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0,\n        ];\n\n        let opt_period = 14;\n        let mut output_plus_di = vec![0.0; input_high.len()];\n        let mut output_smoothed_plus_dm = vec![0.0; input_high.len()];\n        let mut output_smoothed_tr = vec![0.0; input_high.len()];\n\n        plus_di(\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_period,\n            &mut output_plus_di,\n            &mut output_smoothed_plus_dm,\n            &mut output_smoothed_tr,\n        )\n        .unwrap();\n\n        // Check first valid value\n        assert_relative_eq!(\n            output_plus_di[14],\n            17.333_631_354_074_154,\n            epsilon = 0.00001\n        );\n        assert_relative_eq!(output_plus_di[15], 15.208_643_871_406_93, epsilon = 0.00001);\n        assert_relative_eq!(\n            output_plus_di[16],\n            13.584_828_258_444_833,\n            epsilon = 0.00001\n        );\n        assert_relative_eq!(\n            output_plus_di[17],\n            12.779_367_774_255_537,\n            epsilon = 0.00001\n        );\n        assert_relative_eq!(output_plus_di[18], 18.396_699_555_422_47, epsilon = 0.00001);\n\n        // Now test incremental calculation matches regular calculation\n        let mut prev_smoothed_plus_dm = output_smoothed_plus_dm[14]; // First valid smoothed +DM value\n        let mut prev_smoothed_tr = output_smoothed_tr[14]; // First valid smoothed TR value\n\n        // Test each incremental step\n        for i in 15..19 {\n            let (plus_di, new_smoothed_plus_dm, new_smoothed_tr) = plus_di_inc(\n                input_high[i],\n                input_low[i],\n                input_high[i - 1],\n                input_low[i - 1],\n                input_close[i - 1],\n                prev_smoothed_plus_dm,\n                prev_smoothed_tr,\n                opt_period,\n            )\n            .unwrap();\n            assert_relative_eq!(plus_di, output_plus_di[i], epsilon = 0.00001);\n            prev_smoothed_plus_dm = new_smoothed_plus_dm;\n            prev_smoothed_tr = new_smoothed_tr;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/plus_dm.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for Plus DM calculation\n///\n/// # Description\n/// Calculates the number of data points needed before the first valid output value can be generated.\n///\n/// # Arguments\n/// * `opt_period` - The period parameter for Plus DM calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period value if successful\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::plus_dm;\n/// let period = 14;\n/// let lookback = plus_dm::lookback(period).unwrap();\n/// assert_eq!(lookback, 13);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Calculates Plus Directional Movement (+DM) for the entire input array\n///\n/// # Description\n/// Plus Directional Movement (+DM) is a component of the Directional Movement System developed by J. Welles Wilder.\n/// It measures the strength of upward price movement by comparing consecutive highs and lows.\n///\n/// # Mathematical Formula\n/// ```text\n/// 1. Calculate +DM1 (one period directional movement):\n///    If (High[i] - High[i-1]) > (Low[i-1] - Low[i]) AND (High[i] - High[i-1]) > 0:\n///        +DM1 = High[i] - High[i-1]\n///    Else:\n///        +DM1 = 0\n///\n/// 2. Initial +DM:\n///    First +DM = Sum(+DM1, period)\n///\n/// 3. Subsequent +DM using Wilder's smoothing:\n///    +DM[i] = +DM[i-1] - (+DM[i-1]/period) + +DM1\n/// ```\n///\n/// # Arguments\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `opt_period` - The smoothing period (must be >= 2)\n/// * `output_dm` - Array to store calculated +DM values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty Ok value if calculation succeeds\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input arrays have different lengths\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n/// * `KandError::InsufficientData` - If input length < lookback period\n/// * `KandError::NaNDetected` - If any input value is NaN (when \"`check-nan`\" feature enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::plus_dm;\n///\n/// let high = vec![10.0, 11.0, 12.0, 11.5, 10.9];\n/// let low = vec![9.0, 9.5, 10.0, 9.8, 9.2];\n/// let period = 3;\n/// let mut output = vec![0.0; 5];\n///\n/// plus_dm::plus_dm(&high, &low, period, &mut output).unwrap();\n/// ```\npub fn plus_dm(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    opt_period: usize,\n    output_dm: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_low.len() || len != output_dm.len() {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_high[i].is_nan() || input_low[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate first +DM values and initial +DM (sum of +DM1)\n    let mut dm_sum = 0.0;\n\n    for i in 1..opt_period {\n        let high_diff = input_high[i] - input_high[i - 1];\n        let low_diff = input_low[i - 1] - input_low[i];\n\n        let dm = if high_diff > low_diff && high_diff > 0.0 {\n            high_diff\n        } else {\n            0.0\n        };\n        dm_sum += dm;\n    }\n    output_dm[lookback] = dm_sum;\n\n    // Calculate remaining +DM values using Wilder's smoothing\n    for i in opt_period..len {\n        let high_diff = input_high[i] - input_high[i - 1];\n        let low_diff = input_low[i - 1] - input_low[i];\n\n        let dm = if high_diff > low_diff && high_diff > 0.0 {\n            high_diff\n        } else {\n            0.0\n        };\n\n        output_dm[i] = output_dm[i - 1] - (output_dm[i - 1] / opt_period as TAFloat) + dm;\n    }\n\n    // Fill initial values with NAN\n    for value in output_dm.iter_mut().take(lookback) {\n        *value = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the latest Plus DM value incrementally\n///\n/// # Description\n/// This function provides an optimized way to calculate the latest Plus DM value using the previous\n/// Plus DM value and current price data. It implements Wilder's smoothing formula for real-time calculation.\n///\n/// # Mathematical Formula\n/// ```text\n/// +DM[today] = +DM[yesterday] - (+DM[yesterday]/period) + +DM1\n///\n/// where +DM1 is:\n/// If (High[today] - High[yesterday]) > (Low[yesterday] - Low[today]) AND (High[today] - High[yesterday]) > 0:\n///     +DM1 = High[today] - High[yesterday]\n/// Else:\n///     +DM1 = 0\n/// ```\n///\n/// # Arguments\n/// * `input_high` - Current high price\n/// * `prev_high` - Previous high price\n/// * `input_low` - Current low price\n/// * `prev_low` - Previous low price\n/// * `prev_plus_dm` - Previous Plus DM value\n/// * `opt_period` - The smoothing period (must be >= 2)\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The latest Plus DM value if successful\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n/// * `KandError::NaNDetected` - If any input value is NaN (when \"`check-nan`\" feature enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::plus_dm;\n///\n/// let high = 10.5;\n/// let prev_high = 10.0;\n/// let low = 9.8;\n/// let prev_low = 9.5;\n/// let prev_plus_dm = 0.45;\n/// let period = 14;\n///\n/// let new_plus_dm =\n///     plus_dm::plus_dm_inc(high, prev_high, low, prev_low, prev_plus_dm, period).unwrap();\n/// ```\npub fn plus_dm_inc(\n    input_high: TAFloat,\n    prev_high: TAFloat,\n    input_low: TAFloat,\n    prev_low: TAFloat,\n    prev_plus_dm: TAFloat,\n    opt_period: usize,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_high.is_nan()\n            || prev_high.is_nan()\n            || input_low.is_nan()\n            || prev_low.is_nan()\n            || prev_plus_dm.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let high_diff = input_high - prev_high;\n    let low_diff = prev_low - input_low;\n\n    let dm = if high_diff > low_diff && high_diff > 0.0 {\n        high_diff\n    } else {\n        0.0\n    };\n\n    Ok(prev_plus_dm - (prev_plus_dm / opt_period as TAFloat) + dm)\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    // Basic functionality tests\n    #[test]\n    fn test_plus_dm_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4, 35150.4, 35123.9,\n            35110.0, 35092.1, 35179.2,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0, 35073.0, 35055.0,\n            35084.0, 35060.0, 35073.1,\n        ];\n        let opt_period = 14;\n        let mut output_dm = vec![0.0; input_high.len()];\n\n        plus_dm(&input_high, &input_low, opt_period, &mut output_dm).unwrap();\n\n        // First period-1 values should be NaN\n        for value in output_dm.iter().take(opt_period - 1) {\n            assert!(value.is_nan());\n        }\n\n        // Test subsequent values\n        let expected = [\n            155.099_999_999_998_54,\n            144.021_428_571_427_2,\n            133.734_183_673_468_12,\n            124.181_741_982_506_11,\n            115.311_617_555_184_24,\n            166.375_073_444_102_55,\n            160.691_139_626_663_76,\n            149.213_201_081_902_07,\n            138.555_115_290_337_62,\n            155.058_321_341_029_26,\n            180.282_726_959_522_8,\n            211.705_389_319_559_8,\n            255.583_575_796_734_1,\n            237.327_606_096_967_37,\n            220.375_634_232_898_28,\n            204.634_517_501_976_97,\n            277.117_766_251_834_27,\n        ];\n\n        for (i, expected_value) in expected.iter().enumerate() {\n            assert_relative_eq!(output_dm[i + 13], *expected_value, epsilon = 0.00001);\n        }\n\n        // Now test incremental calculation matches regular calculation\n        let mut prev_dm = output_dm[14]; // First valid +DM value\n\n        // Test each incremental step\n        for i in 15..19 {\n            let result = plus_dm_inc(\n                input_high[i],\n                input_high[i - 1],\n                input_low[i],\n                input_low[i - 1],\n                prev_dm,\n                opt_period,\n            )\n            .unwrap();\n            assert_relative_eq!(result, output_dm[i], epsilon = 0.0001);\n            prev_dm = result;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/rma.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Calculates the lookback period required for RMA calculation.\n///\n/// Returns the number of data points needed before RMA can start producing valid values.\n/// The lookback period equals the period minus 1, since RMA requires a full period of data\n/// to calculate the first value.\n///\n/// # Arguments\n/// * `opt_period` - The period length used for RMA calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period on success\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If period is less than 2\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::rma;\n/// let period = 14;\n/// let lookback = rma::lookback(period).unwrap();\n/// assert_eq!(lookback, 13); // lookback is period - 1\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Calculates the Running Moving Average (RMA) for a price series.\n///\n/// RMA is a type of moving average that gives more weight to recent prices while still maintaining\n/// some influence from all past prices. It is similar to EMA but uses a different smoothing factor.\n///\n/// # Mathematical Formula\n/// ```text\n/// RMA = (Current Price * α) + Previous RMA * (1 - α)\n/// where α = 1/period\n/// ```\n///\n/// # Calculation Steps\n/// 1. Calculate initial SMA value using first `period` prices\n/// 2. For remaining values, apply RMA formula using smoothing factor α = 1/period\n/// 3. Fill initial values before period with NaN\n///\n/// # Arguments\n/// * `input` - Array of price values to calculate RMA\n/// * `opt_period` - The smoothing period (must be >= 2)\n/// * `output_rma` - Array to store calculated RMA values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty result on success\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input array is empty\n/// * `KandError::LengthMismatch` - If input and output arrays have different lengths\n/// * `KandError::InvalidParameter` - If period is less than 2\n/// * `KandError::InsufficientData` - If input length is less than period\n/// * `KandError::NaNDetected` - If input contains NaN values (with \"`check-nan`\" feature)\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::rma;\n/// let prices = vec![1.0, 2.0, 3.0, 4.0, 5.0];\n/// let period = 3;\n/// let mut rma_values = vec![0.0; 5];\n/// rma::rma(&prices, period, &mut rma_values).unwrap();\n/// ```\npub fn rma(\n    input: &[TAFloat],\n    opt_period: usize,\n    output_rma: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != output_rma.len() {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for price in input.iter().take(len) {\n            // NaN check\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate first SMA value\n    let mut sum = input[0];\n    for value in input.iter().take(opt_period).skip(1) {\n        sum += *value;\n    }\n    let alpha = 1.0 / opt_period as TAFloat;\n    output_rma[opt_period - 1] = sum / opt_period as TAFloat;\n\n    // Calculate RMA for remaining values\n    for i in opt_period..input.len() {\n        output_rma[i] = input[i].mul_add(alpha, output_rma[i - 1] * (1.0 - alpha));\n    }\n\n    // Fill initial values with NAN\n    for value in output_rma.iter_mut().take(opt_period - 1) {\n        *value = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates a single new RMA value incrementally.\n///\n/// This function enables real-time RMA calculation by computing the next value\n/// using only the current price and previous RMA, without requiring historical data.\n///\n/// # Mathematical Formula\n/// ```text\n/// RMA = (Current Price * α) + Previous RMA * (1 - α)\n/// where α = 1/period\n/// ```\n///\n/// # Arguments\n/// * `input_current` - The current price value\n/// * `prev_rma` - The previous RMA value\n/// * `opt_period` - The smoothing period (must be >= 2)\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The new RMA value on success\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If period is less than 2\n/// * `KandError::NaNDetected` - If any input is NaN (with \"`check-nan`\" feature)\n///\n/// # Examples\n/// ```\n/// use kand::ohlcv::rma;\n/// let current_price = 10.0;\n/// let prev_rma = 9.5;\n/// let period = 14;\n/// let new_rma = rma::rma_inc(current_price, prev_rma, period).unwrap();\n/// ```\npub fn rma_inc(\n    input_current: TAFloat,\n    prev_rma: TAFloat,\n    opt_period: usize,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_current.is_nan() || prev_rma.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let alpha = 1.0 / opt_period as TAFloat;\n    Ok(input_current.mul_add(alpha, prev_rma * (1.0 - alpha)))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_rma_calculation() {\n        let input = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];\n        let opt_period = 5;\n        let mut output_rma = vec![0.0; input.len()];\n\n        rma(&input, opt_period, &mut output_rma).unwrap();\n\n        // First (period-1) values should be NaN\n        for value in output_rma.iter().take(opt_period - 1) {\n            assert!(value.is_nan());\n        }\n\n        // First valid value should be SMA of first 5 values\n        assert_relative_eq!(output_rma[4], 3.0, epsilon = 1e-12); // (1+2+3+4+5)/5 = 3.0\n\n        // Subsequent values follow RMA formula with alpha = 1/5 = 0.2\n        // RMA[5] = 6.0*0.2 + 3.0*0.8 = 1.2 + 2.4 = 3.6\n        assert_relative_eq!(output_rma[5], 3.6, epsilon = 1e-12);\n\n        // RMA[6] = 7.0*0.2 + 3.6*0.8 = 1.4 + 2.88 = 4.28\n        assert_relative_eq!(output_rma[6], 4.28, epsilon = 1e-12);\n\n        // RMA[7] = 8.0*0.2 + 4.28*0.8 = 1.6 + 3.424 = 5.024\n        assert_relative_eq!(output_rma[7], 5.024, epsilon = 1e-12);\n\n        // RMA[8] = 9.0*0.2 + 5.024*0.8 = 1.8 + 4.0192 = 5.8192\n        assert_relative_eq!(output_rma[8], 5.8192, epsilon = 1e-12);\n\n        // RMA[9] = 10.0*0.2 + 5.8192*0.8 = 2.0 + 4.65536 = 6.65536\n        assert_relative_eq!(output_rma[9], 6.65536, epsilon = 1e-12);\n    }\n\n    #[test]\n    fn test_rma_incremental() {\n        let input = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];\n        let opt_period = 4;\n        let mut output_rma = vec![0.0; input.len()];\n\n        rma(&input, opt_period, &mut output_rma).unwrap();\n\n        // Test incremental calculation matches regular calculation\n        // Start from the first valid RMA value (after the lookback period)\n        let lookback = lookback(opt_period).unwrap();\n\n        // Start with the first valid RMA value\n        let mut prev_rma = output_rma[lookback];\n\n        // Test each incremental step\n        for i in lookback + 1..input.len() {\n            // Calculate next RMA using incremental method\n            let next_rma = rma_inc(input[i], prev_rma, opt_period).unwrap();\n\n            // Verify incremental result matches the regular calculation\n            assert_relative_eq!(next_rma, output_rma[i], epsilon = 1e-12);\n\n            // Update prev_rma for next iteration\n            prev_rma = next_rma;\n        }\n    }\n\n    #[test]\n    fn test_rma_edge_cases() {\n        // Test edge case: period = 2 (minimum allowed)\n        let input = vec![10.0, 20.0, 30.0, 40.0];\n        let period = 2;\n        let mut output = vec![0.0; input.len()];\n\n        rma(&input, period, &mut output).unwrap();\n\n        // Verify first value is NaN (lookback = 1)\n        assert!(output[0].is_nan());\n\n        // First valid value should be SMA of first 2 values\n        assert_relative_eq!(output[1], 15.0, epsilon = 1e-12); // (10+20)/2 = 15\n\n        // RMA[2] = 30*0.5 + 15*0.5 = 15 + 7.5 = 22.5\n        assert_relative_eq!(output[2], 22.5, epsilon = 1e-12);\n\n        // RMA[3] = 40*0.5 + 22.5*0.5 = 20 + 11.25 = 31.25\n        assert_relative_eq!(output[3], 31.25, epsilon = 1e-12);\n    }\n\n    #[test]\n    fn test_rma_with_extended_data() {\n        // More extensive test with a larger dataset\n        let prices = vec![\n            35.25, 35.70, 36.10, 36.25, 36.50, 36.75, 36.70, 36.55, 36.80, 36.90, 37.05, 37.15,\n            37.25, 37.40, 37.50, 37.60, 37.55, 37.35, 37.20, 37.10,\n        ];\n        let period = 7;\n        let mut output_full = vec![0.0; prices.len()];\n\n        // Calculate RMA using the rma function\n        rma(&prices, period, &mut output_full).unwrap();\n\n        // Manually compute the full expected RMA for comparison\n        let alpha = 1.0 / period as TAFloat;\n        let mut expected_rma = vec![TAFloat::NAN; prices.len()];\n\n        // First valid value is the SMA of the first `period` elements\n        let mut sum = 0.0;\n        for i in 0..period {\n            sum += prices[i];\n        }\n        expected_rma[period - 1] = sum / period as TAFloat;\n\n        // Remaining values follow the RMA formula\n        for i in period..prices.len() {\n            expected_rma[i] = prices[i].mul_add(alpha, expected_rma[i - 1] * (1.0 - alpha));\n        }\n\n        // Check NaNs for the first (period-1) values\n        for i in 0..period - 1 {\n            assert!(output_full[i].is_nan());\n        }\n\n        // Assert all subsequent values match expected RMA\n        for i in period - 1..prices.len() {\n            assert_relative_eq!(output_full[i], expected_rma[i], epsilon = 1e-12);\n        }\n\n        // Also confirm incremental RMA matches\n        let lookback = lookback(period).unwrap();\n        let mut prev_rma = output_full[lookback];\n        for i in lookback + 1..prices.len() {\n            let next_rma = rma_inc(prices[i], prev_rma, period).unwrap();\n            assert_relative_eq!(next_rma, output_full[i], epsilon = 1e-12);\n            prev_rma = next_rma;\n        }\n    }\n\n    #[test]\n    fn test_rma_error_conditions() {\n        let input = vec![1.0, 2.0, 3.0];\n        let mut output = vec![0.0; 3];\n\n        // Test invalid period\n        assert!(matches!(\n            rma(&input, 1, &mut output),\n            Err(KandError::InvalidParameter)\n        ));\n\n        // Test length mismatch\n        let mut short_output = vec![0.0; 2];\n        assert!(matches!(\n            rma(&input, 2, &mut short_output),\n            Err(KandError::LengthMismatch)\n        ));\n\n        // Test insufficient data\n        assert!(matches!(\n            rma(&input, 4, &mut output),\n            Err(KandError::InsufficientData)\n        ));\n\n        // Test empty data\n        let empty: Vec<TAFloat> = vec![];\n        let mut empty_output: Vec<TAFloat> = vec![];\n        assert!(matches!(\n            rma(&empty, 2, &mut empty_output),\n            Err(KandError::InvalidData)\n        ));\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/roc.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for ROC (Rate of Change) calculation\n///\n/// # Description\n/// Calculates the minimum number of data points needed before the first valid ROC value can be computed.\n/// The lookback period equals the ROC period parameter since we need that many previous prices to calculate\n/// the first value.\n///\n/// # Parameters\n/// * `opt_period` - The time period used for ROC calculation (usize)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period if parameters are valid\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 1 (when \"check\" feature enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::roc;\n/// let lookback = roc::lookback(14).unwrap();\n/// assert_eq!(lookback, 14);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 1 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period)\n}\n\n/// Calculates Rate of Change (ROC) technical indicator for a price series\n///\n/// # Description\n/// The Rate of Change (ROC) is a momentum oscillator that measures the percentage change in price\n/// between the current price and the price n periods ago. ROC indicates both the speed and magnitude\n/// of price movements, making it useful for identifying overbought/oversold conditions and divergences.\n///\n/// # Mathematical Formula\n/// ```text\n/// ROC = ((Current Price - Price n periods ago) / Price n periods ago) * 100\n/// ```\n///\n/// # Calculation Principles\n/// 1. For each data point after the lookback period:\n///    - Take current price and price from n periods ago\n///    - Calculate percentage change between these prices\n///    - Multiply by 100 to get percentage value\n/// 2. Initial values within lookback period are set to NaN\n///\n/// # Parameters\n/// * `input_price` - Array of price values (slice of type `TAFloat`)\n/// * `opt_period` - Number of periods to look back (usize)\n/// * `output_roc` - Array to store calculated ROC values, must be same length as `input_price` (mutable slice of type `TAFloat`)\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok(()) if calculation succeeds\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input array is empty\n/// * `KandError::LengthMismatch` - If input and output arrays have different lengths\n/// * `KandError::InvalidParameter` - If `opt_period` < 1\n/// * `KandError::InsufficientData` - If input length <= lookback period\n/// * `KandError::NaNDetected` - If input contains NaN values (with \"`check-nan`\")\n/// * `KandError::InvalidData` - If division by zero occurs (with \"`check-nan`\")\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::roc;\n///\n/// let input_price = vec![10.0, 10.5, 11.2, 10.8, 11.5];\n/// let opt_period = 2;\n/// let mut output_roc = vec![0.0; 5];\n///\n/// roc::roc(&input_price, opt_period, &mut output_roc).unwrap();\n/// ```\npub fn roc(\n    input_price: &[TAFloat],\n    opt_period: usize,\n    output_roc: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_price.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != output_roc.len() {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for price in input_price {\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate ROC values\n    for i in lookback..len {\n        let current_price = input_price[i];\n        let prev_price = input_price[i - opt_period];\n\n        #[cfg(feature = \"check-nan\")]\n        {\n            if prev_price == 0.0 {\n                return Err(KandError::InvalidData);\n            }\n        }\n\n        output_roc[i] = (current_price - prev_price) / prev_price * 100.0;\n    }\n\n    // Fill initial values with NAN\n    for value in output_roc.iter_mut().take(lookback) {\n        *value = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates a single ROC value incrementally for streaming data\n///\n/// # Description\n/// Provides an optimized way to calculate the latest ROC value when new data arrives,\n/// without recalculating the entire series. This is particularly useful for real-time\n/// data processing and streaming applications.\n///\n/// # Mathematical Formula\n/// ```text\n/// ROC = ((Current Price - Price n periods ago) / Price n periods ago) * 100\n/// ```\n///\n/// # Parameters\n/// * `current_price` - The most recent price value (type `TAFloat`)\n/// * `prev_price` - The price from n periods ago (type `TAFloat`)\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The calculated ROC value if successful\n///\n/// # Errors\n/// * `KandError::NaNDetected` - If either input is NaN (with \"`check-nan`\")\n/// * `KandError::InvalidData` - If `prev_price` is zero (with \"`check-nan`\")\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::roc::roc_inc;\n///\n/// let current_price = 11.5;\n/// let prev_price = 10.0;\n///\n/// let roc_value = roc_inc(current_price, prev_price).unwrap();\n/// assert_eq!(roc_value, 15.0); // ((11.5 - 10.0) / 10.0) * 100\n/// ```\npub fn roc_inc(current_price: TAFloat, prev_price: TAFloat) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if current_price.is_nan() || prev_price.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n        // Division by zero check\n        if prev_price == 0.0 {\n            return Err(KandError::InvalidData);\n        }\n    }\n\n    Ok((current_price - prev_price) / prev_price * 100.0)\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_roc_calculation() {\n        let input_price = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6,\n        ];\n        let opt_period = 14;\n        let mut output_roc = vec![0.0; input_price.len()];\n\n        roc(&input_price, opt_period, &mut output_roc).unwrap();\n\n        // First 13 values should be NaN\n        for value in output_roc.iter().take(14) {\n            assert!(value.is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            -0.357_222_974_718_940_4,\n            -0.511_620_776_005_505_8,\n            -0.543_893_699_187_547_6,\n            -0.445_265_851_578_047_2,\n            -0.319_770_333_840_230_24,\n            -0.652_397_133_991_022_8,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_roc[i + 14], *expected, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation matches regular calculation\n        for i in 15..20 {\n            let result = roc_inc(input_price[i], input_price[i - opt_period]).unwrap();\n            assert_relative_eq!(result, output_roc[i], epsilon = 0.0001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/rocp.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Returns the lookback period for Rate of Change Percentage (ROCP) calculation.\n///\n/// # Description\n/// Calculates the minimum number of data points needed before the first valid ROCP value can be computed.\n/// The lookback period equals the ROCP period parameter since we need that many previous prices to calculate\n/// the first value.\n///\n/// # Arguments\n/// * `opt_period` - The time period used for ROCP calculation (usize)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period if parameters are valid\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 1 (when \"check\" feature enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::rocp;\n/// let lookback = rocp::lookback(14).unwrap();\n/// assert_eq!(lookback, 14);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 1 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period)\n}\n\n/// Calculates Rate of Change Percentage (ROCP) technical indicator for a price series.\n///\n/// # Description\n/// The Rate of Change Percentage (ROCP) is a momentum indicator that measures the percentage change\n/// between the current price and a price n periods ago. ROCP indicates both the speed and magnitude\n/// of price movements, making it useful for identifying overbought/oversold conditions and divergences.\n///\n/// # Mathematical Formula\n/// ```text\n/// ROCP = (Current Price - Price n periods ago) / Price n periods ago\n/// ```\n///\n/// # Calculation Principles\n/// 1. For each data point after the lookback period:\n///    - Take current price and price from n periods ago\n///    - Calculate percentage change between these prices\n/// 2. Initial values within lookback period are set to NaN\n///\n/// # Arguments\n/// * `input_price` - Array of price values\n/// * `opt_period` - Number of periods to look back, must be >= 1\n/// * `output_rocp` - Array to store calculated ROCP values. Must be same length as `input_price`.\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok if calculation succeeds, Err otherwise\n///\n/// # Errors\n/// Returns error if:\n/// * Input arrays are empty (with \"check\" feature)\n/// * Input and output arrays have different lengths (with \"check\" feature)\n/// * `opt_period` < 1 (with \"check\" feature)\n/// * Insufficient data points (with \"check\" feature)\n/// * Input contains NaN values (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::rocp;\n///\n/// let input_price = vec![10.0, 10.5, 11.2, 10.8, 11.5];\n/// let opt_period = 2;\n/// let mut output_rocp = vec![0.0; 5];\n///\n/// rocp::rocp(&input_price, opt_period, &mut output_rocp).unwrap();\n/// ```\npub fn rocp(\n    input_price: &[TAFloat],\n    opt_period: usize,\n    output_rocp: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_price.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != output_rocp.len() {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for price in input_price {\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate ROCP values\n    for i in lookback..len {\n        output_rocp[i] =\n            (input_price[i] - input_price[i - opt_period]) / input_price[i - opt_period];\n    }\n\n    // Fill initial values with NAN\n    for value in output_rocp.iter_mut().take(lookback) {\n        *value = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates a single ROCP value incrementally.\n///\n/// # Description\n/// This function provides an optimized way to calculate the latest ROCP value when\n/// streaming data is received, without recalculating the entire series.\n///\n/// # Mathematical Formula\n/// ```text\n/// ROCP = (Current Price - Previous Price) / Previous Price\n/// ```\n///\n/// # Arguments\n/// * `input` - The most recent price value\n/// * `prev` - The price from n periods ago\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The calculated ROCP value if successful, error otherwise\n///\n/// # Errors\n/// Returns error if (with \"`check-nan`\" feature):\n/// * Either input is NaN\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::rocp::rocp_inc;\n///\n/// let current_price = 11.5;\n/// let prev_price = 10.0;\n///\n/// let output_rocp = rocp_inc(current_price, prev_price).unwrap();\n/// ```\npub fn rocp_inc(input: TAFloat, prev: TAFloat) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input.is_nan() || prev.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    Ok((input - prev) / prev)\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_rocp_calculation() {\n        let input_price = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6,\n        ];\n        let opt_period = 10;\n        let mut output_rocp = vec![0.0; input_price.len()];\n\n        rocp(&input_price, opt_period, &mut output_rocp).unwrap();\n\n        // First 10 values should be NaN\n        for value in output_rocp.iter().take(10) {\n            assert!(value.is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            -0.001_164_240_219_672_252_2,\n            0.000_241_330_554_719_573_87,\n            0.000_619_481_851_739_320_6,\n            -0.000_264_429_911_856_778_8,\n            -0.002_592_271_506_331_37,\n            -0.006_053_110_799_725_468,\n            -0.005_780_790_164_418_739,\n            -0.006_765_592_776_559_561,\n            -0.003_653_658_203_968_411_3,\n            -0.004_550_273_272_189_292,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_rocp[i + 10], *expected, epsilon = 0.000_000_1);\n        }\n\n        // Test incremental calculation matches regular calculation\n        for i in opt_period..input_price.len() {\n            let result = rocp_inc(input_price[i], input_price[i - opt_period]).unwrap();\n            assert_relative_eq!(result, output_rocp[i], epsilon = 0.000_000_1);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/rocr.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Calculates the lookback period required for Rate of Change Ratio (ROCR) calculation.\n///\n/// Returns the number of historical data points needed for ROCR calculation, which equals the input period.\n///\n/// # Arguments\n/// * `opt_period` - The time period used in ROCR calculation, must be >= 2\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period on success\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::rocr;\n/// let lookback = rocr::lookback(10).unwrap();\n/// assert_eq!(lookback, 10);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period)\n}\n\n/// Calculates Rate of Change Ratio (ROCR) for a price series.\n///\n/// ROCR is a momentum indicator that measures the percentage change in price over a specified\n/// time period by comparing the current price to a previous price.\n///\n/// # Mathematical Formula\n/// ```text\n/// ROCR = Current Price / Price n periods ago\n/// ```\n///\n/// # Calculation Principle\n/// 1. For each data point after the lookback period:\n///    - Divide current price by the price n periods ago\n/// 2. Values before the lookback period are set to NaN\n///\n/// # Arguments\n/// * `input_price` - Array of price values for calculation\n/// * `opt_period` - Number of periods to look back (n), must be >= 2\n/// * `output_rocr` - Array to store calculated ROCR values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty Ok value on success\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input array is empty\n/// * `KandError::LengthMismatch` - If input and output arrays have different lengths\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n/// * `KandError::InsufficientData` - If input length is less than or equal to lookback period\n/// * `KandError::NaNDetected` - If any input value is NaN (when \"`check-nan`\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::rocr;\n///\n/// let input_price = vec![10.0, 10.5, 11.2, 10.8, 11.5];\n/// let opt_period = 2;\n/// let mut output_rocr = vec![0.0; 5];\n///\n/// rocr::rocr(&input_price, opt_period, &mut output_rocr).unwrap();\n/// // First opt_period values will be NaN\n/// // Remaining values show ratio between current and historical prices\n/// ```\npub fn rocr(\n    input_price: &[TAFloat],\n    opt_period: usize,\n    output_rocr: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_price.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != output_rocr.len() {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for price in input_price {\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate ROCR values\n    for i in lookback..len {\n        output_rocr[i] = input_price[i] / input_price[i - opt_period];\n    }\n\n    // Fill initial values with NAN\n    for value in output_rocr.iter_mut().take(lookback) {\n        *value = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates a single ROCR value incrementally.\n///\n/// This function provides an optimized way to calculate ROCR for real-time data streams\n/// by only computing the most recent value using the current price and historical price.\n///\n/// # Arguments\n/// * `input` - Current price value\n/// * `prev` - Price value from `opt_period` periods ago\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The calculated ROCR value\n///\n/// # Errors\n/// * `KandError::NaNDetected` - If any input value is NaN (when \"`check-nan`\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::rocr;\n///\n/// let current_price = 15.0;\n/// let historical_price = 12.0;\n///\n/// let rocr_value = rocr::rocr_inc(current_price, historical_price).unwrap();\n/// // rocr_value will be 1.25 (15.0 / 12.0)\n/// ```\npub fn rocr_inc(input: TAFloat, prev: TAFloat) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input.is_nan() || prev.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    Ok(input / prev)\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_rocr_calculation() {\n        let input = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6,\n        ];\n        let opt_period = 10;\n        let mut output_rocr = vec![0.0; input.len()];\n\n        rocr(&input, opt_period, &mut output_rocr).unwrap();\n\n        // First 10 values should be NaN\n        for value in output_rocr.iter().take(10) {\n            assert!(value.is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            0.998_835_759_780_327_8,\n            1.000_241_330_554_719_5,\n            1.000_619_481_851_739_3,\n            0.999_735_570_088_143_2,\n            0.997_407_728_493_668_7,\n            0.993_946_889_200_274_5,\n            0.994_219_209_835_581_2,\n            0.993_234_407_223_440_5,\n            0.996_346_341_796_031_5,\n            0.995_449_726_727_810_7,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_rocr[i + 10], *expected, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation matches regular calculation\n        for i in opt_period..input.len() {\n            let result = rocr_inc(input[i], input[i - opt_period]).unwrap();\n            assert_relative_eq!(result, output_rocr[i], epsilon = 0.0001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/rocr100.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Calculates the lookback period required for ROCR100 (Rate of Change Ratio * 100) calculation.\n///\n/// The lookback period equals the input parameter period since ROCR100 needs historical data points\n/// to compare current prices with past prices.\n///\n/// # Arguments\n/// * `opt_period` - The number of periods to look back for price comparison (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The required lookback period on success\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::rocr100;\n///\n/// let opt_period = 10;\n/// let lookback = rocr100::lookback(opt_period).unwrap();\n/// assert_eq!(lookback, 10);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period)\n}\n\n/// Calculates Rate of Change Ratio * 100 (ROCR100) for a price series.\n///\n/// ROCR100 is a momentum indicator that measures the percentage change in price over a specified period.\n/// It compares the current price to a past price and expresses the ratio as a percentage.\n/// Values above 100 indicate price increases, while values below 100 indicate price decreases.\n///\n/// # Mathematical Formula\n/// ```text\n/// ROCR100 = (Current Price / Price n periods ago) * 100\n/// ```\n///\n/// # Calculation Principles\n/// 1. For each data point, divide current price by price from n periods ago\n/// 2. Multiply the ratio by 100 to get percentage\n/// 3. First n values are set to NaN due to insufficient historical data\n///\n/// # Arguments\n/// * `input_price` - Array of price values for calculation\n/// * `opt_period` - Number of periods to look back (must be >= 2)\n/// * `output_rocr100` - Array to store calculated ROCR100 values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty Ok value on success\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input array is empty\n/// * `KandError::LengthMismatch` - If input and output arrays have different lengths\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n/// * `KandError::InsufficientData` - If input length is less than or equal to lookback period\n/// * `KandError::NaNDetected` - If any input value is NaN (when \"`check-nan`\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::rocr100;\n///\n/// let input_price = vec![10.0, 10.5, 11.2, 10.8, 11.5];\n/// let opt_period = 2;\n/// let mut output_rocr100 = vec![0.0; 5];\n///\n/// rocr100::rocr100(&input_price, opt_period, &mut output_rocr100).unwrap();\n/// // First opt_period values are NaN\n/// // Remaining values show percentage ratio between current and historical prices\n/// ```\npub fn rocr100(\n    input_price: &[TAFloat],\n    opt_period: usize,\n    output_rocr100: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_price.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != output_rocr100.len() {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for price in input_price {\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate ROCR100 values\n    for i in lookback..len {\n        output_rocr100[i] = (input_price[i] / input_price[i - opt_period]) * 100.0;\n    }\n\n    // Fill initial values with NAN\n    for value in output_rocr100.iter_mut().take(lookback) {\n        *value = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates a single ROCR100 value incrementally.\n///\n/// This function provides an optimized way to calculate ROCR100 in real-time scenarios\n/// where only the latest value needs to be computed. It requires storing the historical\n/// price value from `opt_period` periods ago.\n///\n/// # Arguments\n/// * `input` - Current price value\n/// * `prev` - Price value from `opt_period` periods ago\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The calculated ROCR100 value\n///\n/// # Errors\n/// * `KandError::NaNDetected` - If any input value is NaN (when \"`check-nan`\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::rocr100;\n///\n/// let current_price = 15.0;\n/// let price_10_periods_ago = 12.0;\n///\n/// // Calculate ROCR100 for current period\n/// let rocr100_value = rocr100::rocr100_inc(current_price, price_10_periods_ago).unwrap();\n/// // Result shows current price is 125% of price 10 periods ago\n/// ```\npub fn rocr100_inc(input: TAFloat, prev: TAFloat) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input.is_nan() || prev.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    Ok((input / prev) * 100.0)\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_rocr100_calculation() {\n        let input_price = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6,\n        ];\n        let opt_period = 10;\n        let mut output_rocr100 = vec![0.0; input_price.len()];\n\n        rocr100(&input_price, opt_period, &mut output_rocr100).unwrap();\n\n        // First 10 values should be NaN\n        for value in output_rocr100.iter().take(10) {\n            assert!(value.is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            99.883_575_978_032_78,\n            100.024_133_055_471_95,\n            100.061_948_185_173_93,\n            99.973_557_008_814_31,\n            99.740_772_849_366_86,\n            99.394_688_920_027_45,\n            99.421_920_983_558_12,\n            99.323_440_722_344_05,\n            99.634_634_179_603_15,\n            99.544_972_672_781_07,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_rocr100[i + 10], *expected, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation matches regular calculation\n        for i in 11..input_price.len() {\n            let result = rocr100_inc(input_price[i], input_price[i - opt_period]).unwrap();\n            assert_relative_eq!(result, output_rocr100[i], epsilon = 0.0001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/rsi.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Calculates the lookback period required for RSI (Relative Strength Index) calculation.\n///\n/// The lookback period equals the input parameter period since RSI needs historical data points\n/// to establish the initial average gain and loss values.\n///\n/// # Arguments\n/// * `opt_period` - The number of periods to look back for RSI calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The required lookback period on success\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::rsi;\n///\n/// let opt_period = 14;\n/// let lookback = rsi::lookback(opt_period).unwrap();\n/// assert_eq!(lookback, 14);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period)\n}\n\n/// Calculates Relative Strength Index (RSI) for a price series.\n///\n/// RSI is a momentum oscillator that measures the speed and magnitude of recent price changes\n/// to evaluate overbought or oversold conditions. It oscillates between 0 and 100, with\n/// values above 70 generally indicating overbought conditions and values below 30 indicating\n/// oversold conditions.\n///\n/// # Mathematical Formula\n/// ```text\n/// RSI = 100 - (100 / (1 + RS))\n/// where:\n/// RS = Average Gain / Average Loss\n///\n/// Initial Average Gain = Sum of Gains over past n periods / n\n/// Initial Average Loss = Sum of Losses over past n periods / n\n///\n/// Subsequent values:\n/// Average Gain = ((Previous Average Gain) × (n-1) + Current Gain) / n\n/// Average Loss = ((Previous Average Loss) × (n-1) + Current Loss) / n\n/// ```\n///\n/// # Calculation Principle\n/// 1. Calculate price changes between consecutive periods\n/// 2. Separate gains (positive changes) from losses (negative changes)\n/// 3. Calculate initial average gain and loss over first n periods\n/// 4. Apply Wilder's smoothing formula for subsequent periods\n/// 5. Calculate RS ratio and convert to RSI value\n///\n/// # Arguments\n/// * `input_prices` - Array of price values (typically closing prices)\n/// * `opt_period` - The time period for RSI calculation (typical values: 14, 9, or 25)\n/// * `output_rsi` - Array to store calculated RSI values\n/// * `output_avg_gain` - Array to store average gain values for each period\n/// * `output_avg_loss` - Array to store average loss values for each period\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok(()) on successful calculation\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input array is empty\n/// * `KandError::LengthMismatch` - If input and output arrays have different lengths\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n/// * `KandError::InsufficientData` - If input length is less than or equal to lookback period\n/// * `KandError::NaNDetected` - If any input value is NaN (when \"`check-nan`\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::rsi;\n///\n/// let input_prices = vec![44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42];\n/// let opt_period = 5;\n/// let mut output_rsi = vec![0.0; input_prices.len()];\n/// let mut output_avg_gain = vec![0.0; input_prices.len()];\n/// let mut output_avg_loss = vec![0.0; input_prices.len()];\n///\n/// rsi::rsi(\n///     &input_prices,\n///     opt_period,\n///     &mut output_rsi,\n///     &mut output_avg_gain,\n///     &mut output_avg_loss,\n/// )\n/// .unwrap();\n/// ```\npub fn rsi(\n    input_prices: &[TAFloat],\n    opt_period: usize,\n    output_rsi: &mut [TAFloat],\n    output_avg_gain: &mut [TAFloat],\n    output_avg_loss: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_prices.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if output_rsi.len() != len || output_avg_gain.len() != len || output_avg_loss.len() != len {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for price in input_prices {\n            // NaN check\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    let mut gains = 0.0;\n    let mut losses = 0.0;\n\n    // Calculate initial gains/losses sum\n    for i in 1..=lookback {\n        let diff = input_prices[i] - input_prices[i - 1];\n        if diff > 0.0 {\n            gains += diff;\n        } else {\n            losses += diff.abs();\n        }\n    }\n\n    // Calculate first RSI value\n    let first_avg_gain = gains / opt_period as TAFloat;\n    let first_avg_loss = losses / opt_period as TAFloat;\n\n    output_avg_gain[lookback] = first_avg_gain;\n    output_avg_loss[lookback] = first_avg_loss;\n\n    if first_avg_loss == 0.0 {\n        output_rsi[lookback] = 100.0;\n    } else {\n        let rs = first_avg_gain / first_avg_loss;\n        output_rsi[lookback] = 100.0 - (100.0 / (1.0 + rs));\n    }\n\n    // Calculate remaining RSI values using smoothed averages\n    let mut prev_avg_gain = first_avg_gain;\n    let mut prev_avg_loss = first_avg_loss;\n    let smoothing = opt_period as TAFloat;\n\n    for i in lookback + 1..len {\n        let diff = input_prices[i] - input_prices[i - 1];\n        let (curr_gain, curr_loss) = if diff > 0.0 {\n            (diff, 0.0)\n        } else {\n            (0.0, diff.abs())\n        };\n\n        let curr_avg_gain = prev_avg_gain.mul_add(smoothing - 1.0, curr_gain) / smoothing;\n        let curr_avg_loss = prev_avg_loss.mul_add(smoothing - 1.0, curr_loss) / smoothing;\n\n        output_avg_gain[i] = curr_avg_gain;\n        output_avg_loss[i] = curr_avg_loss;\n\n        if curr_avg_loss == 0.0 {\n            output_rsi[i] = 100.0;\n        } else {\n            let rs = curr_avg_gain / curr_avg_loss;\n            output_rsi[i] = 100.0 - (100.0 / (1.0 + rs));\n        }\n\n        prev_avg_gain = curr_avg_gain;\n        prev_avg_loss = curr_avg_loss;\n    }\n\n    // Fill initial values with NAN\n    for i in 0..lookback {\n        output_rsi[i] = TAFloat::NAN;\n        output_avg_gain[i] = TAFloat::NAN;\n        output_avg_loss[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the latest RSI value incrementally using previous average gain and loss values.\n///\n/// This function optimizes RSI calculation for real-time data by using the previous period's\n/// average gain and loss values to calculate the current RSI value, without needing the entire\n/// price history.\n///\n/// # Mathematical Formula\n/// ```text\n/// Average Gain = ((Previous Average Gain) × (n-1) + Current Gain) / n\n/// Average Loss = ((Previous Average Loss) × (n-1) + Current Loss) / n\n/// RS = Average Gain / Average Loss\n/// RSI = 100 - (100 / (1 + RS))\n/// ```\n///\n/// # Arguments\n/// * `input_curr_price` - Current period's price value\n/// * `prev_price` - Previous period's price value\n/// * `prev_avg_gain` - Previous period's average gain\n/// * `prev_avg_loss` - Previous period's average loss\n/// * `opt_period` - The time period for RSI calculation\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat), KandError>` - Tuple containing (RSI value, new average gain, new average loss)\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` is less than 2\n/// * `KandError::NaNDetected` - If any input value is NaN (when \"`check-nan`\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::rsi;\n///\n/// let (rsi_value, avg_gain, avg_loss) = rsi::rsi_inc(\n///     45.42, // current price\n///     45.10, // previous price\n///     0.24,  // previous average gain\n///     0.14,  // previous average loss\n///     14,    // period\n/// )\n/// .unwrap();\n/// ```\npub fn rsi_inc(\n    input_curr_price: TAFloat,\n    prev_price: TAFloat,\n    prev_avg_gain: TAFloat,\n    prev_avg_loss: TAFloat,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_curr_price.is_nan()\n            || prev_price.is_nan()\n            || prev_avg_gain.is_nan()\n            || prev_avg_loss.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let diff = input_curr_price - prev_price;\n    let (curr_gain, curr_loss) = if diff > 0.0 {\n        (diff, 0.0)\n    } else {\n        (0.0, diff.abs())\n    };\n\n    let smoothing = opt_period as TAFloat;\n    let output_avg_gain = prev_avg_gain.mul_add(smoothing - 1.0, curr_gain) / smoothing;\n    let output_avg_loss = prev_avg_loss.mul_add(smoothing - 1.0, curr_loss) / smoothing;\n\n    let output_rsi = if output_avg_loss == 0.0 {\n        100.0\n    } else {\n        let rs = output_avg_gain / output_avg_loss;\n        100.0 - (100.0 / (1.0 + rs))\n    };\n\n    Ok((output_rsi, output_avg_gain, output_avg_loss))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    // Basic functionality tests\n    #[test]\n    fn test_rsi_calculation() {\n        let input_prices = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0, 35114.5, 35097.2,\n            35092.0,\n        ];\n        let opt_period = 14;\n        let mut output_rsi = vec![0.0; input_prices.len()];\n        let mut output_avg_gain = vec![0.0; input_prices.len()];\n        let mut output_avg_loss = vec![0.0; input_prices.len()];\n\n        rsi(\n            &input_prices,\n            opt_period,\n            &mut output_rsi,\n            &mut output_avg_gain,\n            &mut output_avg_loss,\n        )\n        .unwrap();\n\n        // Verify first 14 values are NaN\n        for value in output_rsi.iter().take(opt_period) {\n            assert!(value.is_nan());\n        }\n\n        // Verify against known values\n        assert_relative_eq!(output_rsi[14], 37.748_344_370_861_39, epsilon = 0.00001);\n        assert_relative_eq!(output_rsi[15], 34.223_538_361_225_86, epsilon = 0.00001);\n        assert_relative_eq!(output_rsi[16], 31.518_806_080_459_882, epsilon = 0.00001);\n        assert_relative_eq!(output_rsi[17], 33.425_568_632_418_2, epsilon = 0.00001);\n        assert_relative_eq!(output_rsi[18], 40.465_006_259_629_995, epsilon = 0.00001);\n\n        // Now test incremental calculation matches regular calculation\n        let mut prev_avg_gain = output_avg_gain[opt_period];\n        let mut prev_avg_loss = output_avg_loss[opt_period];\n        let mut prev_price = input_prices[opt_period];\n\n        // Test each incremental step\n        for i in opt_period + 1..input_prices.len() {\n            let (result, new_avg_gain, new_avg_loss) = rsi_inc(\n                input_prices[i],\n                prev_price,\n                prev_avg_gain,\n                prev_avg_loss,\n                opt_period,\n            )\n            .unwrap();\n\n            assert_relative_eq!(result, output_rsi[i], epsilon = 0.00001);\n            assert_relative_eq!(new_avg_gain, output_avg_gain[i], epsilon = 0.00001);\n            assert_relative_eq!(new_avg_loss, output_avg_loss[i], epsilon = 0.00001);\n\n            prev_avg_gain = new_avg_gain;\n            prev_avg_loss = new_avg_loss;\n            prev_price = input_prices[i];\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/sar.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Returns the lookback period required by the Parabolic SAR indicator.\n///\n/// # Description\n/// Calculates the minimum number of data points needed before the first valid SAR value can be computed.\n/// For the Parabolic SAR indicator, this is always 1 period.\n///\n/// # Parameters\n/// * `opt_acceleration` - The acceleration factor used in SAR calculation. Type: `TAFloat`\n/// * `opt_maximum` - The maximum allowed acceleration factor. Type: `TAFloat`\n///\n/// # Returns\n/// * `Ok(usize)` - The lookback period (1)\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if `opt_acceleration` or `opt_maximum` are invalid\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::sar;\n///\n/// let acceleration = 0.02;\n/// let maximum = 0.2;\n/// let lookback = sar::lookback(acceleration, maximum).unwrap();\n/// assert_eq!(lookback, 1);\n/// ```\npub const fn lookback(\n    _opt_acceleration: TAFloat,\n    _opt_maximum: TAFloat,\n) -> Result<usize, KandError> {\n    Ok(1)\n}\n\n/// Calculates the Parabolic SAR (Stop And Reverse) indicator.\n///\n/// # Description\n/// The Parabolic SAR is a trend-following indicator that helps identify potential reversal points\n/// in price movements. It plots points below prices in an uptrend and above prices in a downtrend.\n///\n/// # Mathematical Formula\n/// For each period t:\n/// ```text\n/// Uptrend (Long):\n/// SAR(t) = SAR(t-1) + AF * (EP - SAR(t-1))\n/// where:\n/// - AF (Acceleration Factor) increases by opt_acceleration when new EP is reached\n/// - EP (Extreme Point) is the highest high in current uptrend\n///\n/// Downtrend (Short):\n/// SAR(t) = SAR(t-1) + AF * (EP - SAR(t-1))\n/// where:\n/// - AF increases by opt_acceleration when new EP is reached\n/// - EP is the lowest low in current downtrend\n/// ```\n///\n/// # Parameters\n/// * `input_high` - Array of high prices. Type: `&[TAFloat]`\n/// * `input_low` - Array of low prices. Type: `&[TAFloat]`\n/// * `opt_acceleration` - Initial acceleration factor (e.g. 0.02). Type: `TAFloat`\n/// * `opt_maximum` - Maximum acceleration factor (e.g. 0.2). Type: `TAFloat`\n/// * `output_sar` - Buffer to store SAR values. Type: `&mut [TAFloat]`\n/// * `output_is_long` - Buffer to store trend direction (true=long, false=short). Type: `&mut [bool]`\n/// * `output_af` - Buffer to store acceleration factors. Type: `&mut [TAFloat]`\n/// * `output_ep` - Buffer to store extreme points. Type: `&mut [TAFloat]`\n///\n/// # Returns\n/// * `Ok(())` - Calculation successful\n///\n/// # Errors\n/// * `KandError::InvalidData` - Input arrays are empty\n/// * `KandError::LengthMismatch` - Input/output array lengths don't match\n/// * `KandError::InvalidParameter` - Invalid acceleration/maximum values\n/// * `KandError::InsufficientData` - Not enough data points\n/// * `KandError::NaNDetected` - Input contains NaN values\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::sar;\n///\n/// let high = vec![10.0, 12.0, 15.0, 14.0, 13.0];\n/// let low = vec![8.0, 9.0, 11.0, 10.0, 9.0];\n/// let mut sar = vec![0.0; 5];\n/// let mut is_long = vec![false; 5];\n/// let mut af = vec![0.0; 5];\n/// let mut ep = vec![0.0; 5];\n///\n/// sar::sar(\n///     &high,\n///     &low,\n///     0.02, // acceleration\n///     0.2,  // maximum\n///     &mut sar,\n///     &mut is_long,\n///     &mut af,\n///     &mut ep,\n/// )\n/// .unwrap();\n/// ```\npub fn sar(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    opt_acceleration: TAFloat,\n    opt_maximum: TAFloat,\n    output_sar: &mut [TAFloat],\n    output_is_long: &mut [bool],\n    output_af: &mut [TAFloat],\n    output_ep: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_acceleration, opt_maximum)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n        if len != input_low.len()\n            || len != output_sar.len()\n            || len != output_is_long.len()\n            || len != output_af.len()\n            || len != output_ep.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n        if opt_acceleration <= 0.0 || opt_maximum <= opt_acceleration {\n            return Err(KandError::InvalidParameter);\n        }\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            if input_high[i].is_nan() || input_low[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Determine the initial trend by comparing the positive and negative directional movements.\n    let plus_dm = input_high[1] - input_high[0];\n    let minus_dm = input_low[0] - input_low[1];\n    let initial_trend = plus_dm >= minus_dm; // Default to long if equal\n\n    let mut af = opt_acceleration;\n    let mut ep = if initial_trend {\n        input_high[1]\n    } else {\n        input_low[1]\n    };\n\n    // Initialize the SAR output and auxiliary arrays at the first index.\n    output_sar[0] = TAFloat::NAN;\n    output_is_long[0] = initial_trend;\n    output_af[0] = 0.0;\n    output_ep[0] = TAFloat::NAN;\n\n    // Derive the second SAR value from the first bar's price data.\n    output_sar[1] = if initial_trend {\n        input_low[0]\n    } else {\n        input_high[0]\n    };\n    output_is_long[1] = initial_trend;\n    output_af[1] = af;\n    output_ep[1] = ep;\n\n    let mut is_long = initial_trend;\n    // Record the starting index of the current trend; a new trend begins at index 1.\n    let mut trend_start = 1;\n\n    // Iterate over remaining data points, computing the SAR values and updating state arrays.\n    for i in 2..len {\n        let prev_sar = output_sar[i - 1];\n        let high = input_high[i];\n        let low = input_low[i];\n\n        let mut sar_val = af.mul_add(ep - prev_sar, prev_sar);\n\n        if is_long {\n            if i - trend_start < 2 {\n                sar_val = sar_val.min(input_low[i - 1]);\n            } else {\n                sar_val = sar_val.min(input_low[i - 1]).min(input_low[i - 2]);\n            }\n            if high > ep {\n                ep = high;\n                af = (af + opt_acceleration).min(opt_maximum);\n            }\n            if low < sar_val {\n                is_long = false;\n                sar_val = ep;\n                ep = low;\n                af = opt_acceleration;\n                trend_start = i - 1;\n            }\n        } else {\n            if i - trend_start < 2 {\n                sar_val = sar_val.max(input_high[i - 1]);\n            } else {\n                sar_val = sar_val.max(input_high[i - 1]).max(input_high[i - 2]);\n            }\n            if low < ep {\n                ep = low;\n                af = (af + opt_acceleration).min(opt_maximum);\n            }\n            if high > sar_val {\n                is_long = true;\n                sar_val = ep;\n                ep = high;\n                af = opt_acceleration;\n                trend_start = i - 1;\n            }\n        }\n\n        output_sar[i] = sar_val;\n        output_is_long[i] = is_long;\n        output_af[i] = af;\n        output_ep[i] = ep;\n    }\n\n    Ok(())\n}\n\n/// Incrementally updates the Parabolic SAR with new price data.\n///\n/// # Description\n/// Calculates the next SAR value based on the latest price data without reprocessing historical data.\n/// Useful for real-time calculations where new data arrives sequentially.\n///\n/// # Parameters\n/// * `input_high` - Current period's high price. Type: `TAFloat`\n/// * `input_low` - Current period's low price. Type: `TAFloat`\n/// * `prev_high` - Previous period's high price. Type: `TAFloat`\n/// * `prev_low` - Previous period's low price. Type: `TAFloat`\n/// * `prev_sar` - Previous period's SAR value. Type: `TAFloat`\n/// * `input_is_long` - Current trend direction (true=long, false=short). Type: `bool`\n/// * `input_af` - Current acceleration factor. Type: `TAFloat`\n/// * `input_ep` - Current extreme point. Type: `TAFloat`\n/// * `opt_acceleration` - Acceleration factor increment. Type: `TAFloat`\n/// * `opt_maximum` - Maximum acceleration factor. Type: `TAFloat`\n///\n/// # Returns\n/// A `Result` containing:\n/// * `Ok((TAFloat, bool, TAFloat, TAFloat))` - Tuple containing:\n///   * Updated SAR value\n///   * New trend direction\n///   * Updated acceleration factor\n///   * Updated extreme point\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - Invalid acceleration/maximum values\n/// * `KandError::NaNDetected` - Input contains NaN values\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::sar;\n///\n/// let (new_sar, new_is_long, new_af, new_ep) = sar::sar_inc(\n///     15.0, // current high\n///     14.0, // current low\n///     14.5, // previous high\n///     13.5, // previous low\n///     13.0, // previous SAR\n///     true, // is long trend\n///     0.02, // current AF\n///     14.5, // current EP\n///     0.02, // acceleration\n///     0.2,  // maximum\n/// )\n/// .unwrap();\n/// ```\npub fn sar_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    prev_sar: TAFloat,\n    input_is_long: bool,\n    input_af: TAFloat,\n    input_ep: TAFloat,\n    opt_acceleration: TAFloat,\n    opt_maximum: TAFloat,\n) -> Result<(TAFloat, bool, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_acceleration <= 0.0 || opt_maximum <= opt_acceleration {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_high.is_nan()\n            || input_low.is_nan()\n            || prev_high.is_nan()\n            || prev_low.is_nan()\n            || prev_sar.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let high = input_high;\n    let low = input_low;\n    let mut is_long = input_is_long;\n    let mut af = input_af;\n    let mut ep = input_ep;\n\n    let mut sar = af.mul_add(ep - prev_sar, prev_sar);\n\n    if is_long {\n        sar = sar.min(prev_low);\n        if high > ep {\n            ep = high;\n            af = (af + opt_acceleration).min(opt_maximum);\n        }\n        if low < sar {\n            is_long = false;\n            sar = ep;\n            ep = low;\n            af = opt_acceleration;\n        }\n    } else {\n        sar = sar.max(prev_high);\n        if low < ep {\n            ep = low;\n            af = (af + opt_acceleration).min(opt_maximum);\n        }\n        if high > sar {\n            is_long = true;\n            sar = ep;\n            ep = high;\n            af = opt_acceleration;\n        }\n    }\n\n    Ok((sar, is_long, af, ep))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_sar_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2,\n        ];\n\n        let opt_acceleration = 0.02;\n        let opt_maximum = 0.2;\n        let mut output_sar = vec![0.0; input_high.len()];\n        let mut output_is_long = vec![false; input_high.len()];\n        let mut output_af = vec![0.0; input_high.len()];\n        let mut output_ep = vec![0.0; input_high.len()];\n\n        sar(\n            &input_high,\n            &input_low,\n            opt_acceleration,\n            opt_maximum,\n            &mut output_sar,\n            &mut output_is_long,\n            &mut output_af,\n            &mut output_ep,\n        )\n        .unwrap();\n\n        // First value should be NaN\n        assert!(output_sar[0].is_nan());\n\n        // Compare with known values (offset by one due to initial NaN)\n        let expected_values = [\n            35266.0,\n            35264.81,\n            35261.4176,\n            35_253.574_544,\n            35130.7,\n            35133.246,\n            35138.43216,\n            35_147.016_230_4,\n            35_155.085_256_576,\n            35_162.670_141_181_44,\n            35281.5,\n            35278.952,\n            35_276.454_959_999_995,\n            35_271.152_761_599_995,\n            35_259.689_595_904,\n            35_240.602_428_231_68,\n            35_211.552_185_408_51,\n            35_185.406_966_867_66,\n            35_161.876_270_180_896,\n        ];\n\n        for i in 1..expected_values.len() {\n            assert_relative_eq!(output_sar[i], expected_values[i - 1], epsilon = 0.0001);\n        }\n\n        // Test incremental calculation\n        let mut prev_sar = output_sar[1];\n        let mut is_long = output_is_long[1];\n        let mut af = opt_acceleration;\n        let mut ep = output_ep[1]; // Use the previously calculated EP instead of input_high[1]\n\n        for i in 2..6 {\n            let (sar_value, new_is_long, new_af, new_ep) = sar_inc(\n                input_high[i],\n                input_low[i],\n                input_high[i - 1],\n                input_low[i - 1],\n                prev_sar,\n                is_long,\n                af,\n                ep,\n                opt_acceleration,\n                opt_maximum,\n            )\n            .unwrap();\n\n            assert_relative_eq!(sar_value, output_sar[i], epsilon = 0.0001);\n\n            prev_sar = sar_value;\n            is_long = new_is_long;\n            af = new_af;\n            ep = new_ep;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/sma.rs",
    "content": "use crate::{KandError, TAFloat, TAPeriod};\n\n/// Returns the lookback period for Simple Moving Average (SMA) without input validation.\n///\n/// Assumes valid inputs; returns `opt_period - 1`.\n///\n/// # Panics\n///\n/// Panics in debug builds if `opt_period < 1` due to subtraction overflow.\n#[inline]\n#[must_use]\npub const fn lookback_raw(opt_period: TAPeriod) -> TAPeriod {\n    opt_period - 1\n}\n\n/// Returns the lookback period for SMA calculation (`opt_period - 1`).\n///\n/// # Errors\n///\n/// Returns [`KandError::InvalidParameter`] if `opt_period < 2` (with \"check\" feature).\n#[must_use]\npub const fn lookback(opt_period: TAPeriod) -> Result<TAPeriod, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Computes SMA without input validation for high performance.\n///\n/// Sums prices over `opt_period` and divides by period length.\n/// Stores results in `output`, leaving first `lookback` values unset.\n///\n/// # Assumptions\n///\n/// - `input` and `output` have equal length and sufficient data.\n/// - Inputs are valid; no error checking performed.\npub fn sma_raw(input: &[TAFloat], opt_period: TAPeriod, output: &mut [TAFloat]) {\n    let len = input.len();\n    let lookback = lookback_raw(opt_period);\n\n    let mut sum = input.iter().take(opt_period).sum::<TAFloat>();\n    let period_float = opt_period as TAFloat; // TODO: Safely convert TAPeriod to TAFloat\n    output[lookback] = sum / period_float;\n\n    for i in lookback + 1..len {\n        sum = sum + input[i] - input[i - opt_period];\n        output[i] = sum / period_float;\n    }\n}\n\n/// Computes SMA over a price series, smoothing data to identify trends.\n///\n/// Formula: `SMA = (P1 + P2 + ... + Pn) / n`, where `n` is `opt_period`.\n/// Sets first `period - 1` output values to NaN (with \"allow-nan\" feature).\n///\n/// # Errors\n///\n/// With \"check\" feature:\n/// - [`KandError::InvalidData`] if `input` is empty.\n/// - [`KandError::LengthMismatch`] if `input` and `output` lengths differ.\n/// - [`KandError::InsufficientData`] if `input.len() <= lookback`.\n/// - [`KandError::InvalidParameter`] from `lookback`.\n///\n/// With \"check-nan\" feature:\n/// - [`KandError::NaNDetected`] if any input is NaN.\npub fn sma(\n    input: &[TAFloat],\n    opt_period: TAPeriod,\n    output: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n        if output.len() != len {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input.iter().any(|&price| price.is_nan()) {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    sma_raw(input, opt_period, output);\n\n    #[cfg(feature = \"allow-nan\")]\n    {\n        for value in output.iter_mut().take(lookback) {\n            *value = TAFloat::NAN;\n        }\n    }\n\n    Ok(())\n}\n\n/// Computes the next SMA value incrementally without input validation.\n///\n/// Formula: `Latest SMA = Previous SMA + (New Price - Old Price) / period`.\n///\n/// # Assumptions\n///\n/// Inputs are valid; no error checking performed.\n#[must_use]\npub fn sma_inc_raw(\n    input: TAFloat,\n    prev_input: TAFloat,\n    prev_sma: TAFloat,\n    opt_period: TAPeriod,\n) -> TAFloat {\n    prev_sma + (input - prev_input) / opt_period as TAFloat // TODO: Safely convert TAPeriod to TAFloat\n}\n\n/// Computes the next SMA value incrementally using the previous SMA.\n///\n/// Formula: `Latest SMA = Previous SMA + (New Price - Old Price) / period`.\n///\n/// # Errors\n///\n/// With \"check\" feature:\n/// - [`KandError::InvalidParameter`] if `opt_period < 2`.\n///\n/// With \"check-nan\" feature:\n/// - [`KandError::NaNDetected`] if any input is NaN.\n#[must_use]\npub fn sma_inc(\n    input: TAFloat,\n    prev_input: TAFloat,\n    prev_sma: TAFloat,\n    opt_period: TAPeriod,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if prev_sma.is_nan() || input.is_nan() || prev_input.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    Ok(sma_inc_raw(input, prev_input, prev_sma, opt_period))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use crate::EPSILON;\n\n    use super::*;\n\n    const PERIOD: TAPeriod = 14;\n    const LOOKBACK: TAPeriod = PERIOD - 1;\n\n    const INPUT_DATA: [f64; 39] = [\n        35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6, 35184.7,\n        35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4, 35069.0, 35024.6,\n        34939.5, 34952.6, 35000.0, 35041.8, 35080.0, 35114.5, 35097.2, 35092.0, 35073.2, 35139.3,\n        35092.0, 35126.7, 35106.3, 35124.8, 35170.1, 35215.3, 35154.0, 35216.3, 35211.8,\n    ];\n\n    const EXPECTED_VALUES: [f64; 13] = [\n        35_203.535_714_285_72,\n        35_194.55,\n        35_181.678_571_428_57,\n        35_168.007_142_857_15,\n        35_156.821_428_571_435,\n        35_148.785_714_285_72,\n        35_132.357_142_857_145,\n        35_113.55,\n        35_092.171_428_571_43,\n        35_078.057_142_857_15,\n        35_067.85,\n        35_061.057_142_857_15,\n        35_052.814_285_714_29,\n    ];\n\n    /// Tests SMA with `allow-nan` feature enabled.\n    #[test]\n    #[cfg(feature = \"allow-nan\")]\n    fn test_sma_with_nan() {\n        let mut output = vec![0.0; INPUT_DATA.len()];\n        sma(&INPUT_DATA, PERIOD, &mut output).unwrap();\n\n        // Verify initial values are NaN\n        for &val in output.iter().take(LOOKBACK) {\n            assert!(val.is_nan());\n        }\n\n        // Verify calculated SMA values\n        for (i, &expected) in EXPECTED_VALUES.iter().enumerate() {\n            assert_relative_eq!(output[LOOKBACK + i], expected, epsilon = EPSILON);\n        }\n\n        // Verify incremental calculation\n        let mut prev_sma = output[LOOKBACK];\n        for i in (LOOKBACK + 1)..INPUT_DATA.len() {\n            let result = sma_inc(INPUT_DATA[i], INPUT_DATA[i - PERIOD], prev_sma, PERIOD).unwrap();\n            assert_relative_eq!(result, output[i], epsilon = EPSILON);\n            prev_sma = result;\n        }\n    }\n\n    /// Tests SMA without `allow-nan` feature.\n    #[test]\n    #[cfg(not(feature = \"allow-nan\"))]\n    fn test_sma_without_nan() {\n        let mut output = vec![0.0; INPUT_DATA.len()];\n        sma(&INPUT_DATA, PERIOD, &mut output).unwrap();\n\n        // Verify initial values are 0.0\n        for &val in output.iter().take(LOOKBACK) {\n            assert_eq!(val, 0.0);\n        }\n\n        // Verify calculated SMA values\n        for (i, &expected) in EXPECTED_VALUES.iter().enumerate() {\n            assert_relative_eq!(output[LOOKBACK + i], expected, epsilon = EPSILON);\n        }\n\n        // Verify incremental calculation\n        let mut prev_sma = output[LOOKBACK];\n        for i in (LOOKBACK + 1)..INPUT_DATA.len() {\n            let result = sma_inc(INPUT_DATA[i], INPUT_DATA[i - PERIOD], prev_sma, PERIOD).unwrap();\n            assert_relative_eq!(result, output[i], epsilon = EPSILON);\n            prev_sma = result;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/stoch.rs",
    "content": "use crate::{KandError, TAFloat, ta::ohlcv::sma};\n\n/// Calculates the lookback period required for Stochastic Oscillator calculation.\n///\n/// # Description\n/// The lookback period represents the minimum number of data points needed before\n/// the indicator can generate valid values. It is calculated based on the input parameters.\n///\n/// # Arguments\n/// * `opt_k_period` - The period used for %K calculation, must be >= 2\n/// * `opt_k_slow_period` - The smoothing period for slow %K calculation, must be >= 2\n/// * `opt_d_period` - The period used for %D calculation, must be >= 2\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period if successful\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If any input parameter is less than 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::stoch;\n///\n/// let k_period = 14;\n/// let k_slow_period = 3;\n/// let d_period = 3;\n///\n/// let lookback = stoch::lookback(k_period, k_slow_period, d_period).unwrap();\n/// assert_eq!(lookback, 17); // 14 + 3 + 3 - 3 = 17\n/// ```\npub const fn lookback(\n    opt_k_period: usize,\n    opt_k_slow_period: usize,\n    opt_d_period: usize,\n) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_k_period < 2 || opt_k_slow_period < 2 || opt_d_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_k_period + opt_k_slow_period + opt_d_period - 3)\n}\n\n/// Calculates the Stochastic Oscillator indicator for the entire price series.\n///\n/// # Description\n/// The Stochastic Oscillator is a momentum indicator that shows the location of the close\n/// relative to the high-low range over a set number of periods. The indicator consists of\n/// two lines: %K (the fast line) and %D (the slow line).\n///\n/// # Mathematical Formula\n/// ```text\n/// Fast %K = 100 * (Close - Lowest Low) / (Highest High - Lowest Low)\n/// Slow %K = SMA(Fast %K, k_slow_period)\n/// %D = SMA(Slow %K, d_period)\n/// ```\n///\n/// # Calculation Steps\n/// 1. Calculate the Fast %K by comparing current close to the high-low range\n/// 2. Smooth the Fast %K using SMA to get Slow %K\n/// 3. Calculate %D as the SMA of Slow %K\n///\n/// # Arguments\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of closing prices\n/// * `opt_k_period` - Period for %K calculation, must be >= 2\n/// * `opt_k_slow_period` - Smoothing period for slow %K, must be >= 2\n/// * `opt_d_period` - Period for %D calculation, must be >= 2\n/// * `output_fast_k` - Array to store Fast %K values\n/// * `output_k` - Array to store Slow %K values\n/// * `output_d` - Array to store %D values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Unit type if successful\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input/output arrays have different lengths\n/// * `KandError::InvalidParameter` - If any period parameter is less than 2\n/// * `KandError::InsufficientData` - If input length is less than required lookback period\n/// * `KandError::NaNDetected` - If any input value is NaN (when \"`check-nan`\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::stoch;\n///\n/// let input_high = vec![10.0, 12.0, 15.0, 14.0, 13.0];\n/// let input_low = vec![8.0, 9.0, 11.0, 10.0, 9.0];\n/// let input_close = vec![9.0, 11.0, 14.0, 12.0, 11.0];\n/// let opt_k_period = 3;\n/// let opt_k_slow_period = 2;\n/// let opt_d_period = 2;\n/// let mut output_fast_k = vec![0.0; 5];\n/// let mut output_k = vec![0.0; 5];\n/// let mut output_d = vec![0.0; 5];\n///\n/// stoch::stoch(\n///     &input_high,\n///     &input_low,\n///     &input_close,\n///     opt_k_period,\n///     opt_k_slow_period,\n///     opt_d_period,\n///     &mut output_fast_k,\n///     &mut output_k,\n///     &mut output_d,\n/// )\n/// .unwrap();\n/// ```\n#[allow(clippy::similar_names)]\npub fn stoch(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_k_period: usize,\n    opt_k_slow_period: usize,\n    opt_d_period: usize,\n    output_fast_k: &mut [TAFloat],\n    output_k: &mut [TAFloat],\n    output_d: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_k_period, opt_k_slow_period, opt_d_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_low.len()\n            || len != input_close.len()\n            || len != output_fast_k.len()\n            || len != output_k.len()\n            || len != output_d.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_high[i].is_nan() || input_low[i].is_nan() || input_close[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    let hundred = 100.0;\n\n    // Calculate Fast %K first\n    for i in (opt_k_period - 1)..len {\n        let mut highest_high = input_high[i];\n        let mut lowest_low = input_low[i];\n\n        for j in 0..opt_k_period {\n            let idx = i - j;\n            highest_high = highest_high.max(input_high[idx]);\n            lowest_low = lowest_low.min(input_low[idx]);\n        }\n\n        let range = highest_high - lowest_low;\n        if range > 0.0 {\n            output_fast_k[i] = hundred * (input_close[i] - lowest_low) / range;\n        } else {\n            output_fast_k[i] = 50.0; // Default to 50 when range is zero\n        }\n    }\n\n    // Calculate Slow %K (SMA of Fast %K)\n    sma::sma(output_fast_k, opt_k_slow_period, output_k)?;\n\n    // Calculate %D (SMA of Slow %K)\n    sma::sma(\n        &output_k[opt_k_slow_period - 1..],\n        opt_d_period,\n        &mut output_d[opt_k_slow_period - 1..],\n    )?;\n\n    // Fill initial values with NAN\n    for i in 0..lookback {\n        output_fast_k[i] = TAFloat::NAN;\n        output_k[i] = TAFloat::NAN;\n        output_d[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    #[allow(clippy::similar_names)]\n    fn test_stoch_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0,\n        ];\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0,\n        ];\n\n        let opt_k_period = 14;\n        let opt_k_slow_period = 3;\n        let opt_d_period = 3;\n        let mut output_fast_k = vec![0.0; input_high.len()];\n        let mut output_k = vec![0.0; input_high.len()];\n        let mut output_d = vec![0.0; input_high.len()];\n\n        stoch(\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_k_period,\n            opt_k_slow_period,\n            opt_d_period,\n            &mut output_fast_k,\n            &mut output_k,\n            &mut output_d,\n        )\n        .unwrap();\n\n        // First 17 values should be NaN (lookback = 14 + 3 + 3 - 3 = 17)\n        for i in 0..17 {\n            assert!(output_k[i].is_nan());\n            assert!(output_d[i].is_nan());\n        }\n\n        // Compare with known values\n        let expected_k = [\n            13.888_595_327_554_674,\n            23.274_994_970_831_596,\n            25.819_754_576_544_284,\n            20.205_422_372_883_813,\n            12.265_381_731_365_673,\n            13.761_818_641_200_326,\n            26.221_343_873_517_94,\n            39.272_727_272_727_57,\n        ];\n\n        let expected_d = [\n            11.330_297_439_346_538,\n            15.457_813_387_810_21,\n            20.994_448_291_643_52,\n            23.100_057_306_753_232,\n            19.430_186_226_931_26,\n            15.410_874_248_483_273,\n            17.416_181_415_361_315,\n            26.418_629_929_148_62,\n        ];\n\n        for (i, (&exp_k, &exp_d)) in expected_k.iter().zip(expected_d.iter()).enumerate() {\n            assert_relative_eq!(output_k[i + 17], exp_k, epsilon = 0.0001);\n            assert_relative_eq!(output_d[i + 17], exp_d, epsilon = 0.0001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/supertrend.rs",
    "content": "use super::atr;\nuse crate::{KandError, TAFloat, TAInt, types::Signal};\n\n/// Returns the lookback period required for Supertrend calculation\n///\n/// # Description\n/// Calculates the number of data points needed before the first valid Supertrend value can be generated.\n/// The lookback period is equal to the ATR period.\n///\n/// # Arguments\n/// * `opt_period` - The period used for ATR calculation, must be >= 2\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period if successful\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::supertrend::lookback;\n///\n/// let period = 14;\n/// let lookback_period = lookback(period).unwrap();\n/// assert_eq!(lookback_period, 14);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    atr::lookback(opt_period)\n}\n\n/// Calculates Supertrend values for the entire price series\n///\n/// # Description\n/// The Supertrend indicator is a trend-following indicator that combines Average True Range (ATR)\n/// with dynamic support and resistance bands. It helps:\n/// - Identify trend direction and potential reversals\n/// - Provide adaptive stop-loss levels\n/// - Generate trend-based trading signals\n///\n/// # Calculation Details\n/// 1. Calculate ATR for volatility measurement\n/// 2. Compute basic upper and lower bands:\n///    ```text\n///    Basic Upper = (High + Low) / 2 + Multiplier * ATR\n///    Basic Lower = (High + Low) / 2 - Multiplier * ATR\n///    ```\n/// 3. Determine final bands using price action:\n///    ```text\n///    Final Upper = if Close[i-1] <= Upper[i-1]\n///                  then min(Basic Upper[i], Upper[i-1])\n///                  else Basic Upper[i]\n///\n///    Final Lower = if Close[i-1] >= Lower[i-1]\n///                  then max(Basic Lower[i], Lower[i-1])\n///                  else Basic Lower[i]\n///    ```\n/// 4. Identify trend direction:\n///    ```text\n///    Trend = if Close > Final Upper then 1 (Uptrend)\n///            else if Close < Final Lower then -1 (Downtrend)\n///            else Previous Trend\n///    ```\n/// 5. Generate Supertrend values:\n///    ```text\n///    Supertrend = if Trend == 1 then Final Lower (Support)\n///                 else Final Upper (Resistance)\n///    ```\n///\n/// # Parameters\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of closing prices\n/// * `opt_period` - ATR calculation period (typically 7-14)\n/// * `opt_multiplier` - ATR multiplier (typically 2-4)\n/// * `output_trend` - Output array for trend signals:\n///   - 1: Uptrend\n///   - 0: Initial/undefined\n///   - -1: Downtrend\n/// * `output_supertrend` - Output array for Supertrend values (support/resistance levels)\n/// * `output_atr` - Output array for ATR values\n/// * `output_upper` - Output array for upper band values\n/// * `output_lower` - Output array for lower band values\n///\n/// # Returns\n/// * `Ok(())` - Calculation successful\n///\n/// # Errors\n/// * `KandError::InvalidData` - Empty input arrays\n/// * `KandError::LengthMismatch` - Input/output arrays have different lengths\n/// * `KandError::InvalidParameter` - Invalid `opt_period` (<2)\n/// * `KandError::InsufficientData` - Input length less than required lookback\n/// * `KandError::NaNDetected` - NaN values in input (with `check-nan` feature)\n/// * `KandError::ConversionError` - Numeric conversion error\npub fn supertrend(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_period: usize,\n    opt_multiplier: TAFloat,\n    output_trend: &mut [TAInt],\n    output_supertrend: &mut [TAFloat],\n    output_atr: &mut [TAFloat],\n    output_upper: &mut [TAFloat],\n    output_lower: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != input_low.len()\n            || len != input_close.len()\n            || len != output_trend.len()\n            || len != output_supertrend.len()\n            || len != output_atr.len()\n            || len != output_upper.len()\n            || len != output_lower.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        for i in 0..len {\n            if input_high[i].is_nan() || input_low[i].is_nan() || input_close[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate ATR\n    atr::atr(input_high, input_low, input_close, opt_period, output_atr)?;\n\n    let mut basic_upper = vec![0.0; len];\n    let mut basic_lower = vec![0.0; len];\n\n    // Convert trend direction values\n    let up_trend = Signal::Bullish.into();\n    let down_trend = Signal::Bearish.into();\n    let no_trend = Signal::Neutral.into();\n\n    // Calculate basic bands\n    for i in lookback..len {\n        let hl2 = f64::midpoint(input_high[i], input_low[i]);\n        basic_upper[i] = opt_multiplier.mul_add(output_atr[i], hl2);\n        basic_lower[i] = opt_multiplier.mul_add(-output_atr[i], hl2);\n    }\n\n    // Initialize the first valid point (at lookback)\n    output_trend[lookback] = up_trend;\n    output_supertrend[lookback] = basic_lower[lookback];\n    output_upper[lookback] = basic_upper[lookback];\n    output_lower[lookback] = basic_lower[lookback];\n\n    // Calculate final bands and trend for remaining points\n    for i in (lookback + 1)..len {\n        // Calculate final upper band using min/max logic\n        if input_close[i - 1] <= output_upper[i - 1] {\n            output_upper[i] = basic_upper[i].min(output_upper[i - 1]);\n        } else {\n            output_upper[i] = basic_upper[i];\n        }\n\n        // Calculate final lower band using min/max logic\n        if input_close[i - 1] >= output_lower[i - 1] {\n            output_lower[i] = basic_lower[i].max(output_lower[i - 1]);\n        } else {\n            output_lower[i] = basic_lower[i];\n        }\n\n        // Determine trend direction based on previous trend\n        if output_trend[i - 1] == up_trend {\n            if input_close[i] < output_lower[i] {\n                output_trend[i] = down_trend;\n                output_supertrend[i] = output_upper[i];\n            } else {\n                output_trend[i] = up_trend;\n                output_supertrend[i] = output_lower[i];\n            }\n        } else if input_close[i] > output_upper[i] {\n            output_trend[i] = up_trend;\n            output_supertrend[i] = output_lower[i];\n        } else {\n            output_trend[i] = down_trend;\n            output_supertrend[i] = output_upper[i];\n        }\n    }\n\n    // Fill initial values with 0 (matching Python's initialization)\n    for i in 0..lookback {\n        output_trend[i] = no_trend;\n        output_supertrend[i] = TAFloat::NAN;\n        output_atr[i] = TAFloat::NAN;\n        output_upper[i] = TAFloat::NAN;\n        output_lower[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates a single Supertrend value incrementally\n///\n/// # Description\n/// Provides an optimized method for calculating the latest Supertrend value using previous state.\n/// Ideal for real-time processing as it:\n/// - Utilizes existing ATR, trend, and band values\n/// - Only processes the most recent data point\n/// - Avoids recomputing the entire series\n///\n/// # Calculation Process\n/// 1. Update ATR using previous value\n/// 2. Calculate new basic bands:\n///    ```text\n///    Basic Upper = (High + Low) / 2 + Multiplier * ATR\n///    Basic Lower = (High + Low) / 2 - Multiplier * ATR\n///    ```\n/// 3. Determine final bands based on price position:\n///    ```text\n///    Final Upper = min(Basic Upper, Prev Upper) if Prev Close <= Prev Upper\n///    Final Lower = max(Basic Lower, Prev Lower) if Prev Close >= Prev Lower\n///    ```\n/// 4. Update trend and Supertrend:\n///    - Switch to downtrend if uptrend and price < lower band\n///    - Switch to uptrend if downtrend and price > upper band\n///    - Maintain trend otherwise\n///\n/// # Parameters\n/// * `input_high` - Current period's high price\n/// * `input_low` - Current period's low price\n/// * `input_close` - Current period's close price\n/// * `prev_close` - Previous period's close price\n/// * `prev_atr` - Previous period's ATR value\n/// * `prev_trend` - Previous period's trend (1: up, -1: down)\n/// * `prev_upper` - Previous period's upper band\n/// * `prev_lower` - Previous period's lower band\n/// * `opt_period` - ATR calculation period (typically 7-14)\n/// * `opt_multiplier` - ATR multiplier (typically 2-4)\n///\n/// # Returns\n/// * `Ok((TAInt, TAFloat, TAFloat, TAFloat, TAFloat))` - Tuple containing:\n///   - Current trend signal (1: uptrend, -1: downtrend)\n///   - Current Supertrend value (support/resistance level)\n///   - Current ATR value\n///   - Current upper band\n///   - Current lower band\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - Invalid `opt_period` (<2)\n/// * `KandError::NaNDetected` - NaN values in input (with `check-nan` feature)\n/// * `KandError::ConversionError` - Numeric conversion error\npub fn supertrend_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    input_close: TAFloat,\n    prev_close: TAFloat,\n    prev_atr: TAFloat,\n    prev_trend: TAInt,\n    prev_upper: TAFloat,\n    prev_lower: TAFloat,\n    opt_period: usize,\n    opt_multiplier: TAFloat,\n) -> Result<(TAInt, TAFloat, TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_high.is_nan()\n            || input_low.is_nan()\n            || input_close.is_nan()\n            || prev_close.is_nan()\n            || prev_atr.is_nan()\n            || prev_upper.is_nan()\n            || prev_lower.is_nan()\n            || opt_multiplier.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let output_atr = atr::atr_inc(input_high, input_low, prev_close, prev_atr, opt_period)?;\n\n    let hl2 = f64::midpoint(input_high, input_low);\n    let basic_upper = opt_multiplier.mul_add(output_atr, hl2);\n    let basic_lower = opt_multiplier.mul_add(-output_atr, hl2);\n\n    let output_upper = if prev_close <= prev_upper {\n        basic_upper.min(prev_upper)\n    } else {\n        basic_upper\n    };\n\n    let output_lower = if prev_close >= prev_lower {\n        basic_lower.max(prev_lower)\n    } else {\n        basic_lower\n    };\n\n    let up_trend = Signal::Bullish.into();\n    let down_trend = Signal::Bearish.into();\n\n    let (output_trend, output_supertrend) = if prev_trend == up_trend {\n        if input_close < output_lower {\n            (down_trend, output_upper)\n        } else {\n            (up_trend, output_lower)\n        }\n    } else if input_close > output_upper {\n        (up_trend, output_lower)\n    } else {\n        (down_trend, output_upper)\n    };\n\n    Ok((\n        output_trend,\n        output_supertrend,\n        output_atr,\n        output_upper,\n        output_lower,\n    ))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    /// Test the calculation of Supertrend\n    #[test]\n    fn test_supertrend_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0,\n        ];\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0,\n        ];\n\n        let len = input_high.len();\n        let opt_period = 10;\n        let opt_multiplier = 3.0;\n\n        let mut output_trend = vec![0; len];\n        let mut output_supertrend = vec![0.0; len];\n        let mut output_atr = vec![0.0; len];\n        let mut output_upper = vec![0.0; len];\n        let mut output_lower = vec![0.0; len];\n\n        supertrend(\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_period,\n            opt_multiplier,\n            &mut output_trend,\n            &mut output_supertrend,\n            &mut output_atr,\n            &mut output_upper,\n            &mut output_lower,\n        )\n        .unwrap();\n\n        // Test Supertrend values\n        let expected_supertrend = [\n            35014.05,\n            35_021.590_000_000_004,\n            35_043.585_999_999_996,\n            35_043.585_999_999_996,\n            35_043.585_999_999_996,\n            35_285.012_906,\n            35_217.191_615_399_99,\n            35_205.262_453_86,\n            35_205.262_453_86,\n            35_205.262_453_86,\n            35_201.637_078_863_94,\n            35_166.628_370_977_545,\n            35_166.628_370_977_545,\n            35_166.628_370_977_545,\n            35_166.628_370_977_545,\n        ];\n\n        // Test Trend values\n        let expected_trend = [\n            100, 100, 100, 100, 100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100,\n        ];\n\n        // Check if the first 10 values are NaN\n        for i in 0..10 {\n            assert!(\n                output_supertrend[i].is_nan(),\n                \"Expected NaN for output_supertrend[{i}]\"\n            );\n            assert!(output_trend[i] == 0, \"Expected NaN for output_trend[{i}]\");\n        }\n\n        // Check the remaining values against expected values\n        for i in 10..expected_supertrend.len() {\n            let expected_st = expected_supertrend[i - 10];\n            let expected_tr = expected_trend[i - 10];\n            assert_relative_eq!(output_supertrend[i], expected_st, epsilon = 0.00001);\n            assert!(output_trend[i] == expected_tr);\n        }\n\n        // Test incremental calculation matches regular calculation\n        let prev_close = input_close[len - 2];\n        let prev_atr = output_atr[len - 2];\n        let prev_trend = output_trend[len - 2];\n        let prev_upper = output_upper[len - 2];\n        let prev_lower = output_lower[len - 2];\n\n        let (trend, supertrend, _, _, _) = supertrend_inc(\n            input_high[len - 1],\n            input_low[len - 1],\n            input_close[len - 1],\n            prev_close,\n            prev_atr,\n            prev_trend,\n            prev_upper,\n            prev_lower,\n            opt_period,\n            opt_multiplier,\n        )\n        .unwrap();\n\n        assert!(trend == output_trend[len - 1]);\n        assert_relative_eq!(supertrend, output_supertrend[len - 1], epsilon = 0.00001);\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/t3.rs",
    "content": "use crate::{TAFloat, error::KandError};\n\n/// Calculates the lookback period required for T3 indicator\n///\n/// # Arguments\n/// * `opt_period` - Smoothing period (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - Lookback period if successful\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(6 * (opt_period - 1))\n}\n\n/// Calculates T3 (Triple Exponential Moving Average) indicator for a price series\n///\n/// # Description\n/// T3 is a sophisticated moving average developed by Tim Tillson that reduces lag while maintaining smoothness.\n/// It combines six EMAs with optimized weightings to produce a responsive yet smooth indicator.\n///\n/// # Mathematical Formula\n/// ```text\n/// k = 2/(period + 1)\n/// EMA1 = EMA(price, period)\n/// EMA2 = EMA(EMA1, period)\n/// EMA3 = EMA(EMA2, period)\n/// EMA4 = EMA(EMA3, period)\n/// EMA5 = EMA(EMA4, period)\n/// EMA6 = EMA(EMA5, period)\n///\n/// Coefficients:\n/// a = volume_factor\n/// c1 = -a^3\n/// c2 = 3*a^2 + 3*a^3\n/// c3 = -6*a^2 - 3*a - 3*a^3\n/// c4 = 1 + 3*a + a^3 + 3*a^2\n///\n/// T3 = c1*EMA6 + c2*EMA5 + c3*EMA4 + c4*EMA3\n/// ```\n///\n/// # Arguments\n/// * `input` - Price data array\n/// * `opt_period` - Smoothing period for EMAs (must be >= 2)\n/// * `opt_vfactor` - Volume factor controlling smoothing (typically 0-1)\n/// * `output` - Array to store T3 values\n/// * `output_ema1` - Array to store EMA1 values\n/// * `output_ema2` - Array to store EMA2 values\n/// * `output_ema3` - Array to store EMA3 values\n/// * `output_ema4` - Array to store EMA4 values\n/// * `output_ema5` - Array to store EMA5 values\n/// * `output_ema6` - Array to store EMA6 values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty Ok if successful\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input array is empty\n/// * `KandError::LengthMismatch` - If input/output array lengths don't match\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n/// * `KandError::InsufficientData` - If input length <= lookback period\n/// * `KandError::NaNDetected` - If NaN values found in input\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::t3;\n///\n/// let input = vec![10.0, 12.0, 15.0, 14.0, 13.0, 11.0, 11.0];\n/// let mut output = vec![0.0; input.len()];\n/// let mut ema1 = vec![0.0; input.len()];\n/// let mut ema2 = vec![0.0; input.len()];\n/// let mut ema3 = vec![0.0; input.len()];\n/// let mut ema4 = vec![0.0; input.len()];\n/// let mut ema5 = vec![0.0; input.len()];\n/// let mut ema6 = vec![0.0; input.len()];\n///\n/// t3::t3(\n///     &input,\n///     2,\n///     0.7,\n///     &mut output,\n///     &mut ema1,\n///     &mut ema2,\n///     &mut ema3,\n///     &mut ema4,\n///     &mut ema5,\n///     &mut ema6,\n/// )\n/// .unwrap();\n/// ```\npub fn t3(\n    input: &[TAFloat],\n    opt_period: usize,\n    opt_vfactor: TAFloat,\n    output: &mut [TAFloat],\n    output_ema1: &mut [TAFloat],\n    output_ema2: &mut [TAFloat],\n    output_ema3: &mut [TAFloat],\n    output_ema4: &mut [TAFloat],\n    output_ema5: &mut [TAFloat],\n    output_ema6: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != output.len()\n            || len != output_ema1.len()\n            || len != output_ema2.len()\n            || len != output_ema3.len()\n            || len != output_ema4.len()\n            || len != output_ema5.len()\n            || len != output_ema6.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for value in input.iter().take(len) {\n            if value.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    let a = opt_vfactor as TAFloat;\n    let a2 = a * a;\n    let a3 = a2 * a;\n\n    // Calculate coefficients consistent with TA-Lib\n    let c1 = -a3;\n    let c2 = 3.0 * (a2 + a3);\n    let c3 = 3.0f64.mul_add(-a3, (-6.0f64).mul_add(a2, -(3.0 * a)));\n    let c4 = 3.0f64.mul_add(a2, 3.0f64.mul_add(a, 1.0) + a3);\n\n    let k = crate::helper::period_to_k(opt_period)?;\n    let one_minus_k = 1.0 - k;\n\n    // Sequential initialization mimicking TA-Lib's warm-up process.\n    // \"today\" will serve as our pointer into the input array.\n    let mut today = 0;\n\n    // Initialize EMA1 with a simple moving average (SMA) of the first 'opt_period' values.\n    let mut temp = 0.0;\n    for _ in 0..opt_period {\n        temp += input[today];\n        today += 1;\n    }\n    let mut e1 = temp / (opt_period as TAFloat);\n\n    // Initialize EMA2 using the next (opt_period - 1) values.\n    temp = e1;\n    for _ in 1..opt_period {\n        e1 = k.mul_add(input[today], one_minus_k * e1);\n        temp += e1;\n        today += 1;\n    }\n    let mut e2 = temp / (opt_period as TAFloat);\n\n    // Initialize EMA3.\n    temp = e2;\n    for _ in 1..opt_period {\n        e1 = k.mul_add(input[today], one_minus_k * e1);\n        e2 = k.mul_add(e1, one_minus_k * e2);\n        temp += e2;\n        today += 1;\n    }\n    let mut e3 = temp / (opt_period as TAFloat);\n\n    // Initialize EMA4.\n    temp = e3;\n    for _ in 1..opt_period {\n        e1 = k.mul_add(input[today], one_minus_k * e1);\n        e2 = k.mul_add(e1, one_minus_k * e2);\n        e3 = k.mul_add(e2, one_minus_k * e3);\n        temp += e3;\n        today += 1;\n    }\n    let mut e4 = temp / (opt_period as TAFloat);\n\n    // Initialize EMA5.\n    temp = e4;\n    for _ in 1..opt_period {\n        e1 = k.mul_add(input[today], one_minus_k * e1);\n        e2 = k.mul_add(e1, one_minus_k * e2);\n        e3 = k.mul_add(e2, one_minus_k * e3);\n        e4 = k.mul_add(e3, one_minus_k * e4);\n        temp += e4;\n        today += 1;\n    }\n    let mut e5 = temp / (opt_period as TAFloat);\n\n    // Initialize EMA6.\n    temp = e5;\n    for _ in 1..opt_period {\n        e1 = k.mul_add(input[today], one_minus_k * e1);\n        e2 = k.mul_add(e1, one_minus_k * e2);\n        e3 = k.mul_add(e2, one_minus_k * e3);\n        e4 = k.mul_add(e3, one_minus_k * e4);\n        e5 = k.mul_add(e4, one_minus_k * e5);\n        temp += e5;\n        today += 1;\n    }\n    let mut e6 = temp / (opt_period as TAFloat);\n\n    // Skip the remainder of the unstable period (if any).\n    while today <= lookback {\n        e1 = k.mul_add(input[today], one_minus_k * e1);\n        e2 = k.mul_add(e1, one_minus_k * e2);\n        e3 = k.mul_add(e2, one_minus_k * e3);\n        e4 = k.mul_add(e3, one_minus_k * e4);\n        e5 = k.mul_add(e4, one_minus_k * e5);\n        e6 = k.mul_add(e5, one_minus_k * e6);\n        today += 1;\n    }\n\n    // Write the first valid output at index = lookback.\n    output[lookback] = c4.mul_add(e3, c3.mul_add(e4, c1.mul_add(e6, c2 * e5)));\n    output_ema1[lookback] = e1;\n    output_ema2[lookback] = e2;\n    output_ema3[lookback] = e3;\n    output_ema4[lookback] = e4;\n    output_ema5[lookback] = e5;\n    output_ema6[lookback] = e6;\n\n    // Process the remaining data points.\n    while today < len {\n        e1 = k.mul_add(input[today], one_minus_k * e1);\n        e2 = k.mul_add(e1, one_minus_k * e2);\n        e3 = k.mul_add(e2, one_minus_k * e3);\n        e4 = k.mul_add(e3, one_minus_k * e4);\n        e5 = k.mul_add(e4, one_minus_k * e5);\n        e6 = k.mul_add(e5, one_minus_k * e6);\n\n        output[today] = c4.mul_add(e3, c3.mul_add(e4, c1.mul_add(e6, c2 * e5)));\n        output_ema1[today] = e1;\n        output_ema2[today] = e2;\n        output_ema3[today] = e3;\n        output_ema4[today] = e4;\n        output_ema5[today] = e5;\n        output_ema6[today] = e6;\n        today += 1;\n    }\n\n    // Mark the unstable period indices with NaN.\n    for i in 0..lookback {\n        output[i] = TAFloat::NAN;\n        output_ema1[i] = TAFloat::NAN;\n        output_ema2[i] = TAFloat::NAN;\n        output_ema3[i] = TAFloat::NAN;\n        output_ema4[i] = TAFloat::NAN;\n        output_ema5[i] = TAFloat::NAN;\n        output_ema6[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the latest T3 value incrementally using previous EMA values\n///\n/// # Description\n/// Provides an efficient way to update T3 values in real-time by using previously calculated EMA values.\n/// This avoids recalculating the entire series when only the latest value is needed.\n///\n/// # Arguments\n/// * `input_price` - Latest price value to calculate T3 from\n/// * `prev_ema1` - Previous EMA1 value\n/// * `prev_ema2` - Previous EMA2 value\n/// * `prev_ema3` - Previous EMA3 value\n/// * `prev_ema4` - Previous EMA4 value\n/// * `prev_ema5` - Previous EMA5 value\n/// * `prev_ema6` - Previous EMA6 value\n/// * `opt_period` - Smoothing period for EMAs (must be >= 2)\n/// * `opt_vfactor` - Volume factor (typically 0-1)\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat, TAFloat, TAFloat, TAFloat, TAFloat), KandError>` - Tuple containing:\n///   - Latest T3 value\n///   - Updated EMA1 value\n///   - Updated EMA2 value\n///   - Updated EMA3 value\n///   - Updated EMA4 value\n///   - Updated EMA5 value\n///   - Updated EMA6 value\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n/// * `KandError::NaNDetected` - If any input value is NaN\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::t3;\n///\n/// let (t3_value, ema1, ema2, ema3, ema4, ema5, ema6) = t3::t3_inc(\n///     100.0, // New price\n///     95.0,  // Previous EMA1\n///     94.0,  // Previous EMA2\n///     93.0,  // Previous EMA3\n///     92.0,  // Previous EMA4\n///     91.0,  // Previous EMA5\n///     90.0,  // Previous EMA6\n///     5,     // Period\n///     0.7,   // Volume factor\n/// )\n/// .unwrap();\n/// ```\n#[allow(clippy::too_many_arguments, clippy::type_complexity)]\npub fn t3_inc(\n    input_price: TAFloat,\n    prev_ema1: TAFloat,\n    prev_ema2: TAFloat,\n    prev_ema3: TAFloat,\n    prev_ema4: TAFloat,\n    prev_ema5: TAFloat,\n    prev_ema6: TAFloat,\n    opt_period: usize,\n    opt_vfactor: TAFloat,\n) -> Result<\n    (\n        TAFloat,\n        TAFloat,\n        TAFloat,\n        TAFloat,\n        TAFloat,\n        TAFloat,\n        TAFloat,\n    ),\n    KandError,\n> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_price.is_nan()\n            || prev_ema1.is_nan()\n            || prev_ema2.is_nan()\n            || prev_ema3.is_nan()\n            || prev_ema4.is_nan()\n            || prev_ema5.is_nan()\n            || prev_ema6.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let k = crate::helper::period_to_k(opt_period)?;\n    let one_minus_k = 1.0 - k;\n\n    // Calculate new EMA values\n    let ema1 = input_price.mul_add(k, prev_ema1 * one_minus_k);\n    let ema2 = ema1.mul_add(k, prev_ema2 * one_minus_k);\n    let ema3 = ema2.mul_add(k, prev_ema3 * one_minus_k);\n    let ema4 = ema3.mul_add(k, prev_ema4 * one_minus_k);\n    let ema5 = ema4.mul_add(k, prev_ema5 * one_minus_k);\n    let ema6 = ema5.mul_add(k, prev_ema6 * one_minus_k);\n\n    // Calculate coefficients\n    let a = opt_vfactor as TAFloat;\n    let a2 = a * a;\n    let a3 = a2 * a;\n\n    let c1 = -a3;\n    let c2 = 3.0 * (a2 + a3);\n    let c3 = 3.0f64.mul_add(-a3, (-6.0f64).mul_add(a2, -(3.0 * a)));\n    let c4 = 3.0f64.mul_add(a2, 3.0f64.mul_add(a, 1.0) + a3);\n\n    // Calculate T3\n    let t3 = c4.mul_add(ema3, c3.mul_add(ema4, c1.mul_add(ema6, c2 * ema5)));\n\n    Ok((t3, ema1, ema2, ema3, ema4, ema5, ema6))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_t3_calculation() {\n        let input = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0, 35114.5, 35097.2,\n            35092.0, 35073.2, 35139.3, 35092.0, 35126.7, 35106.3, 35124.8, 35170.1,\n        ];\n\n        let opt_period = 5;\n        let opt_vfactor = 0.7;\n        let mut output = vec![0.0; input.len()];\n        let mut output_ema1 = vec![0.0; input.len()];\n        let mut output_ema2 = vec![0.0; input.len()];\n        let mut output_ema3 = vec![0.0; input.len()];\n        let mut output_ema4 = vec![0.0; input.len()];\n        let mut output_ema5 = vec![0.0; input.len()];\n        let mut output_ema6 = vec![0.0; input.len()];\n\n        // Calculate T3 for the full series\n        t3(\n            &input,\n            opt_period,\n            opt_vfactor,\n            &mut output,\n            &mut output_ema1,\n            &mut output_ema2,\n            &mut output_ema3,\n            &mut output_ema4,\n            &mut output_ema5,\n            &mut output_ema6,\n        )\n        .unwrap();\n\n        // First 24 values should be NaN (lookback = 6 * (period - 1) = 24)\n        for value in output.iter().take(24) {\n            assert!(value.is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            34_990.330_892_685_28,\n            35_014.010_406_572_81,\n            35_039.640_085_147_26,\n            35_061.489_687_430_236,\n            35_075.563_406_161_85,\n            35_090.473_558_789_85,\n            35_100.366_627_049_894,\n            35_109.086_698_426_225,\n            35_114.435_788_643_98,\n            35_118.848_638_076_015,\n            35_127.887_566_614_78,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output[i + 24], *expected, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation\n        let mut prev_ema1: TAFloat = output_ema1[24];\n        let mut prev_ema2: TAFloat = output_ema2[24];\n        let mut prev_ema3: TAFloat = output_ema3[24];\n        let mut prev_ema4: TAFloat = output_ema4[24];\n        let mut prev_ema5: TAFloat = output_ema5[24];\n        let mut prev_ema6: TAFloat = output_ema6[24];\n\n        // Test each incremental step matches the full calculation\n        for i in 25..input.len() {\n            let (t3, ema1, ema2, ema3, ema4, ema5, ema6) = t3_inc(\n                input[i],\n                prev_ema1,\n                prev_ema2,\n                prev_ema3,\n                prev_ema4,\n                prev_ema5,\n                prev_ema6,\n                opt_period,\n                opt_vfactor,\n            )\n            .unwrap();\n\n            assert_relative_eq!(t3, output[i], epsilon = 0.0001);\n            assert_relative_eq!(ema1, output_ema1[i], epsilon = 0.0001);\n            assert_relative_eq!(ema2, output_ema2[i], epsilon = 0.0001);\n            assert_relative_eq!(ema3, output_ema3[i], epsilon = 0.0001);\n            assert_relative_eq!(ema4, output_ema4[i], epsilon = 0.0001);\n            assert_relative_eq!(ema5, output_ema5[i], epsilon = 0.0001);\n            assert_relative_eq!(ema6, output_ema6[i], epsilon = 0.0001);\n\n            prev_ema1 = ema1;\n            prev_ema2 = ema2;\n            prev_ema3 = ema3;\n            prev_ema4 = ema4;\n            prev_ema5 = ema5;\n            prev_ema6 = ema6;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/tema.rs",
    "content": "use crate::{KandError, TAFloat, ta::ohlcv::ema};\n\n/// Calculates the lookback period required for Triple Exponential Moving Average (TEMA)\n///\n/// # Description\n/// The lookback period represents the minimum number of data points needed before the first valid TEMA value\n/// can be calculated. For TEMA, this equals 3 * (period - 1) due to the triple EMA calculation process.\n///\n/// # Arguments\n/// * `opt_period` - The smoothing period used for TEMA calculation. Must be >= 2.\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The required lookback period if successful\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - Returned if period < 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::tema;\n/// let period = 14;\n/// let lookback = tema::lookback(period).unwrap();\n/// assert_eq!(lookback, 39); // 3 * (14 - 1)\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(3 * (opt_period - 1))\n}\n\n/// Calculates Triple Exponential Moving Average (TEMA) for a price series\n///\n/// # Description\n/// TEMA is an enhanced moving average designed to reduce lag while maintaining smoothing properties.\n/// It applies triple exponential smoothing to put more weight on recent data and less on older data.\n///\n/// # Mathematical Formula\n/// ```text\n/// EMA1 = EMA(price, period)\n/// EMA2 = EMA(EMA1, period)\n/// EMA3 = EMA(EMA2, period)\n/// TEMA = (3 × EMA1) - (3 × EMA2) + EMA3\n/// ```\n///\n/// # Calculation Steps\n/// 1. Calculate first EMA of the input prices\n/// 2. Calculate second EMA using the first EMA values\n/// 3. Calculate third EMA using the second EMA values\n/// 4. Apply the TEMA formula to combine all three EMAs\n///\n/// # Arguments\n/// * `input` - Slice of input price values\n/// * `opt_period` - Smoothing period for calculations (must be >= 2)\n/// * `output_tema` - Mutable slice to store TEMA results (first lookback values will be NaN)\n/// * `output_ema1` - Mutable slice to store first EMA series\n/// * `output_ema2` - Mutable slice to store second EMA series\n/// * `output_ema3` - Mutable slice to store third EMA series\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty Ok value on success\n///\n/// # Errors\n/// * `KandError::InvalidData` - Input slice is empty\n/// * `KandError::LengthMismatch` - Output arrays don't match input length\n/// * `KandError::InvalidParameter` - Period is less than 2\n/// * `KandError::InsufficientData` - Input length is less than required lookback period\n/// * `KandError::NaNDetected` - Input contains NaN values (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::tema;\n///\n/// let input = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];\n/// let period = 3;\n/// let mut output_tema = vec![0.0; input.len()];\n/// let mut ema1 = vec![0.0; input.len()];\n/// let mut ema2 = vec![0.0; input.len()];\n/// let mut ema3 = vec![0.0; input.len()];\n///\n/// tema::tema(\n///     &input,\n///     period,\n///     &mut output_tema,\n///     &mut ema1,\n///     &mut ema2,\n///     &mut ema3,\n/// )\n/// .unwrap();\n/// ```\npub fn tema(\n    input: &[TAFloat],\n    opt_period: usize,\n    output_tema: &mut [TAFloat],\n    output_ema1: &mut [TAFloat],\n    output_ema2: &mut [TAFloat],\n    output_ema3: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Check if input is empty\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Check if input length is less than required lookback period\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Check if output arrays have the same length as input\n        if len != output_tema.len()\n            || len != output_ema1.len()\n            || len != output_ema2.len()\n            || len != output_ema3.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // Check if input contains NaN values\n        for value in input {\n            if value.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate first EMA series\n    ema::ema(input, opt_period, None, output_ema1)?;\n\n    // Calculate second EMA series using valid values from first EMA\n    ema::ema(\n        &output_ema1[opt_period - 1..],\n        opt_period,\n        None,\n        &mut output_ema2[opt_period - 1..],\n    )?;\n\n    // Calculate third EMA series\n    ema::ema(\n        &output_ema2[2 * (opt_period - 1)..],\n        opt_period,\n        None,\n        &mut output_ema3[2 * (opt_period - 1)..],\n    )?;\n\n    // Calculate TEMA and store it in the output array (valid only after lookback)\n    for i in lookback..len {\n        output_tema[i] = 3.0f64.mul_add(output_ema1[i], -(3.0 * output_ema2[i])) + output_ema3[i];\n    }\n\n    // Fill initial periods with NAN for all outputs\n    for i in 0..lookback {\n        output_tema[i] = TAFloat::NAN;\n        output_ema1[i] = TAFloat::NAN;\n        output_ema2[i] = TAFloat::NAN;\n        output_ema3[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates TEMA value incrementally using previous EMA values\n///\n/// # Description\n/// This function enables real-time TEMA calculation by using the previous EMA values and latest price.\n/// It avoids recalculating the entire series, making it efficient for streaming data.\n///\n/// # Arguments\n/// * `input` - Latest price value to process\n/// * `prev_ema1` - Previous value of first EMA\n/// * `prev_ema2` - Previous value of second EMA\n/// * `prev_ema3` - Previous value of third EMA\n/// * `opt_period` - Smoothing period for calculations (must be >= 2)\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat, TAFloat), KandError>` - Tuple containing:\n///   - Current TEMA value\n///   - Updated first EMA\n///   - Updated second EMA\n///   - Updated third EMA\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - Period is less than 2\n/// * `KandError::NaNDetected` - Any input value is NaN (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::tema::tema_inc;\n///\n/// let new_price = 10.0;\n/// let prev_ema1 = 9.0;\n/// let prev_ema2 = 8.0;\n/// let prev_ema3 = 7.0;\n/// let period = 3;\n///\n/// let (tema, ema1, ema2, ema3) =\n///     tema_inc(new_price, prev_ema1, prev_ema2, prev_ema3, period).unwrap();\n/// ```\npub fn tema_inc(\n    input: TAFloat,\n    prev_ema1: TAFloat,\n    prev_ema2: TAFloat,\n    prev_ema3: TAFloat,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input.is_nan() || prev_ema1.is_nan() || prev_ema2.is_nan() || prev_ema3.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let ema1 = ema::ema_inc(input, prev_ema1, opt_period, None)?;\n    let ema2 = ema::ema_inc(ema1, prev_ema2, opt_period, None)?;\n    let ema3 = ema::ema_inc(ema2, prev_ema3, opt_period, None)?;\n    let tema = 3.0f64.mul_add(ema1, -(3.0 * ema2)) + ema3;\n\n    Ok((tema, ema1, ema2, ema3))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_tema_calculation() {\n        let input = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6,\n        ];\n        let opt_period = 3;\n        let mut output_tema = vec![0.0; input.len()];\n        let mut ema1 = vec![0.0; input.len()];\n        let mut ema2 = vec![0.0; input.len()];\n        let mut ema3 = vec![0.0; input.len()];\n\n        tema(\n            &input,\n            opt_period,\n            &mut output_tema,\n            &mut ema1,\n            &mut ema2,\n            &mut ema3,\n        )\n        .unwrap();\n\n        // First 6 values should be NaN\n        for value in output_tema.iter().take(6) {\n            assert!(value.is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            35_209.883_333_333_34,\n            35_245.566_666_666_68,\n            35_206.030_208_333_33,\n            35_184.880_729_166_66,\n            35_173.019_270_833_32,\n            35_220.059_635_416_67,\n            35_216.397_591_145_84,\n            35_168.941_569_010_41,\n            35_096.534_114_583_344,\n            35_039.869_694_010_4,\n            34_995.421_651_204_42,\n            35_003.259_470_621_76,\n            35_058.344_179_280_6,\n            35_033.424_372_355_13,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_tema[i + 6], *expected, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation matches regular calculation\n        let mut prev_ema1 = ema1[10];\n        let mut prev_ema2 = ema2[10];\n        let mut prev_ema3 = ema3[10];\n\n        for i in 11..15 {\n            let (tema_val, new_ema1, new_ema2, new_ema3) =\n                tema_inc(input[i], prev_ema1, prev_ema2, prev_ema3, opt_period).unwrap();\n\n            assert_relative_eq!(tema_val, output_tema[i], epsilon = 0.0001);\n            assert_relative_eq!(new_ema1, ema1[i], epsilon = 0.0001);\n            assert_relative_eq!(new_ema2, ema2[i], epsilon = 0.0001);\n            assert_relative_eq!(new_ema3, ema3[i], epsilon = 0.0001);\n\n            prev_ema1 = new_ema1;\n            prev_ema2 = new_ema2;\n            prev_ema3 = new_ema3;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/trange.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for True Range (TR) calculation\n///\n/// # Description\n/// The lookback period indicates the minimum number of data points needed before\n/// the first valid True Range value can be calculated.\n///\n/// # Arguments\n/// * None\n///\n/// # Returns\n/// * `Result<usize, KandError>` - Returns 1, as TR calculation requires the previous close price\n///\n/// # Errors\n/// * None\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::trange;\n/// let lookback = trange::lookback().unwrap();\n/// assert_eq!(lookback, 1);\n/// ```\npub const fn lookback() -> Result<usize, KandError> {\n    Ok(1)\n}\n\n/// Calculates True Range (TR) values for a series of price data\n///\n/// # Description\n/// True Range is a technical indicator that measures market volatility by taking into account\n/// gaps and limit moves between trading periods. It expands the regular high-low range when\n/// there are price gaps between periods.\n///\n/// # Mathematical Formula\n/// ```text\n/// TR = max(\n///     high[i] - low[i],                 // Current period's range\n///     |high[i] - close[i-1]|,           // Current high to previous close\n///     |low[i] - close[i-1]|             // Current low to previous close\n/// )\n/// ```\n///\n/// # Calculation Principles\n/// 1. Calculate the current period's high-low range\n/// 2. Calculate the absolute difference between current high and previous close\n/// 3. Calculate the absolute difference between current low and previous close\n/// 4. Take the maximum of these three values\n///\n/// # Arguments\n/// * `input_high` - Array of high prices for each period\n/// * `input_low` - Array of low prices for each period\n/// * `input_close` - Array of closing prices for each period\n/// * `output_trange` - Array to store calculated TR values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty Ok value on success\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input arrays have different lengths\n/// * `KandError::InsufficientData` - If input length <= lookback period\n/// * `KandError::NaNDetected` - If any input value is NaN (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::trange;\n///\n/// let high = vec![10.0, 12.0, 15.0];\n/// let low = vec![8.0, 9.0, 11.0];\n/// let close = vec![9.0, 11.0, 14.0];\n/// let mut tr = vec![0.0; 3];\n///\n/// trange::trange(&high, &low, &close, &mut tr).unwrap();\n/// assert!(tr[0].is_nan()); // First value is NaN\n/// assert_eq!(tr[1], 3.0); // max(3, 3, 2)\n/// assert_eq!(tr[2], 4.0); // max(4, 4, 3)\n/// ```\npub fn trange(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    output_trange: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback()?;\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Length consistency check\n        if len != input_low.len() || len != input_close.len() || len != output_trange.len() {\n            return Err(KandError::LengthMismatch);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_high[i].is_nan() || input_low[i].is_nan() || input_close[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // First value is NAN since we need previous close\n    output_trange[0] = TAFloat::NAN;\n\n    // Calculate True Range for remaining values\n    for i in 1..len {\n        let h_l = input_high[i] - input_low[i];\n        let h_pc = (input_high[i] - input_close[i - 1]).abs();\n        let l_pc = (input_low[i] - input_close[i - 1]).abs();\n        output_trange[i] = h_l.max(h_pc).max(l_pc);\n    }\n\n    // Fill initial values with NAN\n    for value in output_trange.iter_mut().take(lookback) {\n        *value = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates a single True Range value for the most recent period\n///\n/// # Description\n/// This function provides an optimized way to calculate TR for real-time data updates,\n/// requiring only the current period's prices and previous close.\n///\n/// # Mathematical Formula\n/// ```text\n/// TR = max(\n///     high - low,                // Current period's range\n///     |high - prev_close|,       // High to previous close\n///     |low - prev_close|         // Low to previous close\n/// )\n/// ```\n///\n/// # Arguments\n/// * `input_high` - Current period's high price\n/// * `input_low` - Current period's low price\n/// * `prev_close` - Previous period's closing price\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - Calculated TR value\n///\n/// # Errors\n/// * `KandError::NaNDetected` - If any input value is NaN (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::trange;\n///\n/// let tr = trange::trange_inc(12.0, 9.0, 11.0).unwrap();\n/// assert_eq!(tr, 3.0); // max(3, 1, 2)\n/// ```\npub fn trange_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    prev_close: TAFloat,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_high.is_nan() || input_low.is_nan() || prev_close.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let h_l = input_high - input_low;\n    let h_pc = (input_high - prev_close).abs();\n    let l_pc = (input_low - prev_close).abs();\n    Ok(h_l.max(h_pc).max(l_pc))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    // Basic functionality tests\n    #[test]\n    fn test_trange_calculation() {\n        let input_high = vec![35266.0, 35247.5, 35235.7, 35190.8, 35182.0];\n        let input_low = vec![35216.1, 35206.5, 35180.0, 35130.7, 35153.6];\n        let input_close = vec![35216.1, 35221.4, 35190.7, 35170.0, 35181.5];\n        let mut output_trange = vec![0.0; 5];\n\n        trange(&input_high, &input_low, &input_close, &mut output_trange).unwrap();\n\n        assert!(output_trange[0].is_nan());\n        assert_relative_eq!(output_trange[1], 41.0, epsilon = 0.00001);\n        assert_relative_eq!(output_trange[2], 55.7, epsilon = 0.00001);\n        assert_relative_eq!(output_trange[3], 60.1, epsilon = 0.00001);\n        assert_relative_eq!(output_trange[4], 28.4, epsilon = 0.00001);\n\n        // Test each incremental step matches regular calculation\n        for i in 1..5 {\n            let result = trange_inc(input_high[i], input_low[i], input_close[i - 1]).unwrap();\n            assert_relative_eq!(result, output_trange[i], epsilon = 0.00001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/trima.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for Triangular Moving Average (TRIMA) calculation.\n///\n/// # Description\n/// The lookback period represents the minimum number of data points needed before the first valid TRIMA value\n/// can be calculated. For TRIMA, this equals one less than the specified period.\n///\n/// # Arguments\n/// * `opt_period` - The smoothing period used for TRIMA calculation. Must be >= 2.\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The required lookback period if successful\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - Returned if period < 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::trima;\n/// let period = 14;\n/// let lookback = trima::lookback(period).unwrap();\n/// assert_eq!(lookback, 13); // period - 1\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Calculates Triangular Moving Average (TRIMA) for a price series.\n///\n/// # Description\n/// TRIMA is a double-smoothed moving average that places more weight on the middle portion of the price series\n/// and less weight on the first and last portions. This results in a smoother moving average compared to a\n/// Simple Moving Average (SMA).\n///\n/// # Mathematical Formula\n/// ```text\n/// For odd period:\n///   n = (period + 1) / 2\n///   TRIMA = SMA(SMA(price, n), n)\n///\n/// For even period:\n///   n = (period / 2) + 1\n///   m = period / 2\n///   TRIMA = SMA(SMA(price, n), m)\n/// ```\n///\n/// # Calculation Steps\n/// 1. Determine window sizes n and m based on period\n/// 2. Calculate first SMA using window size n\n/// 3. Calculate second SMA (TRIMA) using window size m\n/// 4. Fill initial values with NaN\n///\n/// # Arguments\n/// * `input` - Slice of input price values\n/// * `opt_period` - Smoothing period for calculations (must be >= 2)\n/// * `output_sma1` - Mutable slice to store intermediate SMA values\n/// * `output_sma2` - Mutable slice to store final TRIMA values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty Ok value on success\n///\n/// # Errors\n/// * `KandError::InvalidData` - Input slice is empty\n/// * `KandError::LengthMismatch` - Output arrays don't match input length\n/// * `KandError::InvalidParameter` - Period is less than 2\n/// * `KandError::InsufficientData` - Input length is less than required lookback period\n/// * `KandError::NaNDetected` - Input contains NaN values (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::trima;\n///\n/// let input = vec![1.0, 2.0, 3.0, 4.0, 5.0];\n/// let period = 3;\n/// let mut output_sma1 = vec![0.0; input.len()];\n/// let mut output_sma2 = vec![0.0; input.len()];\n///\n/// trima::trima(&input, period, &mut output_sma1, &mut output_sma2).unwrap();\n/// ```\npub fn trima(\n    input: &[TAFloat],\n    opt_period: usize,\n    output_sma1: &mut [TAFloat],\n    output_sma2: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if len != output_sma1.len() || len != output_sma2.len() {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for value in input.iter().take(len) {\n            if value.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    let (n, m) = if opt_period % 2 == 1 {\n        let n = opt_period.div_ceil(2);\n        (n, n)\n    } else {\n        let n = (opt_period / 2) + 1;\n        let m = opt_period / 2;\n        (n, m)\n    };\n\n    // First SMA calculation\n    let mut sum = 0.0;\n    for value in input.iter().take(n) {\n        sum += *value;\n    }\n    output_sma1[n - 1] = sum / (n as TAFloat);\n\n    for i in n..len {\n        sum = sum + input[i] - input[i - n];\n        output_sma1[i] = sum / (n as TAFloat);\n    }\n\n    // Second SMA calculation\n    sum = 0.0;\n    for value in output_sma1.iter().take(m) {\n        sum += *value;\n    }\n    output_sma2[m - 1] = sum / (m as TAFloat);\n\n    for i in m..len {\n        sum = sum + output_sma1[i] - output_sma1[i - m];\n        output_sma2[i] = sum / (m as TAFloat);\n    }\n\n    // Fill initial values with NAN\n    for i in 0..lookback {\n        output_sma1[i] = TAFloat::NAN;\n        output_sma2[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the next TRIMA value incrementally using previous SMA values.\n///\n/// # Description\n/// This function allows for real-time TRIMA calculation without needing the entire price history.\n/// It maintains the double-smoothing characteristic of TRIMA while only performing the minimal\n/// required calculations.\n///\n/// # Mathematical Formula\n/// ```text\n/// First SMA update:\n///   new_sma1 = prev_sma1 + (new_price - old_price) / n\n///\n/// Second SMA update:\n///   new_sma2 = prev_sma2 + (new_sma1 - old_sma1) / m\n///\n/// Where:\n/// - For odd period:  n = m = (period + 1)/2\n/// - For even period: n = (period/2) + 1, m = period/2\n/// ```\n///\n/// # Arguments\n/// * `prev_sma1` - Previous first SMA value\n/// * `prev_sma2` - Previous TRIMA value\n/// * `input_new_price` - Latest price to include in calculation\n/// * `input_old_price` - Price dropping out of first window\n/// * `input_old_sma1` - SMA1 value dropping out of second window\n/// * `opt_period` - The smoothing period for calculations (must be >= 2)\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat), KandError>` - Tuple of (`new_sma1`, `new_trima`) on success\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - Period is less than 2\n/// * `KandError::NaNDetected` - Input contains NaN values (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::trima;\n///\n/// // Example with period = 5 (odd period)\n/// let prev_sma1 = 35.5;\n/// let prev_sma2 = 35.2;\n/// let new_price = 36.0;\n/// let old_price = 35.0;\n/// let old_sma1 = 35.1;\n/// let period = 5;\n///\n/// let (new_sma1, new_trima) =\n///     trima::trima_inc(prev_sma1, prev_sma2, new_price, old_price, old_sma1, period).unwrap();\n/// ```\npub fn trima_inc(\n    prev_sma1: TAFloat,\n    prev_sma2: TAFloat,\n    input_new_price: TAFloat,\n    input_old_price: TAFloat,\n    input_old_sma1: TAFloat,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if prev_sma1.is_nan()\n            || prev_sma2.is_nan()\n            || input_new_price.is_nan()\n            || input_old_price.is_nan()\n            || input_old_sma1.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let (n, m) = if opt_period % 2 == 1 {\n        let n = opt_period.div_ceil(2);\n        (n, n)\n    } else {\n        let n = (opt_period / 2) + 1;\n        let m = opt_period / 2;\n        (n, m)\n    };\n\n    let n_t = n as TAFloat;\n    let m_t = m as TAFloat;\n\n    // Incremental update for the first SMA using the correct window length (n)\n    let new_sma1 = prev_sma1 + (input_new_price - input_old_price) / n_t;\n\n    // Incremental update for the second SMA using the correct window length (m)\n    let new_sma2 = prev_sma2 + (new_sma1 - input_old_sma1) / m_t;\n\n    Ok((new_sma1, new_sma2))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_trima_calculation() {\n        let input = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0, 35114.5, 35097.2,\n            35092.0, 35073.2, 35139.3, 35092.0, 35126.7, 35106.3, 35124.8, 35170.1, 35215.3,\n            35154.0, 35216.3, 35211.8, 35158.4, 35172.0, 35176.7, 35113.3, 35114.7, 35129.3,\n            35094.6, 35114.4, 35094.5, 35116.0, 35105.4, 35050.7, 35031.3, 35008.1, 35021.4,\n            35048.4, 35080.1, 35043.6, 34962.7, 34970.1, 34980.1, 34930.6, 35000.0, 34998.0,\n            35024.7, 34982.1, 34972.3, 34971.6, 34953.0, 34937.0, 34964.3, 34975.1, 34995.1,\n            34989.0, 34942.9, 34895.2, 34830.4, 34925.1, 34888.6, 34910.3, 34917.6, 34940.0,\n            35005.4, 34980.1, 34966.8, 34976.1, 34948.6, 34969.3, 34996.5, 35004.0, 35011.0,\n            35059.2, 35036.1, 35062.3, 35067.7, 35087.9, 35076.7, 35041.6, 34993.3, 34974.5,\n            34990.2,\n        ];\n\n        let expected_values = vec![\n            35_106.679_166_666_67,\n            35_097.465_000_000_004,\n            35_089.510_416_666_664,\n            35_082.868_333_333_33,\n            35077.1975,\n            35072.55375,\n            35_069.712_916_666_67,\n            35_069.024_166_666_67,\n            35_070.279_166_666_674,\n            35_073.292_083_333_34,\n            35_077.280_833_333_345,\n            35_081.945_416_666_68,\n            35_087.193_750_000_006,\n            35_093.083_750_000_01,\n            35_099.648_750_000_015,\n            35_106.536_666_666_68,\n            35_113.231_250_000_026,\n            35_119.662_916_666_69,\n            35_125.514_583_333_36,\n            35_130.942_500_000_02,\n            35_135.868_333_333_36,\n            35_139.502_083_333_355,\n            35_141.475_416_666_69,\n            35_141.742_083_333_35,\n            35_140.314_166_666_69,\n            35_137.719_583_333_36,\n            35_134.415_416_666_69,\n            35_130.317_083_333_364,\n            35_125.260_000_000_024,\n            35_119.511_666_666_695,\n            35_112.968_750_000_02,\n            35_105.784_166_666_686,\n            35_098.112_083_333_35,\n            35_090.089_166_666_69,\n            35_081.735_000_000_015,\n            35_072.903_750_000_02,\n            35_064.015_416_666_68,\n            35_055.564_166_666_685,\n            35_047.394_583_333_34,\n            35_039.740_833_333_344,\n            35_032.530_000_000_01,\n            35_025.340_000_000_01,\n            35_018.330_833_333_35,\n            35_011.985_833_333_35,\n            35_006.155_000_000_01,\n            35_000.572_916_666_67,\n            34_995.195_000_000_01,\n            34_990.188_333_333_346,\n            34_985.202_500_000_01,\n            34_980.142_083_333_34,\n            34_975.193_333_333_34,\n            34_970.623_750_000_01,\n            34_966.521_666_666_68,\n            34_962.781_250_000_01,\n            34_959.394_583_333_34,\n            34_956.408_750_000_02,\n            34_953.662_916_666_68,\n            34_951.247_083_333_35,\n            34_949.064_583_333_35,\n            34_947.027_083_333_35,\n            34_945.585_416_666_676,\n            34_945.450_833_333_34,\n            34_946.196_250_000_01,\n            34_947.977_500_000_01,\n            34_950.870_416_666_67,\n            34_954.949_583_333_335,\n            34_959.867_083_333_33,\n            34_965.069_999_999_99,\n            34_970.187_083_333_32,\n            34_975.223_333_333_32,\n            34_980.194_166_666_65,\n        ];\n\n        let opt_period = 30;\n        let mut output_sma1 = vec![0.0; input.len()];\n        let mut output_sma2 = vec![0.0; input.len()];\n\n        trima(&input, opt_period, &mut output_sma1, &mut output_sma2).unwrap();\n\n        // First 29 values should be NaN\n        for i in 0..29 {\n            assert!(output_sma1[i].is_nan());\n            assert!(output_sma2[i].is_nan());\n        }\n\n        // Compare with known values\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_sma2[i + 29], *expected, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation\n        let (n, m) = if opt_period % 2 == 1 {\n            (opt_period.div_ceil(2), opt_period.div_ceil(2))\n        } else {\n            (((opt_period / 2) + 1), (opt_period / 2))\n        };\n\n        let mut prev_sma1 = output_sma1[43];\n        let mut prev_sma2 = output_sma2[43];\n\n        // Test incremental calculation for all remaining values\n        for i in 44..input.len() {\n            let old_price = input[i - n];\n            let old_sma1 = output_sma1[i - m];\n            let (new_sma1, new_sma2) = trima_inc(\n                prev_sma1, prev_sma2, input[i], old_price, old_sma1, opt_period,\n            )\n            .unwrap();\n\n            assert_relative_eq!(new_sma2, output_sma2[i], epsilon = 0.0001);\n            prev_sma1 = new_sma1;\n            prev_sma2 = new_sma2;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/trix.rs",
    "content": "use super::{ema, roc};\nuse crate::{KandError, TAFloat};\n\n/// Calculates the lookback period required for TRIX calculation\n///\n/// # Description\n/// The lookback period represents the minimum number of data points needed before the first valid TRIX value\n/// can be calculated. It is determined by combining the lookback periods of three EMA calculations and one ROC calculation.\n///\n/// # Arguments\n/// * `opt_period` - The time period used for EMA calculations in TRIX. Must be >= 2.\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period if successful, or an error if parameters are invalid\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2 (when \"check\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::trix;\n///\n/// let period = 14;\n/// let lookback = trix::lookback(period).unwrap();\n/// assert_eq!(lookback, 40); // 3 * EMA lookback + 1 ROC lookback\n/// ```\npub fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    Ok(3 * ema::lookback(opt_period)? + roc::lookback(1)?)\n}\n\n/// Calculates the Triple Exponential Moving Average Oscillator (TRIX)\n///\n/// # Description\n/// TRIX is a momentum oscillator that measures the rate of change of a triple exponentially smoothed moving average.\n/// It helps identify oversold and overbought conditions and potential trend reversals through divergences.\n///\n/// # Calculation Principle\n/// 1. Calculate first EMA of the input prices\n/// 2. Calculate second EMA of the first EMA values\n/// 3. Calculate third EMA of the second EMA values\n/// 4. Calculate rate of change (ROC) of the triple EMA\n///\n/// # Mathematical Formula\n/// ```text\n/// EMA1 = EMA(price, period)\n/// EMA2 = EMA(EMA1, period)\n/// EMA3 = EMA(EMA2, period)\n/// TRIX = ROC(EMA3, 1) = ((EMA3 - Previous EMA3) / Previous EMA3) * 100\n/// ```\n///\n/// # Arguments\n/// * `input` - Array of price values to calculate TRIX\n/// * `opt_period` - Number of periods for EMA calculations. Must be >= 2\n/// * `output` - Array to store calculated TRIX values. First lookback values will be NaN\n/// * `ema1_output` - Array to store first EMA values. First lookback values will be NaN\n/// * `ema2_output` - Array to store second EMA values. First lookback values will be NaN\n/// * `ema3_output` - Array to store third EMA values. First lookback values will be NaN\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty Ok if calculation succeeds, otherwise an error\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input array is empty (with \"check\" feature)\n/// * `KandError::LengthMismatch` - If input and output arrays have different lengths (with \"check\" feature)\n/// * `KandError::InvalidParameter` - If `opt_period` < 2 (with \"check\" feature)\n/// * `KandError::InsufficientData` - If input length <= lookback period (with \"check\" feature)\n/// * `KandError::NaNDetected` - If input contains NaN values (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::trix;\n///\n/// let input = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];\n/// let period = 3;\n/// let mut output = vec![0.0; 9];\n/// let mut ema1 = vec![0.0; 9];\n/// let mut ema2 = vec![0.0; 9];\n/// let mut ema3 = vec![0.0; 9];\n///\n/// trix::trix(&input, period, &mut output, &mut ema1, &mut ema2, &mut ema3).unwrap();\n/// ```\npub fn trix(\n    input: &[TAFloat],\n    opt_period: usize,\n    output: &mut [TAFloat],\n    ema1_output: &mut [TAFloat],\n    ema2_output: &mut [TAFloat],\n    ema3_output: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Check if input array is empty\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Check if input length is less than or equal to lookback period\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Check if output, ema1_output, ema2_output, and ema3_output arrays have the same length\n        if len != output.len()\n            || len != ema1_output.len()\n            || len != ema2_output.len()\n            || len != ema3_output.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for value in input {\n            if value.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate first EMA\n    ema::ema(input, opt_period, None, ema1_output)?;\n\n    // Calculate second EMA\n    ema::ema(\n        &ema1_output[opt_period - 1..],\n        opt_period,\n        None,\n        &mut ema2_output[opt_period - 1..],\n    )?;\n\n    // Calculate third EMA\n    ema::ema(\n        &ema2_output[2 * (opt_period - 1)..],\n        opt_period,\n        None,\n        &mut ema3_output[2 * (opt_period - 1)..],\n    )?;\n\n    // Calculate TRIX using ROC\n    roc::roc(\n        &ema3_output[3 * (opt_period - 1)..],\n        1,\n        &mut output[3 * (opt_period - 1)..],\n    )?;\n\n    // Fill initial values with NAN\n    for i in 0..lookback {\n        output[i] = TAFloat::NAN;\n        ema1_output[i] = TAFloat::NAN;\n        ema2_output[i] = TAFloat::NAN;\n        ema3_output[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates a single new TRIX value incrementally using previous EMA values\n///\n/// # Description\n/// This function provides an optimized way to calculate the latest TRIX value when new price data arrives,\n/// without recalculating the entire series. It uses the previous EMA values and current price to compute\n/// the new TRIX value.\n///\n/// # Mathematical Formula\n/// ```text\n/// alpha = 2 / (period + 1)\n/// EMA1 = alpha * price + (1 - alpha) * prev_ema1\n/// EMA2 = alpha * EMA1 + (1 - alpha) * prev_ema2\n/// EMA3 = alpha * EMA2 + (1 - alpha) * prev_ema3\n/// TRIX = ((EMA3 - prev_ema3) / prev_ema3) * 100\n/// ```\n///\n/// # Arguments\n/// * `input` - Current price value\n/// * `prev_ema1` - Previous first EMA value\n/// * `prev_ema2` - Previous second EMA value\n/// * `prev_ema3` - Previous third EMA value\n/// * `opt_period` - Period for EMA calculations. Must be >= 2\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat, TAFloat), KandError>` - Tuple containing (TRIX, `new_ema1`, `new_ema2`, `new_ema3`) if successful\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2 (with \"check\" feature)\n/// * `KandError::NaNDetected` - If any input value is NaN (with \"`check-nan`\" feature)\n/// * `KandError::InvalidData` - If division by zero occurs during ROC calculation\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::trix;\n///\n/// let price = 100.0;\n/// let prev_ema1 = 98.0;\n/// let prev_ema2 = 97.0;\n/// let prev_ema3 = 96.0;\n/// let period = 14;\n///\n/// let (trix, new_ema1, new_ema2, new_ema3) =\n///     trix::trix_inc(price, prev_ema1, prev_ema2, prev_ema3, period).unwrap();\n/// ```\npub fn trix_inc(\n    input: TAFloat,\n    prev_ema1: TAFloat,\n    prev_ema2: TAFloat,\n    prev_ema3: TAFloat,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input.is_nan() || prev_ema1.is_nan() || prev_ema2.is_nan() || prev_ema3.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let new_ema1 = ema::ema_inc(input, prev_ema1, opt_period, None)?;\n    let new_ema2 = ema::ema_inc(new_ema1, prev_ema2, opt_period, None)?;\n    let new_ema3 = ema::ema_inc(new_ema2, prev_ema3, opt_period, None)?;\n    let trix = roc::roc_inc(new_ema3, prev_ema3)?;\n\n    Ok((trix, new_ema1, new_ema2, new_ema3))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_trix_calculation() {\n        let input = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6,\n        ];\n        let opt_period = 3;\n        let mut output = vec![0.0; input.len()];\n        let mut ema1 = vec![0.0; input.len()];\n        let mut ema2 = vec![0.0; input.len()];\n        let mut ema3 = vec![0.0; input.len()];\n\n        trix(\n            &input,\n            opt_period,\n            &mut output,\n            &mut ema1,\n            &mut ema2,\n            &mut ema3,\n        )\n        .unwrap();\n\n        // First 7 values should be NaN (lookback period)\n        for value in output.iter().take(7) {\n            assert!(value.is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            0.023_600_565_750_747_65,\n            0.007_581_993_206_917_659,\n            -0.008_943_588_952_370_352,\n            -0.019_561_690_627_645_234,\n            -0.002_233_206_002_377_752_2,\n            0.004_028_034_632_819_2,\n            -0.013_120_959_352_708_184,\n            -0.047_983_857_499_556_14,\n            -0.079_103_810_365_388_5,\n            -0.099_256_202_780_007_02,\n            -0.090_591_666_739_903_15,\n            -0.051_535_144_257_075_505,\n            -0.037_564_038_843_540_54,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output[i + 7], *expected, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation matches regular calculation\n        let mut prev_ema1 = ema1[10];\n        let mut prev_ema2 = ema2[10];\n        let mut prev_ema3 = ema3[10];\n\n        for i in 11..15 {\n            let (trix_val, new_ema1, new_ema2, new_ema3) =\n                trix_inc(input[i], prev_ema1, prev_ema2, prev_ema3, opt_period).unwrap();\n\n            assert_relative_eq!(trix_val, output[i], epsilon = 0.0001);\n\n            prev_ema1 = new_ema1;\n            prev_ema2 = new_ema2;\n            prev_ema3 = new_ema3;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/typprice.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for Typical Price calculation.\n///\n/// Determines the number of data points needed to calculate the first valid Typical Price value.\n///\n/// # Returns\n/// * `Result<usize, KandError>` - Returns Ok(0) since Typical Price requires no historical data\n///\n/// # Errors\n/// * This function does not return any errors\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::typprice;\n/// let lookback = typprice::lookback().unwrap();\n/// assert_eq!(lookback, 0);\n/// ```\npub const fn lookback() -> Result<usize, KandError> {\n    Ok(0)\n}\n\n/// Calculates Typical Price for a series of OHLCV data.\n///\n/// The Typical Price is a simple average of the high, low and close prices for each period,\n/// providing a single representative price for the trading period.\n///\n/// # Mathematical Formula\n/// ```text\n/// Typical Price = (High + Low + Close) / 3\n/// ```\n///\n/// # Calculation Principle\n/// 1. For each period in the data series:\n///    - Sum the high, low and close prices\n///    - Divide by 3 to get the arithmetic mean\n///\n/// # Parameters\n/// * `input_high` - Array of high prices for each period\n/// * `input_low` - Array of low prices for each period\n/// * `input_close` - Array of close prices for each period\n/// * `output_typprice` - Array to store the calculated Typical Price values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok(()) on successful calculation\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input arrays have different lengths\n/// * `KandError::NaNDetected` - If any input value is NaN (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::typprice;\n/// let input_high = vec![24.20, 24.07, 24.04];\n/// let input_low = vec![23.85, 23.72, 23.64];\n/// let input_close = vec![23.89, 23.95, 23.67];\n/// let mut output_typprice = vec![0.0; 3];\n///\n/// typprice::typprice(&input_high, &input_low, &input_close, &mut output_typprice).unwrap();\n/// // output_typprice ≈ [23.98, 23.91, 23.78]\n/// ```\npub fn typprice(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    output_typprice: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Length consistency check\n        if len != input_low.len() || len != input_close.len() || len != output_typprice.len() {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            // NaN check\n            if input_high[i].is_nan() || input_low[i].is_nan() || input_close[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate typical price\n    for i in 0..len {\n        output_typprice[i] = (input_high[i] + input_low[i] + input_close[i]) / 3.0;\n    }\n\n    Ok(())\n}\n\n/// Calculates a single Typical Price value incrementally.\n///\n/// This function provides an optimized way to calculate the Typical Price for a single period,\n/// useful in real-time calculations or streaming data scenarios.\n///\n/// # Parameters\n/// * `input_high` - High price for the current period\n/// * `input_low` - Low price for the current period\n/// * `input_close` - Close price for the current period\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The calculated Typical Price value\n///\n/// # Errors\n/// * `KandError::NaNDetected` - If any input value is NaN (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::typprice;\n/// let high = 24.20;\n/// let low = 23.85;\n/// let close = 23.89;\n///\n/// let typ_price = typprice::typprice_inc(high, low, close).unwrap();\n/// // typ_price ≈ 23.98\n/// ```\npub fn typprice_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    input_close: TAFloat,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_high.is_nan() || input_low.is_nan() || input_close.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    Ok((input_high + input_low + input_close) / 3.0)\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n    // Basic functionality tests\n    #[test]\n    fn test_typprice_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6,\n        ];\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5,\n        ];\n        let mut output_typprice = vec![0.0; input_high.len()];\n\n        typprice(&input_high, &input_low, &input_close, &mut output_typprice).unwrap();\n\n        assert_relative_eq!(output_typprice[0], 35_232.733_333_333_34, epsilon = 0.00001);\n        assert_relative_eq!(output_typprice[1], 35_225.133_333_333_33, epsilon = 0.00001);\n        assert_relative_eq!(output_typprice[2], 35_202.133_333_333_33, epsilon = 0.00001);\n        assert_relative_eq!(\n            output_typprice[3],\n            35_163.833_333_333_336,\n            epsilon = 0.00001\n        );\n        assert_relative_eq!(output_typprice[4], 35_172.366_666_666_67, epsilon = 0.00001);\n\n        // Test each incremental step matches regular calculation\n        for i in 0..input_high.len() {\n            let result = typprice_inc(input_high[i], input_low[i], input_close[i]).unwrap();\n            assert_relative_eq!(result, output_typprice[i], epsilon = 0.0001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/vegas.rs",
    "content": "use super::ema;\nuse crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for VEGAS (Volume and EMA Guided Adaptive Scaling) calculation\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The number of data points needed before first valid output (675)\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input data is empty\n///\n/// # Example\n/// ```rust\n/// use kand::ohlcv::vegas;\n///\n/// let lookback = vegas::lookback().unwrap();\n/// assert_eq!(lookback, 675);\n/// ```\npub const fn lookback() -> Result<usize, KandError> {\n    Ok(676 - 1) // Longest EMA period - 1\n}\n\n/// Calculates VEGAS (Volume and EMA Guided Adaptive Scaling) indicator for the entire price array\n///\n/// # Description\n/// VEGAS is a trend following indicator that uses multiple EMAs to define channels and boundaries.\n/// It helps identify trend strength and potential trend changes through the spacing between EMAs.\n///\n/// # Mathematical Formula\n/// The indicator consists of 4 EMAs with different periods:\n/// ```text\n/// Channel Upper = EMA(price, 144)\n/// Channel Lower = EMA(price, 169)\n/// Boundary Upper = EMA(price, 576)\n/// Boundary Lower = EMA(price, 676)\n///\n/// Where EMA is calculated as:\n/// EMA = Price * (2 / (n + 1)) + Previous_EMA * (1 - (2 / (n + 1)))\n/// ```\n///\n/// # Parameters\n/// * `input_price` - Array of price values\n/// * `output_channel_upper` - Output array for upper channel (EMA 144)\n/// * `output_channel_lower` - Output array for lower channel (EMA 169)\n/// * `output_boundary_upper` - Output array for upper boundary (EMA 576)\n/// * `output_boundary_lower` - Output array for lower boundary (EMA 676)\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok if calculation succeeds\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input array is empty\n/// * `KandError::LengthMismatch` - If output arrays have different lengths than input\n/// * `KandError::InsufficientData` - If input length < 676\n/// * `KandError::NaNDetected` - If any input value is NaN\n///\n/// # Example\n/// ```rust\n/// use kand::ohlcv::vegas;\n///\n/// let input_price = vec![10.0; 1000];\n/// let mut channel_upper = vec![0.0; 1000];\n/// let mut channel_lower = vec![0.0; 1000];\n/// let mut boundary_upper = vec![0.0; 1000];\n/// let mut boundary_lower = vec![0.0; 1000];\n///\n/// vegas::vegas(\n///     &input_price,\n///     &mut channel_upper,\n///     &mut channel_lower,\n///     &mut boundary_upper,\n///     &mut boundary_lower,\n/// )\n/// .unwrap();\n/// ```\npub fn vegas(\n    input_price: &[TAFloat],\n    output_channel_upper: &mut [TAFloat],\n    output_channel_lower: &mut [TAFloat],\n    output_boundary_upper: &mut [TAFloat],\n    output_boundary_lower: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_price.len();\n    let lookback = lookback()?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Length consistency check\n        if output_channel_upper.len() != len\n            || output_channel_lower.len() != len\n            || output_boundary_upper.len() != len\n            || output_boundary_lower.len() != len\n        {\n            return Err(KandError::LengthMismatch);\n        }\n\n        // Data sufficiency check\n        if len < 676 {\n            return Err(KandError::InsufficientData);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for price in input_price {\n            // NaN check\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate EMAs\n    ema::ema(input_price, 144, None, output_channel_upper)?; // Channel upper - EMA(144)\n    ema::ema(input_price, 169, None, output_channel_lower)?; // Channel lower - EMA(169)\n    ema::ema(input_price, 576, None, output_boundary_upper)?; // Boundary upper - EMA(576)\n    ema::ema(input_price, 676, None, output_boundary_lower)?; // Boundary lower - EMA(676)\n\n    // Fill initial values with NAN\n    for value in output_channel_upper.iter_mut().take(lookback) {\n        *value = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates latest VEGAS indicator values incrementally for real-time updates\n///\n/// # Description\n/// Provides optimized calculation of latest VEGAS values for real-time price updates\n/// without recalculating the entire series. This is particularly useful for streaming data.\n///\n/// # Mathematical Formula\n/// ```text\n/// For each EMA:\n/// EMA_current = Price * multiplier + Previous_EMA * (1 - multiplier)\n/// where multiplier = 2 / (period + 1)\n/// ```\n///\n/// # Parameters\n/// * `input_price` - Current price value\n/// * `prev_channel_upper` - Previous EMA(144) value\n/// * `prev_channel_lower` - Previous EMA(169) value\n/// * `prev_boundary_upper` - Previous EMA(576) value\n/// * `prev_boundary_lower` - Previous EMA(676) value\n///\n/// # Returns\n/// * `Result<(TAFloat,TAFloat,TAFloat,TAFloat), KandError>` - Tuple of (`channel_upper`, `channel_lower`, `boundary_upper`, `boundary_lower`)\n///\n/// # Errors\n/// * `KandError::NaNDetected` - If any input value is NaN\n///\n/// # Example\n/// ```rust\n/// use kand::ohlcv::vegas;\n///\n/// let current_price = 100.0;\n/// let prev_values = (98.0, 97.5, 96.0, 95.5);\n///\n/// let new_values = vegas::vegas_inc(\n///     current_price,\n///     prev_values.0,\n///     prev_values.1,\n///     prev_values.2,\n///     prev_values.3,\n/// )\n/// .unwrap();\n/// ```\npub fn vegas_inc(\n    input_price: TAFloat,\n    prev_channel_upper: TAFloat,\n    prev_channel_lower: TAFloat,\n    prev_boundary_upper: TAFloat,\n    prev_boundary_lower: TAFloat,\n) -> Result<(TAFloat, TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_price.is_nan()\n            || prev_channel_upper.is_nan()\n            || prev_channel_lower.is_nan()\n            || prev_boundary_upper.is_nan()\n            || prev_boundary_lower.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let channel_upper = ema::ema_inc(input_price, prev_channel_upper, 144, None)?;\n    let channel_lower = ema::ema_inc(input_price, prev_channel_lower, 169, None)?;\n    let boundary_upper = ema::ema_inc(input_price, prev_boundary_upper, 576, None)?;\n    let boundary_lower = ema::ema_inc(input_price, prev_boundary_lower, 676, None)?;\n\n    Ok((channel_upper, channel_lower, boundary_upper, boundary_lower))\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/vwap.rs",
    "content": "use super::typprice;\nuse crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for VWAP calculation.\n///\n/// # Function Overview\n/// Determines the minimum number of data points needed before VWAP can be calculated.\n///\n/// # Mathematical Formula\n/// No formula required - VWAP has no lookback period.\n///\n/// # Calculation Principle\n/// VWAP can be calculated from the first data point, so lookback is always 0.\n///\n/// # Parameters\n/// None\n///\n/// # Returns\n/// * `Result<usize, KandError>` - Returns 0 as the lookback period\n///\n/// # Errors\n/// None - this function cannot fail\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::vwap;\n/// let lookback = vwap::lookback().unwrap();\n/// assert_eq!(lookback, 0); // VWAP has no lookback period\n/// ```\npub const fn lookback() -> Result<usize, KandError> {\n    Ok(0)\n}\n\n/// Calculates Volume Weighted Average Price (VWAP).\n///\n/// # Function Overview\n/// VWAP is a trading benchmark that calculates the average price weighted by volume over a period.\n/// It helps traders understand both price trends and trading activity.\n///\n/// # Mathematical Formula\n/// ```text\n/// Typical Price = (High + Low + Close) / 3\n/// VWAP = Σ(Typical Price * Volume) / Σ(Volume)\n/// ```\n///\n/// # Calculation Principle\n/// 1. Calculate typical price for each period\n/// 2. Multiply typical price by volume\n/// 3. Keep running sum of price-volume products and volumes\n/// 4. Divide cumulative price-volume by cumulative volume\n///\n/// # Parameters\n/// * `input_high` - Array of high prices for each period\n/// * `input_low` - Array of low prices for each period\n/// * `input_close` - Array of closing prices for each period\n/// * `input_volume` - Array of trading volumes for each period\n/// * `output_vwap` - Mutable slice to store calculated VWAP values\n/// * `output_cum_pv` - Mutable slice to store cumulative price-volume products\n/// * `output_cum_vol` - Mutable slice to store cumulative volumes\n///\n/// # Returns\n/// * `Result<(), KandError>` - Unit type on successful calculation\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input/output arrays have different lengths\n/// * `KandError::NaNDetected` - If any input contains NaN (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::vwap;\n///\n/// let high = vec![10.0, 12.0, 15.0];\n/// let low = vec![8.0, 9.0, 11.0];\n/// let close = vec![9.0, 10.0, 12.0];\n/// let volume = vec![100.0, 150.0, 200.0];\n/// let mut vwap_values = vec![0.0; 3];\n/// let mut cum_pv = vec![0.0; 3];\n/// let mut cum_vol = vec![0.0; 3];\n///\n/// vwap::vwap(\n///     &high,\n///     &low,\n///     &close,\n///     &volume,\n///     &mut vwap_values,\n///     &mut cum_pv,\n///     &mut cum_vol,\n/// )\n/// .unwrap();\n/// ```\npub fn vwap(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    input_volume: &[TAFloat],\n    output_vwap: &mut [TAFloat],\n    output_cum_pv: &mut [TAFloat],\n    output_cum_vol: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Length consistency check\n        if input_low.len() != len\n            || input_close.len() != len\n            || input_volume.len() != len\n            || output_vwap.len() != len\n            || output_cum_pv.len() != len\n            || output_cum_vol.len() != len\n        {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        for i in 0..len {\n            if input_high[i].is_nan()\n                || input_low[i].is_nan()\n                || input_close[i].is_nan()\n                || input_volume[i].is_nan()\n            {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    let mut cum_pv = 0.0;\n    let mut cum_vol = 0.0;\n\n    for i in 0..len {\n        let (new_cum_pv, new_cum_vol, vwap) = vwap_inc(\n            input_high[i],\n            input_low[i],\n            input_close[i],\n            input_volume[i],\n            cum_pv,\n            cum_vol,\n        )?;\n\n        cum_pv = new_cum_pv;\n        cum_vol = new_cum_vol;\n\n        output_cum_pv[i] = cum_pv;\n        output_cum_vol[i] = cum_vol;\n        output_vwap[i] = vwap;\n    }\n\n    Ok(())\n}\n\n/// Calculates the next VWAP value incrementally.\n///\n/// # Function Overview\n/// Updates cumulative price-volume product and volume for incremental VWAP calculation.\n///\n/// # Mathematical Formula\n/// ```text\n/// Cumulative PV = Previous Cumulative PV + (Typical Price * Volume)\n/// Cumulative Volume = Previous Cumulative Volume + Volume\n/// VWAP = Cumulative PV / Cumulative Volume\n/// ```\n///\n/// # Calculation Principle\n/// Maintains running sums and calculates VWAP by dividing cumulative values.\n///\n/// # Parameters\n/// * `high` - High price for current period\n/// * `low` - Low price for current period\n/// * `close` - Close price for current period\n/// * `volume` - Volume for current period\n/// * `prev_cum_pv` - Previous cumulative price-volume product\n/// * `prev_cum_vol` - Previous cumulative volume\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat), KandError>` - Tuple containing (new cumulative PV, new cumulative volume, new VWAP)\n///\n/// # Errors\n/// None - this function cannot fail\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::vwap;\n///\n/// let high = 10.0;\n/// let low = 8.0;\n/// let close = 9.0;\n/// let volume = 100.0;\n/// let prev_cum_pv = 1000.0;\n/// let prev_cum_vol = 150.0;\n/// let (new_cum_pv, new_cum_vol, new_vwap) =\n///     vwap::vwap_inc(high, low, close, volume, prev_cum_pv, prev_cum_vol).unwrap();\n/// ```\npub fn vwap_inc(\n    high: TAFloat,\n    low: TAFloat,\n    close: TAFloat,\n    volume: TAFloat,\n    prev_cum_pv: TAFloat,\n    prev_cum_vol: TAFloat,\n) -> Result<(TAFloat, TAFloat, TAFloat), KandError> {\n    let typ_price = typprice::typprice_inc(high, low, close)?;\n    let cum_pv = typ_price.mul_add(volume, prev_cum_pv);\n    let cum_vol = prev_cum_vol + volume;\n    let vwap = if cum_vol == 0.0 {\n        0.0\n    } else {\n        cum_pv / cum_vol\n    };\n    Ok((cum_pv, cum_vol, vwap))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_vwap_calculation() {\n        let input_high = vec![\n            96955.7, 96850.0, 96787.8, 97163.0, 97212.0, 96870.7, 96824.2, 97041.9, 96979.8,\n            97127.0, 97150.0, 97094.5, 96844.7, 96660.0,\n        ];\n        let input_low = vec![\n            96490.7, 96309.5, 96407.1, 96492.8, 96707.0, 96505.0, 96556.2, 96765.8, 96743.4,\n            96782.4, 96916.4, 96750.1, 96436.1, 96507.3,\n        ];\n        let input_close = vec![\n            96708.6, 96497.4, 96495.2, 97094.9, 96715.4, 96635.9, 96786.6, 96889.9, 96828.0,\n            97062.0, 96965.8, 96844.6, 96612.3, 96531.2,\n        ];\n        let input_volume = vec![\n            3746.917, 3260.9, 2899.859, 4050.52, 4249.375, 2782.823, 2384.87, 3234.131, 2350.488,\n            3032.885, 2050.853, 2505.323, 3741.102, 811.82,\n        ];\n        let mut output_vwap = vec![0.0; 14];\n        let mut output_cum_pv = vec![0.0; 14];\n        let mut output_cum_vol = vec![0.0; 14];\n\n        vwap(\n            &input_high,\n            &input_low,\n            &input_close,\n            &input_volume,\n            &mut output_vwap,\n            &mut output_cum_pv,\n            &mut output_cum_vol,\n        )\n        .unwrap();\n\n        // Expected VWAP values from the sample data\n        let expected_values = [\n            96_718.333_333_333_33,\n            96_641.074_167_366_7,\n            96_618.330_105_563_28,\n            96_704.971_912_915_3,\n            96_745.385_200_930_96,\n            96_735.461_637_859_99,\n            96_734.122_217_709_84,\n            96_754.185_927_279_93,\n            96_761.995_006_596_99,\n            96_783.653_909_970_1,\n            96_797.333_609_807_04,\n            96_804.124_320_528_47,\n            96_788.052_086_747_02,\n            96_783.669_535_800_8,\n        ];\n\n        // Compare calculated values with expected values\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_vwap[i], *expected, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation\n        let mut prev_cum_pv = 0.0;\n        let mut prev_cum_vol = 0.0;\n\n        for i in 0..input_high.len() {\n            let (new_cum_pv, new_cum_vol, vwap) = vwap_inc(\n                input_high[i],\n                input_low[i],\n                input_close[i],\n                input_volume[i],\n                prev_cum_pv,\n                prev_cum_vol,\n            )\n            .unwrap();\n\n            assert_relative_eq!(vwap, expected_values[i], epsilon = 0.0001);\n            prev_cum_pv = new_cum_pv;\n            prev_cum_vol = new_cum_vol;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/wclprice.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Returns the lookback period required for Weighted Close Price (WCLPRICE) calculation.\n///\n/// # Description\n/// The WCLPRICE indicator does not require any historical data since it only uses current price data.\n///\n/// # Returns\n/// * `Result<usize, KandError>` - Returns `Ok(0)` since no historical data is needed\n///\n/// # Errors\n/// This function does not return any errors.\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::wclprice;\n/// let lookback = wclprice::lookback().unwrap();\n/// assert_eq!(lookback, 0);\n/// ```\npub const fn lookback() -> Result<usize, KandError> {\n    Ok(0)\n}\n\n/// Calculates the Weighted Close Price (WCLPRICE) for a series of price data.\n///\n/// # Description\n/// The Weighted Close Price is a price indicator that assigns more weight to the closing price\n/// compared to high and low prices. It provides a single value that reflects price action\n/// with emphasis on the closing price.\n///\n/// # Mathematical Formula\n/// ```text\n/// WCLPRICE = (High + Low + (Close × 2)) ÷ 4\n/// ```\n///\n/// # Calculation Principles\n/// 1. Takes high, low and closing prices for each period\n/// 2. Multiplies closing price by 2 to give it more weight\n/// 3. Adds high and low prices\n/// 4. Divides the sum by 4 to get weighted average\n///\n/// # Arguments\n/// * `input_high` - Array of high prices for each period\n/// * `input_low` - Array of low prices for each period\n/// * `input_close` - Array of closing prices for each period\n/// * `output` - Array to store the calculated WCLPRICE values\n///\n/// # Returns\n/// * `Result<(), KandError>` - `Ok(())` if calculation succeeds\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input arrays have different lengths\n/// * `KandError::NaNDetected` - If any input value is NaN (when \"`check-nan`\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::wclprice;\n///\n/// let high = vec![10.0, 12.0, 15.0];\n/// let low = vec![8.0, 9.0, 11.0];\n/// let close = vec![9.0, 11.0, 14.0];\n/// let mut output = vec![0.0; 3];\n///\n/// wclprice::wclprice(&high, &low, &close, &mut output).unwrap();\n/// ```\npub fn wclprice(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    output: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n\n    #[cfg(feature = \"check\")]\n    {\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n        if len != input_low.len() || len != input_close.len() || len != output.len() {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            if input_high[i].is_nan() || input_low[i].is_nan() || input_close[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    for i in 0..len {\n        output[i] = input_close[i].mul_add(2.0, input_high[i] + input_low[i]) / 4.0;\n    }\n\n    Ok(())\n}\n\n/// Calculates a single Weighted Close Price (WCLPRICE) value from the latest price data.\n///\n/// # Description\n/// This function provides an optimized way to calculate WCLPRICE for the most recent data point\n/// without requiring historical values. It is useful for real-time calculations.\n///\n/// # Arguments\n/// * `input_high` - Latest high price value\n/// * `input_low` - Latest low price value\n/// * `input_close` - Latest closing price value\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The calculated WCLPRICE value if successful\n///\n/// # Errors\n/// * `KandError::NaNDetected` - If any input value is NaN (when \"`check-nan`\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::wclprice::wclprice_inc;\n///\n/// let high = 15.0;\n/// let low = 11.0;\n/// let close = 14.0;\n///\n/// let wclprice = wclprice_inc(high, low, close).unwrap();\n/// ```\npub fn wclprice_inc(\n    input_high: TAFloat,\n    input_low: TAFloat,\n    input_close: TAFloat,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        if input_high.is_nan() || input_low.is_nan() || input_close.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    Ok(input_close.mul_add(2.0, input_high + input_low) / 4.0)\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_wclprice_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2,\n        ];\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6,\n        ];\n\n        let mut output = vec![0.0; input_high.len()];\n        wclprice(&input_high, &input_low, &input_close, &mut output).unwrap();\n\n        let expected_values = [\n            35228.575, 35224.2, 35199.275, 35165.375, 35174.65, 35235.475, 35217.775, 35247.2,\n            35206.55, 35186.35, 35176.625, 35210.975, 35213.0, 35170.85, 35118.325, 35058.1,\n            34999.1, 35003.075, 35057.275, 35039.1,\n        ];\n\n        for i in 0..expected_values.len() {\n            assert_relative_eq!(output[i], expected_values[i], epsilon = 0.0001);\n        }\n\n        // Test incremental calculation matches regular calculation\n        for i in 0..input_high.len() {\n            let result = wclprice_inc(input_high[i], input_low[i], input_close[i]).unwrap();\n            assert_relative_eq!(result, output[i], epsilon = 0.0001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/willr.rs",
    "content": "use crate::{\n    EPSILON, KandError, TAFloat,\n    helper::{highest_bars, lowest_bars},\n};\n\n/// Returns the lookback period required for Williams %R calculation\n///\n/// # Description\n/// The lookback period is the number of data points needed before the first valid output can be calculated.\n/// For Williams %R, this is one less than the period parameter.\n///\n/// # Arguments\n/// * `opt_period` - The period used for Williams %R calculation. Must be >= 2.\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period if successful\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::willr;\n/// let lookback = willr::lookback(14).unwrap();\n/// assert_eq!(lookback, 13);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Calculates Williams %R (Williams Percent Range) for the entire price series\n///\n/// # Description\n/// Williams %R is a momentum indicator that measures overbought and oversold levels by comparing\n/// the closing price to the high-low range over a specified period. The indicator oscillates\n/// between 0 and -100.\n///\n/// # Mathematical Formula\n/// ```text\n/// %R = (Highest High - Close) / (Highest High - Lowest Low) × -100\n/// ```\n///\n/// # Calculation Principles\n/// 1. Find the highest high and lowest low over the lookback period\n/// 2. Calculate the high-low range (denominator)\n/// 3. Compare current close to the highest high (numerator)\n/// 4. Normalize to -100 to 0 range\n///\n/// # Arguments\n/// * `input_high` - Array of high prices\n/// * `input_low` - Array of low prices\n/// * `input_close` - Array of closing prices\n/// * `opt_period` - Lookback period for calculations. Must be >= 2.\n/// * `output` - Array to store calculated Williams %R values\n/// * `output_highest_high` - Array to store highest high values for each period\n/// * `output_lowest_low` - Array to store lowest low values for each period\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty Ok value if successful\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input arrays are empty\n/// * `KandError::LengthMismatch` - If input/output arrays have different lengths\n/// * `KandError::InvalidParameter` - If `opt_period` < 2\n/// * `KandError::InsufficientData` - If input length <= lookback period\n/// * `KandError::NaNDetected` - If any input value is NaN (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::willr;\n///\n/// let input_high = vec![10.0, 12.0, 15.0, 14.0, 13.0];\n/// let input_low = vec![8.0, 9.0, 11.0, 10.0, 9.0];\n/// let input_close = vec![9.0, 11.0, 14.0, 12.0, 11.0];\n/// let opt_period = 3;\n/// let mut output = vec![0.0; 5];\n/// let mut output_highest_high = vec![0.0; 5];\n/// let mut output_lowest_low = vec![0.0; 5];\n///\n/// willr::willr(\n///     &input_high,\n///     &input_low,\n///     &input_close,\n///     opt_period,\n///     &mut output,\n///     &mut output_highest_high,\n///     &mut output_lowest_low,\n/// )\n/// .unwrap();\n/// ```\npub fn willr(\n    input_high: &[TAFloat],\n    input_low: &[TAFloat],\n    input_close: &[TAFloat],\n    opt_period: usize,\n    output: &mut [TAFloat],\n    output_highest_high: &mut [TAFloat],\n    output_lowest_low: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_high.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n        if len != input_low.len()\n            || len != input_close.len()\n            || len != output.len()\n            || len != output_highest_high.len()\n            || len != output_lowest_low.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for i in 0..len {\n            if input_high[i].is_nan() || input_low[i].is_nan() || input_close[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    for i in lookback..len {\n        let highest_idx = highest_bars(input_high, i, opt_period)?;\n        let lowest_idx = lowest_bars(input_low, i, opt_period)?;\n\n        let highest_high = input_high[i - highest_idx];\n        let lowest_low = input_low[i - lowest_idx];\n\n        output_highest_high[i] = highest_high;\n        output_lowest_low[i] = lowest_low;\n\n        let denom = highest_high - lowest_low;\n        if denom == 0.0 {\n            output[i] = 0.0;\n        } else {\n            output[i] = (highest_high - input_close[i]) / denom * -100.0;\n        }\n    }\n\n    for i in 0..lookback {\n        output[i] = TAFloat::NAN;\n        output_highest_high[i] = TAFloat::NAN;\n        output_lowest_low[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates Williams %R incrementally for the latest data point\n///\n/// # Description\n/// This function provides an optimized way to calculate the latest Williams %R value\n/// by using previously calculated highest high and lowest low values. This is useful\n/// for real-time calculations where a complete recalculation is not necessary.\n///\n/// # Arguments\n/// * `prev_highest_high` - Previous period's highest high value\n/// * `prev_lowest_low` - Previous period's lowest low value\n/// * `prev_high` - Previous period's high price\n/// * `prev_low` - Previous period's low price\n/// * `input_close` - Current period's closing price\n/// * `input_high` - Current period's high price\n/// * `input_low` - Current period's low price\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat), KandError>` - Tuple containing:\n///   - Current Williams %R value\n///   - New highest high\n///   - New lowest low\n///\n/// # Errors\n/// * `KandError::NaNDetected` - If any input value is NaN (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::willr::willr_inc;\n///\n/// let prev_highest_high = 15.0;\n/// let prev_lowest_low = 10.0;\n/// let prev_high = 14.0;\n/// let prev_low = 11.0;\n/// let input_close = 12.0;\n/// let input_high = 13.0;\n/// let input_low = 11.0;\n///\n/// let (willr, new_highest_high, new_lowest_low) = willr_inc(\n///     prev_highest_high,\n///     prev_lowest_low,\n///     prev_high,\n///     prev_low,\n///     input_close,\n///     input_high,\n///     input_low,\n/// )\n/// .unwrap();\n/// ```\npub fn willr_inc(\n    prev_highest_high: TAFloat,\n    prev_lowest_low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    input_close: TAFloat,\n    input_high: TAFloat,\n    input_low: TAFloat,\n) -> Result<(TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        if prev_highest_high.is_nan()\n            || prev_lowest_low.is_nan()\n            || prev_high.is_nan()\n            || prev_low.is_nan()\n            || input_close.is_nan()\n            || input_high.is_nan()\n            || input_low.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    // Update highest high by removing old high and considering new high\n    let new_highest_high = if input_high > prev_highest_high {\n        input_high\n    } else if (prev_high - prev_highest_high).abs() < EPSILON {\n        // If previous high was the highest, need to find new highest between current high and previous highest\n        input_high.max(prev_highest_high)\n    } else {\n        prev_highest_high\n    };\n\n    // Update lowest low by removing old low and considering new low\n    let new_lowest_low = if input_low < prev_lowest_low {\n        input_low\n    } else if (prev_low - prev_lowest_low).abs() < EPSILON {\n        // If previous low was the lowest, need to find new lowest between current low and previous lowest\n        input_low.min(prev_lowest_low)\n    } else {\n        prev_lowest_low\n    };\n\n    let denom = new_highest_high - new_lowest_low;\n    let willr = if denom == 0.0 {\n        0.0\n    } else {\n        (new_highest_high - input_close) / denom * -100.0\n    };\n\n    Ok((willr, new_highest_high, new_lowest_low))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_willr_calculation() {\n        let input_high = vec![\n            35266.0, 35247.5, 35235.7, 35190.8, 35182.0, 35258.0, 35262.9, 35281.5, 35256.0,\n            35210.0, 35185.4, 35230.0, 35241.0, 35218.1, 35212.6, 35128.9, 35047.7, 35019.5,\n            35078.8, 35085.0, 35034.1, 34984.4, 35010.8, 35047.1, 35091.4,\n        ];\n        let input_low = vec![\n            35216.1, 35206.5, 35180.0, 35130.7, 35153.6, 35174.7, 35202.6, 35203.5, 35175.0,\n            35166.0, 35170.9, 35154.1, 35186.0, 35143.9, 35080.1, 35021.1, 34950.1, 34966.0,\n            35012.3, 35022.2, 34931.6, 34911.0, 34952.5, 34977.9, 35039.0,\n        ];\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0,\n        ];\n        let opt_period = 14;\n        let mut output = vec![0.0; input_high.len()];\n        let mut output_highest_high = vec![0.0; input_high.len()];\n        let mut output_lowest_low = vec![0.0; input_high.len()];\n\n        willr(\n            &input_high,\n            &input_low,\n            &input_close,\n            opt_period,\n            &mut output,\n            &mut output_highest_high,\n            &mut output_lowest_low,\n        )\n        .unwrap();\n\n        // First 13 values should be NaN\n        for i in 0..13 {\n            assert!(output[i].is_nan());\n            assert!(output_highest_high[i].is_nan());\n            assert!(output_lowest_low[i].is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            -80.106_100_795_756_35,\n            -94.935_451_837_137_89,\n            -92.281_105_990_784,\n            -85.153_892_576_945_03,\n            -80.899_215_449_606_93,\n            -64.121_907_060_953_25,\n            -77.519_613_759_806_97,\n            -97.742_212_060_588_33,\n            -87.942_028_985_507_66,\n            -73.030_303_030_303_03,\n            -60.363_636_363_635_486,\n            -48.787_878_787_878_79,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output[i + 13], *expected, epsilon = 0.0001);\n        }\n        // Test incremental calculation matches regular calculation\n        let mut prev_highest_high = output_highest_high[13];\n        let mut prev_lowest_low = output_lowest_low[13];\n\n        for i in 14..19 {\n            let (result, highest_high, lowest_low) = willr_inc(\n                prev_highest_high,\n                prev_lowest_low,\n                input_high[i - 1],\n                input_low[i - 1],\n                input_close[i],\n                input_high[i],\n                input_low[i],\n            )\n            .unwrap();\n\n            assert_relative_eq!(result, output[i], epsilon = 0.0001);\n\n            prev_highest_high = highest_high;\n            prev_lowest_low = lowest_low;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/ohlcv/wma.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Calculates the lookback period required for Weighted Moving Average (WMA).\n///\n/// # Description\n/// The lookback period represents the minimum number of data points needed before\n/// the first valid WMA value can be calculated. For WMA, this equals period - 1.\n///\n/// # Arguments\n/// * `opt_period` - The time period for WMA calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period on success\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If period is less than 2\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::wma;\n///\n/// let period = 14;\n/// let lookback = wma::lookback(period).unwrap();\n/// assert_eq!(lookback, 13); // lookback = period - 1\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Calculates Weighted Moving Average (WMA) for a price series.\n///\n/// # Description\n/// WMA assigns linearly decreasing weights to each price in the period, giving more\n/// importance to recent prices and less to older ones.\n///\n/// # Mathematical Formula\n/// ```text\n/// WMA = (P1*n + P2*(n-1) + ... + Pn*1) / (n + (n-1) + ... + 1)\n/// ```\n/// Where:\n/// - P1, P2, ..., Pn are prices from newest to oldest\n/// - n is the time period\n/// - Denominator is the sum of weights: n*(n+1)/2\n///\n/// # Arguments\n/// * `input` - Array of price values\n/// * `opt_period` - The time period for WMA calculation (must be >= 2)\n/// * `output` - Array to store WMA values (first period-1 values are NaN)\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty Ok on success\n///\n/// # Errors\n/// * `KandError::InvalidData` - If input array is empty\n/// * `KandError::LengthMismatch` - If output length != input length\n/// * `KandError::InvalidParameter` - If period < 2\n/// * `KandError::InsufficientData` - If input length <= lookback\n/// * `KandError::NaNDetected` - If any input is NaN (with `check-nan`)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::wma;\n///\n/// let input = vec![1.0, 2.0, 3.0, 4.0, 5.0];\n/// let mut output = vec![0.0; 5];\n///\n/// wma::wma(&input, 3, &mut output).unwrap();\n/// // output = [NaN, NaN, 2.0, 3.0, 4.0]\n/// // First value: (1.0*3 + 2.0*2 + 3.0*1)/(3+2+1) = 2.0\n/// ```\npub fn wma(input: &[TAFloat], opt_period: usize, output: &mut [TAFloat]) -> Result<(), KandError> {\n    let len = input.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n        if len != output.len() {\n            return Err(KandError::LengthMismatch);\n        }\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for &value in input {\n            if value.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate denominator (sum of weights)\n    let denominator = (opt_period * (opt_period + 1)) as TAFloat / 2.0;\n\n    // Fill initial values with NAN\n    for value in output.iter_mut().take(lookback) {\n        *value = TAFloat::NAN;\n    }\n\n    // Calculate WMA for each window\n    for i in lookback..len {\n        let mut weighted_sum = 0.0;\n        let mut weight = opt_period as TAFloat;\n\n        for j in 0..opt_period {\n            weighted_sum += input[i - j] * weight;\n            weight -= 1.0;\n        }\n\n        output[i] = weighted_sum / denominator;\n    }\n\n    Ok(())\n}\n\n/// Calculates the next WMA value incrementally.\n///\n/// # Description\n/// Computes a single WMA value for the most recent period window, useful for\n/// real-time calculations without processing the entire series.\n///\n/// # Mathematical Formula\n/// ```text\n/// WMA = (P1*n + P2*(n-1) + ... + Pn*1) / (n + (n-1) + ... + 1)\n/// ```\n/// Where:\n/// - P1, P2, ..., Pn are prices from newest to oldest\n/// - n is the time period\n/// - Denominator is the sum of weights: n*(n+1)/2\n///\n/// # Arguments\n/// * `input_window` - Price values ordered from newest to oldest\n/// * `opt_period` - The time period for WMA calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The calculated WMA value\n///\n/// # Errors\n/// * `KandError::InvalidParameter` - If period < 2\n/// * `KandError::LengthMismatch` - If `input_window` length != period\n/// * `KandError::NaNDetected` - If any input is NaN (with `check-nan`)\n///\n/// # Example\n/// ```\n/// use kand::ohlcv::wma;\n///\n/// let window = vec![5.0, 4.0, 3.0]; // newest to oldest\n/// let wma = wma::wma_inc(&window, 3).unwrap();\n/// // wma = (5.0*3 + 4.0*2 + 3.0*1)/(3+2+1) = 4.333...\n/// ```\npub fn wma_inc(input_window: &[TAFloat], opt_period: usize) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n        if input_window.len() != opt_period {\n            return Err(KandError::LengthMismatch);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for &value in input_window {\n            if value.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    let denominator = (opt_period * (opt_period + 1)) as TAFloat / 2.0;\n    let mut weighted_sum = 0.0;\n    let mut weight = opt_period as TAFloat;\n\n    for &value in input_window {\n        weighted_sum += value * weight;\n        weight -= 1.0;\n    }\n\n    Ok(weighted_sum / denominator)\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_wma_calculation() {\n        let input = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0, 35114.5, 35097.2,\n            35092.0, 35073.2, 35139.3, 35092.0, 35126.7, 35106.3, 35124.8, 35170.1, 35215.3,\n            35154.0, 35216.3, 35211.8, 35158.4,\n        ];\n\n        let opt_period = 30;\n        let mut output = vec![0.0; input.len()];\n\n        wma(&input, opt_period, &mut output).unwrap();\n\n        // First 29 values should be NaN\n        for value in output.iter().take(29) {\n            assert!(value.is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            35_086.706_666_666_67,\n            35_084.862_795_698_93,\n            35_085.524_516_129_04,\n            35_085.073_763_440_865,\n            35_085.998_064_516_134,\n            35_089.942_150_537_645,\n            35_096.826_881_720_44,\n            35_099.841_290_322_58,\n            35106.98,\n            35_113.904_946_236_566,\n            35_117.354_193_548_395,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output[i + 29], *expected, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation matches regular calculation\n        for i in 30..35 {\n            let window: Vec<TAFloat> = input[i - (opt_period - 1)..=i]\n                .iter()\n                .rev()\n                .copied()\n                .collect();\n            let result = wma_inc(&window, opt_period).unwrap();\n            assert_relative_eq!(result, output[i], epsilon = 0.0001);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/other/mod.rs",
    "content": ""
  },
  {
    "path": "kand/src/ta/stats/alpha.rs",
    "content": ""
  },
  {
    "path": "kand/src/ta/stats/beta.rs",
    "content": ""
  },
  {
    "path": "kand/src/ta/stats/calmar.rs",
    "content": ""
  },
  {
    "path": "kand/src/ta/stats/correl.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Calculates the lookback period required for Correlation calculation.\n///\n/// The lookback period represents the number of data points needed before the first valid output\n/// can be calculated. For Correlation, this equals the period minus 1.\n///\n/// # Arguments\n/// * `opt_period` - The time period for Correlation calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period (period - 1) on success, or error on failure\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if period is less than 2\n///\n/// # Example\n/// ```\n/// use kand::stats::correl;\n/// let period = 30;\n/// let lookback = correl::lookback(period).unwrap();\n/// assert_eq!(lookback, 29); // lookback is period - 1\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Calculates the Pearson Correlation Coefficient (CORREL) for two price series.\n///\n/// NOTE: We don't reuse stddev here for performance reasons.\n/// Calling stddev twice + covariance would require 3 passes through the data.\n///\n/// The Pearson Correlation Coefficient measures the linear correlation between two variables,\n/// returning a value between -1 and +1, where:\n/// - +1 indicates perfect positive correlation\n/// - -1 indicates perfect negative correlation\n/// - 0 indicates no linear correlation\n///\n/// # Mathematical Formula\n/// ```text\n/// r = [n(Σxy) - (Σx)(Σy)] / sqrt([n(Σx²) - (Σx)²][n(Σy²) - (Σy)²])\n/// ```\n/// Where:\n/// - n is the period (number of observations)\n/// - Σx is the sum of values in series 0\n/// - Σy is the sum of values in series 1\n/// - Σxy is the sum of products (x[i] * y[i])\n/// - Σx² is the sum of squares of series 0\n/// - Σy² is the sum of squares of series 1\n///\n/// # Calculation Steps\n/// 1. Calculate initial sums for the first period\n/// 2. Apply Pearson correlation formula\n/// 3. For subsequent periods, update sums incrementally\n/// 4. Fill initial values before lookback period with NaN\n///\n/// # Arguments\n/// * `input_0` - First input series\n/// * `input_1` - Second input series\n/// * `opt_period` - The time period for correlation calculation (must be >= 2)\n/// * `output_correl` - Array to store calculated correlation values\n/// * `output_sum_0` - Array to store running sum of series 0\n/// * `output_sum_1` - Array to store running sum of series 1\n/// * `output_sum_0_sq` - Array to store running sum of squares of series 0\n/// * `output_sum_1_sq` - Array to store running sum of squares of series 1\n/// * `output_sum_01` - Array to store running sum of products\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok(()) on success, or error on failure\n///\n/// # Errors\n/// * Returns `KandError::InvalidData` if input data is empty\n/// * Returns `KandError::LengthMismatch` if arrays have different lengths\n/// * Returns `KandError::InvalidParameter` if period is less than 2\n/// * Returns `KandError::InsufficientData` if input length is less than period\n/// * Returns `KandError::NaNDetected` if input contains NaN values (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::stats::correl;\n/// let series1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];\n/// let series2 = vec![2.0, 4.0, 6.0, 8.0, 10.0];\n/// let period = 3;\n/// let mut output_correl = vec![0.0; 5];\n/// let mut output_sum_0 = vec![0.0; 5];\n/// let mut output_sum_1 = vec![0.0; 5];\n/// let mut output_sum_0_sq = vec![0.0; 5];\n/// let mut output_sum_1_sq = vec![0.0; 5];\n/// let mut output_sum_01 = vec![0.0; 5];\n///\n/// correl::correl(\n///     &series1,\n///     &series2,\n///     period,\n///     &mut output_correl,\n///     &mut output_sum_0,\n///     &mut output_sum_1,\n///     &mut output_sum_0_sq,\n///     &mut output_sum_1_sq,\n///     &mut output_sum_01,\n/// )\n/// .unwrap();\n/// // output_correl = [NaN, NaN, 1.0, 1.0, 1.0] (perfect positive correlation)\n/// ```\npub fn correl(\n    input_0: &[TAFloat],\n    input_1: &[TAFloat],\n    opt_period: usize,\n    output_correl: &mut [TAFloat],\n    output_sum_0: &mut [TAFloat],\n    output_sum_1: &mut [TAFloat],\n    output_sum_0_sq: &mut [TAFloat],\n    output_sum_1_sq: &mut [TAFloat],\n    output_sum_01: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_0.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check - all arrays must have same length\n        if len != input_1.len()\n            || len != output_correl.len()\n            || len != output_sum_0.len()\n            || len != output_sum_1.len()\n            || len != output_sum_0_sq.len()\n            || len != output_sum_1_sq.len()\n            || len != output_sum_01.len()\n        {\n            return Err(KandError::LengthMismatch);\n        }\n\n        // Parameter validation\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check for both input series\n        for i in 0..len {\n            if input_0[i].is_nan() || input_1[i].is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate initial sums for the first period\n    let mut sum_0 = 0.0;\n    let mut sum_1 = 0.0;\n    let mut sum_0_sq = 0.0;\n    let mut sum_1_sq = 0.0;\n    let mut sum_01 = 0.0;\n\n    // Initialize sums for the first window\n    for i in 0..opt_period {\n        let val_0 = input_0[i];\n        let val_1 = input_1[i];\n\n        sum_0 += val_0;\n        sum_1 += val_1;\n        sum_0_sq += val_0 * val_0;\n        sum_1_sq += val_1 * val_1;\n        sum_01 += val_0 * val_1;\n    }\n\n    // Store sums for first valid position\n    output_sum_0[lookback] = sum_0;\n    output_sum_1[lookback] = sum_1;\n    output_sum_0_sq[lookback] = sum_0_sq;\n    output_sum_1_sq[lookback] = sum_1_sq;\n    output_sum_01[lookback] = sum_01;\n\n    // Calculate first correlation value\n    let n = opt_period as TAFloat;\n    let numerator = n.mul_add(sum_01, -(sum_0 * sum_1));\n    let denominator_0 = n.mul_add(sum_0_sq, -(sum_0 * sum_0));\n    let denominator_1 = n.mul_add(sum_1_sq, -(sum_1 * sum_1));\n    let denominator = (denominator_0 * denominator_1).sqrt();\n\n    if denominator > 0.0 {\n        output_correl[lookback] = numerator / denominator;\n    } else {\n        output_correl[lookback] = TAFloat::NAN;\n    }\n\n    // Calculate subsequent correlations using sliding window\n    for i in opt_period..len {\n        // Remove old values and add new values\n        let old_0 = input_0[i - opt_period];\n        let old_1 = input_1[i - opt_period];\n        let new_0 = input_0[i];\n        let new_1 = input_1[i];\n\n        // Update sums\n        sum_0 = sum_0 - old_0 + new_0;\n        sum_1 = sum_1 - old_1 + new_1;\n        sum_0_sq = new_0.mul_add(new_0, old_0.mul_add(-old_0, sum_0_sq));\n        sum_1_sq = new_1.mul_add(new_1, old_1.mul_add(-old_1, sum_1_sq));\n        sum_01 = new_0.mul_add(new_1, old_0.mul_add(-old_1, sum_01));\n\n        // Store updated sums\n        output_sum_0[i] = sum_0;\n        output_sum_1[i] = sum_1;\n        output_sum_0_sq[i] = sum_0_sq;\n        output_sum_1_sq[i] = sum_1_sq;\n        output_sum_01[i] = sum_01;\n\n        // Calculate correlation for this window\n        let numerator = n.mul_add(sum_01, -(sum_0 * sum_1));\n        let denominator_0 = n.mul_add(sum_0_sq, -(sum_0 * sum_0));\n        let denominator_1 = n.mul_add(sum_1_sq, -(sum_1 * sum_1));\n        let denominator = (denominator_0 * denominator_1).sqrt();\n\n        if denominator > 0.0 {\n            output_correl[i] = numerator / denominator;\n        } else {\n            output_correl[i] = TAFloat::NAN;\n        }\n    }\n\n    // Fill initial values with NaN\n    for i in 0..lookback {\n        output_correl[i] = TAFloat::NAN;\n        output_sum_0[i] = TAFloat::NAN;\n        output_sum_1[i] = TAFloat::NAN;\n        output_sum_0_sq[i] = TAFloat::NAN;\n        output_sum_1_sq[i] = TAFloat::NAN;\n        output_sum_01[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the latest Correlation value using incremental calculation.\n///\n/// This function provides an optimized way to update the Correlation value when new data arrives,\n/// avoiding full recalculation of the entire series.\n///\n/// # Mathematical Formula\n/// The correlation is updated by:\n/// 1. Updating all sums by removing old values and adding new values\n/// 2. Recalculating correlation using the updated sums\n///\n/// # Arguments\n/// * `input_new_0` - The newest value from series 0 to add\n/// * `input_new_1` - The newest value from series 1 to add\n/// * `input_old_0` - The oldest value from series 0 to remove\n/// * `input_old_1` - The oldest value from series 1 to remove\n/// * `prev_sum_0` - Previous sum of series 0\n/// * `prev_sum_1` - Previous sum of series 1\n/// * `prev_sum_0_sq` - Previous sum of squares of series 0\n/// * `prev_sum_1_sq` - Previous sum of squares of series 1\n/// * `prev_sum_01` - Previous sum of products\n/// * `opt_period` - The time period (must be >= 2)\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat, TAFloat, TAFloat, TAFloat), KandError>` - Tuple containing:\n///   - New correlation value\n///   - New sum of series 0\n///   - New sum of series 1\n///   - New sum of squares of series 0\n///   - New sum of squares of series 1\n///   - New sum of products\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if period is less than 2\n/// * Returns `KandError::NaNDetected` if any input contains NaN (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::stats::correl;\n/// let prev_sum_0 = 6.0;      // sum of [1.0, 2.0, 3.0]\n/// let prev_sum_1 = 12.0;     // sum of [2.0, 4.0, 6.0]\n/// let prev_sum_0_sq = 14.0;  // sum of squares [1, 4, 9]\n/// let prev_sum_1_sq = 56.0;  // sum of squares [4, 16, 36]\n/// let prev_sum_01 = 28.0;    // sum of products\n/// let new_0 = 4.0;\n/// let new_1 = 8.0;\n/// let old_0 = 1.0;\n/// let old_1 = 2.0;\n/// let period = 3;\n///\n/// let (new_correl, new_sum_0, new_sum_1, new_sum_0_sq, new_sum_1_sq, new_sum_01) =\n///     correl::correl_inc(\n///         new_0, new_1, old_0, old_1,\n///         prev_sum_0, prev_sum_1, prev_sum_0_sq, prev_sum_1_sq, prev_sum_01,\n///         period\n///     ).unwrap();\n/// ```\npub fn correl_inc(\n    input_new_0: TAFloat,\n    input_new_1: TAFloat,\n    input_old_0: TAFloat,\n    input_old_1: TAFloat,\n    prev_sum_0: TAFloat,\n    prev_sum_1: TAFloat,\n    prev_sum_0_sq: TAFloat,\n    prev_sum_1_sq: TAFloat,\n    prev_sum_01: TAFloat,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat, TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check for all inputs\n        if input_new_0.is_nan()\n            || input_new_1.is_nan()\n            || input_old_0.is_nan()\n            || input_old_1.is_nan()\n            || prev_sum_0.is_nan()\n            || prev_sum_1.is_nan()\n            || prev_sum_0_sq.is_nan()\n            || prev_sum_1_sq.is_nan()\n            || prev_sum_01.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    // Update all sums incrementally\n    let new_sum_0 = prev_sum_0 - input_old_0 + input_new_0;\n    let new_sum_1 = prev_sum_1 - input_old_1 + input_new_1;\n    let new_sum_0_sq = input_new_0.mul_add(\n        input_new_0,\n        input_old_0.mul_add(-input_old_0, prev_sum_0_sq),\n    );\n    let new_sum_1_sq = input_new_1.mul_add(\n        input_new_1,\n        input_old_1.mul_add(-input_old_1, prev_sum_1_sq),\n    );\n    let new_sum_01 =\n        input_new_0.mul_add(input_new_1, input_old_0.mul_add(-input_old_1, prev_sum_01));\n\n    // Calculate new correlation using Pearson formula\n    let n = opt_period as TAFloat;\n    let numerator = n.mul_add(new_sum_01, -(new_sum_0 * new_sum_1));\n    let denominator_0 = n.mul_add(new_sum_0_sq, -(new_sum_0 * new_sum_0));\n    let denominator_1 = n.mul_add(new_sum_1_sq, -(new_sum_1 * new_sum_1));\n    let denominator = (denominator_0 * denominator_1).sqrt();\n\n    let new_correl = if denominator > 0.0 {\n        numerator / denominator\n    } else {\n        TAFloat::NAN\n    };\n\n    Ok((\n        new_correl,\n        new_sum_0,\n        new_sum_1,\n        new_sum_0_sq,\n        new_sum_1_sq,\n        new_sum_01,\n    ))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_correl_perfect_positive() {\n        // Test with perfectly correlated data (y = 2x)\n        let input_0 = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];\n        let input_1 = vec![2.0, 4.0, 6.0, 8.0, 10.0, 12.0];\n        let opt_period = 3;\n        let mut output_correl = vec![0.0; 6];\n        let mut output_sum_0 = vec![0.0; 6];\n        let mut output_sum_1 = vec![0.0; 6];\n        let mut output_sum_0_sq = vec![0.0; 6];\n        let mut output_sum_1_sq = vec![0.0; 6];\n        let mut output_sum_01 = vec![0.0; 6];\n\n        correl(\n            &input_0,\n            &input_1,\n            opt_period,\n            &mut output_correl,\n            &mut output_sum_0,\n            &mut output_sum_1,\n            &mut output_sum_0_sq,\n            &mut output_sum_1_sq,\n            &mut output_sum_01,\n        )\n        .unwrap();\n\n        // First 2 values should be NaN\n        for i in 0..2 {\n            assert!(output_correl[i].is_nan());\n        }\n\n        // Perfect positive correlation should be 1.0\n        for i in 2..6 {\n            assert_relative_eq!(output_correl[i], 1.0, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation matches\n        for i in 3..6 {\n            let (inc_correl, _, _, _, _, _) = correl_inc(\n                input_0[i],\n                input_1[i],\n                input_0[i - opt_period],\n                input_1[i - opt_period],\n                output_sum_0[i - 1],\n                output_sum_1[i - 1],\n                output_sum_0_sq[i - 1],\n                output_sum_1_sq[i - 1],\n                output_sum_01[i - 1],\n                opt_period,\n            )\n            .unwrap();\n            assert_relative_eq!(inc_correl, output_correl[i], epsilon = 0.0001);\n        }\n    }\n\n    #[test]\n    fn test_correl_perfect_negative() {\n        // Test with perfectly negative correlated data (y = -x + 10)\n        let input_0 = vec![1.0, 2.0, 3.0, 4.0, 5.0];\n        let input_1 = vec![9.0, 8.0, 7.0, 6.0, 5.0];\n        let opt_period = 3;\n        let mut output_correl = vec![0.0; 5];\n        let mut output_sum_0 = vec![0.0; 5];\n        let mut output_sum_1 = vec![0.0; 5];\n        let mut output_sum_0_sq = vec![0.0; 5];\n        let mut output_sum_1_sq = vec![0.0; 5];\n        let mut output_sum_01 = vec![0.0; 5];\n\n        correl(\n            &input_0,\n            &input_1,\n            opt_period,\n            &mut output_correl,\n            &mut output_sum_0,\n            &mut output_sum_1,\n            &mut output_sum_0_sq,\n            &mut output_sum_1_sq,\n            &mut output_sum_01,\n        )\n        .unwrap();\n\n        // Perfect negative correlation should be -1.0\n        for i in 2..5 {\n            assert_relative_eq!(output_correl[i], -1.0, epsilon = 0.0001);\n        }\n    }\n\n    #[test]\n    fn test_correl_no_variance() {\n        // Test when one series has no variance\n        let input_0 = vec![5.0, 5.0, 5.0, 5.0, 5.0];\n        let input_1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];\n        let opt_period = 3;\n        let mut output_correl = vec![0.0; 5];\n        let mut output_sum_0 = vec![0.0; 5];\n        let mut output_sum_1 = vec![0.0; 5];\n        let mut output_sum_0_sq = vec![0.0; 5];\n        let mut output_sum_1_sq = vec![0.0; 5];\n        let mut output_sum_01 = vec![0.0; 5];\n\n        correl(\n            &input_0,\n            &input_1,\n            opt_period,\n            &mut output_correl,\n            &mut output_sum_0,\n            &mut output_sum_1,\n            &mut output_sum_0_sq,\n            &mut output_sum_1_sq,\n            &mut output_sum_01,\n        )\n        .unwrap();\n\n        // When one series has no variance, correlation is undefined (NaN)\n        for i in 2..5 {\n            assert!(output_correl[i].is_nan());\n        }\n    }\n\n    #[test]\n    fn test_correl_lookback() {\n        let lookback_5 = lookback(5).unwrap();\n        assert_eq!(lookback_5, 4);\n\n        let lookback_30 = lookback(30).unwrap();\n        assert_eq!(lookback_30, 29);\n\n        // Test invalid period\n        #[cfg(feature = \"check\")]\n        {\n            let err = lookback(1);\n            assert!(err.is_err());\n        }\n    }\n\n    #[test]\n    fn test_talib_compatibility() {\n        // Real BTC/ETH daily closing prices from Binance\n        // Testing against TA-Lib CORREL output with period=14\n        let input_0 = vec![\n            103_297.99, 102_120.01, 100_963.87, 105_333.93, 106_083.0, 107_340.58, 106_947.06,\n            107_047.59, 107_296.79, 108_356.93, 107_146.5, 105_681.14, 108_849.6, 109_584.78,\n            107_984.24, 108_198.12, 109_203.84, 108_262.94, 108_922.98, 111_233.99, 116_010.0,\n            117_527.66, 117_420.0, 119_086.64, 119_841.18, 117_758.09, 118_630.43, 119_177.56,\n            117_924.84, 117_893.24,\n        ];\n        let input_1 = vec![\n            2406.49, 2295.73, 2227.7, 2411.66, 2448.45, 2418.49, 2415.96, 2423.17, 2435.62,\n            2500.09, 2485.47, 2405.01, 2570.41, 2591.25, 2508.04, 2516.41, 2570.35, 2542.29,\n            2615.25, 2768.74, 2951.29, 2958.22, 2943.28, 2972.03, 3013.62, 3137.89, 3371.35,\n            3476.87, 3546.92, 3552.85,\n        ];\n\n        let opt_period = 14;\n        let len = input_0.len();\n        let mut output_correl = vec![0.0; len];\n        let mut output_sum_0 = vec![0.0; len];\n        let mut output_sum_1 = vec![0.0; len];\n        let mut output_sum_0_sq = vec![0.0; len];\n        let mut output_sum_1_sq = vec![0.0; len];\n        let mut output_sum_01 = vec![0.0; len];\n\n        correl(\n            &input_0,\n            &input_1,\n            opt_period,\n            &mut output_correl,\n            &mut output_sum_0,\n            &mut output_sum_1,\n            &mut output_sum_0_sq,\n            &mut output_sum_1_sq,\n            &mut output_sum_01,\n        )\n        .unwrap();\n\n        // First 13 values should be NaN (lookback = period - 1)\n        for i in 0..13 {\n            assert!(output_correl[i].is_nan());\n        }\n\n        // Expected correlation values from TA-Lib (full precision)\n        let expected_values = [\n            0.916_557_037_193_843_8,\n            0.948_314_436_127_766,\n            0.942_234_310_162_226_4,\n            0.893_845_548_132_203_3,\n            0.897_952_381_223_419_8,\n            0.911_695_501_638_574,\n            0.952_458_493_129_686_4,\n            0.975_493_184_636_118_3,\n            0.978_325_130_734_52,\n            0.982_342_459_884_886_4,\n            0.981_909_093_602_057_7,\n            0.982_283_404_493_009_1,\n            0.963_186_856_345_845_4,\n            0.918_423_909_392_030_3,\n            0.890_700_952_050_942_8,\n            0.837_192_365_846_861_5,\n            0.786_620_100_233_665_4,\n        ];\n\n        // Verify correlation values match TA-Lib output\n        for (i, &expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_correl[i + 13], expected, epsilon = 0.000_000_000_1);\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/stats/drawdown.rs",
    "content": ""
  },
  {
    "path": "kand/src/ta/stats/fv.rs",
    "content": ""
  },
  {
    "path": "kand/src/ta/stats/kelly.rs",
    "content": ""
  },
  {
    "path": "kand/src/ta/stats/max.rs",
    "content": "use crate::{EPSILON, KandError, TAFloat};\n\n/// Calculates the lookback period required for Maximum Value calculation.\n///\n/// The lookback period represents the number of data points needed before the first valid output\n/// can be calculated. For Maximum Value, this equals the period minus one.\n///\n/// # Arguments\n/// * `opt_period` - The time period for calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period (period - 1) on success\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if period is less than 2\n///\n/// # Example\n/// ```\n/// use kand::stats::max;\n/// let period = 14;\n/// let lookback = max::lookback(period).unwrap();\n/// assert_eq!(lookback, 13);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Calculates Maximum Value for a series of prices over a specified period.\n///\n/// # Calculation Principle\n/// For each period, finds the highest price value within that period by comparing\n/// all values in the window.\n///\n/// # Mathematical Formula\n/// ```text\n/// MAX[i] = max(price[i-n+1], price[i-n+2], ..., price[i])\n/// ```\n/// Where:\n/// - n is the period\n/// - i is the current index\n///\n/// # Arguments\n/// * `input_prices` - Array of input price values\n/// * `opt_period` - The time period for calculation (must be >= 2)\n/// * `output_max` - Array to store calculated MAX values (first period-1 values are NaN)\n///\n/// # Returns\n/// * `Result<(), KandError>` - Empty result on success\n///\n/// # Errors\n/// * Returns `KandError::InvalidData` if input array is empty\n/// * Returns `KandError::LengthMismatch` if output length doesn't match input\n/// * Returns `KandError::InvalidParameter` if period is less than 2\n/// * Returns `KandError::InsufficientData` if input length is less than period\n/// * Returns `KandError::NaNDetected` if any input value is NaN (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::stats::max;\n/// let prices = vec![1.0, 2.0, 3.0, 2.5, 4.0];\n/// let period = 3;\n/// let mut max_values = vec![0.0; 5];\n///\n/// max::max(&prices, period, &mut max_values).unwrap();\n/// // max_values = [NaN, NaN, 3.0, 3.0, 4.0]\n/// ```\npub fn max(\n    input_prices: &[TAFloat],\n    opt_period: usize,\n    output_max: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_prices.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n\n        // Length consistency check\n        if output_max.len() != len {\n            return Err(KandError::LengthMismatch);\n        }\n\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        for price in input_prices {\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate MAX values\n    for i in lookback..len {\n        let mut max_val = input_prices[i - lookback];\n        for price in input_prices.iter().take(i + 1).skip(i - lookback + 1) {\n            if *price > max_val {\n                max_val = *price;\n            }\n        }\n        output_max[i] = max_val;\n    }\n\n    // Fill initial values with NAN\n    for value in output_max.iter_mut().take(lookback) {\n        *value = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the latest Maximum Value incrementally using previous results.\n///\n/// This function provides an optimized way to calculate the latest MAX value\n/// by using the previous MAX value and only considering the new and removed prices.\n///\n/// # Arguments\n/// * `input_price` - The newest price value to include in calculation\n/// * `prev_max` - The previous period's MAX value\n/// * `input_old_price` - The oldest price value being removed from the period\n/// * `opt_period` - The time period for calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The new MAX value on success\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if period is less than 2\n/// * Returns `KandError::NaNDetected` if any input value is NaN (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::stats::max;\n/// let new_price = 10.5;\n/// let prev_max = 11.0;\n/// let old_price = 9.0;\n/// let period = 14;\n///\n/// let new_max = max::max_inc(new_price, prev_max, old_price, period).unwrap();\n/// assert_eq!(new_max, 11.0);\n/// ```\npub fn max_inc(\n    input_price: TAFloat,\n    prev_max: TAFloat,\n    input_old_price: TAFloat,\n    opt_period: usize,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_price.is_nan() || prev_max.is_nan() || input_old_price.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    // If new price is higher than previous max, it becomes the new max\n    if input_price >= prev_max {\n        return Ok(input_price);\n    }\n\n    // If old price being removed was the max, need to recalculate\n    if (prev_max - input_old_price).abs() < EPSILON {\n        return Ok(input_price); // Need full recalculation in this case\n    }\n\n    // Otherwise keep previous max\n    Ok(prev_max)\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_max_calculation() {\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0,\n        ];\n        let opt_period = 14;\n        let mut output_max = vec![0.0; input_close.len()];\n\n        max(&input_close, opt_period, &mut output_max).unwrap();\n\n        // First 13 values should be NaN\n        for value in output_max.iter().take(13) {\n            assert!(value.is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            35254.6, 35254.6, 35254.6, 35254.6, 35254.6, 35254.6, 35251.9, 35251.9, 35229.9,\n            35229.9, 35229.9, 35229.9,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_max[i + 13], *expected, epsilon = 0.0001);\n        }\n\n        // Now test incremental calculation matches regular calculation\n        let mut prev_max = output_max[13]; // First valid max value\n\n        // Test each incremental step\n        for i in 14..19 {\n            let result = max_inc(\n                input_close[i],\n                prev_max,\n                input_close[i - opt_period],\n                opt_period,\n            )\n            .unwrap();\n            assert_relative_eq!(result, output_max[i], epsilon = 0.0001);\n            prev_max = result;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/stats/min.rs",
    "content": "use crate::{EPSILON, KandError, TAFloat};\n\n/// Calculates the lookback period required for Minimum Value calculation.\n///\n/// Returns the number of data points needed before the first valid output can be calculated.\n/// For MIN, this is one less than the specified period.\n///\n/// # Arguments\n/// * `opt_period` - The time period for MIN calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period (period - 1) on success\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if period is less than 2\n///\n/// # Example\n/// ```\n/// use kand::stats::min;\n/// let period = 14;\n/// let lookback = min::lookback(period).unwrap();\n/// assert_eq!(lookback, 13);\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Calculates the Minimum Value (MIN) for a series of prices over a specified period.\n///\n/// The MIN indicator finds the lowest price value within a given time period. For each\n/// calculation point, it looks back over the specified number of periods and returns\n/// the minimum value found.\n///\n/// # Mathematical Formula\n/// ```text\n/// MIN[i] = min(price[i], price[i-1], ..., price[i-n+1])\n/// ```\n/// Where:\n/// - i is the current index\n/// - n is the time period\n/// - price[] represents the input price series\n///\n/// # Calculation Steps\n/// 1. For each point, look back n periods\n/// 2. Find the minimum value in that range\n/// 3. Store the minimum as the current MIN value\n///\n/// # Arguments\n/// * `input_prices` - Array of input price values\n/// * `opt_period` - The time period for MIN calculation (must be >= 2)\n/// * `output_min` - Array to store the calculated MIN values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok(()) on success\n///\n/// # Errors\n/// * Returns `KandError::InvalidData` if input array is empty\n/// * Returns `KandError::LengthMismatch` if output length doesn't match input\n/// * Returns `KandError::InvalidParameter` if period is less than 2\n/// * Returns `KandError::InsufficientData` if input length is less than period\n/// * Returns `KandError::NaNDetected` if any input value is NaN (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::stats::min;\n/// let input = vec![10.0, 8.0, 6.0, 7.0, 9.0];\n/// let period = 3;\n/// let mut output = vec![0.0; 5];\n///\n/// min::min(&input, period, &mut output).unwrap();\n/// // output = [NaN, NaN, 6.0, 6.0, 6.0]\n/// ```\npub fn min(\n    input_prices: &[TAFloat],\n    opt_period: usize,\n    output_min: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_prices.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Length consistency check\n        if output_min.len() != len {\n            return Err(KandError::LengthMismatch);\n        }\n\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        for price in input_prices {\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate MIN values\n    for i in lookback..len {\n        let mut min_val = input_prices[i - lookback];\n        for price in input_prices.iter().take(i + 1).skip(i - lookback + 1) {\n            if *price < min_val {\n                min_val = *price;\n            }\n        }\n        output_min[i] = min_val;\n    }\n\n    // Fill initial values with NAN\n    for value in output_min.iter_mut().take(lookback) {\n        *value = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the latest Minimum Value incrementally using the previous MIN value.\n///\n/// This function provides an optimized way to calculate the current MIN value\n/// when you already have the previous MIN value and are adding a new price point.\n///\n/// # Arguments\n/// * `input_price` - The new price value to include in calculation\n/// * `prev_min` - The previous MIN value\n/// * `prev_price` - The price value that will drop out of the period\n/// * `opt_period` - The time period for MIN calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The new MIN value on success\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if period is less than 2\n/// * Returns `KandError::NaNDetected` if any input value is NaN (with \"`check-nan`\" feature)\n/// * Returns `KandError::InsufficientData` if full recalculation is needed\n///\n/// # Example\n/// ```\n/// use kand::stats::min;\n/// let new_price = 15.0;\n/// let prev_min = 12.0;\n/// let dropping_price = 14.0;\n/// let period = 14;\n///\n/// let new_min = min::min_inc(new_price, prev_min, dropping_price, period).unwrap();\n/// assert_eq!(new_min, 12.0);\n/// ```\npub fn min_inc(\n    input_price: TAFloat,\n    prev_min: TAFloat,\n    prev_price: TAFloat,\n    opt_period: usize,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_price.is_nan() || prev_min.is_nan() || prev_price.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    // If the new price is less than previous min, it becomes the new min\n    if input_price < prev_min {\n        return Ok(input_price);\n    }\n\n    // If the price being removed was the previous min,\n    // we need to scan the period for the new min\n    if (prev_price - prev_min).abs() < EPSILON {\n        // In this case we need the full period data to recalculate\n        return Err(KandError::InsufficientData);\n    }\n\n    // Otherwise the previous min is still valid\n    Ok(prev_min)\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_min_calculation() {\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0, 35114.5, 35097.2,\n            35092.0, 35073.2, 35139.3,\n        ];\n        let opt_period = 14;\n        let mut output_min = vec![0.0; input_close.len()];\n\n        min(&input_close, opt_period, &mut output_min).unwrap();\n\n        // First 13 values should be NaN\n        for value in output_min.iter().take(13) {\n            assert!(value.is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            35160.7, 35090.3, 35041.2, 34999.3, 34999.3, 34999.3, 34999.3, 34939.5, 34939.5,\n            34939.5, 34939.5, 34939.5, 34939.5, 34939.5, 34939.5, 34939.5, 34939.5,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_min[i + 13], *expected, epsilon = 0.0001);\n        }\n\n        // Now test incremental calculation matches regular calculation\n        let mut prev_min = output_min[13]; // First valid min value\n\n        // Test each incremental step\n        for i in 14..19 {\n            let result = min_inc(\n                input_close[i],\n                prev_min,\n                input_close[i - opt_period],\n                opt_period,\n            )\n            .unwrap();\n            assert_relative_eq!(result, output_min[i], epsilon = 0.0001);\n            prev_min = result;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/stats/mod.rs",
    "content": "// pub mod beta;\npub mod correl;\npub mod max;\npub mod min;\npub mod stddev;\npub mod sum;\npub mod var;\n"
  },
  {
    "path": "kand/src/ta/stats/nper.rs",
    "content": ""
  },
  {
    "path": "kand/src/ta/stats/ret.rs",
    "content": ""
  },
  {
    "path": "kand/src/ta/stats/sharpe.rs",
    "content": ""
  },
  {
    "path": "kand/src/ta/stats/sortino.rs",
    "content": ""
  },
  {
    "path": "kand/src/ta/stats/stddev.rs",
    "content": "use crate::{KandError, TAFloat, ta::stats::var};\n\n/// Calculates the lookback period required for Standard Deviation calculation.\n///\n/// The lookback period represents the number of data points needed before the first valid output\n/// can be generated. For Standard Deviation, this equals the period minus 1.\n///\n/// # Arguments\n/// * `opt_period` - The time period for Standard Deviation calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period (period - 1) on success, or error on failure\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if period is less than 2\n///\n/// # Example\n/// ```\n/// use kand::stats::stddev;\n/// let period = 14;\n/// let lookback = stddev::lookback(period).unwrap();\n/// assert_eq!(lookback, 13); // lookback is period - 1\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    var::lookback(opt_period)\n}\n\n/// Calculates Standard Deviation for a price series.\n///\n/// Standard Deviation measures the dispersion of values from their mean over a specified period.\n/// It is calculated by taking the square root of the variance.\n///\n/// # Mathematical Formula\n/// ```text\n/// STDDEV = sqrt(VAR)\n/// where:\n/// VAR = sum((x - mean)^2) / n\n/// mean = sum(x) / n\n/// ```\n/// Where:\n/// - x: Each value in the dataset\n/// - n: Time period\n///\n/// # Arguments\n/// * `input_prices` - Array of input values to calculate Standard Deviation\n/// * `opt_period` - The time period for calculation (must be >= 2)\n/// * `output_stddev` - Array to store calculated Standard Deviation values\n/// * `output_sum` - Array to store running sum values\n/// * `output_sum_sq` - Array to store running sum of squares values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok(()) on success, or error on failure\n///\n/// # Errors\n/// * Returns `KandError::InvalidData` if input array is empty\n/// * Returns `KandError::LengthMismatch` if output arrays don't match input length\n/// * Returns `KandError::InvalidParameter` if period is less than 2\n/// * Returns `KandError::InsufficientData` if input length <= lookback period\n/// * Returns `KandError::NaNDetected` if any input value is NaN (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::stats::stddev;\n/// let input = vec![1.0, 2.0, 3.0, 4.0, 5.0];\n/// let period = 3;\n/// let mut output_stddev = vec![0.0; 5];\n/// let mut output_sum = vec![0.0; 5];\n/// let mut output_sum_sq = vec![0.0; 5];\n///\n/// stddev::stddev(\n///     &input,\n///     period,\n///     &mut output_stddev,\n///     &mut output_sum,\n///     &mut output_sum_sq,\n/// )\n/// .unwrap();\n/// ```\npub fn stddev(\n    input_prices: &[TAFloat],\n    opt_period: usize,\n    output_stddev: &mut [TAFloat],\n    output_sum: &mut [TAFloat],\n    output_sum_sq: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_prices.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n        if output_stddev.len() != len || output_sum.len() != len || output_sum_sq.len() != len {\n            return Err(KandError::LengthMismatch);\n        }\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        for price in input_prices {\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate variance first\n    var::var(\n        input_prices,\n        opt_period,\n        output_stddev,\n        output_sum,\n        output_sum_sq,\n    )?;\n\n    // Take square root to get standard deviation\n    for value in output_stddev.iter_mut().take(len).skip(lookback) {\n        *value = value.sqrt();\n    }\n\n    Ok(())\n}\n\n/// Calculates the latest Standard Deviation value incrementally.\n///\n/// This function provides an optimized way to calculate the latest Standard Deviation value\n/// by using the previous sum and sum of squares values, avoiding recalculation of the entire series.\n///\n/// # Arguments\n/// * `input_price` - The latest price value to include in calculation\n/// * `prev_sum` - Previous sum of values in the period\n/// * `prev_sum_sq` - Previous sum of squared values in the period\n/// * `input_old_price` - Price value to remove from the period\n/// * `opt_period` - The time period for calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat), KandError>` - Tuple containing:\n///   - Latest Standard Deviation value\n///   - New sum\n///   - New sum of squares\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if period is less than 2\n/// * Returns `KandError::NaNDetected` if any input value is NaN (when `check-nan` enabled)\n///\n/// # Example\n/// ```\n/// use kand::stats::stddev;\n/// let (stddev, new_sum, new_sum_sq) = stddev::stddev_inc(\n///     10.0,   // new price\n///     100.0,  // previous sum\n///     1050.0, // previous sum of squares\n///     8.0,    // old price to remove\n///     14,     // period\n/// )\n/// .unwrap();\n/// ```\npub fn stddev_inc(\n    input_price: TAFloat,\n    prev_sum: TAFloat,\n    prev_sum_sq: TAFloat,\n    input_old_price: TAFloat,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat), KandError> {\n    let (var, new_sum, new_sum_sq) = var::var_inc(\n        input_price,\n        prev_sum,\n        prev_sum_sq,\n        input_old_price,\n        opt_period,\n    )?;\n\n    Ok((var.sqrt(), new_sum, new_sum_sq))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_stddev_calculation() {\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0,\n        ];\n        let opt_period = 14;\n        let mut output_stddev = vec![0.0; input_close.len()];\n        let mut output_sum = vec![0.0; input_close.len()];\n        let mut output_sum_sq = vec![0.0; input_close.len()];\n\n        stddev(\n            &input_close,\n            opt_period,\n            &mut output_stddev,\n            &mut output_sum,\n            &mut output_sum_sq,\n        )\n        .unwrap();\n\n        // First 13 values should be NaN\n        for i in 0..13 {\n            assert!(output_stddev[i].is_nan());\n            assert!(output_sum[i].is_nan());\n            assert!(output_sum_sq[i].is_nan());\n        }\n\n        // Compare with known values from CSV file\n        let expected_values = [\n            28.040_929_452_086_566,\n            40.126_741_172_470_275,\n            55.432_097_183_551_12,\n            72.497_236_047_872_85,\n            82.691_165_345_452_96,\n            85.326_748_921_912_07,\n            85.513_580_494_380_46,\n            96.234_748_328_008_62,\n            96.370_323_146_837_6,\n            94.344_639_592_305_9,\n            89.879_298_030_442_56,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_stddev[i + 13], *expected, epsilon = 0.0001);\n        }\n\n        // Test incremental calculation\n        let mut prev_sum = output_sum[13];\n        let mut prev_sum_sq = output_sum_sq[13];\n\n        // Test each incremental step\n        for i in 14..19 {\n            let (stddev, new_sum, new_sum_sq) = stddev_inc(\n                input_close[i],\n                prev_sum,\n                prev_sum_sq,\n                input_close[i - opt_period],\n                opt_period,\n            )\n            .unwrap();\n            assert_relative_eq!(stddev, output_stddev[i], epsilon = 0.0001);\n            prev_sum = new_sum;\n            prev_sum_sq = new_sum_sq;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/stats/sum.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Calculates the lookback period required for Sum calculation.\n///\n/// The lookback period represents the number of data points needed before the first valid output\n/// can be calculated. For Sum, this equals the period minus 1.\n///\n/// # Arguments\n/// * `opt_period` - The time period for Sum calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period (period - 1) on success, or error on failure\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if period is less than 2\n///\n/// # Example\n/// ```\n/// use kand::stats::sum;\n/// let period = 14;\n/// let lookback = sum::lookback(period).unwrap();\n/// assert_eq!(lookback, 13); // lookback is period - 1\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Calculates the Sum indicator for a price series.\n///\n/// The Sum indicator represents the rolling sum of values over a specified period.\n///\n/// # Mathematical Formula\n/// ```text\n/// SUM[i] = Price[i] + Price[i-1] + ... + Price[i-period+1]\n/// ```\n/// Where:\n/// - SUM\\[i\\] is the sum value at position i\n/// - Price\\[i\\] represents the input price at position i\n/// - period is the calculation timeframe\n///\n/// # Calculation Principle\n/// 1. Calculate initial sum for the first period\n/// 2. For subsequent periods, add new price and subtract oldest price\n/// 3. Fill initial values before lookback period with NaN\n///\n/// # Arguments\n/// * `input_prices` - Slice of input price values\n/// * `opt_period` - The time period for Sum calculation (must be >= 2)\n/// * `output_sum` - Mutable slice to store calculated Sum values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok(()) on success, or error on failure\n///\n/// # Errors\n/// * Returns `KandError::InvalidData` if input data is empty\n/// * Returns `KandError::LengthMismatch` if output length doesn't match input\n/// * Returns `KandError::InvalidParameter` if period is less than 2\n/// * Returns `KandError::InsufficientData` if input length is less than period\n/// * Returns `KandError::NaNDetected` if input contains NaN values (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::stats::sum;\n/// let input = vec![1.0, 2.0, 3.0, 4.0, 5.0];\n/// let period = 3;\n/// let mut output = vec![0.0; 5];\n///\n/// sum::sum(&input, period, &mut output).unwrap();\n/// // output = [NaN, NaN, 6.0, 9.0, 12.0]\n/// ```\npub fn sum(\n    input_prices: &[TAFloat],\n    opt_period: usize,\n    output_sum: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_prices.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Length consistency check\n        if output_sum.len() != len {\n            return Err(KandError::LengthMismatch);\n        }\n\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        for price in input_prices {\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate initial sum\n    let mut sum_val = 0.0;\n    for price in input_prices.iter().take(opt_period) {\n        sum_val += *price;\n    }\n    output_sum[lookback] = sum_val;\n\n    // Calculate subsequent sums incrementally\n    for i in opt_period..len {\n        sum_val = sum_val + input_prices[i] - input_prices[i - opt_period];\n        output_sum[i] = sum_val;\n    }\n\n    // Fill initial values with NAN\n    for value in output_sum.iter_mut().take(lookback) {\n        *value = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the latest Sum value using incremental calculation.\n///\n/// This function provides an optimized way to update the Sum value when new data arrives,\n/// avoiding full recalculation of the entire series.\n///\n/// # Arguments\n/// * `input_new_price` - The newest price value to add to the sum\n/// * `input_old_price` - The oldest price value to remove from the sum\n/// * `prev_sum` - The previous sum value\n///\n/// # Returns\n/// * `Result<TAFloat, KandError>` - The new sum value on success, or error on failure\n///\n/// # Errors\n/// * Returns `KandError::NaNDetected` if any input contains NaN (with \"`check-nan`\" feature)\n///\n/// # Example\n/// ```\n/// use kand::stats::sum;\n/// let prev_sum = 10.0;\n/// let new_price = 5.0;\n/// let old_price = 3.0;\n///\n/// let new_sum = sum::sum_inc(new_price, old_price, prev_sum).unwrap();\n/// assert_eq!(new_sum, 12.0); // 10.0 + 5.0 - 3.0\n/// ```\npub fn sum_inc(\n    input_new_price: TAFloat,\n    input_old_price: TAFloat,\n    prev_sum: TAFloat,\n) -> Result<TAFloat, KandError> {\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_new_price.is_nan() || input_old_price.is_nan() || prev_sum.is_nan() {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    Ok(prev_sum + input_new_price - input_old_price)\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_sum_calculation() {\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0,\n        ];\n        let opt_period = 14;\n        let mut output_sum = vec![0.0; input_close.len()];\n\n        sum(&input_close, opt_period, &mut output_sum).unwrap();\n\n        // First 13 values should be NaN\n        for value in output_sum.iter().take(13) {\n            assert!(value.is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            492_849.500_000_000_06,\n            492_723.700_000_000_07,\n            492_543.500_000_000_06,\n            492_352.100_000_000_03,\n            492_195.500_000_000_06,\n            492_083.000_000_000_06,\n            491_853.000_000_000_06,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_sum[i + 13], *expected, epsilon = 0.0001);\n        }\n\n        // Now test incremental calculation matches regular calculation\n        let mut prev_sum = output_sum[13]; // First valid sum value\n\n        // Test each incremental step\n        for i in 14..19 {\n            let result = sum_inc(input_close[i], input_close[i - opt_period], prev_sum).unwrap();\n            assert_relative_eq!(result, output_sum[i], epsilon = 0.0001);\n            prev_sum = result;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/stats/var.rs",
    "content": "use crate::{KandError, TAFloat};\n\n/// Calculates the lookback period required for Variance calculation.\n///\n/// # Description\n/// The lookback period represents the number of data points needed before the first valid output\n/// can be calculated. For Variance calculation, this equals the specified period minus one.\n///\n/// # Arguments\n/// * `opt_period` - The time period used for Variance calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<usize, KandError>` - The lookback period (period - 1) on success\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if `opt_period` is less than 2\n///\n/// # Example\n/// ```\n/// use kand::stats::var;\n/// let period = 14;\n/// let lookback = var::lookback(period).unwrap();\n/// assert_eq!(lookback, 13); // lookback is period - 1\n/// ```\npub const fn lookback(opt_period: usize) -> Result<usize, KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n    Ok(opt_period - 1)\n}\n\n/// Calculates Variance (VAR) for an entire price series.\n///\n/// # Description\n/// Variance measures the average squared deviation of data points from their mean over a specified period.\n/// It helps quantify the spread or dispersion of values in a dataset.\n///\n/// # Mathematical Formula\n/// ```text\n/// VAR = sum((x - mean)^2) / n\n/// ```\n/// Where:\n/// - x represents each value in the dataset\n/// - mean is the average of all values in the period (sum(x) / n)\n/// - n is the time period\n///\n/// # Calculation Steps\n/// 1. Calculate sum and sum of squares for initial period\n/// 2. Calculate mean for the period\n/// 3. Apply variance formula using sum and sum of squares\n/// 4. Update sums incrementally for subsequent periods\n///\n/// # Arguments\n/// * `input_prices` - Array of input price values\n/// * `opt_period` - The time period for Variance calculation (must be >= 2)\n/// * `output_var` - Array to store calculated Variance values\n/// * `output_sum` - Array to store running sum values\n/// * `output_sum_sq` - Array to store running sum of squares values\n///\n/// # Returns\n/// * `Result<(), KandError>` - Ok(()) on success\n///\n/// # Errors\n/// * Returns `KandError::InvalidData` if input array is empty\n/// * Returns `KandError::LengthMismatch` if output arrays don't match input length\n/// * Returns `KandError::InvalidParameter` if period is less than 2\n/// * Returns `KandError::InsufficientData` if input length is less than period\n/// * Returns `KandError::NaNDetected` if any input value is NaN (when \"`check-nan`\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::stats::var;\n/// let input_prices = vec![2.0, 4.0, 6.0, 8.0, 10.0];\n/// let period = 3;\n/// let mut output_var = vec![0.0; 5];\n/// let mut output_sum = vec![0.0; 5];\n/// let mut output_sum_sq = vec![0.0; 5];\n///\n/// var::var(\n///     &input_prices,\n///     period,\n///     &mut output_var,\n///     &mut output_sum,\n///     &mut output_sum_sq,\n/// )\n/// .unwrap();\n/// // First (period-1) values are NaN, followed by calculated Variance values\n/// ```\npub fn var(\n    input_prices: &[TAFloat],\n    opt_period: usize,\n    output_var: &mut [TAFloat],\n    output_sum: &mut [TAFloat],\n    output_sum_sq: &mut [TAFloat],\n) -> Result<(), KandError> {\n    let len = input_prices.len();\n    let lookback = lookback(opt_period)?;\n\n    #[cfg(feature = \"check\")]\n    {\n        // Empty data check\n        if len == 0 {\n            return Err(KandError::InvalidData);\n        }\n\n        // Length consistency check\n        if output_var.len() != len || output_sum.len() != len || output_sum_sq.len() != len {\n            return Err(KandError::LengthMismatch);\n        }\n\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n\n        // Data sufficiency check\n        if len <= lookback {\n            return Err(KandError::InsufficientData);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        for price in input_prices {\n            if price.is_nan() {\n                return Err(KandError::NaNDetected);\n            }\n        }\n    }\n\n    // Calculate initial values\n    let mut sum = 0.0;\n    let mut sum_sq = 0.0;\n    for val in input_prices.iter().take(opt_period) {\n        sum += *val;\n        sum_sq += *val * *val;\n    }\n\n    let period_t = opt_period as TAFloat;\n    let mean = sum / period_t;\n    output_var[lookback] = sum.mul_add(-mean, sum_sq) / period_t;\n    output_sum[lookback] = sum;\n    output_sum_sq[lookback] = sum_sq;\n\n    // Calculate remaining VAR values incrementally\n    for i in opt_period..len {\n        let old_val = input_prices[i - opt_period];\n        let new_val = input_prices[i];\n\n        sum = sum - old_val + new_val;\n        sum_sq = new_val.mul_add(new_val, old_val.mul_add(-old_val, sum_sq));\n\n        let mean = sum / period_t;\n        output_var[i] = sum.mul_add(-mean, sum_sq) / period_t;\n        output_sum[i] = sum;\n        output_sum_sq[i] = sum_sq;\n    }\n\n    // Fill initial values with NAN\n    for i in 0..lookback {\n        output_var[i] = TAFloat::NAN;\n        output_sum[i] = TAFloat::NAN;\n        output_sum_sq[i] = TAFloat::NAN;\n    }\n\n    Ok(())\n}\n\n/// Calculates the latest Variance value using incremental computation.\n///\n/// # Description\n/// This function efficiently updates the Variance by using the previous sum and sum of squares values,\n/// removing the oldest value and adding the newest value to the calculation window.\n///\n/// # Arguments\n/// * `input_price` - The newest price value to include in calculation\n/// * `prev_sum` - Previous sum of values in the period\n/// * `prev_sum_sq` - Previous sum of squared values in the period\n/// * `input_old_price` - Oldest price value to remove from calculation\n/// * `opt_period` - The time period for Variance calculation (must be >= 2)\n///\n/// # Returns\n/// * `Result<(TAFloat, TAFloat, TAFloat), KandError>` - Tuple containing (variance, `new_sum`, `new_sum_sq`) on success\n///\n/// # Errors\n/// * Returns `KandError::InvalidParameter` if period is less than 2\n/// * Returns `KandError::NaNDetected` if any input value is NaN (when \"`check-nan`\" feature is enabled)\n///\n/// # Example\n/// ```\n/// use kand::stats::var;\n/// let (var_value, new_sum, new_sum_sq) = var::var_inc(\n///     10.0,  // new price\n///     25.0,  // previous sum\n///     220.0, // previous sum of squares\n///     5.0,   // price to remove\n///     3,     // period\n/// )\n/// .unwrap();\n/// ```\npub fn var_inc(\n    input_price: TAFloat,\n    prev_sum: TAFloat,\n    prev_sum_sq: TAFloat,\n    input_old_price: TAFloat,\n    opt_period: usize,\n) -> Result<(TAFloat, TAFloat, TAFloat), KandError> {\n    #[cfg(feature = \"check\")]\n    {\n        // Parameter range check\n        if opt_period < 2 {\n            return Err(KandError::InvalidParameter);\n        }\n    }\n\n    #[cfg(feature = \"check-nan\")]\n    {\n        // NaN check\n        if input_price.is_nan()\n            || prev_sum.is_nan()\n            || prev_sum_sq.is_nan()\n            || input_old_price.is_nan()\n        {\n            return Err(KandError::NaNDetected);\n        }\n    }\n\n    let new_sum = prev_sum - input_old_price + input_price;\n    let new_sum_sq = input_price.mul_add(\n        input_price,\n        input_old_price.mul_add(-input_old_price, prev_sum_sq),\n    );\n\n    let period_t = opt_period as TAFloat;\n    let mean = new_sum / period_t;\n    let var = new_sum.mul_add(-mean, new_sum_sq) / period_t;\n\n    Ok((var, new_sum, new_sum_sq))\n}\n\n#[cfg(test)]\nmod tests {\n    use approx::assert_relative_eq;\n\n    use super::*;\n\n    #[test]\n    fn test_var_calculation() {\n        let input_close = vec![\n            35216.1, 35221.4, 35190.7, 35170.0, 35181.5, 35254.6, 35202.8, 35251.9, 35197.6,\n            35184.7, 35175.1, 35229.9, 35212.5, 35160.7, 35090.3, 35041.2, 34999.3, 35013.4,\n            35069.0, 35024.6, 34939.5, 34952.6, 35000.0, 35041.8, 35080.0,\n        ];\n        let opt_period = 14;\n        let mut output_var = vec![0.0; input_close.len()];\n        let mut output_sum = vec![0.0; input_close.len()];\n        let mut output_sum_sq = vec![0.0; input_close.len()];\n\n        var(\n            &input_close,\n            opt_period,\n            &mut output_var,\n            &mut output_sum,\n            &mut output_sum_sq,\n        )\n        .unwrap();\n\n        // First 13 values should be NaN\n        for i in 0..13 {\n            assert!(output_var[i].is_nan());\n            assert!(output_sum[i].is_nan());\n            assert!(output_sum_sq[i].is_nan());\n        }\n\n        // Compare with known values\n        let expected_values = [\n            786.293_724_536_895_8,\n            1_610.155_357_122_421_3,\n            3_072.717_398_166_656_5,\n            5_255.849_234_580_994,\n            6_837.828_826_189_041,\n            7_280.654_081_583_023,\n            7_312.572_448_968_887,\n            9_261.126_785_755_157,\n            9_287.239_183_425_903,\n            8_900.911_019_802_094,\n            8_078.288_214_445_114,\n        ];\n\n        for (i, expected) in expected_values.iter().enumerate() {\n            assert_relative_eq!(output_var[i + 13], *expected, epsilon = 0.0001);\n        }\n\n        // Now test incremental calculation matches regular calculation\n        let mut prev_sum = output_sum[13];\n        let mut prev_sum_sq = output_sum_sq[13];\n\n        // Test each incremental step\n        for i in 14..19 {\n            let (var, new_sum, new_sum_sq) = var_inc(\n                input_close[i],\n                prev_sum,\n                prev_sum_sq,\n                input_close[i - opt_period],\n                opt_period,\n            )\n            .unwrap();\n            assert_relative_eq!(var, output_var[i], epsilon = 0.0001);\n            assert_relative_eq!(new_sum, output_sum[i], epsilon = 0.0001);\n            assert_relative_eq!(new_sum_sq, output_sum_sq[i], epsilon = 0.0001);\n            prev_sum = new_sum;\n            prev_sum_sq = new_sum_sq;\n        }\n    }\n}\n"
  },
  {
    "path": "kand/src/ta/stats/winrate.rs",
    "content": ""
  },
  {
    "path": "kand/src/ta/types.rs",
    "content": "use num_enum::{IntoPrimitive, TryFromPrimitive};\n\n/// Moving Average types for technical analysis.\n///\n/// The integer representation of this enum is determined by the enabled features:\n/// - With feature \"i64\": Uses i64 representation (extended precision)\n/// - With feature \"i32\": Uses i32 representation (standard precision)\n/// - With no features enabled: Defaults to i32\n///\n/// # Variants\n///\n/// * `DEMA` - Double Exponential Moving Average\n/// * `EMA` - Exponential Moving Average\n/// * `KAMA` - Kaufman Adaptive Moving Average\n/// * `MAMA` - MESA Adaptive Moving Average\n/// * `RMA` - Wilder's Smoothed Moving Average\n/// * `SMA` - Simple Moving Average\n/// * `T3` - Triple Exponential Moving Average (T3)\n/// * `TEMA` - Triple Exponential Moving Average\n/// * `TRIMA` - Triangular Moving Average\n/// * `WMA` - Weighted Moving Average\n#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]\n#[cfg(feature = \"i64\")]\n#[repr(i64)]\npub enum MAType {\n    DEMA = 0,\n    EMA = 1,\n    KAMA = 2,\n    MAMA = 3,\n    RMA = 4,\n    SMA = 5,\n    T3 = 6,\n    TEMA = 7,\n    TRIMA = 8,\n    WMA = 9,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]\n#[cfg(not(feature = \"i64\"))]\n#[repr(i32)]\npub enum MAType {\n    DEMA = 0,\n    EMA = 1,\n    KAMA = 2,\n    MAMA = 3,\n    RMA = 4,\n    SMA = 5,\n    T3 = 6,\n    TEMA = 7,\n    TRIMA = 8,\n    WMA = 9,\n}\n\nimpl Default for MAType {\n    /// Returns the default Moving Average type (SMA).\n    ///\n    /// # Returns\n    ///\n    /// * [`MAType::SMA`] - Simple Moving Average, chosen for its:\n    ///     - Widespread use and familiarity\n    ///     - Computational simplicity\n    ///     - Reliable baseline performance\n    fn default() -> Self {\n        Self::SMA\n    }\n}\n\n/// Standard signal values for technical indicators.\n///\n/// The integer representation of this enum is determined by the enabled features:\n/// - With feature \"i64\": Uses i64 representation (extended precision)\n/// - With feature \"i32\": Uses i32 representation (standard precision)\n/// - With no features enabled: Defaults to i32\n///\n/// Signal values are designed to be consistent across precision modes:\n/// * `Bullish` (+100): Strong upward potential\n/// * `Balance` (+50): Moderate upward bias\n/// * `Bearish` (-100): Strong downward potential\n/// * `Neutral` (0): No clear directional bias\n/// * `Pattern` (+1): Technical pattern detected\n/// * `Invalid` (-1): Invalid or error state\n#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]\n#[cfg(feature = \"i64\")]\n#[repr(i64)]\npub enum Signal {\n    Bullish = 100,\n    Balance = 50,\n    Bearish = -100,\n    Neutral = 0,\n    Pattern = 1,\n    Invalid = -1,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]\n#[cfg(not(feature = \"i64\"))]\n#[repr(i32)]\npub enum Signal {\n    Bullish = 100,\n    Balance = 50,\n    Bearish = -100,\n    Neutral = 0,\n    Pattern = 1,\n    Invalid = -1,\n}\n\nimpl Default for Signal {\n    /// Returns the default signal value (Neutral).\n    ///\n    /// # Returns\n    /// * [`Signal::Neutral`] - Represents no directional bias (0)\n    fn default() -> Self {\n        Self::Neutral\n    }\n}\n"
  },
  {
    "path": "kand-py/.gitignore",
    "content": "/target\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n.pytest_cache/\n*.py[cod]\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\n.venv/\nenv/\nbin/\nbuild/\ndevelop-eggs/\ndist/\neggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\ninclude/\nman/\nvenv/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\npip-selfcheck.json\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.cache\nnosetests.xml\ncoverage.xml\n\n# Translations\n*.mo\n\n# Mr Developer\n.mr.developer.cfg\n.project\n.pydevproject\n\n# Rope\n.ropeproject\n\n# Django stuff:\n*.log\n*.pot\n\n.DS_Store\n\n# Sphinx documentation\ndocs/_build/\n\n# PyCharm\n.idea/\n\n# VSCode\n.vscode/\n\n# Pyenv\n.python-version\n"
  },
  {
    "path": "kand-py/Cargo.toml",
    "content": "[package]\nname = \"kand-py\"\nversion = \"0.2.2\"\nedition = \"2024\"\nreadme = \"../README.md\"\nauthors = [\"CtrlX <gitctrlx@gmail.com>\"]\ndescription = \"Kand: A Pure Rust technical analysis library inspired by TA-Lib.\"\nlicense = \"Apache-2.0 OR MIT\"\nrepository = \"https://github.com/rust-ta/kand\"\ndocumentation = \"https://docs.rs/kand\"\nkeywords = [\"technical-analysis\", \"finance\", \"rust\", \"talib\", \"indicators\"]\ncategories = [\"finance\", \"algorithms\", \"data-structures\", \"science\", \"mathematics\"]\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n[lib]\nname = \"_kand\"\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nnumpy = {workspace = true}\npyo3 = {workspace = true, features = [\"extension-module\"]}\nkand = { workspace = true}\n\n[features]\ndefault = [\"f64\", \"i64\", \"check\"]         # Default: extended precision with basic checks\nf32 = [\"kand/f32\"]                        # 32-bit floating point\nf64 = [\"kand/f64\"]                        # 64-bit floating point\ni32 = [\"kand/i32\"]                        # 32-bit integer\ni64 = [\"kand/i64\"]                        # 64-bit integer\ncheck = [\"kand/check\"]                    # Basic validation checks\ncheck-nan = [\"kand/check-nan\"]            # Input NaN checks\nallow-nan = [\"kand/allow-nan\"]            # Allow NaN values in output data\n"
  },
  {
    "path": "kand-py/python/benches/.gitkeep",
    "content": ""
  },
  {
    "path": "kand-py/python/benches/bench_ema.py",
    "content": "import numpy as np\nimport talib\nfrom kand import ema\nimport time\nimport matplotlib.pyplot as plt\n\n# Set modern, professional style for matplotlib\nplt.style.use('default')  # Reset to default and customize\nplt.rcParams['font.family'] = 'Arial'  # Use Arial for a clean, scientific look\nplt.rcParams['font.size'] = 12\nplt.rcParams['axes.linewidth'] = 0.8\nplt.rcParams['figure.facecolor'] = 'white'\nplt.rcParams['axes.facecolor'] = 'white'\n\ndef generate_test_data(size):\n    \"\"\"Generate random price data for testing.\"\"\"\n    return np.random.random(size) * 100\n\ndef test_batch_performance(data_sizes, period=3, runs=5):\n    \"\"\"Test and compare the batch computation performance between talib and kand.\"\"\"\n    talib_times = []\n    kand_times = []\n\n    for size in data_sizes:\n        data = generate_test_data(size)\n        talib.EMA(data, timeperiod=period)\n        ema(data, period=period)\n\n        talib_start = time.perf_counter()\n        for _ in range(runs):\n            talib_ema = talib.EMA(data, timeperiod=period)\n        talib_times.append((time.perf_counter() - talib_start) / runs)\n\n        kand_start = time.perf_counter()\n        for _ in range(runs):\n            kand_ema = ema(data, period=period)\n        kand_times.append((time.perf_counter() - kand_start) / runs)\n\n    return talib_times, kand_times\n\ndef plot_batch_results(data_sizes, talib_times, kand_times):\n    \"\"\"Create a professional grouped bar plot for performance comparison.\"\"\"\n    relative_diff = [(t - k) / k * 100 for t, k in zip(talib_times, kand_times)]\n\n    # Create figure with two subplots\n    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), height_ratios=[2, 1],\n                                   sharex=False)\n    fig.subplots_adjust(hspace=0.4, bottom=0.20)  # Increase bottom margin\n\n    # Determine scaling factor for time based on minimum time\n    min_time = min(min(talib_times), min(kand_times))\n    if min_time < 0.001:\n        scale = 1000\n        unit = 'ms'\n    else:\n        scale = 1\n        unit = 's'\n    scaled_talib_times = [t * scale for t in talib_times]\n    scaled_kand_times = [k * scale for k in kand_times]\n\n    # Plot 1: Grouped Bar Plot for Absolute Times\n    bar_width = 0.35\n    index = np.arange(len(data_sizes))\n\n    bars1 = ax1.bar(index - bar_width/2, scaled_talib_times, bar_width,\n                    label='TA-Lib EMA', color='#1f77b4', edgecolor='black', linewidth=0.5)\n    bars2 = ax1.bar(index + bar_width/2, scaled_kand_times, bar_width,\n                    label='Kand EMA', color='#ff7f0e', edgecolor='black', linewidth=0.5)\n\n    # Dynamically set precision based on scaled time values\n    max_time = max(max(scaled_talib_times), max(scaled_kand_times))\n    if max_time < 1:\n        precision = 5\n    elif max_time < 10:\n        precision = 4\n    else:\n        precision = 3\n\n    # Add time labels above bars with slight offset\n    for bars in [bars1, bars2]:\n        for bar in bars:\n            height = bar.get_height()\n            ax1.text(bar.get_x() + bar.get_width()/2., height + 0.001 * scale,\n                     f'{height:.{precision}f}', ha='center', va='bottom', fontsize=9)\n\n    # Customize Plot 1\n    ax1.set_ylabel(f'Time ({unit})', fontsize=12)\n    ax1.set_title('EMA Computation Time', fontsize=14, pad=10)\n    ax1.set_xticks(index)\n    ax1.set_xticklabels([f'{x:,}' for x in data_sizes], rotation=45, ha='right')\n    ax1.grid(True, linestyle='--', alpha=0.3, which='both')\n    ax1.legend(loc='upper left', fontsize=10, frameon=False)\n    ax1.spines['top'].set_visible(False)\n    ax1.spines['right'].set_visible(False)\n    ax1.spines['left'].set_color('gray')\n    ax1.spines['bottom'].set_color('gray')\n\n    # Plot 2: Relative Performance Difference\n    bars = ax2.bar(index, relative_diff, bar_width * 1.2,\n                   color='#2ca02c', edgecolor='black', linewidth=0.5)\n\n    # Add percentage labels with dynamic positioning\n    for bar in bars:\n        height = bar.get_height()\n        label_y = height + 1 if height >= 0 else height - 1  # Dynamic offset\n        ax2.text(bar.get_x() + bar.get_width()/2., label_y,\n                 f'{height:.1f}%', ha='center', va='bottom' if height >= 0 else 'top', fontsize=9)\n\n    # Customize Plot 2\n    ax2.set_xlabel('Data Size', fontsize=12)\n    ax2.set_ylabel('Overhead (%)', fontsize=12)\n    ax2.set_xticks(index)\n    ax2.set_xticklabels([f'{x:,}' for x in data_sizes], rotation=45, ha='right')\n    ax2.grid(True, linestyle='--', alpha=0.3, which='both')\n    ax2.spines['top'].set_visible(False)\n    ax2.spines['right'].set_visible(False)\n    ax2.spines['left'].set_color('gray')\n    ax2.spines['bottom'].set_color('gray')\n\n    # Adjust layout and save\n    plt.tight_layout()\n\n    plt.savefig('batch_ema_performance.png', dpi=600, bbox_inches='tight',\n                facecolor='white', edgecolor='none',\n                pil_kwargs={'height': 1920, 'width': 2400})\n\n    # plt.show()\n\ndef main():\n    \"\"\"Execute the main benchmark suite.\"\"\"\n    data_sizes = [\n        50000,\n        100000,\n        250000,\n        500000,\n        1000000,\n        2500000,\n        5000000,\n        10000000\n    ]\n    period = 30\n    runs = 1000\n\n    print(\"\\nRunning performance tests...\")\n    talib_times, kand_times = test_batch_performance(data_sizes, period, runs)\n    plot_batch_results(data_sizes, talib_times, kand_times)\n\n    print(\"\\nBatch Computation Results:\")\n    print(\"-\" * 60)\n    print(\"   Data Size | TA-Lib (s) |   Kand (s) |  Speedup\")\n    print(\"-\" * 60)\n\n    for size, t_time, k_time in zip(data_sizes, talib_times, kand_times):\n        speedup = t_time / k_time\n        print(f\"{size:10,d} | {t_time:9.6f} | {k_time:9.6f} | {speedup:7.2f}x\")\n\n    print(\"-\" * 60)\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "kand-py/python/benches/bench_ema_incremental.py",
    "content": ""
  },
  {
    "path": "kand-py/python/benches/bench_ema_incremental_mt.py",
    "content": ""
  },
  {
    "path": "kand-py/python/benches/bench_ema_mt.py",
    "content": "import numpy as np\nimport talib\nfrom kand import ema\nimport time\nimport matplotlib.pyplot as plt\nfrom concurrent.futures import ThreadPoolExecutor\nimport os\n\n# Set modern, professional style for matplotlib\nplt.style.use('default')\nplt.rcParams['font.family'] = 'Arial'\nplt.rcParams['font.size'] = 12\nplt.rcParams['axes.linewidth'] = 0.8\nplt.rcParams['figure.facecolor'] = 'white'\nplt.rcParams['axes.facecolor'] = 'white'\n\ndef generate_test_data(size):\n    \"\"\"Generate random price data for testing.\"\"\"\n    return np.random.random(size) * 100\n\ndef run_ema(data, period, runs_per_thread, func):\n    \"\"\"Helper function to run EMA in a thread.\"\"\"\n    start = time.perf_counter()\n    for _ in range(runs_per_thread):\n        if func.__module__ == 'talib._ta_lib':  # Check if it's talib.EMA\n            func(data, timeperiod=period)\n        else:  # Assume kand.ema\n            func(data, period=period)\n    return time.perf_counter() - start\n\ndef test_batch_performance(data_sizes, period=3, runs=5, num_threads=None):\n    \"\"\"Test and compare the batch computation performance between talib and kand using multithreading.\"\"\"\n    if num_threads is None:\n        num_threads = os.cpu_count() or 1\n\n    talib_times = []\n    kand_times = []\n\n    for size in data_sizes:\n        data = generate_test_data(size)\n\n        # Warm up\n        talib.EMA(data, timeperiod=period)\n        ema(data, period=period)\n\n        runs_per_thread = max(1, runs // num_threads)\n        total_runs = runs_per_thread * num_threads\n\n        # Test talib EMA with multithreading\n        with ThreadPoolExecutor(max_workers=num_threads) as executor:\n            futures = [executor.submit(run_ema, data, period, runs_per_thread, talib.EMA)\n                      for _ in range(num_threads)]\n            talib_thread_times = [f.result() for f in futures]\n            talib_total_time = sum(talib_thread_times)\n            talib_times.append(talib_total_time / total_runs)\n\n        # Test kand EMA with multithreading\n        with ThreadPoolExecutor(max_workers=num_threads) as executor:\n            futures = [executor.submit(run_ema, data, period, runs_per_thread, ema)\n                      for _ in range(num_threads)]\n            kand_thread_times = [f.result() for f in futures]\n            kand_total_time = sum(kand_thread_times)\n            kand_times.append(kand_total_time / total_runs)\n\n    return talib_times, kand_times\n\ndef plot_batch_results(data_sizes, talib_times, kand_times):\n    \"\"\"Create a professional grouped bar plot for performance comparison.\"\"\"\n    relative_diff = [(t - k) / k * 100 for t, k in zip(talib_times, kand_times)]\n\n    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), height_ratios=[2, 1],\n                                   sharex=False)\n    fig.subplots_adjust(hspace=0.4, bottom=0.20)\n\n    min_time = min(min(talib_times), min(kand_times))\n    if min_time < 0.001:\n        scale = 1000\n        unit = 'ms'\n    else:\n        scale = 1\n        unit = 's'\n    scaled_talib_times = [t * scale for t in talib_times]\n    scaled_kand_times = [k * scale for k in kand_times]\n\n    bar_width = 0.35\n    index = np.arange(len(data_sizes))\n\n    bars1 = ax1.bar(index - bar_width/2, scaled_talib_times, bar_width,\n                    label='TA-Lib EMA', color='#1f77b4', edgecolor='black', linewidth=0.5)\n    bars2 = ax1.bar(index + bar_width/2, scaled_kand_times, bar_width,\n                    label='Kand EMA', color='#ff7f0e', edgecolor='black', linewidth=0.5)\n\n    max_time = max(max(scaled_talib_times), max(scaled_kand_times))\n    if max_time < 1:\n        precision = 5\n    elif max_time < 10:\n        precision = 4\n    else:\n        precision = 3\n\n    for bars in [bars1, bars2]:\n        for bar in bars:\n            height = bar.get_height()\n            ax1.text(bar.get_x() + bar.get_width()/2., height + 0.001 * scale,\n                     f'{height:.{precision}f}', ha='center', va='bottom', fontsize=9)\n\n    ax1.set_ylabel(f'Time ({unit})', fontsize=12)\n    ax1.set_title('EMA Computation Time', fontsize=14, pad=10)\n    ax1.set_xticks(index)\n    ax1.set_xticklabels([f'{x:,}' for x in data_sizes], rotation=45, ha='right')\n    ax1.grid(True, linestyle='--', alpha=0.3, which='both')\n    ax1.legend(loc='upper left', fontsize=10, frameon=False)\n    ax1.spines['top'].set_visible(False)\n    ax1.spines['right'].set_visible(False)\n    ax1.spines['left'].set_color('gray')\n    ax1.spines['bottom'].set_color('gray')\n\n    bars = ax2.bar(index, relative_diff, bar_width * 1.2,\n                   color='#2ca02c', edgecolor='black', linewidth=0.5)\n\n    for bar in bars:\n        height = bar.get_height()\n        label_y = height + 1 if height >= 0 else height - 1\n        ax2.text(bar.get_x() + bar.get_width()/2., label_y,\n                 f'{height:.1f}%', ha='center', va='bottom' if height >= 0 else 'top', fontsize=9)\n\n    ax2.set_xlabel('Data Size', fontsize=12)\n    ax2.set_ylabel('Overhead (%)', fontsize=12)\n    ax2.set_xticks(index)\n    ax2.set_xticklabels([f'{x:,}' for x in data_sizes], rotation=45, ha='right')\n    ax2.grid(True, linestyle='--', alpha=0.3, which='both')\n    ax2.spines['top'].set_visible(False)\n    ax2.spines['right'].set_visible(False)\n    ax2.spines['left'].set_color('gray')\n    ax2.spines['bottom'].set_color('gray')\n\n    plt.tight_layout()\n    plt.savefig('batch_ema_performance_mt.png', dpi=600, bbox_inches='tight',\n                facecolor='white', edgecolor='none',\n                pil_kwargs={'height': 1920, 'width': 2400})\n    # plt.show()\n\ndef main():\n    \"\"\"Execute the main benchmark suite with multithreading.\"\"\"\n    data_sizes = [\n        50000,\n        100000,\n        250000,\n        500000,\n        1000000,\n        2500000,\n        5000000,\n        10000000\n    ]\n    period = 30\n    runs = 1000\n    num_threads = 2  # Specify number of threads (adjust as needed)\n\n    print(f\"\\nRunning performance tests with {num_threads} threads...\")\n    talib_times, kand_times = test_batch_performance(data_sizes, period, runs, num_threads)\n    plot_batch_results(data_sizes, talib_times, kand_times)\n\n    print(\"\\nBatch Computation Results:\")\n    print(\"-\" * 60)\n    print(\"   Data Size | TA-Lib (s) |   Kand (s) |  Speedup\")\n    print(\"-\" * 60)\n\n    for size, t_time, k_time in zip(data_sizes, talib_times, kand_times):\n        speedup = t_time / k_time\n        print(f\"{size:10,d} | {t_time:9.6f} | {k_time:9.6f} | {speedup:7.2f}x\")\n\n    print(\"-\" * 60)\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "kand-py/python/benches/ema.py",
    "content": "import numpy as np\nimport pandas as pd\nimport talib\nimport kand\nimport time\nfrom typing import List, Dict\nimport matplotlib.pyplot as plt\n\ndef pure_python_ema(data: List[float], span: int) -> List[float]:\n    \"\"\"Calculates EMA using a pure Python implementation.\n\n    Args:\n        data: List of float values for EMA calculation.\n        span: Time period for the EMA.\n\n    Returns:\n        List of EMA values.\n    \"\"\"\n    alpha = 2 / (span + 1)\n    result = [data[0]]\n    for n in range(1, len(data)):\n        result.append(alpha * data[n] + (1 - alpha) * result[n - 1])\n    return result\n\ndef pandas_ema(data: pd.Series, span: int) -> pd.Series:\n    \"\"\"Calculates EMA using Pandas EWM (Exponential Weighted Moving Average).\n\n    Args:\n        data: Pandas Series of float values for EMA calculation.\n        span: Time period for the EMA.\n\n    Returns:\n        Pandas Series of EMA values.\n    \"\"\"\n    return data.ewm(span=span, adjust=False).mean()\n\ndef plot_performance_comparison(results: Dict[str, float]) -> plt.Figure:\n    \"\"\"Plots a horizontal bar chart comparing EMA implementation performance.\n\n    The chart is styled for academic publication with a clean, minimal design,\n    using orange bars, a grid, and concise labels.\n\n    Args:\n        results: Dictionary mapping implementation names to execution times (ms).\n\n    Returns:\n        Matplotlib Figure object.\n    \"\"\"\n    # Sort data by performance (shortest to longest time)\n    sorted_items = sorted(results.items(), key=lambda x: x[1])\n    categories = [item[0] for item in sorted_items]\n    values = [item[1] for item in sorted_items]\n\n    # Create figure with tight layout for publication-quality appearance\n    plt.figure(figsize=(6, 3), dpi=300)  # Smaller size but keep high DPI\n    bars = plt.barh(categories, values, color='#FF8C00',  # Soft orange for readability\n                    height=0.5,  # Thinner bars for minimalism\n                    edgecolor='black', linewidth=0.8)  # Slightly thicker outlines for clarity\n\n    # Set limits and remove unnecessary padding\n    plt.xlim(0, max(values) * 1.05)\n    plt.grid(axis='x', linestyle=':', alpha=0.3, color='gray')  # Light, subtle grid\n\n    # Format x-axis ticks with 'ms' unit\n    ax = plt.gca()\n    ax.xaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'{x:.0f} ms'))\n\n    # Add value labels with adjusted font size\n    for bar in bars:\n        width = bar.get_width()\n        plt.text(width, bar.get_y() + bar.get_height() / 2,\n                 f'{width:.1f} ms', ha='left', va='center',\n                 fontsize=8, color='black', fontweight='normal')\n\n    # Customize labels and title for clarity and minimalism\n    plt.title('EMA Performance Comparison', fontsize=10, pad=10)\n    plt.xlabel('Execution Time (ms)', fontsize=9)\n\n    # Remove top and right spines, adjust tick parameters for simplicity\n    plt.gca().spines['top'].set_visible(False)\n    plt.gca().spines['right'].set_visible(False)\n    plt.tick_params(axis='both', which='both', length=0, labelsize=8)\n\n    # Ensure tight layout for publication\n    plt.tight_layout(pad=0.2)\n    return plt\n\ndef benchmark_ema(size: int = 10_000_000) -> Dict[str, float]:\n    \"\"\"Benchmarks the performance of different EMA implementations.\n\n    Generates random data and measures execution time for each implementation\n    in milliseconds.\n\n    Args:\n        size: Number of data points to generate for testing (default: 10,000,000).\n\n    Returns:\n        Dictionary mapping implementation names to execution times (ms).\n    \"\"\"\n    # Generate test data\n    print(f\"Generating {size:,} random data points...\")\n    data = np.random.random(size)\n    span = 10\n    results = {}\n\n    # Benchmark pure Python implementation\n    print(\"\\nBenchmarking pure Python implementation...\")\n    start = time.perf_counter()\n    pure_python_result = pure_python_ema(data.tolist(), span)\n    results['pure python'] = (time.perf_counter() - start) * 1000\n    print(f\"Pure Python time: {results['pure python']:.2f} ms\")\n\n    # Benchmark Pandas implementation\n    print(\"\\nBenchmarking Pandas implementation...\")\n    start = time.perf_counter()\n    pandas_result = pandas_ema(pd.Series(data), span)\n    results['pandas'] = (time.perf_counter() - start) * 1000\n    print(f\"Pandas time: {results['pandas']:.2f} ms\")\n\n    # Benchmark TA-Lib implementation\n    print(\"\\nBenchmarking TA-Lib implementation...\")\n    start = time.perf_counter()\n    talib_result = talib.EMA(data, timeperiod=span)\n    results['ta-lib'] = (time.perf_counter() - start) * 1000\n    print(f\"TA-Lib time: {results['ta-lib']:.2f} ms\")\n\n    # Benchmark Kand implementation\n    print(\"\\nBenchmarking Kand implementation...\")\n    start = time.perf_counter()\n    kand_result = kand.ema(data, span)\n    results['kand'] = (time.perf_counter() - start) * 1000\n    print(f\"Kand time: {results['kand']:.2f} ms\")\n\n    return results\n\nif __name__ == \"__main__\":\n    # Run performance benchmarks\n    results = benchmark_ema()\n\n    # Plot and display results\n    plt = plot_performance_comparison(results)\n    plt.show()\n"
  },
  {
    "path": "kand-py/python/examples/sma.py",
    "content": "import numpy as np\nfrom kand import sma\nimport time\n\ndata = np.array([float(i) for i in range(10_000_000)])\n\nstart_time = time.perf_counter()\nresult = sma(data, 10)\nend_time = time.perf_counter()\n\nprint(f\"Result: {result}\")\nprint(f\"Result type: {type(result)}\")\nprint(f\"Result dtype: {result.dtype}\")\nprint(f\"Execution time: {end_time - start_time:.4f} seconds\")\n\nprint(sma.__doc__)\n"
  },
  {
    "path": "kand-py/python/examples/sma_thread.py",
    "content": "import numpy as np\nimport time\nfrom concurrent.futures import ThreadPoolExecutor\nfrom kand import sma\n\n# Generate test data\ndata = np.array([float(i) for i in range(10_000_000)])  # 10 million data points\nperiod = 100\n\n# Single-threaded test\nstart_time = time.perf_counter()\n_ = sma(data, period)  # First call\n_ = sma(data, period)  # Second call\nsingle_thread_time = (time.perf_counter() - start_time) / 2\nprint(f\"Single-threaded execution time: {single_thread_time:.4f} seconds\")\n\n# Multi-threaded test (2 threads)\nwith ThreadPoolExecutor(max_workers=2) as executor:\n    start_time = time.perf_counter()\n    futures = [executor.submit(sma, data, period) for _ in range(2)]  # Two concurrent calls\n    _ = [future.result() for future in futures]\n    thread_pool_time = (time.perf_counter() - start_time) / 2\n\nprint(f\"Multi-threaded execution time: {thread_pool_time:.4f} seconds\")\nprint(f\"Speedup: {single_thread_time / thread_pool_time:.2f}x\")\n"
  },
  {
    "path": "kand-py/python/kand/__init__.py",
    "content": "from ._kand import *\n"
  },
  {
    "path": "kand-py/python/kand/_kand.pyi",
    "content": "# Auto-generated stub file for kand\n\"\"\"\nType hints and function signatures stub file for IDE autocompletion.\nAuto-generated to avoid manual maintenance. Can be enhanced with more precise type annotations.\n\"\"\"\n\ndef ad(high, low, close, volume):\n    \"\"\"\n    Computes the Accumulation/Distribution (A/D) indicator over NumPy arrays.\n\n    The A/D indicator measures the cumulative flow of money into and out of a security by\n    combining price and volume data. It helps identify whether buying or selling pressure\n    is dominant.\n\n    Args:\n        high: High prices as a 1-D NumPy array of type `TAFloat`.\n        low: Low prices as a 1-D NumPy array of type `TAFloat`.\n        close: Close prices as a 1-D NumPy array of type `TAFloat`.\n        volume: Volume data as a 1-D NumPy array of type `TAFloat`.\n\n    Returns:\n        A new 1-D NumPy array containing the A/D values. The array has the same length as the inputs.\n    Examples:\n        ```python\n        >>> import numpy as np\n        >>> import kand\n        >>> high = np.array([10.0, 12.0, 15.0])\n        >>> low = np.array([8.0, 9.0, 11.0])\n        >>> close = np.array([9.0, 11.0, 13.0])\n        >>> volume = np.array([100.0, 150.0, 200.0])\n        >>> result = kand.ad(high, low, close, volume)\n        >>> print(result)\n        [-50.0, 25.0, 125.0]\n        ```\n    \"\"\"\n    ...\n\ndef ad_inc(high, low, close, volume, prev_ad):\n    \"\"\"\n    Computes the latest Accumulation/Distribution (A/D) value incrementally.\n\n    This function calculates only the latest A/D value using the previous A/D value,\n    avoiding recalculation of the entire series.\n\n    Args:\n        high: Latest high price.\n        low: Latest low price.\n        close: Latest closing price.\n        volume: Latest volume.\n        prev_ad: Previous A/D value.\n\n    Returns:\n        The latest A/D value.\n\n    Examples:\n        ```python\n        >>> import kand\n        >>> high = 15.0\n        >>> low = 11.0\n        >>> close = 13.0\n        >>> volume = 200.0\n        >>> prev_ad = 25.0\n        >>> result = kand.ad_inc(high, low, close, volume, prev_ad)\n        >>> print(result)\n        125.0\n        ```\n    \"\"\"\n    ...\n\ndef adosc(high, low, close, volume, fast_period, slow_period):\n    \"\"\"\n    Calculate Accumulation/Distribution Oscillator (A/D Oscillator or ADOSC)\n\n    The A/D Oscillator is a momentum indicator that measures the difference between a fast and slow EMA of the\n    Accumulation/Distribution Line. It helps identify trend strength and potential reversals.\n\n    Args:\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      volume: Volume as a 1-D NumPy array of type `TAFloat`.\n      fast_period: Fast period for A/D Oscillator calculation.\n      slow_period: Slow period for A/D Oscillator calculation.\n\n    Returns:\n      A tuple of 4 1-D NumPy arrays containing:\n      - ADOSC values\n      - A/D Line values\n      - Fast EMA values\n      - Slow EMA values\n      Each array has the same length as the input, with the first `slow_period-1` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([10.0, 11.0, 12.0, 11.5, 10.5])\n      >>> low = np.array([8.0, 9.0, 10.0, 9.5, 8.5])\n      >>> close = np.array([9.0, 10.0, 11.0, 10.0, 9.0])\n      >>> volume = np.array([100.0, 150.0, 200.0, 150.0, 100.0])\n      >>> adosc, ad, fast_ema, slow_ema = kand.adosc(high, low, close, volume, 3, 5)\n      ```\n    \"\"\"\n    ...\n\ndef adosc_inc(high, low, close, volume, prev_ad, prev_fast_ema, prev_slow_ema, fast_period, slow_period):\n    \"\"\"\n    Calculate latest A/D Oscillator value incrementally\n\n    Provides optimized calculation of the latest ADOSC value when new data arrives,\n    without recalculating the entire series.\n\n    Args:\n        high: Latest high price.\n        low: Latest low price.\n        close: Latest closing price.\n        volume: Latest volume.\n        prev_ad: Previous A/D value.\n        prev_fast_ema: Previous fast EMA value.\n        prev_slow_ema: Previous slow EMA value.\n        fast_period: Fast EMA period.\n        slow_period: Slow EMA period.\n\n    Returns:\n        A tuple containing (ADOSC, AD, Fast EMA, Slow EMA) values.\n\n    Examples:\n        ```python\n        >>> import kand\n        >>> adosc, ad, fast_ema, slow_ema = kand.adosc_inc(\n        ...     10.5,  # high\n        ...     9.5,   # low\n        ...     10.0,  # close\n        ...     150.0, # volume\n        ...     100.0, # prev_ad\n        ...     95.0,  # prev_fast_ema\n        ...     90.0,  # prev_slow_ema\n        ...     3,     # fast_period\n        ...     10,    # slow_period\n        ... )\n        ```\n    \"\"\"\n    ...\n\ndef adx(high, low, close, period):\n    \"\"\"\n    Calculate Average Directional Index (ADX) for a NumPy array\n\n    The ADX (Average Directional Index) measures the strength of a trend, regardless of whether it's up or down.\n    Values range from 0 to 100, with higher values indicating stronger trends.\n\n    Args:\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      period: Period for ADX calculation (typically 14). Must be positive.\n\n    Returns:\n      A tuple of four 1-D NumPy arrays containing:\n      - ADX values\n      - Smoothed +DM values\n      - Smoothed -DM values\n      - Smoothed TR values\n      Each array has the same length as the input, with the first (2*period-1) elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([24.20, 24.07, 24.04, 23.87, 23.67])\n      >>> low = np.array([23.85, 23.72, 23.64, 23.37, 23.46])\n      >>> close = np.array([23.89, 23.95, 23.67, 23.78, 23.50])\n      >>> adx, plus_dm, minus_dm, tr = kand.adx(high, low, close, 2)\n      ```\n    \"\"\"\n    ...\n\ndef adx_inc(high, low, prev_high, prev_low, prev_close, prev_adx, prev_smoothed_plus_dm, prev_smoothed_minus_dm, prev_smoothed_tr, period):\n    \"\"\"\n    Calculate the latest ADX value incrementally\n\n    Args:\n      py: Python interpreter token\n      high: Current period's high price\n      low: Current period's low price\n      prev_high: Previous period's high price\n      prev_low: Previous period's low price\n      prev_close: Previous period's close price\n      prev_adx: Previous period's ADX value\n      prev_smoothed_plus_dm: Previous period's smoothed +DM\n      prev_smoothed_minus_dm: Previous period's smoothed -DM\n      prev_smoothed_tr: Previous period's smoothed TR\n      period: Period for ADX calculation (typically 14)\n\n    Returns:\n      A tuple containing:\n      - Latest ADX value\n      - New smoothed +DM\n      - New smoothed -DM\n      - New smoothed TR\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> adx, plus_dm, minus_dm, tr = kand.adx_inc(\n      ...     24.20,  # current high\n      ...     23.85,  # current low\n      ...     24.07,  # previous high\n      ...     23.72,  # previous low\n      ...     23.95,  # previous close\n      ...     25.0,   # previous ADX\n      ...     0.5,    # previous smoothed +DM\n      ...     0.3,    # previous smoothed -DM\n      ...     1.2,    # previous smoothed TR\n      ...     14      # period\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef adxr(high, low, close, period):\n    \"\"\"\n    Calculate Average Directional Index Rating (ADXR) for a NumPy array.\n\n    ADXR is a momentum indicator that measures the strength of a trend by comparing\n    the current ADX value with the ADX value from `period` days ago.\n\n    Args:\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      period: Period for ADX calculation (typically 14).\n\n    Returns:\n      A tuple of 5 1-D NumPy arrays containing:\n      - ADXR values\n      - ADX values\n      - Smoothed +DM values\n      - Smoothed -DM values\n      - Smoothed TR values\n      The first (3*period-2) elements of each array contain NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([24.20, 24.07, 24.04, 23.87, 23.67])\n      >>> low = np.array([23.85, 23.72, 23.64, 23.37, 23.46])\n      >>> close = np.array([23.89, 23.95, 23.67, 23.78, 23.50])\n      >>> adxr, adx, plus_dm, minus_dm, tr = kand.adxr(high, low, close, 2)\n      ```\n    \"\"\"\n    ...\n\ndef adxr_inc(high, low, prev_high, prev_low, prev_close, prev_adx, prev_adx_period_ago, prev_smoothed_plus_dm, prev_smoothed_minus_dm, prev_smoothed_tr, period):\n    \"\"\"\n    Calculate the latest ADXR value incrementally\n\n    Args:\n\n      high: Current high price as TAFloat.\n      low: Current low price as TAFloat.\n      prev_high: Previous high price as TAFloat.\n      prev_low: Previous low price as TAFloat.\n      prev_close: Previous close price as TAFloat.\n      prev_adx: Previous ADX value as TAFloat.\n      prev_adx_period_ago: ADX value from period days ago as TAFloat.\n      prev_smoothed_plus_dm: Previous smoothed +DM value as TAFloat.\n      prev_smoothed_minus_dm: Previous smoothed -DM value as TAFloat.\n      prev_smoothed_tr: Previous smoothed TR value as TAFloat.\n      period: Period for ADX calculation (typically 14).\n\n    Returns:\n      A tuple of 5 values:\n      - Latest ADXR value\n      - Latest ADX value\n      - New smoothed +DM value\n      - New smoothed -DM value\n      - New smoothed TR value\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> adxr, adx, plus_dm, minus_dm, tr = kand.adxr_inc(\n      ...     24.20,  # high\n      ...     23.85,  # low\n      ...     24.07,  # prev_high\n      ...     23.72,  # prev_low\n      ...     23.95,  # prev_close\n      ...     25.0,   # prev_adx\n      ...     20.0,   # prev_adx_period_ago\n      ...     0.5,    # prev_smoothed_plus_dm\n      ...     0.3,    # prev_smoothed_minus_dm\n      ...     1.2,    # prev_smoothed_tr\n      ...     14      # period\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef aroon(high, low, period):\n    \"\"\"\n    Calculate Aroon indicator for a NumPy array.\n\n    The Aroon indicator consists of two lines that measure the time since the last high/low\n    relative to a lookback period. It helps identify the start of new trends and trend reversals.\n\n    Args:\n      high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n      low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n      period: The lookback period for calculations (must be >= 2).\n\n    Returns:\n      A tuple of 6 1-D NumPy arrays containing:\n      - Aroon Up values\n      - Aroon Down values\n      - Previous high values\n      - Previous low values\n      - Days since high values\n      - Days since low values\n      The first (period) elements of each array contain NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n      >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n      >>> aroon_up, aroon_down, prev_high, prev_low, days_high, days_low = kand.aroon(high, low, 3)\n      ```\n    \"\"\"\n    ...\n\ndef aroon_inc(high, low, prev_high, prev_low, days_since_high, days_since_low, period):\n    \"\"\"\n    Calculate the next Aroon values incrementally.\n\n    Args:\n\n      high: Current period's high price.\n      low: Current period's low price.\n      prev_high: Previous highest price in period.\n      prev_low: Previous lowest price in period.\n      days_since_high: Days since previous highest price.\n      days_since_low: Days since previous lowest price.\n      period: The lookback period (must be >= 2).\n\n    Returns:\n      A tuple containing:\n      - Aroon Up value\n      - Aroon Down value\n      - New highest price\n      - New lowest price\n      - Updated days since high\n      - Updated days since low\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> aroon_up, aroon_down, new_high, new_low, days_high, days_low = kand.aroon_inc(\n      ...     15.0,  # high\n      ...     12.0,  # low\n      ...     14.0,  # prev_high\n      ...     11.0,  # prev_low\n      ...     2,     # days_since_high\n      ...     1,     # days_since_low\n      ...     14     # period\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef aroonosc(high, low, period):\n    \"\"\"\n    Calculate Aroon Oscillator for a NumPy array.\n\n    The Aroon Oscillator measures the strength of a trend by comparing the time since the last high and low.\n    It oscillates between -100 and +100, with positive values indicating an uptrend and negative values a downtrend.\n\n    Args:\n      high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n      low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n      period: The lookback period for calculations (must be >= 2).\n\n    Returns:\n      A tuple of 5 1-D NumPy arrays containing:\n      - Aroon Oscillator values\n      - Previous high values\n      - Previous low values\n      - Days since high values\n      - Days since low values\n      The first (period) elements of each array contain NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n      >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n      >>> osc, prev_high, prev_low, days_high, days_low = kand.aroonosc(high, low, 3)\n      ```\n    \"\"\"\n    ...\n\ndef aroonosc_inc(high, low, prev_high, prev_low, days_since_high, days_since_low, period):\n    \"\"\"\n    Calculate the next Aroon Oscillator value incrementally.\n\n    Args:\n\n      high: Current period's high price.\n      low: Current period's low price.\n      prev_high: Previous highest price within the period.\n      prev_low: Previous lowest price within the period.\n      days_since_high: Days since previous highest price.\n      days_since_low: Days since previous lowest price.\n      period: The lookback period for calculations (must be >= 2).\n\n    Returns:\n      A tuple containing:\n      - Aroon Oscillator value\n      - New highest price\n      - New lowest price\n      - Updated days since high\n      - Updated days since low\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> osc, high, low, days_high, days_low = kand.aroonosc_inc(\n      ...     15.0,  # high\n      ...     12.0,  # low\n      ...     14.0,  # prev_high\n      ...     11.0,  # prev_low\n      ...     2,     # days_since_high\n      ...     1,     # days_since_low\n      ...     14     # period\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef atr(high, low, close, period):\n    \"\"\"\n    Computes the Average True Range (ATR) over NumPy arrays.\n\n    The Average True Range (ATR) is a technical analysis indicator that measures market volatility\n    by decomposing the entire range of an asset price for a given period.\n\n    Args:\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      period: Window size for ATR calculation. Must be greater than 1.\n\n    Returns:\n      A new 1-D NumPy array containing the ATR values. The array has the same length as the input,\n      with the first `period` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n      >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n      >>> close = np.array([9.0, 11.0, 14.0, 12.0, 11.0])\n      >>> result = kand.atr(high, low, close, 3)\n      ```\n    \"\"\"\n    ...\n\ndef atr_inc(high, low, prev_close, prev_atr, period):\n    \"\"\"\n    Calculate the next ATR value incrementally.\n\n    Args:\n\n      high: Current period's high price.\n      low: Current period's low price.\n      prev_close: Previous period's close price.\n      prev_atr: Previous period's ATR value.\n      period: The time period for ATR calculation (must be >= 2).\n\n    Returns:\n      The calculated ATR value.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> atr = kand.atr_inc(\n      ...     15.0,  # high\n      ...     11.0,  # low\n      ...     12.0,  # prev_close\n      ...     3.0,   # prev_atr\n      ...     14     # period\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef bbands(price, period, dev_up, dev_down):\n    \"\"\"\n    Calculate Bollinger Bands for a NumPy array.\n\n    Bollinger Bands consist of:\n    - A middle band (N-period simple moving average)\n    - An upper band (K standard deviations above middle band)\n    - A lower band (K standard deviations below middle band)\n\n    Args:\n      price: Input price values as a 1-D NumPy array of type `TAFloat`.\n      period: The time period for calculations (must be >= 2).\n      dev_up: Number of standard deviations for upper band.\n      dev_down: Number of standard deviations for lower band.\n\n    Returns:\n      A tuple of 7 1-D NumPy arrays containing:\n      - Upper band values\n      - Middle band values\n      - Lower band values\n      - SMA values\n      - Variance values\n      - Sum values\n      - Sum of squares values\n      The first (period-1) elements of each array contain NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> price = np.array([10.0, 11.0, 12.0, 13.0, 14.0])\n      >>> upper, middle, lower, sma, var, sum, sum_sq = kand.bbands(price, 3, 2.0, 2.0)\n      ```\n    \"\"\"\n    ...\n\ndef bbands_inc(price, prev_sma, prev_sum, prev_sum_sq, old_price, period, dev_up, dev_down):\n    \"\"\"\n    Calculate the next Bollinger Bands values incrementally.\n\n    Args:\n\n      price: The current price value.\n      prev_sma: The previous SMA value.\n      prev_sum: The previous sum for variance calculation.\n      prev_sum_sq: The previous sum of squares for variance calculation.\n      old_price: The oldest price value to be removed from the period.\n      period: The time period for calculations (must be >= 2).\n      dev_up: Number of standard deviations for upper band.\n      dev_down: Number of standard deviations for lower band.\n\n    Returns:\n      A tuple containing:\n      - Upper Band value\n      - Middle Band value\n      - Lower Band value\n      - New SMA value\n      - New Sum value\n      - New Sum of Squares value\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> upper, middle, lower, sma, sum, sum_sq = kand.bbands_inc(\n      ...     10.0,   # price\n      ...     9.5,    # prev_sma\n      ...     28.5,   # prev_sum\n      ...     272.25, # prev_sum_sq\n      ...     9.0,    # old_price\n      ...     3,      # period\n      ...     2.0,    # dev_up\n      ...     2.0     # dev_down\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef bop(open, high, low, close):\n    \"\"\"\n    Calculate Balance of Power (BOP) indicator for NumPy arrays.\n\n    The Balance of Power (BOP) is a momentum oscillator that measures the relative strength\n    between buyers and sellers by comparing the closing price to the opening price and\n    normalizing it by the trading range (high - low).\n\n    Args:\n      open: Input opening prices as a 1-D NumPy array of type `TAFloat`.\n      high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n      low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Input closing prices as a 1-D NumPy array of type `TAFloat`.\n\n    Returns:\n      A 1-D NumPy array containing the BOP values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> open = np.array([10.0, 11.0, 12.0, 13.0])\n      >>> high = np.array([12.0, 13.0, 14.0, 15.0])\n      >>> low = np.array([8.0, 9.0, 10.0, 11.0])\n      >>> close = np.array([11.0, 12.0, 13.0, 14.0])\n      >>> bop = kand.bop(open, high, low, close)\n      ```\n    \"\"\"\n    ...\n\ndef bop_inc(open, high, low, close):\n    \"\"\"\n    Calculate a single Balance of Power (BOP) value for the latest price data.\n\n    Args:\n      open: Current period's opening price\n      high: Current period's high price\n      low: Current period's low price\n      close: Current period's closing price\n\n    Returns:\n      The calculated BOP value\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> bop = kand.bop_inc(10.0, 12.0, 8.0, 11.0)\n      ```\n    \"\"\"\n    ...\n\ndef cci(high, low, close, period):\n    \"\"\"\n    Computes the Commodity Channel Index (CCI) over NumPy arrays.\n\n    The CCI is a momentum-based oscillator used to help determine when an investment vehicle is reaching\n    a condition of being overbought or oversold.\n\n    Args:\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      period: Window size for CCI calculation. Must be positive and less than input length.\n\n    Returns:\n      A tuple of 1-D NumPy arrays containing:\n      - CCI values\n      - Typical prices\n      - SMA of typical prices\n      - Mean deviation values\n      Each array has the same length as the input, with the first `period-1` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([24.20, 24.07, 24.04, 23.87, 23.67])\n      >>> low = np.array([23.85, 23.72, 23.64, 23.37, 23.46])\n      >>> close = np.array([23.89, 23.95, 23.67, 23.78, 23.50])\n      >>> cci, tp, sma_tp, mean_dev = kand.cci(high, low, close, 3)\n      >>> print(cci)\n      [nan, nan, -100.0, 66.67, -133.33]\n      ```\n    \"\"\"\n    ...\n\ndef cci_inc(prev_sma_tp, new_high, new_low, new_close, old_high, old_low, old_close, period, tp_buffer):\n    \"\"\"\n    Calculates the next CCI value incrementally.\n\n    Args:\n\n      prev_sma_tp: Previous SMA value of typical prices.\n      new_high: New high price.\n      new_low: New low price.\n      new_close: New close price.\n      old_high: Old high price to be removed.\n      old_low: Old low price to be removed.\n      old_close: Old close price to be removed.\n      period: Window size for CCI calculation.\n      tp_buffer: List containing the last `period` typical prices.\n\n    Returns:\n      The next CCI value.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> prev_sma_tp = 100.0\n      >>> new_high = 105.0\n      >>> new_low = 95.0\n      >>> new_close = 100.0\n      >>> old_high = 102.0\n      >>> old_low = 98.0\n      >>> old_close = 100.0\n      >>> period = 14\n      >>> tp_buffer = [100.0] * period\n      >>> next_cci = kand.cci_inc(prev_sma_tp, new_high, new_low, new_close,\n      ...                                  old_high, old_low, old_close, period, tp_buffer)\n      ```\n    \"\"\"\n    ...\n\ndef cdl_doji(open, high, low, close, body_percent, shadow_equal_percent):\n    \"\"\"\n    Detects Doji candlestick patterns in price data.\n\n    Args:\n      open: Opening prices as a 1-D NumPy array of type `TAFloat`.\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      body_percent: Maximum body size as percentage of range (e.g. 5.0 for 5%).\n      shadow_equal_percent: Maximum shadow length difference percentage (e.g. 100.0).\n\n    Returns:\n      A 1-D NumPy array containing pattern signals (1.0 = pattern, 0.0 = no pattern).\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> open = np.array([10.0, 10.5, 10.2])\n      >>> high = np.array([11.0, 11.2, 10.8])\n      >>> low = np.array([9.8, 10.1, 9.9])\n      >>> close = np.array([10.3, 10.4, 10.25])\n      >>> signals = kand.cdl_doji(open, high, low, close, 5.0, 100.0)\n      ```\n    \"\"\"\n    ...\n\ndef cdl_doji_inc(open, high, low, close, body_percent, shadow_equal_percent):\n    \"\"\"\n    Detects a Doji pattern in a single candlestick.\n\n    Args:\n\n      open: Opening price.\n      high: High price.\n      low: Low price.\n      close: Close price.\n      body_percent: Maximum body size as percentage of range.\n      shadow_equal_percent: Maximum shadow length difference percentage.\n\n    Returns:\n      Signal value (1.0 for Doji pattern, 0.0 for no pattern).\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> signal = kand.cdl_doji_inc(10.0, 11.0, 9.8, 10.3, 5.0, 100.0)\n      ```\n    \"\"\"\n    ...\n\ndef cdl_dragonfly_doji(open, high, low, close, body_percent):\n    \"\"\"\n    Detects Dragonfly Doji candlestick patterns in price data.\n\n    Args:\n      open: Opening prices as a 1-D NumPy array of type `TAFloat`.\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      body_percent: Maximum body size as percentage of total range (typically 5%).\n\n    Returns:\n      A 1-D NumPy array containing pattern signals:\n      - 100: Bullish Dragonfly Doji pattern detected\n      - 0: No pattern detected\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> open = np.array([100.0, 101.0, 102.0])\n      >>> high = np.array([102.0, 103.0, 104.0])\n      >>> low = np.array([98.0, 99.0, 100.0])\n      >>> close = np.array([101.0, 102.0, 103.0])\n      >>> signals = kand.cdl_dragonfly_doji(open, high, low, close, 5.0)\n      ```\n    \"\"\"\n    ...\n\ndef cdl_dragonfly_doji_inc(open, high, low, close, body_percent):\n    \"\"\"\n    Detects a Dragonfly Doji pattern in a single candlestick.\n\n    Args:\n\n      open: Opening price.\n      high: High price.\n      low: Low price.\n      close: Close price.\n      body_percent: Maximum body size as percentage of total range.\n\n    Returns:\n      Signal value:\n      - 100: Bullish Dragonfly Doji pattern detected\n      - 0: No pattern detected\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> signal = kand.cdl_dragonfly_doji_inc(100.0, 102.0, 98.0, 100.1, 5.0)\n      ```\n    \"\"\"\n    ...\n\ndef cdl_gravestone_doji(open, high, low, close, body_percent):\n    \"\"\"\n    Detects Gravestone Doji candlestick patterns in price data.\n\n    Args:\n      open: Opening prices as a 1-D NumPy array of type `TAFloat`.\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      body_percent: Maximum body size as percentage of total range (typically 5%).\n\n    Returns:\n      A 1-D NumPy array containing pattern signals:\n      - -100: Bearish Gravestone Doji pattern detected\n      - 0: No pattern detected\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> open = np.array([100.0, 101.0, 102.0])\n      >>> high = np.array([102.0, 103.0, 104.0])\n      >>> low = np.array([98.0, 99.0, 100.0])\n      >>> close = np.array([101.0, 102.0, 103.0])\n      >>> signals = kand.cdl_gravestone_doji(open, high, low, close, 5.0)\n      ```\n    \"\"\"\n    ...\n\ndef cdl_gravestone_doji_inc(open, high, low, close, body_percent):\n    \"\"\"\n    Detects a Gravestone Doji pattern in a single candlestick.\n\n    Args:\n\n      open: Opening price.\n      high: High price.\n      low: Low price.\n      close: Close price.\n      body_percent: Maximum body size as percentage of total range.\n\n    Returns:\n      Signal value:\n      - -100: Bearish Gravestone Doji pattern detected\n      - 0: No pattern detected\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> signal = kand.cdl_gravestone_doji_inc(100.0, 102.0, 98.0, 100.1, 5.0)\n      ```\n    \"\"\"\n    ...\n\ndef cdl_hammer(open, high, low, close, period, factor):\n    \"\"\"\n    Detects Hammer candlestick patterns in price data.\n\n    Args:\n      open: Opening prices as a 1-D NumPy array of type `TAFloat`.\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      period: Period for EMA calculation of body sizes.\n      factor: Minimum ratio of lower shadow to body length.\n\n    Returns:\n      A tuple of two 1-D NumPy arrays containing:\n      - Pattern signals:\n        - 100: Bullish Hammer pattern detected\n        - 0: No pattern detected\n      - EMA values of candle body sizes\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> open = np.array([100.0, 101.0, 102.0])\n      >>> high = np.array([102.0, 103.0, 104.0])\n      >>> low = np.array([98.0, 99.0, 100.0])\n      >>> close = np.array([101.0, 102.0, 103.0])\n      >>> signals, body_avg = kand.cdl_hammer(open, high, low, close, 14, 2.0)\n      ```\n    \"\"\"\n    ...\n\ndef cdl_hammer_inc(open, high, low, close, prev_body_avg, period, factor):\n    \"\"\"\n    Detects a Hammer pattern in a single candlestick.\n\n    Args:\n\n      open: Opening price.\n      high: High price.\n      low: Low price.\n      close: Close price.\n      prev_body_avg: Previous EMA value of body sizes.\n      period: Period for EMA calculation.\n      factor: Minimum ratio of lower shadow to body length.\n\n    Returns:\n      A tuple containing:\n      - Signal value:\n        - 100: Bullish Hammer pattern detected\n        - 0: No pattern detected\n      - Updated EMA value of body sizes\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> signal, body_avg = kand.cdl_hammer_inc(100.0, 102.0, 98.0, 100.1, 0.5, 14, 2.0)\n      ```\n    \"\"\"\n    ...\n\ndef cdl_inverted_hammer(open, high, low, close, period, factor):\n    \"\"\"\n    Detects Inverted Hammer candlestick patterns in price data.\n\n    Args:\n      open: Opening prices as a 1-D NumPy array of type `TAFloat`.\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      period: Period for EMA calculation of body sizes.\n      factor: Minimum ratio of upper shadow to body length.\n\n    Returns:\n      A tuple of two 1-D NumPy arrays containing:\n      - Pattern signals:\n        - 100: Bullish Inverted Hammer pattern detected\n        - 0: No pattern detected\n      - EMA values of candle body sizes\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> open = np.array([100.0, 101.0, 102.0])\n      >>> high = np.array([102.0, 103.0, 104.0])\n      >>> low = np.array([98.0, 99.0, 100.0])\n      >>> close = np.array([101.0, 102.0, 103.0])\n      >>> signals, body_avg = kand.cdl_inverted_hammer(open, high, low, close, 14, 2.0)\n      ```\n    \"\"\"\n    ...\n\ndef cdl_inverted_hammer_inc(open, high, low, close, prev_body_avg, period, factor):\n    \"\"\"\n    Detects an Inverted Hammer pattern in a single candlestick.\n\n    Args:\n\n      open: Opening price.\n      high: High price.\n      low: Low price.\n      close: Close price.\n      prev_body_avg: Previous EMA value of body sizes.\n      period: Period for EMA calculation.\n      factor: Minimum ratio of upper shadow to body length.\n\n    Returns:\n      A tuple containing:\n      - Signal value:\n        - 100: Bullish Inverted Hammer pattern detected\n        - 0: No pattern detected\n      - Updated EMA value of body sizes\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> signal, body_avg = kand.cdl_inverted_hammer_inc(100.0, 102.0, 98.0, 100.1, 0.5, 14, 2.0)\n      ```\n    \"\"\"\n    ...\n\ndef cdl_long_shadow(open, high, low, close, period, shadow_factor):\n    \"\"\"\n    Detects Long Shadow candlestick patterns in price data.\n\n    Args:\n      open: Opening prices as a 1-D NumPy array of type `TAFloat`.\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      period: Period for EMA calculation of body sizes.\n      shadow_factor: Minimum percentage of total range that shadow must be.\n\n    Returns:\n      A tuple of two 1-D NumPy arrays containing:\n      - Pattern signals:\n        - 100: Bullish Long Lower Shadow pattern detected\n        - -100: Bearish Long Upper Shadow pattern detected\n        - 0: No pattern detected\n      - EMA values of candle body sizes\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> open = np.array([100.0, 101.0, 102.0])\n      >>> high = np.array([102.0, 103.0, 104.0])\n      >>> low = np.array([98.0, 99.0, 100.0])\n      >>> close = np.array([101.0, 102.0, 103.0])\n      >>> signals, body_avg = kand.cdl_long_shadow(open, high, low, close, 14, 75.0)\n      ```\n    \"\"\"\n    ...\n\ndef cdl_long_shadow_inc(open, high, low, close, prev_body_avg, period, shadow_factor):\n    \"\"\"\n    Detects a Long Shadow pattern in a single candlestick.\n\n    Args:\n\n      open: Opening price.\n      high: High price.\n      low: Low price.\n      close: Close price.\n      prev_body_avg: Previous EMA value of body sizes.\n      period: Period for EMA calculation.\n      shadow_factor: Minimum percentage of total range that shadow must be.\n\n    Returns:\n      A tuple containing:\n      - Signal value:\n        - 100: Bullish Long Lower Shadow pattern detected\n        - -100: Bearish Long Upper Shadow pattern detected\n        - 0: No pattern detected\n      - Updated EMA value of body sizes\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> signal, body_avg = kand.cdl_long_shadow_inc(100.0, 102.0, 98.0, 100.1, 0.5, 14, 75.0)\n      ```\n    \"\"\"\n    ...\n\ndef cdl_marubozu(open, high, low, close, period, shadow_percent):\n    \"\"\"\n    Detects Marubozu candlestick patterns in price data.\n\n    Args:\n      open: Opening prices as a 1-D NumPy array of type `TAFloat`.\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      period: Period for EMA calculation of body sizes.\n      shadow_percent: Maximum shadow size as percentage of body.\n\n    Returns:\n      A tuple of two 1-D NumPy arrays containing:\n      - Pattern signals:\n        - 1.0: Bullish Marubozu pattern detected\n        - -1.0: Bearish Marubozu pattern detected\n        - 0.0: No pattern detected\n      - EMA values of candle body sizes\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> open = np.array([100.0, 101.0, 102.0])\n      >>> high = np.array([102.0, 103.0, 104.0])\n      >>> low = np.array([98.0, 99.0, 100.0])\n      >>> close = np.array([101.0, 102.0, 103.0])\n      >>> signals, body_avg = kand.cdl_marubozu(open, high, low, close, 14, 5.0)\n      ```\n    \"\"\"\n    ...\n\ndef cdl_marubozu_inc(open, high, low, close, prev_body_avg, period, shadow_percent):\n    \"\"\"\n    Detects a Marubozu pattern in a single candlestick.\n\n    Args:\n\n      open: Opening price.\n      high: High price.\n      low: Low price.\n      close: Close price.\n      prev_body_avg: Previous EMA value of body sizes.\n      period: Period for EMA calculation.\n      shadow_percent: Maximum shadow size as percentage of body.\n\n    Returns:\n      A tuple containing:\n      - Signal value:\n        - 1.0: Bullish Marubozu pattern detected\n        - -1.0: Bearish Marubozu pattern detected\n        - 0.0: No pattern detected\n      - Updated EMA value of body sizes\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> signal, body_avg = kand.cdl_marubozu_inc(100.0, 102.0, 98.0, 100.1, 0.5, 14, 5.0)\n      ```\n    \"\"\"\n    ...\n\ndef dema(input_price, period):\n    \"\"\"\n    Calculates Double Exponential Moving Average (DEMA) over NumPy arrays.\n\n    Args:\n      input_price: Price values as a 1-D NumPy array of type `TAFloat`.\n      period: Smoothing period for EMA calculations. Must be >= 2.\n\n    Returns:\n      A tuple of 1-D NumPy arrays containing:\n      - DEMA values\n      - First EMA values\n      - Second EMA values\n      Each array has the same length as the input, with the first `2*(period-1)` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> prices = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0])\n      >>> dema, ema1, ema2 = kand.dema(prices, 3)\n      ```\n    \"\"\"\n    ...\n\ndef dema_inc(price, prev_ema1, prev_ema2, period):\n    \"\"\"\n    Calculates the next DEMA value incrementally.\n\n    Args:\n\n      price: Current price value.\n      prev_ema1: Previous value of first EMA.\n      prev_ema2: Previous value of second EMA.\n      period: Smoothing period. Must be >= 2.\n\n    Returns:\n      A tuple containing (DEMA, new_ema1, new_ema2).\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> dema, ema1, ema2 = kand.dema_inc(10.0, 9.5, 9.0, 3)\n      ```\n    \"\"\"\n    ...\n\ndef dx(high, low, close, period):\n    \"\"\"\n    Computes the Directional Movement Index (DX) over NumPy arrays.\n\n    The DX indicator measures the strength of a trend by comparing positive and negative directional movements.\n\n    Args:\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      period: Window size for DX calculation. Must be positive and less than input length.\n\n    Returns:\n      A tuple of four 1-D NumPy arrays containing:\n      - DX values\n      - Smoothed +DM values\n      - Smoothed -DM values\n      - Smoothed TR values\n      Each array has the same length as the input, with the first `period` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([24.20, 24.07, 24.04, 23.87, 23.67])\n      >>> low = np.array([23.85, 23.72, 23.64, 23.37, 23.46])\n      >>> close = np.array([23.89, 23.95, 23.67, 23.78, 23.50])\n      >>> dx, plus_dm, minus_dm, tr = kand.dx(high, low, close, 3)\n      ```\n    \"\"\"\n    ...\n\ndef dx_inc(input_high, input_low, prev_high, prev_low, prev_close, prev_smoothed_plus_dm, prev_smoothed_minus_dm, prev_smoothed_tr, opt_period):\n    \"\"\"\n    Calculates the latest DX value incrementally.\n\n    Computes only the most recent DX value using previous smoothed values.\n    Optimized for real-time calculations where only the latest value is needed.\n\n    For the formula, refer to the [`dx`] function documentation.\n\n    Args:\n        input_high (float): Current high price.\n        input_low (float): Current low price.\n        prev_high (float): Previous period's high price.\n        prev_low (float): Previous period's low price.\n        prev_close (float): Previous period's close price.\n        prev_smoothed_plus_dm (float): Previous smoothed +DM value.\n        prev_smoothed_minus_dm (float): Previous smoothed -DM value.\n        prev_smoothed_tr (float): Previous smoothed TR value.\n        opt_period (int): Period for DX calculation (typically 14).\n\n    Returns:\n        tuple: A tuple containing:\n            - Latest DX value (float)\n            - New smoothed +DM (float)\n            - New smoothed -DM (float)\n            - New smoothed TR (float)\n\n    Example:\n        >>> import kand\n        >>> high, low = 24.20, 23.85\n        >>> prev_high, prev_low, prev_close = 24.07, 23.72, 23.95\n        >>> prev_smoothed_plus_dm = 0.5\n        >>> prev_smoothed_minus_dm = 0.3\n        >>> prev_smoothed_tr = 1.2\n        >>> period = 14\n        >>> dx, plus_dm, minus_dm, tr = kand.dx_inc(\n        ...     high, low, prev_high, prev_low, prev_close,\n        ...     prev_smoothed_plus_dm, prev_smoothed_minus_dm,\n        ...     prev_smoothed_tr, period)\n    \"\"\"\n    ...\n\ndef ecl(high, low, close):\n    \"\"\"\n    Computes the Expanded Camarilla Levels (ECL) over NumPy arrays.\n\n    The ECL indicator calculates multiple support and resistance levels based on the previous period's\n    high, low and close prices.\n\n    Args:\n      high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n      low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Input close prices as a 1-D NumPy array of type `TAFloat`.\n\n    Returns:\n      A tuple of ten 1-D NumPy arrays containing the ECL values (H5,H4,H3,H2,H1,L1,L2,L3,L4,L5).\n      Each array has the same length as the input, with the first element containing NaN value.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([24.20, 24.07, 24.04, 23.87, 23.67])\n      >>> low = np.array([23.85, 23.72, 23.64, 23.37, 23.46])\n      >>> close = np.array([23.89, 23.95, 23.67, 23.78, 23.50])\n      >>> h5,h4,h3,h2,h1,l1,l2,l3,l4,l5 = kand.ecl(high, low, close)\n      ```\n    \"\"\"\n    ...\n\ndef ecl_inc(prev_high, prev_low, prev_close):\n    \"\"\"\n    Computes the latest Expanded Camarilla Levels (ECL) values incrementally.\n\n    This function provides an efficient way to calculate ECL values for new data without\n    reprocessing the entire dataset.\n\n    Args:\n\n      prev_high: Previous period's high price as `TAFloat`.\n      prev_low: Previous period's low price as `TAFloat`.\n      prev_close: Previous period's close price as `TAFloat`.\n\n    Returns:\n      A tuple of ten values (H5,H4,H3,H2,H1,L1,L2,L3,L4,L5) containing the latest ECL levels.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> h5,h4,h3,h2,h1,l1,l2,l3,l4,l5 = kand.ecl_inc(24.20, 23.85, 23.89)\n      ```\n    \"\"\"\n    ...\n\ndef ema(data, period, k=None):\n    \"\"\"\n    Computes the Exponential Moving Average (EMA) over a NumPy array.\n\n    The Exponential Moving Average is calculated by applying more weight to recent prices\n    via a smoothing factor k. Each value is calculated as:\n    EMA = Price * k + EMA(previous) * (1 - k)\n    where k is typically 2/(period+1).\n\n    Args:\n      data: Input data as a 1-D NumPy array of type `TAFloat`.\n      period: Window size for EMA calculation. Must be positive and less than input length.\n      k: Optional custom smoothing factor. If None, uses default k = 2/(period+1).\n\n    Returns:\n      A new 1-D NumPy array containing the EMA values. The array has the same length as the input,\n      with the first `period-1` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n      >>> result = kand.ema(data, 3)\n      >>> print(result)\n      [nan, nan, 2.0, 3.0, 4.2]\n      ```\n    \"\"\"\n    ...\n\ndef ema_inc(price, prev_ema, period, k=None):\n    \"\"\"\n    Computes the latest EMA value incrementally.\n\n    This function provides an efficient way to calculate EMA values for new data without\n    reprocessing the entire dataset.\n\n    Args:\n\n      price: Current period's price value as `TAFloat`.\n      prev_ema: Previous period's EMA value as `TAFloat`.\n      period: Window size for EMA calculation. Must be >= 2.\n      k: Optional custom smoothing factor. If None, uses default k = 2/(period+1).\n\n    Returns:\n      The new EMA value as `TAFloat`.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> current_price = 15.0\n      >>> prev_ema = 14.5\n      >>> period = 14\n      >>> new_ema = kand.ema_inc(current_price, prev_ema, period)\n      ```\n    \"\"\"\n    ...\n\ndef macd(data, fast_period, slow_period, signal_period):\n    \"\"\"\n    Computes the Moving Average Convergence Divergence (MACD) over a NumPy array.\n\n    MACD is a trend-following momentum indicator that shows the relationship between two moving averages\n    of an asset's price. It consists of three components:\n    - MACD Line: Difference between fast and slow EMAs\n    - Signal Line: EMA of the MACD line\n    - Histogram: Difference between MACD line and signal line\n\n    Args:\n      data: Input price data as a 1-D NumPy array of type `TAFloat`.\n      fast_period: Period for fast EMA calculation (typically 12).\n      slow_period: Period for slow EMA calculation (typically 26).\n      signal_period: Period for signal line calculation (typically 9).\n\n    Returns:\n      A tuple of five 1-D NumPy arrays containing:\n      - MACD line values\n      - Signal line values\n      - MACD histogram values\n      - Fast EMA values\n      - Slow EMA values\n      Each array has the same length as the input, with initial elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n      >>> macd_line, signal_line, histogram, fast_ema, slow_ema = kand.macd(data, 2, 3, 2)\n      ```\n    \"\"\"\n    ...\n\ndef macd_inc(price, prev_fast_ema, prev_slow_ema, prev_signal, fast_period, slow_period, signal_period):\n    \"\"\"\n    Computes the latest MACD values incrementally from previous state.\n\n    This function provides an efficient way to calculate MACD for streaming data by using\n    previous EMA values instead of recalculating the entire series.\n\n    Args:\n\n      price: Current price value as `TAFloat`.\n      prev_fast_ema: Previous fast EMA value as `TAFloat`.\n      prev_slow_ema: Previous slow EMA value as `TAFloat`.\n      prev_signal: Previous signal line value as `TAFloat`.\n      fast_period: Period for fast EMA calculation (typically 12).\n      slow_period: Period for slow EMA calculation (typically 26).\n      signal_period: Period for signal line calculation (typically 9).\n\n    Returns:\n      A tuple of three values:\n      - MACD line value\n      - Signal line value\n      - MACD histogram value\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> macd_line, signal_line, histogram = kand.macd_inc(\n      ...     100.0,  # current price\n      ...     95.0,   # previous fast EMA\n      ...     98.0,   # previous slow EMA\n      ...     -2.5,   # previous signal\n      ...     12,     # fast period\n      ...     26,     # slow period\n      ...     9       # signal period\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef max(prices, period):\n    \"\"\"\n    Calculate Maximum Value for a NumPy array\n\n    Args:\n      prices: Input prices as a 1-D NumPy array of type `TAFloat`.\n      period: Period for MAX calculation (must be >= 2).\n\n    Returns:\n      A 1-D NumPy array containing MAX values. The first (period-1) elements contain NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> prices = np.array([1.0, 2.0, 3.0, 2.5, 4.0])\n      >>> max_values = kand.max(prices, 3)\n      ```\n    \"\"\"\n    ...\n\ndef max_inc(price, prev_max, old_price, period):\n    \"\"\"\n    Calculate the latest Maximum Value incrementally\n\n    Args:\n      py: Python interpreter token\n      price: Current period's price\n      prev_max: Previous period's MAX value\n      old_price: Price being removed from the period\n      period: Period for MAX calculation (must be >= 2)\n\n    Returns:\n      The new MAX value\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> new_max = kand.max_inc(10.5, 11.0, 9.0, 14)\n      ```\n    \"\"\"\n    ...\n\ndef medprice(high, low):\n    \"\"\"\n    Calculates the Median Price (MEDPRICE) for a NumPy array.\n\n    The Median Price is a technical analysis indicator that represents the middle point between\n    high and low prices for each period.\n\n    Args:\n      high: Array of high prices as a 1-D NumPy array of type `TAFloat`.\n      low: Array of low prices as a 1-D NumPy array of type `TAFloat`.\n\n    Returns:\n      A 1-D NumPy array containing the median price values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([10.0, 11.0, 12.0])\n      >>> low = np.array([8.0, 9.0, 10.0])\n      >>> result = kand.medprice(high, low)\n      >>> print(result)\n      [9.0, 10.0, 11.0]\n      ```\n    \"\"\"\n    ...\n\ndef medprice_inc(high, low):\n    \"\"\"\n    Calculates a single Median Price value incrementally.\n\n    Args:\n\n      high: Current period's high price as `TAFloat`.\n      low: Current period's low price as `TAFloat`.\n\n    Returns:\n      The calculated median price value.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> result = kand.medprice_inc(10.0, 8.0)\n      >>> print(result)\n      9.0\n      ```\n    \"\"\"\n    ...\n\ndef mfi(high, low, close, volume, period):\n    \"\"\"\n    Calculates the Money Flow Index (MFI) for a NumPy array.\n\n    The Money Flow Index (MFI) is a technical oscillator that uses price and volume data to identify\n    overbought or oversold conditions in an asset.\n\n    Args:\n      high: Array of high prices as a 1-D NumPy array of type `TAFloat`.\n      low: Array of low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Array of close prices as a 1-D NumPy array of type `TAFloat`.\n      volume: Array of volume data as a 1-D NumPy array of type `TAFloat`.\n      period: The time period for MFI calculation (typically 14).\n\n    Returns:\n      A tuple of five 1-D NumPy arrays containing:\n      - MFI values (0-100)\n      - Typical prices\n      - Money flows\n      - Positive money flows\n      - Negative money flows\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([10.0, 11.0, 12.0, 11.0])\n      >>> low = np.array([8.0, 9.0, 10.0, 9.0])\n      >>> close = np.array([9.0, 10.0, 11.0, 10.0])\n      >>> volume = np.array([100.0, 150.0, 200.0, 150.0])\n      >>> mfi, typ_prices, money_flows, pos_flows, neg_flows = kand.mfi(high, low, close, volume, 2)\n      ```\n    \"\"\"\n    ...\n\ndef midpoint(data, period):\n    \"\"\"\n    Calculates Midpoint values for a NumPy array.\n\n    The Midpoint is a technical indicator that represents the arithmetic mean of the highest and lowest\n    prices over a specified period.\n\n    Args:\n      data: Input price data as a 1-D NumPy array of type `TAFloat`.\n      period: Time period for calculation (must be >= 2).\n\n    Returns:\n      A tuple of three 1-D NumPy arrays containing:\n      - Midpoint values\n      - Highest values for each period\n      - Lowest values for each period\n      Each array has the same length as the input, with initial elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> data = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n      >>> midpoint, highest, lowest = kand.midpoint(data, 3)\n      ```\n    \"\"\"\n    ...\n\ndef midpoint_inc(price, prev_highest, prev_lowest, period):\n    \"\"\"\n    Calculates the next Midpoint value incrementally.\n\n    Provides an optimized way to calculate the next Midpoint value when new data arrives,\n    without recalculating the entire series.\n\n    Args:\n      price: Current price value as `TAFloat`.\n      prev_highest: Previous highest value as `TAFloat`.\n      prev_lowest: Previous lowest value as `TAFloat`.\n      period: Time period for calculation (must be >= 2).\n\n    Returns:\n      A tuple containing:\n      - Midpoint value\n      - New highest value\n      - New lowest value\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> midpoint, new_highest, new_lowest = kand.midpoint_inc(\n      ...     15.0,  # current price\n      ...     16.0,  # previous highest\n      ...     14.0,  # previous lowest\n      ...     14     # period\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef midprice(high, low, period):\n    \"\"\"\n    Calculates Midpoint Price values for a NumPy array.\n\n    The Midpoint Price is a technical indicator that represents the mean value between the highest high\n    and lowest low prices over a specified period.\n\n    Args:\n      high: Input high price data as a 1-D NumPy array of type `TAFloat`.\n      low: Input low price data as a 1-D NumPy array of type `TAFloat`.\n      period: Time period for calculation (must be >= 2).\n\n    Returns:\n      A tuple of three 1-D NumPy arrays containing:\n      - Midpoint Price values\n      - Highest high values for each period\n      - Lowest low values for each period\n      Each array has the same length as the input, with initial elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n      >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n      >>> midprice, highest, lowest = kand.midprice(high, low, 3)\n      ```\n    \"\"\"\n    ...\n\ndef midprice_inc(high, low, prev_highest, prev_lowest, period):\n    \"\"\"\n    Calculates the next Midpoint Price value incrementally.\n\n    Provides an optimized way to calculate the next Midpoint Price value when new data arrives,\n    without recalculating the entire series.\n\n    Args:\n\n      high: Current high price value as `TAFloat`.\n      low: Current low price value as `TAFloat`.\n      prev_highest: Previous highest high value as `TAFloat`.\n      prev_lowest: Previous lowest low value as `TAFloat`.\n      period: Time period for calculation (must be >= 2).\n\n    Returns:\n      A tuple containing:\n      - Midpoint Price value\n      - New highest high value\n      - New lowest low value\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> midprice, new_highest, new_lowest = kand.midprice_inc(\n      ...     10.5,  # current high\n      ...     9.8,   # current low\n      ...     10.2,  # previous highest high\n      ...     9.5,   # previous lowest low\n      ...     14     # period\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef min(prices, period):\n    \"\"\"\n    Calculate Minimum Value (MIN) for a NumPy array\n\n    The MIN indicator finds the lowest price value within a given time period.\n\n    Args:\n      prices: Input prices as a 1-D NumPy array of type `TAFloat`.\n      period: Period for MIN calculation (must be >= 2).\n\n    Returns:\n      A 1-D NumPy array containing MIN values. First (period-1) elements contain NaN.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> prices = np.array([10.0, 8.0, 6.0, 7.0, 9.0])\n      >>> min_values = kand.min(prices, 3)\n      ```\n    \"\"\"\n    ...\n\ndef min_inc(price, prev_min, prev_price, period):\n    \"\"\"\n    Calculate the latest MIN value incrementally\n\n    Args:\n      py: Python interpreter token\n      price: Current period's price\n      prev_min: Previous period's MIN value\n      prev_price: Price value being removed from the period\n      period: Period for MIN calculation (must be >= 2)\n\n    Returns:\n      The new MIN value\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> new_min = kand.min_inc(15.0, 12.0, 14.0, 14)\n      ```\n    \"\"\"\n    ...\n\ndef minus_di(high, low, close, period):\n    \"\"\"\n    Computes the Minus Directional Indicator (-DI) over NumPy arrays.\n\n    The -DI measures the presence and strength of a downward price trend. It is one component used in calculating\n    the Average Directional Index (ADX), which helps determine trend strength.\n\n    Args:\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      period: Window size for -DI calculation. Must be positive and less than input length.\n\n    Returns:\n      A tuple of three 1-D NumPy arrays containing:\n      - The -DI values\n      - The smoothed -DM values\n      - The smoothed TR values\n      Each array has the same length as the input, with the first `period` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([35.0, 36.0, 35.5, 35.8, 36.2])\n      >>> low = np.array([34.0, 35.0, 34.5, 34.8, 35.2])\n      >>> close = np.array([34.5, 35.5, 35.0, 35.3, 35.7])\n      >>> minus_di, smoothed_minus_dm, smoothed_tr = kand.minus_di(high, low, close, 3)\n      >>> print(minus_di)\n      [nan, nan, nan, 25.3, 24.1]\n      ```\n    \"\"\"\n    ...\n\ndef minus_dm(high, low, period):\n    \"\"\"\n    Computes the Minus Directional Movement (-DM) over NumPy arrays.\n\n    Minus Directional Movement (-DM) measures downward price movement and is used as part of the\n    Directional Movement System developed by J. Welles Wilder.\n\n    Args:\n      high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n      low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n      period: Window size for -DM calculation. Must be positive and less than input length.\n\n    Returns:\n      A new 1-D NumPy array containing the -DM values. The array has the same length as the input,\n      with the first `period-1` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([35266.0, 35247.5, 35235.7, 35190.8, 35182.0])\n      >>> low = np.array([35216.1, 35206.5, 35180.0, 35130.7, 35153.6])\n      >>> result = kand.minus_dm(high, low, 3)\n      ```\n    \"\"\"\n    ...\n\ndef mom(data, period):\n    \"\"\"\n    Computes the Momentum (MOM) over a NumPy array.\n\n    Momentum measures the change in price between the current price and the price n periods ago.\n\n    Args:\n      data: Input data as a 1-D NumPy array of type `TAFloat`.\n      period: Window size for momentum calculation. Must be positive and less than input length.\n\n    Returns:\n      A new 1-D NumPy array containing the momentum values. The array has the same length as the input,\n      with the first `period` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> data = np.array([2.0, 4.0, 6.0, 8.0, 10.0])\n      >>> result = kand.mom(data, 2)\n      >>> print(result)\n      [nan, nan, 4.0, 4.0, 4.0]\n      ```\n    \"\"\"\n    ...\n\ndef mom_inc(current_price, old_price):\n    \"\"\"\n    Calculates the next Momentum (MOM) value incrementally.\n\n    This function provides an optimized way to calculate the latest momentum value\n    when streaming data is available, without needing the full price history.\n\n    Args:\n\n      current_price: The current period's price value.\n      old_price: The price value from n periods ago.\n\n    Returns:\n      The calculated momentum value.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> momentum = kand.mom_inc(10.0, 6.0)\n      >>> print(momentum)\n      4.0\n      ```\n    \"\"\"\n    ...\n\ndef natr(high, low, close, period):\n    \"\"\"\n    Computes the Normalized Average True Range (NATR) over NumPy arrays.\n\n    The NATR is a measure of volatility that accounts for the price level of the instrument.\n    It expresses the ATR as a percentage of the closing price.\n\n    Args:\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      period: Window size for NATR calculation. Must be positive and less than input length.\n\n    Returns:\n      A new 1-D NumPy array containing the NATR values. The array has the same length as the input,\n      with the first `period` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n      >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n      >>> close = np.array([9.0, 11.0, 14.0, 12.0, 11.0])\n      >>> result = kand.natr(high, low, close, 3)\n      ```\n    \"\"\"\n    ...\n\ndef natr_inc(high, low, close, prev_close, prev_atr, period):\n    \"\"\"\n    Calculates the next NATR value incrementally.\n\n    This function provides an optimized way to calculate a single new NATR value\n    using the previous ATR value and current price data, without recalculating the entire series.\n\n    Args:\n\n      high: Current period's high price.\n      low: Current period's low price.\n      close: Current period's closing price.\n      prev_close: Previous period's closing price.\n      prev_atr: Previous period's ATR value.\n      period: Period for NATR calculation (must be >= 2).\n\n    Returns:\n      The calculated NATR value.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> natr = kand.natr_inc(\n      ...     15.0,  # high\n      ...     11.0,  # low\n      ...     14.0,  # close\n      ...     12.0,  # prev_close\n      ...     3.0,   # prev_atr\n      ...     3      # period\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef obv(close, volume):\n    \"\"\"\n    Computes the On Balance Volume (OBV) over NumPy arrays.\n\n    On Balance Volume (OBV) is a momentum indicator that uses volume flow to predict changes in stock price.\n    When volume increases without a significant price change, the price will eventually jump upward.\n    When volume decreases without a significant price change, the price will eventually jump downward.\n\n    Args:\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      volume: Volume data as a 1-D NumPy array of type `TAFloat`.\n\n    Returns:\n      A new 1-D NumPy array containing the OBV values. The array has the same length as the input.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> close = np.array([10.0, 12.0, 11.0, 13.0])\n      >>> volume = np.array([100.0, 150.0, 120.0, 200.0])\n      >>> result = kand.obv(close, volume)\n      >>> print(result)\n      [100.0, 250.0, 130.0, 330.0]\n      ```\n    \"\"\"\n    ...\n\ndef obv_inc(curr_close, prev_close, volume, prev_obv):\n    \"\"\"\n    Calculates the next OBV value incrementally.\n\n    This function provides an optimized way to calculate a single new OBV value\n    using the previous OBV value and current price/volume data.\n\n    Args:\n      curr_close: Current closing price as `TAFloat`.\n      prev_close: Previous closing price as `TAFloat`.\n      volume: Current volume as `TAFloat`.\n      prev_obv: Previous OBV value as `TAFloat`.\n\n    Returns:\n      The calculated OBV value.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> curr_close = 12.0\n      >>> prev_close = 10.0\n      >>> volume = 150.0\n      >>> prev_obv = 100.0\n      >>> result = kand.obv_inc(curr_close, prev_close, volume, prev_obv)\n      >>> print(result)\n      250.0\n      ```\n    \"\"\"\n    ...\n\ndef plus_di(high, low, close, period):\n    \"\"\"\n    Computes the Plus Directional Indicator (+DI) over NumPy arrays.\n\n    +DI measures the presence and strength of an upward price trend. It is one component used in calculating\n    the Average Directional Index (ADX), which helps determine trend strength.\n\n    Args:\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      period: Window size for +DI calculation. Must be positive and less than input length.\n\n    Returns:\n      A tuple of three 1-D NumPy arrays containing:\n      - +DI values\n      - Smoothed +DM values\n      - Smoothed TR values\n      Each array has the same length as the input, with the first `period` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([10.0, 12.0, 11.5, 11.0])\n      >>> low = np.array([9.0, 10.0, 10.0, 9.5])\n      >>> close = np.array([9.5, 11.0, 10.5, 10.0])\n      >>> plus_di, smoothed_plus_dm, smoothed_tr = kand.plus_di(high, low, close, 2)\n      ```\n    \"\"\"\n    ...\n\ndef plus_di_inc(high, low, prev_high, prev_low, prev_close, prev_smoothed_plus_dm, prev_smoothed_tr, period):\n    \"\"\"\n    Calculates the next +DI value incrementally using previous smoothed values.\n\n    This function enables real-time calculation of +DI by using the previous smoothed values\n    and current price data, avoiding the need to recalculate the entire series.\n\n    Args:\n      high: Current high price as `TAFloat`.\n      low: Current low price as `TAFloat`.\n      prev_high: Previous high price as `TAFloat`.\n      prev_low: Previous low price as `TAFloat`.\n      prev_close: Previous close price as `TAFloat`.\n      prev_smoothed_plus_dm: Previous smoothed +DM value as `TAFloat`.\n      prev_smoothed_tr: Previous smoothed TR value as `TAFloat`.\n      period: Smoothing period (>= 2).\n\n    Returns:\n      A tuple containing (latest +DI, new smoothed +DM, new smoothed TR).\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> plus_di, smoothed_plus_dm, smoothed_tr = kand.plus_di_inc(\n      ...     10.5,  # high\n      ...     9.5,   # low\n      ...     10.0,  # prev_high\n      ...     9.0,   # prev_low\n      ...     9.5,   # prev_close\n      ...     15.0,  # prev_smoothed_plus_dm\n      ...     20.0,  # prev_smoothed_tr\n      ...     14     # period\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef plus_dm(high, low, period):\n    \"\"\"\n    Computes the Plus Directional Movement (+DM) over NumPy arrays.\n\n    Plus Directional Movement (+DM) measures upward price movement and is used as part of the\n    Directional Movement System developed by J. Welles Wilder.\n\n    Args:\n      high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n      low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n      period: Window size for +DM calculation. Must be positive and less than input length.\n\n    Returns:\n      A new 1-D NumPy array containing the +DM values. The array has the same length as the input,\n      with the first `period-1` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([35266.0, 35247.5, 35235.7, 35190.8, 35182.0])\n      >>> low = np.array([35216.1, 35206.5, 35180.0, 35130.7, 35153.6])\n      >>> result = kand.plus_dm(high, low, 3)\n      ```\n    \"\"\"\n    ...\n\ndef plus_dm_inc(high, prev_high, low, prev_low, prev_plus_dm, period):\n    \"\"\"\n    Calculates the next Plus DM value incrementally using previous values.\n\n    This function enables real-time calculation of Plus DM by using the previous Plus DM value\n    and current price data, avoiding the need to recalculate the entire series.\n\n    Args:\n      high: Current high price as `TAFloat`.\n      prev_high: Previous high price as `TAFloat`.\n      low: Current low price as `TAFloat`.\n      prev_low: Previous low price as `TAFloat`.\n      prev_plus_dm: Previous Plus DM value as `TAFloat`.\n      period: Smoothing period (>= 2).\n\n    Returns:\n      The latest Plus DM value.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> new_plus_dm = kand.plus_dm_inc(\n      ...     10.5,  # high\n      ...     10.0,  # prev_high\n      ...     9.8,   # low\n      ...     9.5,   # prev_low\n      ...     0.45,  # prev_plus_dm\n      ...     14     # period\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef rma(data, period):\n    \"\"\"\n    Computes the Running Moving Average (RMA) over a NumPy array.\n\n    The Running Moving Average is similar to an Exponential Moving Average (EMA) but uses a different\n    smoothing factor. It is calculated using a weighted sum of the current value and previous RMA value,\n    with weights determined by the period size.\n\n    Args:\n      data: Input data as a 1-D NumPy array of type `TAFloat`.\n      period: Window size for RMA calculation. Must be positive and less than input length.\n\n    Returns:\n      A new 1-D NumPy array containing the RMA values. The array has the same length as the input,\n      with the first `period-1` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n      >>> result = kand.rma(data, 3)\n      >>> print(result)\n      [nan, nan, 2.0, 2.67, 3.44]\n      ```\n    \"\"\"\n    ...\n\ndef rma_inc(current_price, prev_rma, period):\n    \"\"\"\n    Calculates the next RMA value incrementally.\n\n    This function provides an optimized way to calculate the latest RMA value\n    when streaming data is available, without needing the full price history.\n\n    Args:\n      current_price: The current period's price value.\n      prev_rma: The previous period's RMA value.\n      period: The smoothing period (must be >= 2).\n\n    Returns:\n      The calculated RMA value.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> new_rma = kand.rma_inc(10.0, 9.5, 14)\n      ```\n    \"\"\"\n    ...\n\ndef roc(data, period):\n    \"\"\"\n    Computes the Rate of Change (ROC) over a NumPy array.\n\n    The Rate of Change (ROC) is a momentum oscillator that measures the percentage change in price\n    between the current price and the price n periods ago.\n\n    Args:\n      data: Input price data as a 1-D NumPy array of type `TAFloat`.\n      period: Number of periods to look back. Must be positive.\n\n    Returns:\n      A new 1-D NumPy array containing the ROC values. The array has the same length as the input,\n      with the first `period` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> data = np.array([10.0, 10.5, 11.2, 10.8, 11.5])\n      >>> result = kand.roc(data, 2)\n      >>> print(result)\n      [nan, nan, 12.0, 2.86, 6.48]\n      ```\n    \"\"\"\n    ...\n\ndef roc_inc(current_price, prev_price):\n    \"\"\"\n    Calculates a single ROC value incrementally.\n\n    This function provides an optimized way to calculate the latest ROC value\n    when streaming data is available, without needing the full price history.\n\n    Args:\n      current_price: The current period's price value.\n      prev_price: The price from n periods ago.\n\n    Returns:\n      The calculated ROC value.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> roc = kand.roc_inc(11.5, 10.0)\n      >>> print(roc)\n      15.0\n      ```\n    \"\"\"\n    ...\n\ndef rocp(data, period):\n    \"\"\"\n    Computes the Rate of Change Percentage (ROCP) over a NumPy array.\n\n    The Rate of Change Percentage (ROCP) is a momentum indicator that measures the percentage change\n    between the current price and the price n periods ago.\n\n    Args:\n      data: Input price data as a 1-D NumPy array of type `TAFloat`.\n      period: Number of periods to look back. Must be positive.\n\n    Returns:\n      A new 1-D NumPy array containing the ROCP values. The array has the same length as the input,\n      with the first `period` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> data = np.array([10.0, 10.5, 11.2, 10.8, 11.5])\n      >>> result = kand.rocp(data, 2)\n      >>> print(result)\n      [nan, nan, 0.12, 0.0286, 0.0648]\n      ```\n    \"\"\"\n    ...\n\ndef rocp_inc(current_price, prev_price):\n    \"\"\"\n    Calculates a single ROCP value incrementally.\n\n    This function provides an optimized way to calculate the latest ROCP value\n    when streaming data is available, without needing the full price history.\n\n    Args:\n      current_price: The current period's price value.\n      prev_price: The price from n periods ago.\n\n    Returns:\n      The calculated ROCP value.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> rocp = kand.rocp_inc(11.5, 10.0)\n      >>> print(rocp)\n      0.15\n      ```\n    \"\"\"\n    ...\n\ndef rocr(data, period):\n    \"\"\"\n    Computes the Rate of Change Ratio (ROCR) over a NumPy array.\n\n    The Rate of Change Ratio (ROCR) is a momentum indicator that measures the ratio between\n    the current price and the price n periods ago.\n\n    Args:\n      data: Input price data as a 1-D NumPy array of type `TAFloat`.\n      period: Number of periods to look back. Must be >= 2.\n\n    Returns:\n      A new 1-D NumPy array containing the ROCR values. The array has the same length as the input,\n      with the first `period` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> data = np.array([10.0, 10.5, 11.2, 10.8, 11.5])\n      >>> result = kand.rocr(data, 2)\n      >>> print(result)\n      [nan, nan, 1.12, 1.0286, 1.0648]\n      ```\n    \"\"\"\n    ...\n\ndef rocr100(data, period):\n    \"\"\"\n    Computes the Rate of Change Ratio * 100 (ROCR100) over a NumPy array.\n\n    ROCR100 is a momentum indicator that measures the percentage change in price over a specified period.\n    It compares the current price to a past price and expresses the ratio as a percentage.\n    Values above 100 indicate price increases, while values below 100 indicate price decreases.\n\n    Args:\n      data: Input price data as a 1-D NumPy array of type `TAFloat`.\n      period: Number of periods to look back. Must be >= 2.\n\n    Returns:\n      A new 1-D NumPy array containing the ROCR100 values. The array has the same length as the input,\n      with the first `period` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> data = np.array([10.0, 10.5, 11.2, 10.8, 11.5])\n      >>> result = kand.rocr100(data, 2)\n      >>> print(result)\n      [nan, nan, 106.67, 102.86, 106.48]\n      ```\n    \"\"\"\n    ...\n\ndef rocr100_inc(current_price, prev_price):\n    \"\"\"\n    Calculates a single ROCR100 value incrementally.\n\n    This function provides an optimized way to calculate the latest ROCR100 value\n    when streaming data is available, without needing the full price history.\n\n    Args:\n      current_price: The current period's price value.\n      prev_price: The price from n periods ago.\n\n    Returns:\n      The calculated ROCR100 value.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> rocr100 = kand.rocr100_inc(11.5, 10.0)\n      >>> print(rocr100)\n      115.0\n      ```\n    \"\"\"\n    ...\n\ndef rocr_inc(current_price, prev_price):\n    \"\"\"\n    Calculates a single ROCR value incrementally.\n\n    This function provides an optimized way to calculate the latest ROCR value\n    when streaming data is available, without needing the full price history.\n\n    Args:\n      current_price: The current period's price value.\n      prev_price: The price from n periods ago.\n\n    Returns:\n      The calculated ROCR value.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> rocr = kand.rocr_inc(11.5, 10.0)\n      >>> print(rocr)\n      1.15\n      ```\n    \"\"\"\n    ...\n\ndef rsi(prices, period):\n    \"\"\"\n    Computes the Relative Strength Index (RSI) over NumPy arrays.\n\n    The RSI is a momentum oscillator that measures the speed and magnitude of recent price changes\n    to evaluate overbought or oversold conditions.\n\n    Args:\n      prices: Input prices as a 1-D NumPy array of type `TAFloat`.\n      period: Window size for RSI calculation. Must be positive and less than input length.\n\n    Returns:\n      A tuple of three 1-D NumPy arrays containing:\n      - RSI values\n      - Average gain values\n      - Average loss values\n      Each array has the same length as the input, with the first `period` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> prices = np.array([44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42])\n      >>> rsi, avg_gain, avg_loss = kand.rsi(prices, 5)\n      ```\n    \"\"\"\n    ...\n\ndef rsi_inc(current_price, prev_price, prev_avg_gain, prev_avg_loss, period):\n    \"\"\"\n    Calculates a single RSI value incrementally.\n\n    This function provides an optimized way to calculate the latest RSI value\n    when streaming data is available, without needing the full price history.\n\n    Args:\n      current_price: The current period's price value.\n      prev_price: The previous period's price value.\n      prev_avg_gain: The previous period's average gain.\n      prev_avg_loss: The previous period's average loss.\n      period: The time period for RSI calculation.\n\n    Returns:\n      A tuple containing (RSI value, new average gain, new average loss).\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> rsi, avg_gain, avg_loss = kand.rsi_inc(45.42, 45.10, 0.24, 0.14, 14)\n      ```\n    \"\"\"\n    ...\n\ndef sar(high, low, acceleration, maximum):\n    \"\"\"\n    Calculates the Parabolic SAR (Stop And Reverse) indicator over NumPy arrays.\n\n    Args:\n      high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n      low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n      acceleration: Initial acceleration factor (e.g. 0.02).\n      maximum: Maximum acceleration factor (e.g. 0.2).\n\n    Returns:\n      A tuple of four 1-D NumPy arrays containing:\n      - SAR values\n      - Trend direction (true=long, false=short)\n      - Acceleration factors\n      - Extreme points\n      Each array has the same length as the input, with the first element containing NaN.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n      >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n      >>> sar, is_long, af, ep = kand.sar(high, low, 0.02, 0.2)\n      ```\n    \"\"\"\n    ...\n\ndef sar_inc(high, low, prev_high, prev_low, prev_sar, is_long, af, ep, acceleration, maximum):\n    \"\"\"\n    Incrementally updates the Parabolic SAR with new price data.\n\n    Args:\n      high: Current period's high price.\n      low: Current period's low price.\n      prev_high: Previous period's high price.\n      prev_low: Previous period's low price.\n      prev_sar: Previous period's SAR value.\n      is_long: Current trend direction (true=long, false=short).\n      af: Current acceleration factor.\n      ep: Current extreme point.\n      acceleration: Acceleration factor increment.\n      maximum: Maximum acceleration factor.\n\n    Returns:\n      A tuple containing (SAR value, trend direction, acceleration factor, extreme point).\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> sar, is_long, af, ep = kand.sar_inc(\n      ...     15.0, 14.0, 14.5, 13.5, 13.0, True, 0.02, 14.5, 0.02, 0.2\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef sma(data, period):\n    \"\"\"\n    Computes the Simple Moving Average (SMA) over a NumPy array.\n\n    The Simple Moving Average is calculated by taking the arithmetic mean of a window of values\n    that moves across the input array. For each position, it sums the previous `period` values\n    and divides by the period size.\n\n    Args:\n        data: Input data as a 1-D NumPy array of type `TAFloat`.\n        period: Window size for SMA calculation. Must be positive and less than input length.\n\n    Returns:\n        A new 1-D NumPy array containing the SMA values. The array has the same length as the input,\n        with the first `period-1` elements containing NaN values.\n\n    Examples:\n        ```python\n        >>> import numpy as np\n        >>> import kand\n        >>> data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n        >>> result = kand.sma(data, 3)\n        >>> print(result)\n        [nan, nan, 2.0, 3.0, 4.0]\n        ```\n    \"\"\"\n    ...\n\ndef sma_inc(prev_sma, new_price, old_price, period):\n    \"\"\"\n    Incrementally calculates the next SMA value.\n\n    This function provides an optimized way to update an existing SMA value\n    when new data arrives, without recalculating the entire series.\n\n    Args:\n        prev_sma: Previous SMA value.\n        new_price: New price to include in calculation.\n        old_price: Oldest price to remove from calculation.\n        period: The time period for SMA calculation (must be >= 2).\n\n    Returns:\n        The next SMA value.\n\n    Examples:\n        ```python\n        >>> import kand\n        >>> prev_sma = 4.0\n        >>> new_price = 10.0\n        >>> old_price = 2.0\n        >>> period = 3\n        >>> next_sma = kand.sma_inc(prev_sma, new_price, old_price, period)\n        >>> print(next_sma)\n        6.666666666666666\n        ```\n    \"\"\"\n    ...\n\ndef stddev(input, period):\n    \"\"\"\n    Calculate Standard Deviation for a NumPy array\n\n    Standard Deviation measures the dispersion of values from their mean over a specified period.\n    It is calculated by taking the square root of the variance.\n\n    Args:\n      input: Input values as a 1-D NumPy array of type `TAFloat`.\n      period: Period for calculation (must be >= 2).\n\n    Returns:\n      A tuple of three 1-D NumPy arrays containing:\n      - Standard Deviation values\n      - Running sum values\n      - Running sum of squares values\n      Each array has the same length as the input, with the first (period-1) elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> prices = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n      >>> stddev, sum, sum_sq = kand.stddev(prices, 3)\n      ```\n    \"\"\"\n    ...\n\ndef stddev_inc(price, prev_sum, prev_sum_sq, old_price, period):\n    \"\"\"\n    Calculate the latest Standard Deviation value incrementally\n\n    Args:\n      py: Python interpreter token\n      price: Current period's price\n      prev_sum: Previous period's sum\n      prev_sum_sq: Previous period's sum of squares\n      old_price: Price being removed from the period\n      period: Period for calculation (must be >= 2)\n\n    Returns:\n      A tuple containing:\n      - Latest Standard Deviation value\n      - New sum\n      - New sum of squares\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> stddev, sum, sum_sq = kand.stddev_inc(\n      ...     10.0,   # current price\n      ...     100.0,  # previous sum\n      ...     1050.0, # previous sum of squares\n      ...     8.0,    # old price\n      ...     14      # period\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef stoch(high, low, close, k_period, k_slow_period, d_period):\n    \"\"\"\n    Computes the Stochastic Oscillator indicator over NumPy arrays.\n\n    The Stochastic Oscillator is a momentum indicator that shows the location of the close\n    relative to the high-low range over a set number of periods. The indicator consists of\n    two lines: %K (the fast line) and %D (the slow line).\n\n    Args:\n        high: High prices as a 1-D NumPy array of type `TAFloat`.\n        low: Low prices as a 1-D NumPy array of type `TAFloat`.\n        close: Close prices as a 1-D NumPy array of type `TAFloat`.\n        k_period: Period for %K calculation. Must be >= 2.\n        k_slow_period: Smoothing period for slow %K. Must be >= 2.\n        d_period: Period for %D calculation. Must be >= 2.\n\n    Returns:\n        A tuple of three 1-D NumPy arrays containing:\n        - Fast %K values\n        - Slow %K values\n        - %D values\n        Each array has the same length as the input, with initial values being NaN.\n\n    Examples:\n        ```python\n        >>> import numpy as np\n        >>> import kand\n        >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n        >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n        >>> close = np.array([9.0, 11.0, 14.0, 12.0, 11.0])\n        >>> fast_k, k, d = kand.stoch(high, low, close, 3, 2, 2)\n        ```\n    \"\"\"\n    ...\n\ndef sum(input, period):\n    \"\"\"\n    Calculate Sum for a NumPy array\n\n    Calculates the rolling sum of values over a specified period.\n\n    Args:\n      input: Input values as a 1-D NumPy array of type `TAFloat`.\n      period: Period for sum calculation (must be >= 2).\n\n    Returns:\n      A 1-D NumPy array containing the sum values.\n      The first (period-1) elements contain NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n      >>> sums = kand.sum(data, 3)\n      ```\n    \"\"\"\n    ...\n\ndef sum_inc(new_price, old_price, prev_sum):\n    \"\"\"\n    Calculate the latest sum value incrementally\n\n    Args:\n      py: Python interpreter token\n      new_price: The newest price value to add\n      old_price: The oldest price value to remove\n      prev_sum: The previous sum value\n\n    Returns:\n      The new sum value\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> new_sum = kand.sum_inc(\n      ...     5.0,    # new price\n      ...     3.0,    # old price\n      ...     10.0,   # previous sum\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef supertrend(high, low, close, period, multiplier):\n    \"\"\"\n    Computes the Supertrend indicator over NumPy arrays.\n\n    The Supertrend indicator is a trend-following indicator that combines Average True Range (ATR)\n    with basic upper and lower bands to identify trend direction and potential reversal points.\n\n    Args:\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      period: Period for ATR calculation (typically 7-14). Must be positive.\n      multiplier: ATR multiplier (typically 2-4).\n\n    Returns:\n      A tuple of five 1-D NumPy arrays:\n      - trend: Array containing trend direction (1.0 for uptrend, -1.0 for downtrend)\n      - supertrend: Array containing Supertrend values\n      - atr: Array containing ATR values\n      - upper: Array containing upper band values\n      - lower: Array containing lower band values\n      All arrays have the same length as the input, with the first `period-1` elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n      >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n      >>> close = np.array([9.0, 11.0, 14.0, 12.0, 11.0])\n      >>> trend, supertrend, atr, upper, lower = kand.supertrend(high, low, close, 3, 3.0)\n      ```\n    \"\"\"\n    ...\n\ndef supertrend_inc(high, low, close, prev_close, prev_atr, prev_trend, prev_upper, prev_lower, period, multiplier):\n    \"\"\"\n    Calculates a single Supertrend value incrementally.\n\n    This function provides an optimized way to calculate the latest Supertrend value\n    using previous values, making it ideal for real-time calculations.\n\n    Args:\n      high: Current period's high price\n      low: Current period's low price\n      close: Current period's close price\n      prev_close: Previous period's close price\n      prev_atr: Previous period's ATR value\n      prev_trend: Previous period's trend direction (1 for uptrend, -1 for downtrend)\n      prev_upper: Previous period's upper band\n      prev_lower: Previous period's lower band\n      period: ATR calculation period (typically 7-14)\n      multiplier: ATR multiplier (typically 2-4)\n\n    Returns:\n      A tuple containing:\n      - trend: Current trend direction (1 for uptrend, -1 for downtrend)\n      - supertrend: Current Supertrend value\n      - atr: Current ATR value\n      - upper: Current upper band\n      - lower: Current lower band\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> trend, supertrend, atr, upper, lower = kand.supertrend_inc(\n      ...     15.0,   # Current high\n      ...     11.0,   # Current low\n      ...     14.0,   # Current close\n      ...     11.0,   # Previous close\n      ...     2.0,    # Previous ATR\n      ...     1,      # Previous trend\n      ...     16.0,   # Previous upper band\n      ...     10.0,   # Previous lower band\n      ...     7,      # ATR period\n      ...     3.0,    # Multiplier\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef t3(data, period, vfactor):\n    \"\"\"\n    Computes the T3 (Triple Exponential Moving Average) indicator over a NumPy array.\n\n    T3 is a sophisticated moving average developed by Tim Tillson that reduces lag while maintaining smoothness.\n    It combines six EMAs with optimized weightings to produce a responsive yet smooth indicator.\n\n    Args:\n        data: Input data as a 1-D NumPy array of type `TAFloat`.\n        period: Smoothing period for EMAs (must be >= 2).\n        vfactor: Volume factor controlling smoothing (typically 0-1).\n\n    Returns:\n        A tuple of seven 1-D NumPy arrays containing:\n        - T3 values\n        - EMA1 values\n        - EMA2 values\n        - EMA3 values\n        - EMA4 values\n        - EMA5 values\n        - EMA6 values\n        Each array has the same length as the input, with initial values being NaN.\n\n    Examples:\n        ```python\n        >>> import numpy as np\n        >>> import kand\n        >>> data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n        >>> t3, e1, e2, e3, e4, e5, e6 = kand.t3(data, 2, 0.7)\n        ```\n    \"\"\"\n    ...\n\ndef t3_inc(price, prev_ema1, prev_ema2, prev_ema3, prev_ema4, prev_ema5, prev_ema6, period, vfactor):\n    \"\"\"\n    Incrementally calculates the next T3 value.\n\n    This function provides an optimized way to update T3 values in real-time by using\n    previously calculated EMA values.\n\n    Args:\n        price: Latest price value to calculate T3 from.\n        prev_ema1: Previous EMA1 value.\n        prev_ema2: Previous EMA2 value.\n        prev_ema3: Previous EMA3 value.\n        prev_ema4: Previous EMA4 value.\n        prev_ema5: Previous EMA5 value.\n        prev_ema6: Previous EMA6 value.\n        period: Smoothing period for EMAs (must be >= 2).\n        vfactor: Volume factor (typically 0-1).\n\n    Returns:\n        A tuple containing:\n        - Latest T3 value\n        - Updated EMA1 value\n        - Updated EMA2 value\n        - Updated EMA3 value\n        - Updated EMA4 value\n        - Updated EMA5 value\n        - Updated EMA6 value\n\n    Examples:\n        ```python\n        >>> import kand\n        >>> t3, e1, e2, e3, e4, e5, e6 = kand.t3_inc(\n        ...     100.0,  # New price\n        ...     95.0,   # Previous EMA1\n        ...     94.0,   # Previous EMA2\n        ...     93.0,   # Previous EMA3\n        ...     92.0,   # Previous EMA4\n        ...     91.0,   # Previous EMA5\n        ...     90.0,   # Previous EMA6\n        ...     5,      # Period\n        ...     0.7,    # Volume factor\n        ... )\n        ```\n    \"\"\"\n    ...\n\ndef tema(prices, period):\n    \"\"\"\n    Calculate Triple Exponential Moving Average (TEMA) for a NumPy array.\n\n    TEMA is an enhanced moving average designed to reduce lag while maintaining smoothing properties.\n    It applies triple exponential smoothing to put more weight on recent data and less on older data.\n\n    Args:\n      prices: Input prices as a 1-D NumPy array of type `TAFloat`.\n      period: Smoothing period for calculations (must be >= 2).\n\n    Returns:\n      A tuple of 4 1-D NumPy arrays containing:\n      - TEMA values\n      - First EMA values\n      - Second EMA values\n      - Third EMA values\n      The first (3 * (period - 1)) elements of each array contain NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> prices = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])\n      >>> tema, ema1, ema2, ema3 = kand.tema(prices, 3)\n      ```\n    \"\"\"\n    ...\n\ndef tema_inc(new_price, prev_ema1, prev_ema2, prev_ema3, period):\n    \"\"\"\n    Calculate the next TEMA value incrementally.\n\n    Args:\n      new_price: Latest price value to process.\n      prev_ema1: Previous value of first EMA.\n      prev_ema2: Previous value of second EMA.\n      prev_ema3: Previous value of third EMA.\n      period: Smoothing period for calculations (must be >= 2).\n\n    Returns:\n      A tuple containing:\n      - Current TEMA value\n      - Updated first EMA\n      - Updated second EMA\n      - Updated third EMA\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> tema, ema1, ema2, ema3 = kand.tema_inc(\n      ...     10.0,  # new_price\n      ...     9.0,   # prev_ema1\n      ...     8.0,   # prev_ema2\n      ...     7.0,   # prev_ema3\n      ...     3      # period\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef trange(high, low, close):\n    \"\"\"\n    Computes the True Range (TR) over NumPy arrays.\n\n    True Range measures the market's volatility by considering the current high-low range\n    and the previous close price.\n\n    Args:\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n\n    Returns:\n      A new 1-D NumPy array containing the TR values. The array has the same length as the input,\n      with the first element containing NaN value.\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([10.0, 12.0, 15.0])\n      >>> low = np.array([8.0, 9.0, 11.0])\n      >>> close = np.array([9.0, 11.0, 14.0])\n      >>> result = kand.trange(high, low, close)\n      >>> print(result)\n      [nan, 3.0, 4.0]\n      ```\n    \"\"\"\n    ...\n\ndef trange_inc(high, low, prev_close):\n    \"\"\"\n    Calculates a single True Range value for the most recent period.\n\n    Args:\n      high: Current period's high price.\n      low: Current period's low price.\n      prev_close: Previous period's closing price.\n\n    Returns:\n      The calculated True Range value.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> tr = kand.trange_inc(12.0, 9.0, 11.0)\n      >>> print(tr)\n      3.0  # max(3, 1, 2)\n      ```\n    \"\"\"\n    ...\n\ndef trima(prices, period):\n    \"\"\"\n    Calculate Triangular Moving Average (TRIMA) for a NumPy array.\n\n    TRIMA is a double-smoothed moving average that places more weight on the middle portion of the price series\n    and less weight on the first and last portions. This results in a smoother moving average compared to a\n    Simple Moving Average (SMA).\n\n    Args:\n      prices: Input prices as a 1-D NumPy array of type `TAFloat`.\n      period: Smoothing period for calculations (must be >= 2).\n\n    Returns:\n      A tuple of 2 1-D NumPy arrays containing:\n      - First SMA values\n      - Final TRIMA values\n      The first (period - 1) elements of each array contain NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> prices = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n      >>> sma1, trima = kand.trima(prices, 3)\n      ```\n    \"\"\"\n    ...\n\ndef trima_inc(prev_sma1, prev_sma2, new_price, old_price, old_sma1, period):\n    \"\"\"\n    Calculate the next TRIMA value incrementally.\n\n    Args:\n      prev_sma1: Previous first SMA value.\n      prev_sma2: Previous TRIMA value.\n      new_price: Latest price to include in calculation.\n      old_price: Price dropping out of first window.\n      old_sma1: SMA1 value dropping out of second window.\n      period: Smoothing period for calculations (must be >= 2).\n\n    Returns:\n      A tuple containing:\n      - Updated first SMA value\n      - Updated TRIMA value\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> trima, sma1 = kand.trima_inc(\n      ...     35.5,  # prev_sma1\n      ...     35.2,  # prev_sma2\n      ...     36.0,  # new_price\n      ...     35.0,  # old_price\n      ...     35.1,  # old_sma1\n      ...     5      # period\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef trix(prices, period):\n    \"\"\"\n    Calculates the Triple Exponential Moving Average Oscillator (TRIX) over a NumPy array.\n\n    TRIX is a momentum oscillator that measures the rate of change of a triple exponentially smoothed moving average.\n    It helps identify oversold and overbought conditions and potential trend reversals through divergences.\n\n    Args:\n      prices: Input prices as a 1-D NumPy array of type `TAFloat`.\n      period: Period for EMA calculations (must be >= 2).\n\n    Returns:\n      A tuple of 4 1-D NumPy arrays containing:\n      - TRIX values\n      - First EMA values\n      - Second EMA values\n      - Third EMA values\n      The first lookback elements of each array contain NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> prices = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n      >>> trix, ema1, ema2, ema3 = kand.trix(prices, 2)\n      ```\n    \"\"\"\n    ...\n\ndef trix_inc(price, prev_ema1, prev_ema2, prev_ema3, period):\n    \"\"\"\n    Calculates a single new TRIX value incrementally.\n\n    Args:\n      price: Current price value.\n      prev_ema1: Previous first EMA value.\n      prev_ema2: Previous second EMA value.\n      prev_ema3: Previous third EMA value.\n      period: Period for EMA calculations (must be >= 2).\n\n    Returns:\n      A tuple containing:\n      - TRIX value\n      - Updated first EMA value\n      - Updated second EMA value\n      - Updated third EMA value\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> trix, ema1, ema2, ema3 = kand.trix_inc(\n      ...     100.0,  # price\n      ...     98.0,   # prev_ema1\n      ...     97.0,   # prev_ema2\n      ...     96.0,   # prev_ema3\n      ...     14      # period\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef typprice(high, low, close):\n    \"\"\"\n    Computes the Typical Price over NumPy arrays.\n\n    The Typical Price is calculated by taking the arithmetic mean of the high, low and close prices\n    for each period.\n\n    Args:\n      high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n      low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Input close prices as a 1-D NumPy array of type `TAFloat`.\n\n    Returns:\n      A new 1-D NumPy array containing the Typical Price values. The array has the same length as the inputs.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([24.20, 24.07, 24.04])\n      >>> low = np.array([23.85, 23.72, 23.64])\n      >>> close = np.array([23.89, 23.95, 23.67])\n      >>> result = kand.typprice(high, low, close)\n      >>> print(result)\n      [23.98, 23.91, 23.78]\n      ```\n    \"\"\"\n    ...\n\ndef typprice_inc(high, low, close):\n    \"\"\"\n    Calculates a single Typical Price value incrementally.\n\n    Args:\n      high: Current period's high price.\n      low: Current period's low price.\n      close: Current period's close price.\n\n    Returns:\n      The calculated Typical Price value.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> typ_price = kand.typprice_inc(24.20, 23.85, 23.89)\n      >>> print(typ_price)\n      23.98  # (24.20 + 23.85 + 23.89) / 3\n      ```\n    \"\"\"\n    ...\n\ndef var(prices, period):\n    \"\"\"\n    Calculate Variance (VAR) for a NumPy array\n\n    Variance measures the average squared deviation of data points from their mean over a specified period.\n\n    Args:\n      prices: Input prices as a 1-D NumPy array of type `TAFloat`.\n      period: Period for Variance calculation (must be >= 2).\n\n    Returns:\n      A tuple of three 1-D NumPy arrays containing:\n      - Variance values\n      - Running sum values\n      - Running sum of squares values\n      Each array has the same length as the input, with the first (period-1) elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> prices = np.array([2.0, 4.0, 6.0, 8.0, 10.0])\n      >>> var, sum, sum_sq = kand.var(prices, 3)\n      ```\n    \"\"\"\n    ...\n\ndef var_inc(price, prev_sum, prev_sum_sq, old_price, period):\n    \"\"\"\n    Calculate the latest Variance value incrementally\n\n    Args:\n      py: Python interpreter token\n      price: Current period's price\n      prev_sum: Previous period's sum\n      prev_sum_sq: Previous period's sum of squares\n      old_price: Price being removed from the period\n      period: Period for Variance calculation (must be >= 2)\n\n    Returns:\n      A tuple containing:\n      - Latest Variance value\n      - New sum\n      - New sum of squares\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> var, sum, sum_sq = kand.var_inc(\n      ...     10.0,  # current price\n      ...     25.0,  # previous sum\n      ...     220.0, # previous sum of squares\n      ...     5.0,   # price to remove\n      ...     3      # period\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef vegas(prices):\n    \"\"\"\n    Computes the VEGAS (Volume and EMA Guided Adaptive Scaling) indicator over NumPy arrays.\n\n    VEGAS is a trend following indicator that uses multiple EMAs to define channels and boundaries.\n\n    Args:\n      prices: Input prices as a 1-D NumPy array of type `TAFloat`.\n\n    Returns:\n      A tuple of four 1-D NumPy arrays containing:\n      - Channel Upper (EMA 144)\n      - Channel Lower (EMA 169)\n      - Boundary Upper (EMA 576)\n      - Boundary Lower (EMA 676)\n      Each array has the same length as the input, with the first 675 elements containing NaN values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> prices = np.array([44.34, 44.09, 44.15, 43.61, 44.33])\n      >>> ch_upper, ch_lower, b_upper, b_lower = kand.vegas(prices)\n      ```\n    \"\"\"\n    ...\n\ndef vegas_inc(price, prev_channel_upper, prev_channel_lower, prev_boundary_upper, prev_boundary_lower):\n    \"\"\"\n    Incrementally calculates the next VEGAS values.\n\n    Args:\n      price: Current price value.\n      prev_channel_upper: Previous EMA(144) value.\n      prev_channel_lower: Previous EMA(169) value.\n      prev_boundary_upper: Previous EMA(576) value.\n      prev_boundary_lower: Previous EMA(676) value.\n\n    Returns:\n      A tuple containing:\n      - Updated Channel Upper value\n      - Updated Channel Lower value\n      - Updated Boundary Upper value\n      - Updated Boundary Lower value\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> price = 100.0\n      >>> prev_values = (98.0, 97.5, 96.0, 95.5)\n      >>> ch_upper, ch_lower, b_upper, b_lower = kand.vegas_inc(\n      ...     price,\n      ...     prev_values[0],\n      ...     prev_values[1],\n      ...     prev_values[2],\n      ...     prev_values[3]\n      ... )\n      ```\n    \"\"\"\n    ...\n\ndef vwap(high, low, close, volume):\n    \"\"\"\n    Calculates Volume Weighted Average Price (VWAP) for a series of price data.\n\n    Args:\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n      volume: Volume data as a 1-D NumPy array of type `TAFloat`.\n\n    Returns:\n      A tuple of three 1-D NumPy arrays containing:\n      - VWAP values\n      - Cumulative price-volume products\n      - Cumulative volumes\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([10.0, 12.0, 15.0])\n      >>> low = np.array([8.0, 9.0, 11.0])\n      >>> close = np.array([9.0, 10.0, 12.0])\n      >>> volume = np.array([100.0, 150.0, 200.0])\n      >>> vwap, cum_pv, cum_vol = kand.vwap(high, low, close, volume)\n      ```\n    \"\"\"\n    ...\n\ndef vwap_inc(high, low, close, volume, prev_cum_pv, prev_cum_vol):\n    \"\"\"\n    Calculates a single VWAP value from the latest price and volume data.\n\n    Args:\n      high: Latest high price value as `TAFloat`.\n      low: Latest low price value as `TAFloat`.\n      close: Latest close price value as `TAFloat`.\n      volume: Latest volume value as `TAFloat`.\n      prev_cum_pv: Previous cumulative price-volume product as `TAFloat`.\n      prev_cum_vol: Previous cumulative volume as `TAFloat`.\n\n    Returns:\n      A tuple containing (new cumulative PV, new cumulative volume, new VWAP).\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> new_cum_pv, new_cum_vol, vwap = kand.vwap_inc(15.0, 11.0, 14.0, 200.0, 1000.0, 150.0)\n      ```\n    \"\"\"\n    ...\n\ndef wclprice(high, low, close):\n    \"\"\"\n    Calculates the Weighted Close Price (WCLPRICE) for a series of price data.\n\n    The Weighted Close Price is a price indicator that assigns more weight to the closing price\n    compared to high and low prices. It provides a single value that reflects price action\n    with emphasis on the closing price.\n\n    Args:\n      high: High prices as a 1-D NumPy array of type `TAFloat`.\n      low: Low prices as a 1-D NumPy array of type `TAFloat`.\n      close: Close prices as a 1-D NumPy array of type `TAFloat`.\n\n    Returns:\n      A 1-D NumPy array containing the WCLPRICE values.\n\n    Examples:\n      ```python\n      >>> import numpy as np\n      >>> import kand\n      >>> high = np.array([10.0, 12.0, 15.0])\n      >>> low = np.array([8.0, 9.0, 11.0])\n      >>> close = np.array([9.0, 11.0, 14.0])\n      >>> wclprice = kand.wclprice(high, low, close)\n      ```\n    \"\"\"\n    ...\n\ndef wclprice_inc(high, low, close):\n    \"\"\"\n    Calculates a single Weighted Close Price (WCLPRICE) value from the latest price data.\n\n    Args:\n      high: Latest high price value as `TAFloat`.\n      low: Latest low price value as `TAFloat`.\n      close: Latest close price value as `TAFloat`.\n\n    Returns:\n      The calculated WCLPRICE value.\n\n    Examples:\n      ```python\n      >>> import kand\n      >>> wclprice = kand.wclprice_inc(15.0, 11.0, 14.0)\n      ```\n    \"\"\"\n    ...\n\ndef willr(high, low, close, period):\n    \"\"\"\n    Calculates Williams %R (Williams Percent Range) for a series of prices.\n\n    Williams %R is a momentum indicator that measures overbought and oversold levels by comparing\n    the closing price to the high-low range over a specified period. The indicator oscillates\n    between 0 and -100.\n\n    Args:\n        high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n        low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n        close: Input closing prices as a 1-D NumPy array of type `TAFloat`.\n        period: Lookback period for calculations. Must be >= 2.\n\n    Returns:\n        A tuple of three 1-D NumPy arrays containing:\n        - Williams %R values\n        - Highest high values for each period\n        - Lowest low values for each period\n        Each array has the same length as the input, with the first `period-1` elements containing NaN values.\n\n    Examples:\n        ```python\n        >>> import numpy as np\n        >>> import kand\n        >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n        >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n        >>> close = np.array([9.0, 11.0, 14.0, 12.0, 11.0])\n        >>> willr, highest, lowest = kand.willr(high, low, close, 3)\n        ```\n    \"\"\"\n    ...\n\ndef willr_inc(prev_highest_high, prev_lowest_low, prev_high, prev_low, close, high, low):\n    \"\"\"\n    Incrementally calculates Williams %R for the latest data point.\n\n    This function provides an optimized way to calculate the latest Williams %R value\n    by using previously calculated highest high and lowest low values.\n\n    Args:\n        prev_highest_high: Previous period's highest high value.\n        prev_lowest_low: Previous period's lowest low value.\n        prev_high: Previous period's high price.\n        prev_low: Previous period's low price.\n        close: Current period's closing price.\n        high: Current period's high price.\n        low: Current period's low price.\n\n    Returns:\n        A tuple containing:\n        - Current Williams %R value\n        - New highest high\n        - New lowest low\n\n    Examples:\n        ```python\n        >>> import kand\n        >>> willr, high, low = kand.willr_inc(15.0, 10.0, 14.0, 11.0, 12.0, 13.0, 11.0)\n        ```\n    \"\"\"\n    ...\n\ndef wma(data, period):\n    \"\"\"\n    Computes the Weighted Moving Average (WMA) over a NumPy array.\n\n    The Weighted Moving Average assigns linearly decreasing weights to each price in the period,\n    giving more importance to recent prices and less to older ones.\n\n    Args:\n        data: Input data as a 1-D NumPy array of type `TAFloat`.\n        period: Window size for WMA calculation. Must be >= 2.\n\n    Returns:\n        A new 1-D NumPy array containing the WMA values. The array has the same length as the input,\n        with the first `period-1` elements containing NaN values.\n\n    Examples:\n        ```python\n        >>> import numpy as np\n        >>> import kand\n        >>> data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n        >>> result = kand.wma(data, 3)\n        >>> print(result)\n        [nan, nan, 2.0, 3.0, 4.0]\n        ```\n    \"\"\"\n    ...\n\ndef wma_inc(input_window, period):\n    \"\"\"\n    Incrementally calculates the next WMA value.\n\n    This function provides an optimized way to calculate the latest WMA value\n    by using a window of the most recent prices.\n\n    Args:\n        input_window: Array of price values ordered from newest to oldest.\n        period: The time period for WMA calculation (must be >= 2).\n\n    Returns:\n        The next WMA value.\n\n    Examples:\n        ```python\n        >>> import kand\n        >>> window = [5.0, 4.0, 3.0]  # newest to oldest\n        >>> wma = kand.wma_inc(window, 3)\n        >>> print(wma)\n        4.333333333333333\n        ```\n    \"\"\"\n    ...\n"
  },
  {
    "path": "kand-py/python/kand/py.typed",
    "content": ""
  },
  {
    "path": "kand-py/src/helper.rs",
    "content": ""
  },
  {
    "path": "kand-py/src/lib.rs",
    "content": "use pyo3::prelude::*;\n\npub mod ta;\n\n/// A Python module implemented in Rust.\n#[rustfmt::skip]\n#[pymodule]\n#[pyo3(name = \"_kand\")]\nfn kand(m: &Bound<'_, PyModule>) -> PyResult<()> {\n\n    // Add all OHLCV functions\n    m.add_function(wrap_pyfunction!(ta::ohlcv::ad::ad_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::ad::ad_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::adosc::adosc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::adosc::adosc_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::adx::adx_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::adx::adx_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::adxr::adxr_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::adxr::adxr_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::aroon::aroon_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::aroon::aroon_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::aroonosc::aroonosc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::aroonosc::aroonosc_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::atr::atr_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::atr::atr_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::bbands::bbands_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::bbands::bbands_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::bop::bop_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::bop::bop_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::cci::cci_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::cci::cci_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::cdl_doji::cdl_doji_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::cdl_doji::cdl_doji_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::cdl_dragonfly_doji::cdl_dragonfly_doji_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::cdl_dragonfly_doji::cdl_dragonfly_doji_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::cdl_gravestone_doji::cdl_gravestone_doji_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::cdl_gravestone_doji::cdl_gravestone_doji_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::cdl_hammer::cdl_hammer_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::cdl_hammer::cdl_hammer_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::cdl_inverted_hammer::cdl_inverted_hammer_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::cdl_inverted_hammer::cdl_inverted_hammer_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::cdl_long_shadow::cdl_long_shadow_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::cdl_long_shadow::cdl_long_shadow_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::cdl_marubozu::cdl_marubozu_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::cdl_marubozu::cdl_marubozu_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::dema::dema_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::dema::dema_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::dx::dx_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::dx::dx_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::ecl::ecl_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::ecl::ecl_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::ema::ema_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::ema::ema_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::macd::macd_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::macd::macd_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::medprice::medprice_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::medprice::medprice_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::mfi::mfi_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::midpoint::midpoint_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::midpoint::midpoint_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::midprice::midprice_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::midprice::midprice_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::minus_di::minus_di_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::minus_dm::minus_dm_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::mom::mom_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::mom::mom_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::natr::natr_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::natr::natr_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::obv::obv_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::obv::obv_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::plus_di::plus_di_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::plus_di::plus_di_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::plus_dm::plus_dm_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::plus_dm::plus_dm_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::rma::rma_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::rma::rma_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::roc::roc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::roc::roc_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::rocp::rocp_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::rocp::rocp_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::rocr::rocr_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::rocr::rocr_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::rocr100::rocr100_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::rocr100::rocr100_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::rsi::rsi_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::rsi::rsi_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::sar::sar_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::sar::sar_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::sma::sma_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::sma::sma_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::stoch::stoch_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::supertrend::supertrend_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::supertrend::supertrend_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::t3::t3_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::t3::t3_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::tema::tema_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::tema::tema_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::trange::trange_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::trange::trange_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::trima::trima_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::trima::trima_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::trix::trix_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::trix::trix_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::typprice::typprice_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::typprice::typprice_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::vegas::vegas_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::vegas::vegas_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::wclprice::wclprice_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::wclprice::wclprice_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::willr::willr_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::willr::willr_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::wma::wma_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::wma::wma_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::vwap::vwap_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::ohlcv::vwap::vwap_inc_py, m)?)?;\n\n    // Add all stats functions\n    m.add_function(wrap_pyfunction!(ta::stats::correl::correl_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::stats::correl::correl_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::stats::max::max_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::stats::max::max_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::stats::min::min_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::stats::min::min_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::stats::stddev::stddev_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::stats::stddev::stddev_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::stats::sum::sum_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::stats::sum::sum_inc_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::stats::var::var_py, m)?)?;\n    m.add_function(wrap_pyfunction!(ta::stats::var::var_inc_py, m)?)?;\n\n    // Add all helper functions\n\n    Ok(())\n}\n"
  },
  {
    "path": "kand-py/src/ta/mod.rs",
    "content": "pub mod ohlcv;\npub mod stats;\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/ad.rs",
    "content": "use kand::{TAFloat, ohlcv::ad};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Accumulation/Distribution (A/D) indicator over NumPy arrays.\n///\n/// The A/D indicator measures the cumulative flow of money into and out of a security by\n/// combining price and volume data. It helps identify whether buying or selling pressure\n/// is dominant.\n///\n/// Args:\n///     high: High prices as a 1-D NumPy array of type `TAFloat`.\n///     low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///     close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///     volume: Volume data as a 1-D NumPy array of type `TAFloat`.\n///\n/// Returns:\n///     A new 1-D NumPy array containing the A/D values. The array has the same length as the inputs.\n/// Examples:\n///     ```python\n///     >>> import numpy as np\n///     >>> import kand\n///     >>> high = np.array([10.0, 12.0, 15.0])\n///     >>> low = np.array([8.0, 9.0, 11.0])\n///     >>> close = np.array([9.0, 11.0, 13.0])\n///     >>> volume = np.array([100.0, 150.0, 200.0])\n///     >>> result = kand.ad(high, low, close, volume)\n///     >>> print(result)\n///     [-50.0, 25.0, 125.0]\n///     ```\n#[pyfunction]\n#[pyo3(name = \"ad\", signature = (high, low, close, volume))]\npub fn ad_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    volume: PyReadonlyArray1<TAFloat>,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy arrays to Rust slices\n    let high_input = high.as_slice()?;\n    let low_input = low.as_slice()?;\n    let close_input = close.as_slice()?;\n    let volume_input = volume.as_slice()?;\n    let len = high_input.len();\n\n    // Create a new output array\n    let mut output = vec![0.0; len];\n\n    // Perform the A/D calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| {\n        ad::ad(\n            high_input,\n            low_input,\n            close_input,\n            volume_input,\n            &mut output,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Computes the latest Accumulation/Distribution (A/D) value incrementally.\n///\n/// This function calculates only the latest A/D value using the previous A/D value,\n/// avoiding recalculation of the entire series.\n///\n/// Args:\n///     high: Latest high price.\n///     low: Latest low price.\n///     close: Latest closing price.\n///     volume: Latest volume.\n///     prev_ad: Previous A/D value.\n///\n/// Returns:\n///     The latest A/D value.\n///\n/// Examples:\n///     ```python\n///     >>> import kand\n///     >>> high = 15.0\n///     >>> low = 11.0\n///     >>> close = 13.0\n///     >>> volume = 200.0\n///     >>> prev_ad = 25.0\n///     >>> result = kand.ad_inc(high, low, close, volume, prev_ad)\n///     >>> print(result)\n///     125.0\n///     ```\n#[pyfunction]\n#[pyo3(name = \"ad_inc\", signature = (high, low, close, volume, prev_ad))]\npub fn ad_inc_py(\n    py: Python,\n    high: TAFloat,\n    low: TAFloat,\n    close: TAFloat,\n    volume: TAFloat,\n    prev_ad: TAFloat,\n) -> PyResult<TAFloat> {\n    // Perform the incremental A/D calculation while releasing the GIL\n    py.allow_threads(|| ad::ad_inc(high, low, close, volume, prev_ad))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/adosc.rs",
    "content": "use kand::{TAFloat, ohlcv::adosc};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculate Accumulation/Distribution Oscillator (A/D Oscillator or ADOSC)\n///\n/// The A/D Oscillator is a momentum indicator that measures the difference between a fast and slow EMA of the\n/// Accumulation/Distribution Line. It helps identify trend strength and potential reversals.\n///\n/// Args:\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   volume: Volume as a 1-D NumPy array of type `TAFloat`.\n///   fast_period: Fast period for A/D Oscillator calculation.\n///   slow_period: Slow period for A/D Oscillator calculation.\n///\n/// Returns:\n///   A tuple of 4 1-D NumPy arrays containing:\n///   - ADOSC values\n///   - A/D Line values\n///   - Fast EMA values\n///   - Slow EMA values\n///   Each array has the same length as the input, with the first `slow_period-1` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([10.0, 11.0, 12.0, 11.5, 10.5])\n///   >>> low = np.array([8.0, 9.0, 10.0, 9.5, 8.5])\n///   >>> close = np.array([9.0, 10.0, 11.0, 10.0, 9.0])\n///   >>> volume = np.array([100.0, 150.0, 200.0, 150.0, 100.0])\n///   >>> adosc, ad, fast_ema, slow_ema = kand.adosc(high, low, close, volume, 3, 5)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"adosc\", signature = (high, low, close, volume, fast_period, slow_period))]\npub fn adosc_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    volume: PyReadonlyArray1<TAFloat>,\n    fast_period: usize,\n    slow_period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    // Convert input NumPy arrays to Rust slices\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let input_volume = volume.as_slice()?;\n    let len = input_high.len();\n\n    // Create output arrays\n    let mut output_adosc = vec![0.0; len];\n    let mut output_ad = vec![0.0; len];\n    let mut output_fast_ema = vec![0.0; len];\n    let mut output_slow_ema = vec![0.0; len];\n\n    // Perform ADOSC calculation while releasing the GIL\n    py.allow_threads(|| {\n        adosc::adosc(\n            input_high,\n            input_low,\n            input_close,\n            input_volume,\n            fast_period,\n            slow_period,\n            &mut output_adosc,\n            &mut output_ad,\n            &mut output_fast_ema,\n            &mut output_slow_ema,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert output arrays to Python objects\n    Ok((\n        output_adosc.into_pyarray(py).into(),\n        output_ad.into_pyarray(py).into(),\n        output_fast_ema.into_pyarray(py).into(),\n        output_slow_ema.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculate latest A/D Oscillator value incrementally\n///\n/// Provides optimized calculation of the latest ADOSC value when new data arrives,\n/// without recalculating the entire series.\n///\n/// Args:\n///     high: Latest high price.\n///     low: Latest low price.\n///     close: Latest closing price.\n///     volume: Latest volume.\n///     prev_ad: Previous A/D value.\n///     prev_fast_ema: Previous fast EMA value.\n///     prev_slow_ema: Previous slow EMA value.\n///     fast_period: Fast EMA period.\n///     slow_period: Slow EMA period.\n///\n/// Returns:\n///     A tuple containing (ADOSC, AD, Fast EMA, Slow EMA) values.\n///\n/// Examples:\n///     ```python\n///     >>> import kand\n///     >>> adosc, ad, fast_ema, slow_ema = kand.adosc_inc(\n///     ...     10.5,  # high\n///     ...     9.5,   # low\n///     ...     10.0,  # close\n///     ...     150.0, # volume\n///     ...     100.0, # prev_ad\n///     ...     95.0,  # prev_fast_ema\n///     ...     90.0,  # prev_slow_ema\n///     ...     3,     # fast_period\n///     ...     10,    # slow_period\n///     ... )\n///     ```\n#[pyfunction]\n#[pyo3(name = \"adosc_inc\", signature = (high, low, close, volume, prev_ad, prev_fast_ema, prev_slow_ema, fast_period, slow_period))]\npub fn adosc_inc_py(\n    py: Python,\n    high: TAFloat,\n    low: TAFloat,\n    close: TAFloat,\n    volume: TAFloat,\n    prev_ad: TAFloat,\n    prev_fast_ema: TAFloat,\n    prev_slow_ema: TAFloat,\n    fast_period: usize,\n    slow_period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat, TAFloat)> {\n    // Perform the incremental ADOSC calculation while releasing the GIL\n    py.allow_threads(|| {\n        adosc::adosc_inc(\n            high,\n            low,\n            close,\n            volume,\n            prev_ad,\n            prev_fast_ema,\n            prev_slow_ema,\n            fast_period,\n            slow_period,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/adr.rs",
    "content": "use kand::{TAFloat, ohlcv::adr};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Average Daily Range (ADR) over NumPy arrays.\n///\n/// The ADR measures the average price range over a specified period, helping to identify\n/// volatility levels in the market.\n///\n/// Args:\n///     high: High prices as a 1-D NumPy array of type `TAFloat`.\n///     low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///     period: The time period for ADR calculation (must be >= 2).\n///\n/// Returns:\n///     A new 1-D NumPy array containing the ADR values. The array has the same length as the inputs.\n///\n/// Examples:\n///     ```python\n///     >>> import numpy as np\n///     >>> import kand\n///     >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n///     >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n///     >>> period = 3\n///     >>> result = kand.adr(high, low, period)\n///     ```\n#[pyfunction]\n#[pyo3(name = \"adr\", signature = (high, low, period))]\npub fn adr_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy arrays to Rust slices\n    let high_input = high.as_slice()?;\n    let low_input = low.as_slice()?;\n    let len = high_input.len();\n\n    // Create a new output array using vec\n    let mut output = vec![0.0; len];\n\n    // Perform the ADR calculation while releasing the GIL\n    py.allow_threads(|| adr::adr(high_input, low_input, period, output.as_mut_slice()))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Computes the latest Average Daily Range (ADR) value incrementally.\n///\n/// This function calculates only the latest ADR value using the previous ADR value,\n/// avoiding recalculation of the entire series.\n///\n/// Args:\n///     prev_adr: Previous ADR value.\n///     new_high: Latest high price.\n///     new_low: Latest low price.\n///     old_high: Oldest high price to be removed from period.\n///     old_low: Oldest low price to be removed from period.\n///     period: The time period for ADR calculation (must be >= 2).\n///\n/// Returns:\n///     The latest ADR value.\n///\n/// Examples:\n///     ```python\n///     >>> import kand\n///     >>> prev_adr = 3.0\n///     >>> new_high = 15.0\n///     >>> new_low = 12.0\n///     >>> old_high = 10.0\n///     >>> old_low = 8.0\n///     >>> period = 14\n///     >>> result = kand.adr_inc(prev_adr, new_high, new_low, old_high, old_low, period)\n///     ```\n#[pyfunction]\n#[pyo3(name = \"adr_inc\", signature = (prev_adr, new_high, new_low, old_high, old_low, period))]\npub fn adr_inc_py(\n    py: Python,\n    prev_adr: TAFloat,\n    new_high: TAFloat,\n    new_low: TAFloat,\n    old_high: TAFloat,\n    old_low: TAFloat,\n    period: usize,\n) -> PyResult<TAFloat> {\n    // Perform the incremental ADR calculation while releasing the GIL\n    py.allow_threads(|| adr::adr_inc(prev_adr, new_high, new_low, old_high, old_low, period))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/adx.rs",
    "content": "use kand::{TAFloat, ohlcv::adx};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculate Average Directional Index (ADX) for a NumPy array\n///\n/// The ADX (Average Directional Index) measures the strength of a trend, regardless of whether it's up or down.\n/// Values range from 0 to 100, with higher values indicating stronger trends.\n///\n/// Args:\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Period for ADX calculation (typically 14). Must be positive.\n///\n/// Returns:\n///   A tuple of four 1-D NumPy arrays containing:\n///   - ADX values\n///   - Smoothed +DM values\n///   - Smoothed -DM values\n///   - Smoothed TR values\n///   Each array has the same length as the input, with the first (2*period-1) elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([24.20, 24.07, 24.04, 23.87, 23.67])\n///   >>> low = np.array([23.85, 23.72, 23.64, 23.37, 23.46])\n///   >>> close = np.array([23.89, 23.95, 23.67, 23.78, 23.50])\n///   >>> adx, plus_dm, minus_dm, tr = kand.adx(high, low, close, 2)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"adx\", signature = (high, low, close, period))]\npub fn adx_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    // Convert input NumPy arrays to Rust slices\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_high.len();\n\n    // Create output arrays using vec\n    let mut output_adx = vec![0.0; len];\n    let mut output_smoothed_plus_dm = vec![0.0; len];\n    let mut output_smoothed_minus_dm = vec![0.0; len];\n    let mut output_smoothed_tr = vec![0.0; len];\n\n    // Perform ADX calculation while releasing the GIL\n    py.allow_threads(|| {\n        adx::adx(\n            input_high,\n            input_low,\n            input_close,\n            period,\n            &mut output_adx,\n            &mut output_smoothed_plus_dm,\n            &mut output_smoothed_minus_dm,\n            &mut output_smoothed_tr,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert output arrays to Python objects\n    Ok((\n        output_adx.into_pyarray(py).into(),\n        output_smoothed_plus_dm.into_pyarray(py).into(),\n        output_smoothed_minus_dm.into_pyarray(py).into(),\n        output_smoothed_tr.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculate the latest ADX value incrementally\n///\n/// Args:\n///   py: Python interpreter token\n///   high: Current period's high price\n///   low: Current period's low price\n///   prev_high: Previous period's high price\n///   prev_low: Previous period's low price\n///   prev_close: Previous period's close price\n///   prev_adx: Previous period's ADX value\n///   prev_smoothed_plus_dm: Previous period's smoothed +DM\n///   prev_smoothed_minus_dm: Previous period's smoothed -DM\n///   prev_smoothed_tr: Previous period's smoothed TR\n///   period: Period for ADX calculation (typically 14)\n///\n/// Returns:\n///   A tuple containing:\n///   - Latest ADX value\n///   - New smoothed +DM\n///   - New smoothed -DM\n///   - New smoothed TR\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> adx, plus_dm, minus_dm, tr = kand.adx_inc(\n///   ...     24.20,  # current high\n///   ...     23.85,  # current low\n///   ...     24.07,  # previous high\n///   ...     23.72,  # previous low\n///   ...     23.95,  # previous close\n///   ...     25.0,   # previous ADX\n///   ...     0.5,    # previous smoothed +DM\n///   ...     0.3,    # previous smoothed -DM\n///   ...     1.2,    # previous smoothed TR\n///   ...     14      # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"adx_inc\")]\npub fn adx_inc_py(\n    py: Python,\n    high: TAFloat,\n    low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    prev_close: TAFloat,\n    prev_adx: TAFloat,\n    prev_smoothed_plus_dm: TAFloat,\n    prev_smoothed_minus_dm: TAFloat,\n    prev_smoothed_tr: TAFloat,\n    period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat, TAFloat)> {\n    // Perform incremental ADX calculation while releasing the GIL\n    py.allow_threads(|| {\n        adx::adx_inc(\n            high,\n            low,\n            prev_high,\n            prev_low,\n            prev_close,\n            prev_adx,\n            prev_smoothed_plus_dm,\n            prev_smoothed_minus_dm,\n            prev_smoothed_tr,\n            period,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/adxr.rs",
    "content": "use kand::{TAFloat, ohlcv::adxr};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculate Average Directional Index Rating (ADXR) for a NumPy array.\n///\n/// ADXR is a momentum indicator that measures the strength of a trend by comparing\n/// the current ADX value with the ADX value from `period` days ago.\n///\n/// Args:\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Period for ADX calculation (typically 14).\n///\n/// Returns:\n///   A tuple of 5 1-D NumPy arrays containing:\n///   - ADXR values\n///   - ADX values\n///   - Smoothed +DM values\n///   - Smoothed -DM values\n///   - Smoothed TR values\n///   The first (3*period-2) elements of each array contain NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([24.20, 24.07, 24.04, 23.87, 23.67])\n///   >>> low = np.array([23.85, 23.72, 23.64, 23.37, 23.46])\n///   >>> close = np.array([23.89, 23.95, 23.67, 23.78, 23.50])\n///   >>> adxr, adx, plus_dm, minus_dm, tr = kand.adxr(high, low, close, 2)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"adxr\", signature = (high, low, close, period))]\npub fn adxr_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let high_slice = high.as_slice()?;\n    let low_slice = low.as_slice()?;\n    let close_slice = close.as_slice()?;\n    let len = high_slice.len();\n\n    let mut output_adxr = vec![0.0; len];\n    let mut output_adx = vec![0.0; len];\n    let mut output_plus_dm = vec![0.0; len];\n    let mut output_minus_dm = vec![0.0; len];\n    let mut output_tr = vec![0.0; len];\n\n    py.allow_threads(|| {\n        adxr::adxr(\n            high_slice,\n            low_slice,\n            close_slice,\n            period,\n            &mut output_adxr,\n            &mut output_adx,\n            &mut output_plus_dm,\n            &mut output_minus_dm,\n            &mut output_tr,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_adxr.into_pyarray(py).into(),\n        output_adx.into_pyarray(py).into(),\n        output_plus_dm.into_pyarray(py).into(),\n        output_minus_dm.into_pyarray(py).into(),\n        output_tr.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculate the latest ADXR value incrementally\n///\n/// Args:\n///\n///   high: Current high price as TAFloat.\n///   low: Current low price as TAFloat.\n///   prev_high: Previous high price as TAFloat.\n///   prev_low: Previous low price as TAFloat.\n///   prev_close: Previous close price as TAFloat.\n///   prev_adx: Previous ADX value as TAFloat.\n///   prev_adx_period_ago: ADX value from period days ago as TAFloat.\n///   prev_smoothed_plus_dm: Previous smoothed +DM value as TAFloat.\n///   prev_smoothed_minus_dm: Previous smoothed -DM value as TAFloat.\n///   prev_smoothed_tr: Previous smoothed TR value as TAFloat.\n///   period: Period for ADX calculation (typically 14).\n///\n/// Returns:\n///   A tuple of 5 values:\n///   - Latest ADXR value\n///   - Latest ADX value\n///   - New smoothed +DM value\n///   - New smoothed -DM value\n///   - New smoothed TR value\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> adxr, adx, plus_dm, minus_dm, tr = kand.adxr_inc(\n///   ...     24.20,  # high\n///   ...     23.85,  # low\n///   ...     24.07,  # prev_high\n///   ...     23.72,  # prev_low\n///   ...     23.95,  # prev_close\n///   ...     25.0,   # prev_adx\n///   ...     20.0,   # prev_adx_period_ago\n///   ...     0.5,    # prev_smoothed_plus_dm\n///   ...     0.3,    # prev_smoothed_minus_dm\n///   ...     1.2,    # prev_smoothed_tr\n///   ...     14      # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"adxr_inc\", signature = (\n    high,\n    low,\n    prev_high,\n    prev_low,\n    prev_close,\n    prev_adx,\n    prev_adx_period_ago,\n    prev_smoothed_plus_dm,\n    prev_smoothed_minus_dm,\n    prev_smoothed_tr,\n    period\n))]\npub fn adxr_inc_py(\n    py: Python,\n    high: TAFloat,\n    low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    prev_close: TAFloat,\n    prev_adx: TAFloat,\n    prev_adx_period_ago: TAFloat,\n    prev_smoothed_plus_dm: TAFloat,\n    prev_smoothed_minus_dm: TAFloat,\n    prev_smoothed_tr: TAFloat,\n    period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat, TAFloat, TAFloat)> {\n    py.allow_threads(|| {\n        adxr::adxr_inc(\n            high,\n            low,\n            prev_high,\n            prev_low,\n            prev_close,\n            prev_adx,\n            prev_adx_period_ago,\n            prev_smoothed_plus_dm,\n            prev_smoothed_minus_dm,\n            prev_smoothed_tr,\n            period,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/aroon.rs",
    "content": "use kand::{TAFloat, ohlcv::aroon};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculate Aroon indicator for a NumPy array.\n///\n/// The Aroon indicator consists of two lines that measure the time since the last high/low\n/// relative to a lookback period. It helps identify the start of new trends and trend reversals.\n///\n/// Args:\n///   high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n///   period: The lookback period for calculations (must be >= 2).\n///\n/// Returns:\n///   A tuple of 6 1-D NumPy arrays containing:\n///   - Aroon Up values\n///   - Aroon Down values\n///   - Previous high values\n///   - Previous low values\n///   - Days since high values\n///   - Days since low values\n///   The first (period) elements of each array contain NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n///   >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n///   >>> aroon_up, aroon_down, prev_high, prev_low, days_high, days_low = kand.aroon(high, low, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"aroon\", signature = (high, low, period))]\npub fn aroon_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<usize>>,\n    Py<PyArray1<usize>>,\n)> {\n    let high_slice = high.as_slice()?;\n    let low_slice = low.as_slice()?;\n    let len = high_slice.len();\n\n    let mut output_aroon_up = vec![0.0; len];\n    let mut output_aroon_down = vec![0.0; len];\n    let mut output_prev_high = vec![0.0; len];\n    let mut output_prev_low = vec![0.0; len];\n    let mut output_days_since_high = vec![0_usize; len];\n    let mut output_days_since_low = vec![0_usize; len];\n\n    py.allow_threads(|| {\n        aroon::aroon(\n            high_slice,\n            low_slice,\n            period,\n            output_aroon_up.as_mut_slice(),\n            output_aroon_down.as_mut_slice(),\n            output_prev_high.as_mut_slice(),\n            output_prev_low.as_mut_slice(),\n            output_days_since_high.as_mut_slice(),\n            output_days_since_low.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_aroon_up.into_pyarray(py).into(),\n        output_aroon_down.into_pyarray(py).into(),\n        output_prev_high.into_pyarray(py).into(),\n        output_prev_low.into_pyarray(py).into(),\n        output_days_since_high.into_pyarray(py).into(),\n        output_days_since_low.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculate the next Aroon values incrementally.\n///\n/// Args:\n///\n///   high: Current period's high price.\n///   low: Current period's low price.\n///   prev_high: Previous highest price in period.\n///   prev_low: Previous lowest price in period.\n///   days_since_high: Days since previous highest price.\n///   days_since_low: Days since previous lowest price.\n///   period: The lookback period (must be >= 2).\n///\n/// Returns:\n///   A tuple containing:\n///   - Aroon Up value\n///   - Aroon Down value\n///   - New highest price\n///   - New lowest price\n///   - Updated days since high\n///   - Updated days since low\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> aroon_up, aroon_down, new_high, new_low, days_high, days_low = kand.aroon_inc(\n///   ...     15.0,  # high\n///   ...     12.0,  # low\n///   ...     14.0,  # prev_high\n///   ...     11.0,  # prev_low\n///   ...     2,     # days_since_high\n///   ...     1,     # days_since_low\n///   ...     14     # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"aroon_inc\", signature = (\n    high,\n    low,\n    prev_high,\n    prev_low,\n    days_since_high,\n    days_since_low,\n    period\n))]\npub fn aroon_inc_py(\n    py: Python,\n    high: TAFloat,\n    low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    days_since_high: usize,\n    days_since_low: usize,\n    period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat, TAFloat, usize, usize)> {\n    py.allow_threads(|| {\n        aroon::aroon_inc(\n            high,\n            low,\n            prev_high,\n            prev_low,\n            days_since_high,\n            days_since_low,\n            period,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/aroonosc.rs",
    "content": "use kand::{TAFloat, ohlcv::aroonosc};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculate Aroon Oscillator for a NumPy array.\n///\n/// The Aroon Oscillator measures the strength of a trend by comparing the time since the last high and low.\n/// It oscillates between -100 and +100, with positive values indicating an uptrend and negative values a downtrend.\n///\n/// Args:\n///   high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n///   period: The lookback period for calculations (must be >= 2).\n///\n/// Returns:\n///   A tuple of 5 1-D NumPy arrays containing:\n///   - Aroon Oscillator values\n///   - Previous high values\n///   - Previous low values\n///   - Days since high values\n///   - Days since low values\n///   The first (period) elements of each array contain NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n///   >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n///   >>> osc, prev_high, prev_low, days_high, days_low = kand.aroonosc(high, low, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"aroonosc\", signature = (high, low, period))]\npub fn aroonosc_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<usize>>,\n    Py<PyArray1<usize>>,\n)> {\n    let high_slice = high.as_slice()?;\n    let low_slice = low.as_slice()?;\n    let len = high_slice.len();\n\n    let mut output_aroonosc = vec![0.0; len];\n    let mut output_prev_high = vec![0.0; len];\n    let mut output_prev_low = vec![0.0; len];\n    let mut output_days_since_high = vec![0_usize; len];\n    let mut output_days_since_low = vec![0_usize; len];\n\n    py.allow_threads(|| {\n        aroonosc::aroonosc(\n            high_slice,\n            low_slice,\n            period,\n            output_aroonosc.as_mut_slice(),\n            output_prev_high.as_mut_slice(),\n            output_prev_low.as_mut_slice(),\n            output_days_since_high.as_mut_slice(),\n            output_days_since_low.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_aroonosc.into_pyarray(py).into(),\n        output_prev_high.into_pyarray(py).into(),\n        output_prev_low.into_pyarray(py).into(),\n        output_days_since_high.into_pyarray(py).into(),\n        output_days_since_low.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculate the next Aroon Oscillator value incrementally.\n///\n/// Args:\n///\n///   high: Current period's high price.\n///   low: Current period's low price.\n///   prev_high: Previous highest price within the period.\n///   prev_low: Previous lowest price within the period.\n///   days_since_high: Days since previous highest price.\n///   days_since_low: Days since previous lowest price.\n///   period: The lookback period for calculations (must be >= 2).\n///\n/// Returns:\n///   A tuple containing:\n///   - Aroon Oscillator value\n///   - New highest price\n///   - New lowest price\n///   - Updated days since high\n///   - Updated days since low\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> osc, high, low, days_high, days_low = kand.aroonosc_inc(\n///   ...     15.0,  # high\n///   ...     12.0,  # low\n///   ...     14.0,  # prev_high\n///   ...     11.0,  # prev_low\n///   ...     2,     # days_since_high\n///   ...     1,     # days_since_low\n///   ...     14     # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"aroonosc_inc\", signature = (\n    high,\n    low,\n    prev_high,\n    prev_low,\n    days_since_high,\n    days_since_low,\n    period\n))]\npub fn aroonosc_inc_py(\n    py: Python,\n    high: TAFloat,\n    low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    days_since_high: usize,\n    days_since_low: usize,\n    period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat, usize, usize)> {\n    py.allow_threads(|| {\n        aroonosc::aroonosc_inc(\n            high,\n            low,\n            prev_high,\n            prev_low,\n            days_since_high,\n            days_since_low,\n            period,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/atr.rs",
    "content": "use kand::{TAFloat, ohlcv::atr};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Average True Range (ATR) over NumPy arrays.\n///\n/// The Average True Range (ATR) is a technical analysis indicator that measures market volatility\n/// by decomposing the entire range of an asset price for a given period.\n///\n/// Args:\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Window size for ATR calculation. Must be greater than 1.\n///\n/// Returns:\n///   A new 1-D NumPy array containing the ATR values. The array has the same length as the input,\n///   with the first `period` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n///   >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n///   >>> close = np.array([9.0, 11.0, 14.0, 12.0, 11.0])\n///   >>> result = kand.atr(high, low, close, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"atr\", signature = (high, low, close, period))]\npub fn atr_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy arrays to Rust slices\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_high.len();\n\n    // Create a new output array using vec\n    let mut output = vec![0.0; len];\n\n    // Perform the ATR calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| {\n        atr::atr(\n            input_high,\n            input_low,\n            input_close,\n            period,\n            output.as_mut_slice(),\n        )\n    })\n    .map_err(|e| {\n        PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(\"Error computing ATR: {:?}\", e))\n    })?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Calculate the next ATR value incrementally.\n///\n/// Args:\n///\n///   high: Current period's high price.\n///   low: Current period's low price.\n///   prev_close: Previous period's close price.\n///   prev_atr: Previous period's ATR value.\n///   period: The time period for ATR calculation (must be >= 2).\n///\n/// Returns:\n///   The calculated ATR value.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> atr = kand.atr_inc(\n///   ...     15.0,  # high\n///   ...     11.0,  # low\n///   ...     12.0,  # prev_close\n///   ...     3.0,   # prev_atr\n///   ...     14     # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"atr_inc\", signature = (high, low, prev_close, prev_atr, period))]\npub fn atr_inc_py(\n    py: Python,\n    high: TAFloat,\n    low: TAFloat,\n    prev_close: TAFloat,\n    prev_atr: TAFloat,\n    period: usize,\n) -> PyResult<TAFloat> {\n    py.allow_threads(|| atr::atr_inc(high, low, prev_close, prev_atr, period))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/bbands.rs",
    "content": "use kand::{TAFloat, ohlcv::bbands};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculate Bollinger Bands for a NumPy array.\n///\n/// Bollinger Bands consist of:\n/// - A middle band (N-period simple moving average)\n/// - An upper band (K standard deviations above middle band)\n/// - A lower band (K standard deviations below middle band)\n///\n/// Args:\n///   price: Input price values as a 1-D NumPy array of type `TAFloat`.\n///   period: The time period for calculations (must be >= 2).\n///   dev_up: Number of standard deviations for upper band.\n///   dev_down: Number of standard deviations for lower band.\n///\n/// Returns:\n///   A tuple of 7 1-D NumPy arrays containing:\n///   - Upper band values\n///   - Middle band values\n///   - Lower band values\n///   - SMA values\n///   - Variance values\n///   - Sum values\n///   - Sum of squares values\n///   The first (period-1) elements of each array contain NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> price = np.array([10.0, 11.0, 12.0, 13.0, 14.0])\n///   >>> upper, middle, lower, sma, var, sum, sum_sq = kand.bbands(price, 3, 2.0, 2.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"bbands\", signature = (price, period, dev_up, dev_down))]\npub fn bbands_py(\n    py: Python,\n    price: PyReadonlyArray1<TAFloat>,\n    period: usize,\n    dev_up: TAFloat,\n    dev_down: TAFloat,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let price_slice = price.as_slice()?;\n    let len = price_slice.len();\n\n    let mut output_upper = vec![0.0; len];\n    let mut output_middle = vec![0.0; len];\n    let mut output_lower = vec![0.0; len];\n    let mut output_sma = vec![0.0; len];\n    let mut output_var = vec![0.0; len];\n    let mut output_sum = vec![0.0; len];\n    let mut output_sum_sq = vec![0.0; len];\n\n    py.allow_threads(|| {\n        bbands::bbands(\n            price_slice,\n            period,\n            dev_up,\n            dev_down,\n            output_upper.as_mut_slice(),\n            output_middle.as_mut_slice(),\n            output_lower.as_mut_slice(),\n            output_sma.as_mut_slice(),\n            output_var.as_mut_slice(),\n            output_sum.as_mut_slice(),\n            output_sum_sq.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_upper.into_pyarray(py).into(),\n        output_middle.into_pyarray(py).into(),\n        output_lower.into_pyarray(py).into(),\n        output_sma.into_pyarray(py).into(),\n        output_var.into_pyarray(py).into(),\n        output_sum.into_pyarray(py).into(),\n        output_sum_sq.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculate the next Bollinger Bands values incrementally.\n///\n/// Args:\n///\n///   price: The current price value.\n///   prev_sma: The previous SMA value.\n///   prev_sum: The previous sum for variance calculation.\n///   prev_sum_sq: The previous sum of squares for variance calculation.\n///   old_price: The oldest price value to be removed from the period.\n///   period: The time period for calculations (must be >= 2).\n///   dev_up: Number of standard deviations for upper band.\n///   dev_down: Number of standard deviations for lower band.\n///\n/// Returns:\n///   A tuple containing:\n///   - Upper Band value\n///   - Middle Band value\n///   - Lower Band value\n///   - New SMA value\n///   - New Sum value\n///   - New Sum of Squares value\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> upper, middle, lower, sma, sum, sum_sq = kand.bbands_inc(\n///   ...     10.0,   # price\n///   ...     9.5,    # prev_sma\n///   ...     28.5,   # prev_sum\n///   ...     272.25, # prev_sum_sq\n///   ...     9.0,    # old_price\n///   ...     3,      # period\n///   ...     2.0,    # dev_up\n///   ...     2.0     # dev_down\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"bbands_inc\", signature = (\n    price,\n    prev_sma,\n    prev_sum,\n    prev_sum_sq,\n    old_price,\n    period,\n    dev_up,\n    dev_down\n))]\npub fn bbands_inc_py(\n    py: Python,\n    price: TAFloat,\n    prev_sma: TAFloat,\n    prev_sum: TAFloat,\n    prev_sum_sq: TAFloat,\n    old_price: TAFloat,\n    period: usize,\n    dev_up: TAFloat,\n    dev_down: TAFloat,\n) -> PyResult<(TAFloat, TAFloat, TAFloat, TAFloat, TAFloat, TAFloat)> {\n    py.allow_threads(|| {\n        bbands::bbands_inc(\n            price,\n            prev_sma,\n            prev_sum,\n            prev_sum_sq,\n            old_price,\n            period,\n            dev_up,\n            dev_down,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/bop.rs",
    "content": "use kand::{TAFloat, ta::ohlcv::bop};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculate Balance of Power (BOP) indicator for NumPy arrays.\n///\n/// The Balance of Power (BOP) is a momentum oscillator that measures the relative strength\n/// between buyers and sellers by comparing the closing price to the opening price and\n/// normalizing it by the trading range (high - low).\n///\n/// Args:\n///   open: Input opening prices as a 1-D NumPy array of type `TAFloat`.\n///   high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Input closing prices as a 1-D NumPy array of type `TAFloat`.\n///\n/// Returns:\n///   A 1-D NumPy array containing the BOP values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> open = np.array([10.0, 11.0, 12.0, 13.0])\n///   >>> high = np.array([12.0, 13.0, 14.0, 15.0])\n///   >>> low = np.array([8.0, 9.0, 10.0, 11.0])\n///   >>> close = np.array([11.0, 12.0, 13.0, 14.0])\n///   >>> bop = kand.bop(open, high, low, close)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"bop\", signature = (open, high, low, close))]\npub fn bop_py(\n    py: Python,\n    open: PyReadonlyArray1<TAFloat>,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    let open_slice = open.as_slice()?;\n    let high_slice = high.as_slice()?;\n    let low_slice = low.as_slice()?;\n    let close_slice = close.as_slice()?;\n    let len = open_slice.len();\n    let mut output = vec![0.0; len];\n\n    py.allow_threads(|| {\n        bop::bop(\n            open_slice,\n            high_slice,\n            low_slice,\n            close_slice,\n            output.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Calculate a single Balance of Power (BOP) value for the latest price data.\n///\n/// Args:\n///   open: Current period's opening price\n///   high: Current period's high price\n///   low: Current period's low price\n///   close: Current period's closing price\n///\n/// Returns:\n///   The calculated BOP value\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> bop = kand.bop_inc(10.0, 12.0, 8.0, 11.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"bop_inc\", signature = (open, high, low, close))]\npub fn bop_inc_py(\n    py: Python,\n    open: TAFloat,\n    high: TAFloat,\n    low: TAFloat,\n    close: TAFloat,\n) -> PyResult<TAFloat> {\n    py.allow_threads(|| bop::bop_inc(open, high, low, close))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/cci.rs",
    "content": "use kand::{TAFloat, ohlcv::cci};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Commodity Channel Index (CCI) over NumPy arrays.\n///\n/// The CCI is a momentum-based oscillator used to help determine when an investment vehicle is reaching\n/// a condition of being overbought or oversold.\n///\n/// Args:\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Window size for CCI calculation. Must be positive and less than input length.\n///\n/// Returns:\n///   A tuple of 1-D NumPy arrays containing:\n///   - CCI values\n///   - Typical prices\n///   - SMA of typical prices\n///   - Mean deviation values\n///   Each array has the same length as the input, with the first `period-1` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([24.20, 24.07, 24.04, 23.87, 23.67])\n///   >>> low = np.array([23.85, 23.72, 23.64, 23.37, 23.46])\n///   >>> close = np.array([23.89, 23.95, 23.67, 23.78, 23.50])\n///   >>> cci, tp, sma_tp, mean_dev = kand.cci(high, low, close, 3)\n///   >>> print(cci)\n///   [nan, nan, -100.0, 66.67, -133.33]\n///   ```\n#[pyfunction]\n#[pyo3(name = \"cci\", signature = (high, low, close, period))]\npub fn cci_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    // Convert the input NumPy arrays to Rust slices\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_high.len();\n\n    // Create new output arrays using vec\n    let mut output_cci = vec![0.0; len];\n    let mut output_tp = vec![0.0; len];\n    let mut output_sma_tp = vec![0.0; len];\n    let mut output_mean_dev = vec![0.0; len];\n\n    // Perform the CCI calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| {\n        cci::cci(\n            input_high,\n            input_low,\n            input_close,\n            period,\n            output_cci.as_mut_slice(),\n            output_tp.as_mut_slice(),\n            output_sma_tp.as_mut_slice(),\n            output_mean_dev.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output arrays to Python objects\n    Ok((\n        output_cci.into_pyarray(py).into(),\n        output_tp.into_pyarray(py).into(),\n        output_sma_tp.into_pyarray(py).into(),\n        output_mean_dev.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculates the next CCI value incrementally.\n///\n/// Args:\n///\n///   prev_sma_tp: Previous SMA value of typical prices.\n///   new_high: New high price.\n///   new_low: New low price.\n///   new_close: New close price.\n///   old_high: Old high price to be removed.\n///   old_low: Old low price to be removed.\n///   old_close: Old close price to be removed.\n///   period: Window size for CCI calculation.\n///   tp_buffer: List containing the last `period` typical prices.\n///\n/// Returns:\n///   The next CCI value.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> prev_sma_tp = 100.0\n///   >>> new_high = 105.0\n///   >>> new_low = 95.0\n///   >>> new_close = 100.0\n///   >>> old_high = 102.0\n///   >>> old_low = 98.0\n///   >>> old_close = 100.0\n///   >>> period = 14\n///   >>> tp_buffer = [100.0] * period\n///   >>> next_cci = kand.cci_inc(prev_sma_tp, new_high, new_low, new_close,\n///   ...                                  old_high, old_low, old_close, period, tp_buffer)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"cci_inc\", signature = (prev_sma_tp, new_high, new_low, new_close, old_high, old_low, old_close, period, tp_buffer))]\npub fn cci_inc_py(\n    py: Python,\n    prev_sma_tp: TAFloat,\n    new_high: TAFloat,\n    new_low: TAFloat,\n    new_close: TAFloat,\n    old_high: TAFloat,\n    old_low: TAFloat,\n    old_close: TAFloat,\n    period: usize,\n    tp_buffer: Vec<TAFloat>,\n) -> PyResult<TAFloat> {\n    let mut buffer = tp_buffer;\n    py.allow_threads(|| {\n        cci::cci_inc(\n            prev_sma_tp,\n            new_high,\n            new_low,\n            new_close,\n            old_high,\n            old_low,\n            old_close,\n            period,\n            &mut buffer,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/cdl_doji.rs",
    "content": "use kand::{TAFloat, TAInt, ohlcv::cdl_doji};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Detects Doji candlestick patterns in price data.\n///\n/// Args:\n///   open: Opening prices as a 1-D NumPy array of type `TAFloat`.\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   body_percent: Maximum body size as percentage of range (e.g. 5.0 for 5%).\n///   shadow_equal_percent: Maximum shadow length difference percentage (e.g. 100.0).\n///\n/// Returns:\n///   A 1-D NumPy array containing pattern signals (1.0 = pattern, 0.0 = no pattern).\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> open = np.array([10.0, 10.5, 10.2])\n///   >>> high = np.array([11.0, 11.2, 10.8])\n///   >>> low = np.array([9.8, 10.1, 9.9])\n///   >>> close = np.array([10.3, 10.4, 10.25])\n///   >>> signals = kand.cdl_doji(open, high, low, close, 5.0, 100.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"cdl_doji\", signature = (open, high, low, close, body_percent, shadow_equal_percent))]\npub fn cdl_doji_py(\n    py: Python,\n    open: PyReadonlyArray1<TAFloat>,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    body_percent: TAFloat,\n    shadow_equal_percent: TAFloat,\n) -> PyResult<Py<PyArray1<TAInt>>> {\n    let input_open = open.as_slice()?;\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_open.len();\n\n    let mut output_signals = vec![0; len];\n\n    py.allow_threads(|| {\n        cdl_doji::cdl_doji(\n            input_open,\n            input_high,\n            input_low,\n            input_close,\n            body_percent,\n            shadow_equal_percent,\n            &mut output_signals,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok(output_signals.into_pyarray(py).into())\n}\n\n/// Detects a Doji pattern in a single candlestick.\n///\n/// Args:\n///\n///   open: Opening price.\n///   high: High price.\n///   low: Low price.\n///   close: Close price.\n///   body_percent: Maximum body size as percentage of range.\n///   shadow_equal_percent: Maximum shadow length difference percentage.\n///\n/// Returns:\n///   Signal value (1.0 for Doji pattern, 0.0 for no pattern).\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> signal = kand.cdl_doji_inc(10.0, 11.0, 9.8, 10.3, 5.0, 100.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"cdl_doji_inc\", signature = (open, high, low, close, body_percent, shadow_equal_percent))]\npub fn cdl_doji_inc_py(\n    py: Python,\n    open: TAFloat,\n    high: TAFloat,\n    low: TAFloat,\n    close: TAFloat,\n    body_percent: TAFloat,\n    shadow_equal_percent: TAFloat,\n) -> PyResult<TAInt> {\n    py.allow_threads(|| {\n        cdl_doji::cdl_doji_inc(open, high, low, close, body_percent, shadow_equal_percent)\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/cdl_dragonfly_doji.rs",
    "content": "use kand::{TAFloat, TAInt, ohlcv::cdl_dragonfly_doji};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Detects Dragonfly Doji candlestick patterns in price data.\n///\n/// Args:\n///   open: Opening prices as a 1-D NumPy array of type `TAFloat`.\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   body_percent: Maximum body size as percentage of total range (typically 5%).\n///\n/// Returns:\n///   A 1-D NumPy array containing pattern signals:\n///   - 100: Bullish Dragonfly Doji pattern detected\n///   - 0: No pattern detected\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> open = np.array([100.0, 101.0, 102.0])\n///   >>> high = np.array([102.0, 103.0, 104.0])\n///   >>> low = np.array([98.0, 99.0, 100.0])\n///   >>> close = np.array([101.0, 102.0, 103.0])\n///   >>> signals = kand.cdl_dragonfly_doji(open, high, low, close, 5.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"cdl_dragonfly_doji\", signature = (open, high, low, close, body_percent))]\npub fn cdl_dragonfly_doji_py(\n    py: Python,\n    open: PyReadonlyArray1<TAFloat>,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    body_percent: TAFloat,\n) -> PyResult<Py<PyArray1<TAInt>>> {\n    let input_open = open.as_slice()?;\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_open.len();\n\n    let mut output_signals = vec![0; len];\n\n    py.allow_threads(|| {\n        cdl_dragonfly_doji::cdl_dragonfly_doji(\n            input_open,\n            input_high,\n            input_low,\n            input_close,\n            body_percent,\n            &mut output_signals,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok(output_signals.into_pyarray(py).into())\n}\n\n/// Detects a Dragonfly Doji pattern in a single candlestick.\n///\n/// Args:\n///\n///   open: Opening price.\n///   high: High price.\n///   low: Low price.\n///   close: Close price.\n///   body_percent: Maximum body size as percentage of total range.\n///\n/// Returns:\n///   Signal value:\n///   - 100: Bullish Dragonfly Doji pattern detected\n///   - 0: No pattern detected\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> signal = kand.cdl_dragonfly_doji_inc(100.0, 102.0, 98.0, 100.1, 5.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"cdl_dragonfly_doji_inc\", signature = (open, high, low, close, body_percent))]\npub fn cdl_dragonfly_doji_inc_py(\n    py: Python,\n    open: TAFloat,\n    high: TAFloat,\n    low: TAFloat,\n    close: TAFloat,\n    body_percent: TAFloat,\n) -> PyResult<TAInt> {\n    py.allow_threads(|| {\n        cdl_dragonfly_doji::cdl_dragonfly_doji_inc(open, high, low, close, body_percent)\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/cdl_gravestone_doji.rs",
    "content": "use kand::{TAFloat, TAInt, ohlcv::cdl_gravestone_doji};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Detects Gravestone Doji candlestick patterns in price data.\n///\n/// Args:\n///   open: Opening prices as a 1-D NumPy array of type `TAFloat`.\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   body_percent: Maximum body size as percentage of total range (typically 5%).\n///\n/// Returns:\n///   A 1-D NumPy array containing pattern signals:\n///   - -100: Bearish Gravestone Doji pattern detected\n///   - 0: No pattern detected\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> open = np.array([100.0, 101.0, 102.0])\n///   >>> high = np.array([102.0, 103.0, 104.0])\n///   >>> low = np.array([98.0, 99.0, 100.0])\n///   >>> close = np.array([101.0, 102.0, 103.0])\n///   >>> signals = kand.cdl_gravestone_doji(open, high, low, close, 5.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"cdl_gravestone_doji\", signature = (open, high, low, close, body_percent))]\npub fn cdl_gravestone_doji_py(\n    py: Python,\n    open: PyReadonlyArray1<TAFloat>,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    body_percent: TAFloat,\n) -> PyResult<Py<PyArray1<TAInt>>> {\n    let input_open = open.as_slice()?;\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_open.len();\n\n    let mut output_signals = vec![0; len];\n\n    py.allow_threads(|| {\n        cdl_gravestone_doji::cdl_gravestone_doji(\n            input_open,\n            input_high,\n            input_low,\n            input_close,\n            body_percent,\n            &mut output_signals,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok(output_signals.into_pyarray(py).into())\n}\n\n/// Detects a Gravestone Doji pattern in a single candlestick.\n///\n/// Args:\n///\n///   open: Opening price.\n///   high: High price.\n///   low: Low price.\n///   close: Close price.\n///   body_percent: Maximum body size as percentage of total range.\n///\n/// Returns:\n///   Signal value:\n///   - -100: Bearish Gravestone Doji pattern detected\n///   - 0: No pattern detected\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> signal = kand.cdl_gravestone_doji_inc(100.0, 102.0, 98.0, 100.1, 5.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"cdl_gravestone_doji_inc\", signature = (open, high, low, close, body_percent))]\npub fn cdl_gravestone_doji_inc_py(\n    py: Python,\n    open: TAFloat,\n    high: TAFloat,\n    low: TAFloat,\n    close: TAFloat,\n    body_percent: TAFloat,\n) -> PyResult<TAInt> {\n    py.allow_threads(|| {\n        cdl_gravestone_doji::cdl_gravestone_doji_inc(open, high, low, close, body_percent)\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/cdl_hammer.rs",
    "content": "use kand::{TAFloat, TAInt, ohlcv::cdl_hammer};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Detects Hammer candlestick patterns in price data.\n///\n/// Args:\n///   open: Opening prices as a 1-D NumPy array of type `TAFloat`.\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Period for EMA calculation of body sizes.\n///   factor: Minimum ratio of lower shadow to body length.\n///\n/// Returns:\n///   A tuple of two 1-D NumPy arrays containing:\n///   - Pattern signals:\n///     - 100: Bullish Hammer pattern detected\n///     - 0: No pattern detected\n///   - EMA values of candle body sizes\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> open = np.array([100.0, 101.0, 102.0])\n///   >>> high = np.array([102.0, 103.0, 104.0])\n///   >>> low = np.array([98.0, 99.0, 100.0])\n///   >>> close = np.array([101.0, 102.0, 103.0])\n///   >>> signals, body_avg = kand.cdl_hammer(open, high, low, close, 14, 2.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"cdl_hammer\", signature = (open, high, low, close, period, factor))]\npub fn cdl_hammer_py(\n    py: Python,\n    open: PyReadonlyArray1<TAFloat>,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    period: usize,\n    factor: TAFloat,\n) -> PyResult<(Py<PyArray1<TAInt>>, Py<PyArray1<TAFloat>>)> {\n    let input_open = open.as_slice()?;\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_open.len();\n\n    let mut output_signals = vec![0; len];\n    let mut output_body_avg = vec![0.0; len];\n\n    py.allow_threads(|| {\n        cdl_hammer::cdl_hammer(\n            input_open,\n            input_high,\n            input_low,\n            input_close,\n            period,\n            factor,\n            &mut output_signals,\n            &mut output_body_avg,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_signals.into_pyarray(py).into(),\n        output_body_avg.into_pyarray(py).into(),\n    ))\n}\n\n/// Detects a Hammer pattern in a single candlestick.\n///\n/// Args:\n///\n///   open: Opening price.\n///   high: High price.\n///   low: Low price.\n///   close: Close price.\n///   prev_body_avg: Previous EMA value of body sizes.\n///   period: Period for EMA calculation.\n///   factor: Minimum ratio of lower shadow to body length.\n///\n/// Returns:\n///   A tuple containing:\n///   - Signal value:\n///     - 100: Bullish Hammer pattern detected\n///     - 0: No pattern detected\n///   - Updated EMA value of body sizes\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> signal, body_avg = kand.cdl_hammer_inc(100.0, 102.0, 98.0, 100.1, 0.5, 14, 2.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"cdl_hammer_inc\", signature = (open, high, low, close, prev_body_avg, period, factor))]\npub fn cdl_hammer_inc_py(\n    py: Python,\n    open: TAFloat,\n    high: TAFloat,\n    low: TAFloat,\n    close: TAFloat,\n    prev_body_avg: TAFloat,\n    period: usize,\n    factor: TAFloat,\n) -> PyResult<(TAInt, TAFloat)> {\n    py.allow_threads(|| {\n        cdl_hammer::cdl_hammer_inc(open, high, low, close, prev_body_avg, period, factor)\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/cdl_inverted_hammer.rs",
    "content": "use kand::{TAFloat, TAInt, ohlcv::cdl_inverted_hammer};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Detects Inverted Hammer candlestick patterns in price data.\n///\n/// Args:\n///   open: Opening prices as a 1-D NumPy array of type `TAFloat`.\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Period for EMA calculation of body sizes.\n///   factor: Minimum ratio of upper shadow to body length.\n///\n/// Returns:\n///   A tuple of two 1-D NumPy arrays containing:\n///   - Pattern signals:\n///     - 100: Bullish Inverted Hammer pattern detected\n///     - 0: No pattern detected\n///   - EMA values of candle body sizes\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> open = np.array([100.0, 101.0, 102.0])\n///   >>> high = np.array([102.0, 103.0, 104.0])\n///   >>> low = np.array([98.0, 99.0, 100.0])\n///   >>> close = np.array([101.0, 102.0, 103.0])\n///   >>> signals, body_avg = kand.cdl_inverted_hammer(open, high, low, close, 14, 2.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"cdl_inverted_hammer\", signature = (open, high, low, close, period, factor))]\npub fn cdl_inverted_hammer_py(\n    py: Python,\n    open: PyReadonlyArray1<TAFloat>,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    period: usize,\n    factor: TAFloat,\n) -> PyResult<(Py<PyArray1<TAInt>>, Py<PyArray1<TAFloat>>)> {\n    let input_open = open.as_slice()?;\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_open.len();\n\n    let mut output_signals = vec![0; len];\n    let mut output_body_avg = vec![0.0; len];\n\n    py.allow_threads(|| {\n        cdl_inverted_hammer::cdl_inverted_hammer(\n            input_open,\n            input_high,\n            input_low,\n            input_close,\n            period,\n            factor,\n            &mut output_signals,\n            &mut output_body_avg,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_signals.into_pyarray(py).into(),\n        output_body_avg.into_pyarray(py).into(),\n    ))\n}\n\n/// Detects an Inverted Hammer pattern in a single candlestick.\n///\n/// Args:\n///\n///   open: Opening price.\n///   high: High price.\n///   low: Low price.\n///   close: Close price.\n///   prev_body_avg: Previous EMA value of body sizes.\n///   period: Period for EMA calculation.\n///   factor: Minimum ratio of upper shadow to body length.\n///\n/// Returns:\n///   A tuple containing:\n///   - Signal value:\n///     - 100: Bullish Inverted Hammer pattern detected\n///     - 0: No pattern detected\n///   - Updated EMA value of body sizes\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> signal, body_avg = kand.cdl_inverted_hammer_inc(100.0, 102.0, 98.0, 100.1, 0.5, 14, 2.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"cdl_inverted_hammer_inc\", signature = (open, high, low, close, prev_body_avg, period, factor))]\npub fn cdl_inverted_hammer_inc_py(\n    py: Python,\n    open: TAFloat,\n    high: TAFloat,\n    low: TAFloat,\n    close: TAFloat,\n    prev_body_avg: TAFloat,\n    period: usize,\n    factor: TAFloat,\n) -> PyResult<(TAInt, TAFloat)> {\n    py.allow_threads(|| {\n        cdl_inverted_hammer::cdl_inverted_hammer_inc(\n            open,\n            high,\n            low,\n            close,\n            prev_body_avg,\n            period,\n            factor,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/cdl_long_shadow.rs",
    "content": "use kand::{TAFloat, TAInt, ohlcv::cdl_long_shadow};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Detects Long Shadow candlestick patterns in price data.\n///\n/// Args:\n///   open: Opening prices as a 1-D NumPy array of type `TAFloat`.\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Period for EMA calculation of body sizes.\n///   shadow_factor: Minimum percentage of total range that shadow must be.\n///\n/// Returns:\n///   A tuple of two 1-D NumPy arrays containing:\n///   - Pattern signals:\n///     - 100: Bullish Long Lower Shadow pattern detected\n///     - -100: Bearish Long Upper Shadow pattern detected\n///     - 0: No pattern detected\n///   - EMA values of candle body sizes\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> open = np.array([100.0, 101.0, 102.0])\n///   >>> high = np.array([102.0, 103.0, 104.0])\n///   >>> low = np.array([98.0, 99.0, 100.0])\n///   >>> close = np.array([101.0, 102.0, 103.0])\n///   >>> signals, body_avg = kand.cdl_long_shadow(open, high, low, close, 14, 75.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"cdl_long_shadow\", signature = (open, high, low, close, period, shadow_factor))]\npub fn cdl_long_shadow_py(\n    py: Python,\n    open: PyReadonlyArray1<TAFloat>,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    period: usize,\n    shadow_factor: TAFloat,\n) -> PyResult<(Py<PyArray1<TAInt>>, Py<PyArray1<TAFloat>>)> {\n    let input_open = open.as_slice()?;\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_open.len();\n\n    let mut output_signals = vec![0; len];\n    let mut output_body_avg = vec![0.0; len];\n\n    py.allow_threads(|| {\n        cdl_long_shadow::cdl_long_shadow(\n            input_open,\n            input_high,\n            input_low,\n            input_close,\n            period,\n            shadow_factor,\n            &mut output_signals,\n            &mut output_body_avg,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_signals.into_pyarray(py).into(),\n        output_body_avg.into_pyarray(py).into(),\n    ))\n}\n\n/// Detects a Long Shadow pattern in a single candlestick.\n///\n/// Args:\n///\n///   open: Opening price.\n///   high: High price.\n///   low: Low price.\n///   close: Close price.\n///   prev_body_avg: Previous EMA value of body sizes.\n///   period: Period for EMA calculation.\n///   shadow_factor: Minimum percentage of total range that shadow must be.\n///\n/// Returns:\n///   A tuple containing:\n///   - Signal value:\n///     - 100: Bullish Long Lower Shadow pattern detected\n///     - -100: Bearish Long Upper Shadow pattern detected\n///     - 0: No pattern detected\n///   - Updated EMA value of body sizes\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> signal, body_avg = kand.cdl_long_shadow_inc(100.0, 102.0, 98.0, 100.1, 0.5, 14, 75.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"cdl_long_shadow_inc\", signature = (open, high, low, close, prev_body_avg, period, shadow_factor))]\npub fn cdl_long_shadow_inc_py(\n    py: Python,\n    open: TAFloat,\n    high: TAFloat,\n    low: TAFloat,\n    close: TAFloat,\n    prev_body_avg: TAFloat,\n    period: usize,\n    shadow_factor: TAFloat,\n) -> PyResult<(TAInt, TAFloat)> {\n    py.allow_threads(|| {\n        cdl_long_shadow::cdl_long_shadow_inc(\n            open,\n            high,\n            low,\n            close,\n            prev_body_avg,\n            period,\n            shadow_factor,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/cdl_marubozu.rs",
    "content": "use kand::{TAFloat, TAInt, ohlcv::cdl_marubozu};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Detects Marubozu candlestick patterns in price data.\n///\n/// Args:\n///   open: Opening prices as a 1-D NumPy array of type `TAFloat`.\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Period for EMA calculation of body sizes.\n///   shadow_percent: Maximum shadow size as percentage of body.\n///\n/// Returns:\n///   A tuple of two 1-D NumPy arrays containing:\n///   - Pattern signals:\n///     - 1.0: Bullish Marubozu pattern detected\n///     - -1.0: Bearish Marubozu pattern detected\n///     - 0.0: No pattern detected\n///   - EMA values of candle body sizes\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> open = np.array([100.0, 101.0, 102.0])\n///   >>> high = np.array([102.0, 103.0, 104.0])\n///   >>> low = np.array([98.0, 99.0, 100.0])\n///   >>> close = np.array([101.0, 102.0, 103.0])\n///   >>> signals, body_avg = kand.cdl_marubozu(open, high, low, close, 14, 5.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"cdl_marubozu\", signature = (open, high, low, close, period, shadow_percent))]\npub fn cdl_marubozu_py(\n    py: Python,\n    open: PyReadonlyArray1<TAFloat>,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    period: usize,\n    shadow_percent: TAFloat,\n) -> PyResult<(Py<PyArray1<TAInt>>, Py<PyArray1<TAFloat>>)> {\n    let input_open = open.as_slice()?;\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_open.len();\n\n    let mut output_signals = vec![0; len];\n    let mut output_body_avg = vec![0.0; len];\n\n    py.allow_threads(|| {\n        cdl_marubozu::cdl_marubozu(\n            input_open,\n            input_high,\n            input_low,\n            input_close,\n            period,\n            shadow_percent,\n            &mut output_signals,\n            &mut output_body_avg,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_signals.into_pyarray(py).into(),\n        output_body_avg.into_pyarray(py).into(),\n    ))\n}\n\n/// Detects a Marubozu pattern in a single candlestick.\n///\n/// Args:\n///\n///   open: Opening price.\n///   high: High price.\n///   low: Low price.\n///   close: Close price.\n///   prev_body_avg: Previous EMA value of body sizes.\n///   period: Period for EMA calculation.\n///   shadow_percent: Maximum shadow size as percentage of body.\n///\n/// Returns:\n///   A tuple containing:\n///   - Signal value:\n///     - 1.0: Bullish Marubozu pattern detected\n///     - -1.0: Bearish Marubozu pattern detected\n///     - 0.0: No pattern detected\n///   - Updated EMA value of body sizes\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> signal, body_avg = kand.cdl_marubozu_inc(100.0, 102.0, 98.0, 100.1, 0.5, 14, 5.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"cdl_marubozu_inc\", signature = (open, high, low, close, prev_body_avg, period, shadow_percent))]\npub fn cdl_marubozu_inc_py(\n    py: Python,\n    open: TAFloat,\n    high: TAFloat,\n    low: TAFloat,\n    close: TAFloat,\n    prev_body_avg: TAFloat,\n    period: usize,\n    shadow_percent: TAFloat,\n) -> PyResult<(TAInt, TAFloat)> {\n    py.allow_threads(|| {\n        cdl_marubozu::cdl_marubozu_inc(\n            open,\n            high,\n            low,\n            close,\n            prev_body_avg,\n            period,\n            shadow_percent,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/dema.rs",
    "content": "use kand::{TAFloat, ohlcv::dema};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculates Double Exponential Moving Average (DEMA) over NumPy arrays.\n///\n/// Args:\n///   input_price: Price values as a 1-D NumPy array of type `TAFloat`.\n///   period: Smoothing period for EMA calculations. Must be >= 2.\n///\n/// Returns:\n///   A tuple of 1-D NumPy arrays containing:\n///   - DEMA values\n///   - First EMA values\n///   - Second EMA values\n///   Each array has the same length as the input, with the first `2*(period-1)` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> prices = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0])\n///   >>> dema, ema1, ema2 = kand.dema(prices, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"dema\", signature = (input_price, period))]\npub fn dema_py(\n    py: Python,\n    input_price: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let input = input_price.as_slice()?;\n    let len = input.len();\n\n    let mut output_dema = vec![0.0; len];\n    let mut output_ema1 = vec![0.0; len];\n    let mut output_ema2 = vec![0.0; len];\n\n    py.allow_threads(|| {\n        dema::dema(\n            input,\n            period,\n            output_dema.as_mut_slice(),\n            output_ema1.as_mut_slice(),\n            output_ema2.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_dema.into_pyarray(py).into(),\n        output_ema1.into_pyarray(py).into(),\n        output_ema2.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculates the next DEMA value incrementally.\n///\n/// Args:\n///\n///   price: Current price value.\n///   prev_ema1: Previous value of first EMA.\n///   prev_ema2: Previous value of second EMA.\n///   period: Smoothing period. Must be >= 2.\n///\n/// Returns:\n///   A tuple containing (DEMA, new_ema1, new_ema2).\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> dema, ema1, ema2 = kand.dema_inc(10.0, 9.5, 9.0, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"dema_inc\", signature = (price, prev_ema1, prev_ema2, period))]\npub fn dema_inc_py(\n    py: Python,\n    price: TAFloat,\n    prev_ema1: TAFloat,\n    prev_ema2: TAFloat,\n    period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat)> {\n    py.allow_threads(|| dema::dema_inc(price, prev_ema1, prev_ema2, period))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/dx.rs",
    "content": "use kand::{TAFloat, ohlcv::dx};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Directional Movement Index (DX) over NumPy arrays.\n///\n/// The DX indicator measures the strength of a trend by comparing positive and negative directional movements.\n///\n/// Args:\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Window size for DX calculation. Must be positive and less than input length.\n///\n/// Returns:\n///   A tuple of four 1-D NumPy arrays containing:\n///   - DX values\n///   - Smoothed +DM values\n///   - Smoothed -DM values\n///   - Smoothed TR values\n///   Each array has the same length as the input, with the first `period` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([24.20, 24.07, 24.04, 23.87, 23.67])\n///   >>> low = np.array([23.85, 23.72, 23.64, 23.37, 23.46])\n///   >>> close = np.array([23.89, 23.95, 23.67, 23.78, 23.50])\n///   >>> dx, plus_dm, minus_dm, tr = kand.dx(high, low, close, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"dx\", signature = (high, low, close, period))]\npub fn dx_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    // Convert the input NumPy arrays to Rust slices\n    let high_input = high.as_slice()?;\n    let low_input = low.as_slice()?;\n    let close_input = close.as_slice()?;\n    let len = high_input.len();\n\n    // Create new output arrays using vec\n    let mut output_dx = vec![0.0; len];\n    let mut output_smoothed_plus_dm = vec![0.0; len];\n    let mut output_smoothed_minus_dm = vec![0.0; len];\n    let mut output_smoothed_tr = vec![0.0; len];\n\n    // Perform the DX calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| {\n        dx::dx(\n            high_input,\n            low_input,\n            close_input,\n            period,\n            output_dx.as_mut_slice(),\n            output_smoothed_plus_dm.as_mut_slice(),\n            output_smoothed_minus_dm.as_mut_slice(),\n            output_smoothed_tr.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output arrays to Python objects\n    Ok((\n        output_dx.into_pyarray(py).into(),\n        output_smoothed_plus_dm.into_pyarray(py).into(),\n        output_smoothed_minus_dm.into_pyarray(py).into(),\n        output_smoothed_tr.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculates the latest DX value incrementally.\n///\n/// Computes only the most recent DX value using previous smoothed values.\n/// Optimized for real-time calculations where only the latest value is needed.\n///\n/// For the formula, refer to the [`dx`] function documentation.\n///\n/// Args:\n///     input_high (float): Current high price.\n///     input_low (float): Current low price.\n///     prev_high (float): Previous period's high price.\n///     prev_low (float): Previous period's low price.\n///     prev_close (float): Previous period's close price.\n///     prev_smoothed_plus_dm (float): Previous smoothed +DM value.\n///     prev_smoothed_minus_dm (float): Previous smoothed -DM value.\n///     prev_smoothed_tr (float): Previous smoothed TR value.\n///     opt_period (int): Period for DX calculation (typically 14).\n///\n/// Returns:\n///     tuple: A tuple containing:\n///         - Latest DX value (float)\n///         - New smoothed +DM (float)\n///         - New smoothed -DM (float)\n///         - New smoothed TR (float)\n///\n/// Example:\n///     >>> import kand\n///     >>> high, low = 24.20, 23.85\n///     >>> prev_high, prev_low, prev_close = 24.07, 23.72, 23.95\n///     >>> prev_smoothed_plus_dm = 0.5\n///     >>> prev_smoothed_minus_dm = 0.3\n///     >>> prev_smoothed_tr = 1.2\n///     >>> period = 14\n///     >>> dx, plus_dm, minus_dm, tr = kand.dx_inc(\n///     ...     high, low, prev_high, prev_low, prev_close,\n///     ...     prev_smoothed_plus_dm, prev_smoothed_minus_dm,\n///     ...     prev_smoothed_tr, period)\n#[pyfunction]\n#[pyo3(name = \"dx_inc\", signature = (\n    input_high,\n    input_low,\n    prev_high,\n    prev_low,\n    prev_close,\n    prev_smoothed_plus_dm,\n    prev_smoothed_minus_dm,\n    prev_smoothed_tr,\n    opt_period\n))]\npub fn dx_inc_py(\n    py: Python,\n    input_high: TAFloat,\n    input_low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    prev_close: TAFloat,\n    prev_smoothed_plus_dm: TAFloat,\n    prev_smoothed_minus_dm: TAFloat,\n    prev_smoothed_tr: TAFloat,\n    opt_period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat, TAFloat)> {\n    // Perform the incremental DX calculation while releasing the GIL\n    py.allow_threads(|| {\n        dx::dx_inc(\n            input_high,\n            input_low,\n            prev_high,\n            prev_low,\n            prev_close,\n            prev_smoothed_plus_dm,\n            prev_smoothed_minus_dm,\n            prev_smoothed_tr,\n            opt_period,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/ecl.rs",
    "content": "use kand::{TAFloat, ohlcv::ecl};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Expanded Camarilla Levels (ECL) over NumPy arrays.\n///\n/// The ECL indicator calculates multiple support and resistance levels based on the previous period's\n/// high, low and close prices.\n///\n/// Args:\n///   high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Input close prices as a 1-D NumPy array of type `TAFloat`.\n///\n/// Returns:\n///   A tuple of ten 1-D NumPy arrays containing the ECL values (H5,H4,H3,H2,H1,L1,L2,L3,L4,L5).\n///   Each array has the same length as the input, with the first element containing NaN value.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([24.20, 24.07, 24.04, 23.87, 23.67])\n///   >>> low = np.array([23.85, 23.72, 23.64, 23.37, 23.46])\n///   >>> close = np.array([23.89, 23.95, 23.67, 23.78, 23.50])\n///   >>> h5,h4,h3,h2,h1,l1,l2,l3,l4,l5 = kand.ecl(high, low, close)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"ecl\", signature = (high, low, close))]\npub fn ecl_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_high.len();\n\n    let mut output_h5 = vec![0.0; len];\n    let mut output_h4 = vec![0.0; len];\n    let mut output_h3 = vec![0.0; len];\n    let mut output_h2 = vec![0.0; len];\n    let mut output_h1 = vec![0.0; len];\n    let mut output_l1 = vec![0.0; len];\n    let mut output_l2 = vec![0.0; len];\n    let mut output_l3 = vec![0.0; len];\n    let mut output_l4 = vec![0.0; len];\n    let mut output_l5 = vec![0.0; len];\n\n    py.allow_threads(|| {\n        ecl::ecl(\n            input_high,\n            input_low,\n            input_close,\n            output_h5.as_mut_slice(),\n            output_h4.as_mut_slice(),\n            output_h3.as_mut_slice(),\n            output_h2.as_mut_slice(),\n            output_h1.as_mut_slice(),\n            output_l1.as_mut_slice(),\n            output_l2.as_mut_slice(),\n            output_l3.as_mut_slice(),\n            output_l4.as_mut_slice(),\n            output_l5.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_h5.into_pyarray(py).into(),\n        output_h4.into_pyarray(py).into(),\n        output_h3.into_pyarray(py).into(),\n        output_h2.into_pyarray(py).into(),\n        output_h1.into_pyarray(py).into(),\n        output_l1.into_pyarray(py).into(),\n        output_l2.into_pyarray(py).into(),\n        output_l3.into_pyarray(py).into(),\n        output_l4.into_pyarray(py).into(),\n        output_l5.into_pyarray(py).into(),\n    ))\n}\n\n/// Computes the latest Expanded Camarilla Levels (ECL) values incrementally.\n///\n/// This function provides an efficient way to calculate ECL values for new data without\n/// reprocessing the entire dataset.\n///\n/// Args:\n///\n///   prev_high: Previous period's high price as `TAFloat`.\n///   prev_low: Previous period's low price as `TAFloat`.\n///   prev_close: Previous period's close price as `TAFloat`.\n///\n/// Returns:\n///   A tuple of ten values (H5,H4,H3,H2,H1,L1,L2,L3,L4,L5) containing the latest ECL levels.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> h5,h4,h3,h2,h1,l1,l2,l3,l4,l5 = kand.ecl_inc(24.20, 23.85, 23.89)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"ecl_inc\", signature = (prev_high, prev_low, prev_close))]\npub fn ecl_inc_py(\n    py: Python,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    prev_close: TAFloat,\n) -> PyResult<(\n    TAFloat,\n    TAFloat,\n    TAFloat,\n    TAFloat,\n    TAFloat,\n    TAFloat,\n    TAFloat,\n    TAFloat,\n    TAFloat,\n    TAFloat,\n)> {\n    py.allow_threads(|| {\n        ecl::ecl_inc(prev_high, prev_low, prev_close)\n            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n    })\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/ema.rs",
    "content": "use kand::{TAFloat, ohlcv::ema};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Exponential Moving Average (EMA) over a NumPy array.\n///\n/// The Exponential Moving Average is calculated by applying more weight to recent prices\n/// via a smoothing factor k. Each value is calculated as:\n/// EMA = Price * k + EMA(previous) * (1 - k)\n/// where k is typically 2/(period+1).\n///\n/// Args:\n///   data: Input data as a 1-D NumPy array of type `TAFloat`.\n///   period: Window size for EMA calculation. Must be positive and less than input length.\n///   k: Optional custom smoothing factor. If None, uses default k = 2/(period+1).\n///\n/// Returns:\n///   A new 1-D NumPy array containing the EMA values. The array has the same length as the input,\n///   with the first `period-1` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n///   >>> result = kand.ema(data, 3)\n///   >>> print(result)\n///   [nan, nan, 2.0, 3.0, 4.2]\n///   ```\n#[pyfunction]\n#[pyo3(name = \"ema\", signature = (data, period, k=None))]\npub fn ema_py(\n    py: Python,\n    data: PyReadonlyArray1<TAFloat>,\n    period: usize,\n    k: Option<TAFloat>,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy array to a Rust slice.\n    let input = data.as_slice()?;\n    let len = input.len();\n\n    // Create a new output array using vec\n    let mut output = vec![0.0; len];\n\n    // Perform the EMA calculation while releasing the GIL to allow other Python threads to run.\n    py.allow_threads(|| ema::ema(input, period, k, output.as_mut_slice()))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Computes the latest EMA value incrementally.\n///\n/// This function provides an efficient way to calculate EMA values for new data without\n/// reprocessing the entire dataset.\n///\n/// Args:\n///\n///   price: Current period's price value as `TAFloat`.\n///   prev_ema: Previous period's EMA value as `TAFloat`.\n///   period: Window size for EMA calculation. Must be >= 2.\n///   k: Optional custom smoothing factor. If None, uses default k = 2/(period+1).\n///\n/// Returns:\n///   The new EMA value as `TAFloat`.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> current_price = 15.0\n///   >>> prev_ema = 14.5\n///   >>> period = 14\n///   >>> new_ema = kand.ema_inc(current_price, prev_ema, period)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"ema_inc\", signature = (price, prev_ema, period, k=None))]\npub fn ema_inc_py(\n    py: Python,\n    price: TAFloat,\n    prev_ema: TAFloat,\n    period: usize,\n    k: Option<TAFloat>,\n) -> PyResult<TAFloat> {\n    py.allow_threads(|| {\n        ema::ema_inc(price, prev_ema, period, k)\n            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n    })\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/macd.rs",
    "content": "use kand::{TAFloat, ohlcv::macd};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Moving Average Convergence Divergence (MACD) over a NumPy array.\n///\n/// MACD is a trend-following momentum indicator that shows the relationship between two moving averages\n/// of an asset's price. It consists of three components:\n/// - MACD Line: Difference between fast and slow EMAs\n/// - Signal Line: EMA of the MACD line\n/// - Histogram: Difference between MACD line and signal line\n///\n/// Args:\n///   data: Input price data as a 1-D NumPy array of type `TAFloat`.\n///   fast_period: Period for fast EMA calculation (typically 12).\n///   slow_period: Period for slow EMA calculation (typically 26).\n///   signal_period: Period for signal line calculation (typically 9).\n///\n/// Returns:\n///   A tuple of five 1-D NumPy arrays containing:\n///   - MACD line values\n///   - Signal line values\n///   - MACD histogram values\n///   - Fast EMA values\n///   - Slow EMA values\n///   Each array has the same length as the input, with initial elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n///   >>> macd_line, signal_line, histogram, fast_ema, slow_ema = kand.macd(data, 2, 3, 2)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"macd\", signature = (data, fast_period, slow_period, signal_period))]\npub fn macd_py(\n    py: Python,\n    data: PyReadonlyArray1<TAFloat>,\n    fast_period: usize,\n    slow_period: usize,\n    signal_period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    // Convert the input NumPy array to a Rust slice\n    let input = data.as_slice()?;\n    let len = input.len();\n\n    // Create output arrays using vec\n    let mut macd_line = vec![0.0; len];\n    let mut signal_line = vec![0.0; len];\n    let mut histogram = vec![0.0; len];\n    let mut fast_ema = vec![0.0; len];\n    let mut slow_ema = vec![0.0; len];\n\n    // Perform MACD calculation while releasing the GIL\n    py.allow_threads(|| {\n        macd::macd(\n            input,\n            fast_period,\n            slow_period,\n            signal_period,\n            macd_line.as_mut_slice(),\n            signal_line.as_mut_slice(),\n            histogram.as_mut_slice(),\n            fast_ema.as_mut_slice(),\n            slow_ema.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert output arrays to Python objects\n    Ok((\n        macd_line.into_pyarray(py).into(),\n        signal_line.into_pyarray(py).into(),\n        histogram.into_pyarray(py).into(),\n        fast_ema.into_pyarray(py).into(),\n        slow_ema.into_pyarray(py).into(),\n    ))\n}\n\n/// Computes the latest MACD values incrementally from previous state.\n///\n/// This function provides an efficient way to calculate MACD for streaming data by using\n/// previous EMA values instead of recalculating the entire series.\n///\n/// Args:\n///\n///   price: Current price value as `TAFloat`.\n///   prev_fast_ema: Previous fast EMA value as `TAFloat`.\n///   prev_slow_ema: Previous slow EMA value as `TAFloat`.\n///   prev_signal: Previous signal line value as `TAFloat`.\n///   fast_period: Period for fast EMA calculation (typically 12).\n///   slow_period: Period for slow EMA calculation (typically 26).\n///   signal_period: Period for signal line calculation (typically 9).\n///\n/// Returns:\n///   A tuple of three values:\n///   - MACD line value\n///   - Signal line value\n///   - MACD histogram value\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> macd_line, signal_line, histogram = kand.macd_inc(\n///   ...     100.0,  # current price\n///   ...     95.0,   # previous fast EMA\n///   ...     98.0,   # previous slow EMA\n///   ...     -2.5,   # previous signal\n///   ...     12,     # fast period\n///   ...     26,     # slow period\n///   ...     9       # signal period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"macd_inc\", signature = (price, prev_fast_ema, prev_slow_ema, prev_signal, fast_period, slow_period, signal_period))]\npub fn macd_inc_py(\n    py: Python,\n    price: TAFloat,\n    prev_fast_ema: TAFloat,\n    prev_slow_ema: TAFloat,\n    prev_signal: TAFloat,\n    fast_period: usize,\n    slow_period: usize,\n    signal_period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat)> {\n    // Perform incremental MACD calculation while releasing the GIL\n    py.allow_threads(|| {\n        macd::macd_inc(\n            price,\n            prev_fast_ema,\n            prev_slow_ema,\n            prev_signal,\n            fast_period,\n            slow_period,\n            signal_period,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/medprice.rs",
    "content": "use kand::{TAFloat, ohlcv::medprice};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculates the Median Price (MEDPRICE) for a NumPy array.\n///\n/// The Median Price is a technical analysis indicator that represents the middle point between\n/// high and low prices for each period.\n///\n/// Args:\n///   high: Array of high prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Array of low prices as a 1-D NumPy array of type `TAFloat`.\n///\n/// Returns:\n///   A 1-D NumPy array containing the median price values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([10.0, 11.0, 12.0])\n///   >>> low = np.array([8.0, 9.0, 10.0])\n///   >>> result = kand.medprice(high, low)\n///   >>> print(result)\n///   [9.0, 10.0, 11.0]\n///   ```\n#[pyfunction]\n#[pyo3(name = \"medprice\")]\npub fn medprice_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    let high_slice = high.as_slice()?;\n    let low_slice = low.as_slice()?;\n    let len = high_slice.len();\n    let mut output = vec![0.0; len];\n\n    py.allow_threads(|| medprice::medprice(high_slice, low_slice, output.as_mut_slice()))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Calculates a single Median Price value incrementally.\n///\n/// Args:\n///\n///   high: Current period's high price as `TAFloat`.\n///   low: Current period's low price as `TAFloat`.\n///\n/// Returns:\n///   The calculated median price value.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> result = kand.medprice_inc(10.0, 8.0)\n///   >>> print(result)\n///   9.0\n///   ```\n#[pyfunction]\n#[pyo3(name = \"medprice_inc\")]\npub fn medprice_inc_py(py: Python, high: TAFloat, low: TAFloat) -> PyResult<TAFloat> {\n    py.allow_threads(|| medprice::medprice_inc(high, low))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/mfi.rs",
    "content": "use kand::{TAFloat, ohlcv::mfi};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculates the Money Flow Index (MFI) for a NumPy array.\n///\n/// The Money Flow Index (MFI) is a technical oscillator that uses price and volume data to identify\n/// overbought or oversold conditions in an asset.\n///\n/// Args:\n///   high: Array of high prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Array of low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Array of close prices as a 1-D NumPy array of type `TAFloat`.\n///   volume: Array of volume data as a 1-D NumPy array of type `TAFloat`.\n///   period: The time period for MFI calculation (typically 14).\n///\n/// Returns:\n///   A tuple of five 1-D NumPy arrays containing:\n///   - MFI values (0-100)\n///   - Typical prices\n///   - Money flows\n///   - Positive money flows\n///   - Negative money flows\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([10.0, 11.0, 12.0, 11.0])\n///   >>> low = np.array([8.0, 9.0, 10.0, 9.0])\n///   >>> close = np.array([9.0, 10.0, 11.0, 10.0])\n///   >>> volume = np.array([100.0, 150.0, 200.0, 150.0])\n///   >>> mfi, typ_prices, money_flows, pos_flows, neg_flows = kand.mfi(high, low, close, volume, 2)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"mfi\", signature = (high, low, close, volume, period))]\npub fn mfi_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    volume: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let high_slice = high.as_slice()?;\n    let low_slice = low.as_slice()?;\n    let close_slice = close.as_slice()?;\n    let volume_slice = volume.as_slice()?;\n    let len = high_slice.len();\n\n    let mut mfi = vec![0.0; len];\n    let mut typ_prices = vec![0.0; len];\n    let mut money_flows = vec![0.0; len];\n    let mut pos_flows = vec![0.0; len];\n    let mut neg_flows = vec![0.0; len];\n\n    py.allow_threads(|| {\n        mfi::mfi(\n            high_slice,\n            low_slice,\n            close_slice,\n            volume_slice,\n            period,\n            mfi.as_mut_slice(),\n            typ_prices.as_mut_slice(),\n            money_flows.as_mut_slice(),\n            pos_flows.as_mut_slice(),\n            neg_flows.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        mfi.into_pyarray(py).into(),\n        typ_prices.into_pyarray(py).into(),\n        money_flows.into_pyarray(py).into(),\n        pos_flows.into_pyarray(py).into(),\n        neg_flows.into_pyarray(py).into(),\n    ))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/midpoint.rs",
    "content": "use kand::{TAFloat, ohlcv::midpoint};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculates Midpoint values for a NumPy array.\n///\n/// The Midpoint is a technical indicator that represents the arithmetic mean of the highest and lowest\n/// prices over a specified period.\n///\n/// Args:\n///   data: Input price data as a 1-D NumPy array of type `TAFloat`.\n///   period: Time period for calculation (must be >= 2).\n///\n/// Returns:\n///   A tuple of three 1-D NumPy arrays containing:\n///   - Midpoint values\n///   - Highest values for each period\n///   - Lowest values for each period\n///   Each array has the same length as the input, with initial elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> data = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n///   >>> midpoint, highest, lowest = kand.midpoint(data, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"midpoint\", signature = (data, period))]\npub fn midpoint_py(\n    py: Python,\n    data: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let input = data.as_slice()?;\n    let len = input.len();\n\n    let mut output_midpoint = vec![0.0; len];\n    let mut output_highest = vec![0.0; len];\n    let mut output_lowest = vec![0.0; len];\n\n    py.allow_threads(|| {\n        midpoint::midpoint(\n            input,\n            period,\n            output_midpoint.as_mut_slice(),\n            output_highest.as_mut_slice(),\n            output_lowest.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_midpoint.into_pyarray(py).into(),\n        output_highest.into_pyarray(py).into(),\n        output_lowest.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculates the next Midpoint value incrementally.\n///\n/// Provides an optimized way to calculate the next Midpoint value when new data arrives,\n/// without recalculating the entire series.\n///\n/// Args:\n///   price: Current price value as `TAFloat`.\n///   prev_highest: Previous highest value as `TAFloat`.\n///   prev_lowest: Previous lowest value as `TAFloat`.\n///   period: Time period for calculation (must be >= 2).\n///\n/// Returns:\n///   A tuple containing:\n///   - Midpoint value\n///   - New highest value\n///   - New lowest value\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> midpoint, new_highest, new_lowest = kand.midpoint_inc(\n///   ...     15.0,  # current price\n///   ...     16.0,  # previous highest\n///   ...     14.0,  # previous lowest\n///   ...     14     # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"midpoint_inc\", signature = (price, prev_highest, prev_lowest, period))]\npub fn midpoint_inc_py(\n    py: Python,\n    price: TAFloat,\n    prev_highest: TAFloat,\n    prev_lowest: TAFloat,\n    period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat)> {\n    py.allow_threads(|| midpoint::midpoint_inc(price, prev_highest, prev_lowest, period))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/midprice.rs",
    "content": "use kand::{TAFloat, ohlcv::midprice};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculates Midpoint Price values for a NumPy array.\n///\n/// The Midpoint Price is a technical indicator that represents the mean value between the highest high\n/// and lowest low prices over a specified period.\n///\n/// Args:\n///   high: Input high price data as a 1-D NumPy array of type `TAFloat`.\n///   low: Input low price data as a 1-D NumPy array of type `TAFloat`.\n///   period: Time period for calculation (must be >= 2).\n///\n/// Returns:\n///   A tuple of three 1-D NumPy arrays containing:\n///   - Midpoint Price values\n///   - Highest high values for each period\n///   - Lowest low values for each period\n///   Each array has the same length as the input, with initial elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n///   >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n///   >>> midprice, highest, lowest = kand.midprice(high, low, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"midprice\", signature = (high, low, period))]\npub fn midprice_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let len = input_high.len();\n\n    let mut output_midprice = vec![0.0; len];\n    let mut output_highest = vec![0.0; len];\n    let mut output_lowest = vec![0.0; len];\n\n    py.allow_threads(|| {\n        midprice::midprice(\n            input_high,\n            input_low,\n            period,\n            output_midprice.as_mut_slice(),\n            output_highest.as_mut_slice(),\n            output_lowest.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_midprice.into_pyarray(py).into(),\n        output_highest.into_pyarray(py).into(),\n        output_lowest.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculates the next Midpoint Price value incrementally.\n///\n/// Provides an optimized way to calculate the next Midpoint Price value when new data arrives,\n/// without recalculating the entire series.\n///\n/// Args:\n///\n///   high: Current high price value as `TAFloat`.\n///   low: Current low price value as `TAFloat`.\n///   prev_highest: Previous highest high value as `TAFloat`.\n///   prev_lowest: Previous lowest low value as `TAFloat`.\n///   period: Time period for calculation (must be >= 2).\n///\n/// Returns:\n///   A tuple containing:\n///   - Midpoint Price value\n///   - New highest high value\n///   - New lowest low value\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> midprice, new_highest, new_lowest = kand.midprice_inc(\n///   ...     10.5,  # current high\n///   ...     9.8,   # current low\n///   ...     10.2,  # previous highest high\n///   ...     9.5,   # previous lowest low\n///   ...     14     # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"midprice_inc\", signature = (high, low, prev_highest, prev_lowest, period))]\npub fn midprice_inc_py(\n    py: Python,\n    high: TAFloat,\n    low: TAFloat,\n    prev_highest: TAFloat,\n    prev_lowest: TAFloat,\n    period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat)> {\n    py.allow_threads(|| midprice::midprice_inc(high, low, prev_highest, prev_lowest, period))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/minus_di.rs",
    "content": "use kand::{TAFloat, ohlcv::minus_di};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Minus Directional Indicator (-DI) over NumPy arrays.\n///\n/// The -DI measures the presence and strength of a downward price trend. It is one component used in calculating\n/// the Average Directional Index (ADX), which helps determine trend strength.\n///\n/// Args:\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Window size for -DI calculation. Must be positive and less than input length.\n///\n/// Returns:\n///   A tuple of three 1-D NumPy arrays containing:\n///   - The -DI values\n///   - The smoothed -DM values\n///   - The smoothed TR values\n///   Each array has the same length as the input, with the first `period` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([35.0, 36.0, 35.5, 35.8, 36.2])\n///   >>> low = np.array([34.0, 35.0, 34.5, 34.8, 35.2])\n///   >>> close = np.array([34.5, 35.5, 35.0, 35.3, 35.7])\n///   >>> minus_di, smoothed_minus_dm, smoothed_tr = kand.minus_di(high, low, close, 3)\n///   >>> print(minus_di)\n///   [nan, nan, nan, 25.3, 24.1]\n///   ```\n#[pyfunction]\n#[pyo3(name = \"minus_di\", signature = (high, low, close, period))]\npub fn minus_di_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    // Convert the input NumPy arrays to Rust slices\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_high.len();\n\n    // Create new output arrays using vec\n    let mut output_minus_di = vec![0.0; len];\n    let mut output_smoothed_minus_dm = vec![0.0; len];\n    let mut output_smoothed_tr = vec![0.0; len];\n\n    // Perform the -DI calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| {\n        minus_di::minus_di(\n            input_high,\n            input_low,\n            input_close,\n            period,\n            output_minus_di.as_mut_slice(),\n            output_smoothed_minus_dm.as_mut_slice(),\n            output_smoothed_tr.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output arrays to Python objects\n    Ok((\n        output_minus_di.into_pyarray(py).into(),\n        output_smoothed_minus_dm.into_pyarray(py).into(),\n        output_smoothed_tr.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculates the next -DI value incrementally using previous smoothed values.\n///\n/// This function provides an efficient way to update -DI with new price data without recalculating the entire series.\n/// It maintains the same mathematical properties as the full calculation.\n///\n/// Args:\n///\n///   high: Current high price as `TAFloat`.\n///   low: Current low price as `TAFloat`.\n///   prev_high: Previous high price as `TAFloat`.\n///   prev_low: Previous low price as `TAFloat`.\n///   prev_close: Previous close price as `TAFloat`.\n///   prev_smoothed_minus_dm: Previous smoothed -DM value as `TAFloat`.\n///   prev_smoothed_tr: Previous smoothed TR value as `TAFloat`.\n///   period: Calculation period (>= 2).\n///\n/// Returns:\n///   A tuple of three values:\n///   - The new -DI value\n///   - The new smoothed -DM value\n///   - The new smoothed TR value\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> minus_di, smoothed_minus_dm, smoothed_tr = kand.minus_di_inc(\n///   ...     36.2,  # high\n///   ...     35.2,  # low\n///   ...     35.8,  # prev_high\n///   ...     34.8,  # prev_low\n///   ...     35.3,  # prev_close\n///   ...     0.5,   # prev_smoothed_minus_dm\n///   ...     1.5,   # prev_smoothed_tr\n///   ...     14     # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"minus_di_inc\", signature = (high, low, prev_high, prev_low, prev_close, prev_smoothed_minus_dm, prev_smoothed_tr, period))]\npub fn minus_di_inc_py(\n    py: Python,\n    high: TAFloat,\n    low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    prev_close: TAFloat,\n    prev_smoothed_minus_dm: TAFloat,\n    prev_smoothed_tr: TAFloat,\n    period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat)> {\n    // Perform the incremental -DI calculation while releasing the GIL\n    py.allow_threads(|| {\n        minus_di::minus_di_inc(\n            high,\n            low,\n            prev_high,\n            prev_low,\n            prev_close,\n            prev_smoothed_minus_dm,\n            prev_smoothed_tr,\n            period,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/minus_dm.rs",
    "content": "use kand::{TAFloat, ohlcv::minus_dm};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Minus Directional Movement (-DM) over NumPy arrays.\n///\n/// Minus Directional Movement (-DM) measures downward price movement and is used as part of the\n/// Directional Movement System developed by J. Welles Wilder.\n///\n/// Args:\n///   high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Window size for -DM calculation. Must be positive and less than input length.\n///\n/// Returns:\n///   A new 1-D NumPy array containing the -DM values. The array has the same length as the input,\n///   with the first `period-1` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([35266.0, 35247.5, 35235.7, 35190.8, 35182.0])\n///   >>> low = np.array([35216.1, 35206.5, 35180.0, 35130.7, 35153.6])\n///   >>> result = kand.minus_dm(high, low, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"minus_dm\", signature = (high, low, period))]\npub fn minus_dm_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy arrays to Rust slices\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let len = input_high.len();\n\n    // Create a new output array using vec\n    let mut output = vec![0.0; len];\n\n    // Perform the -DM calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| minus_dm::minus_dm(input_high, input_low, period, output.as_mut_slice()))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Calculates the next -DM value incrementally using previous values.\n///\n/// This function provides an efficient way to update -DM with new price data without recalculating the entire series.\n/// It maintains the same mathematical properties as the full calculation.\n///\n/// Args:\n///\n///   high: Current high price as `TAFloat`.\n///   prev_high: Previous high price as `TAFloat`.\n///   low: Current low price as `TAFloat`.\n///   prev_low: Previous low price as `TAFloat`.\n///   prev_minus_dm: Previous -DM value as `TAFloat`.\n///   period: Calculation period (must be between 2 and 100).\n///\n/// Returns:\n///   The next -DM value.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> next_minus_dm = kand.minus_dm_inc(\n///   ...     35182.0,  # high\n///   ...     35190.8,  # prev_high\n///   ...     35153.6,  # low\n///   ...     35130.7,  # prev_low\n///   ...     2.5,      # prev_minus_dm\n///   ...     14        # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"minus_dm_inc\", signature = (high, prev_high, low, prev_low, prev_minus_dm, period))]\npub fn minus_dm_inc_py(\n    py: Python,\n    high: TAFloat,\n    prev_high: TAFloat,\n    low: TAFloat,\n    prev_low: TAFloat,\n    prev_minus_dm: TAFloat,\n    period: usize,\n) -> PyResult<TAFloat> {\n    // Perform the incremental -DM calculation while releasing the GIL\n    py.allow_threads(|| {\n        minus_dm::minus_dm_inc(high, prev_high, low, prev_low, prev_minus_dm, period)\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/mod.rs",
    "content": "pub mod ad;\npub mod adosc;\npub mod adr;\npub mod adx;\npub mod adxr;\npub mod aroon;\npub mod aroonosc;\npub mod atr;\npub mod bbands;\npub mod bop;\npub mod cci;\npub mod cdl_doji;\npub mod cdl_dragonfly_doji;\npub mod cdl_gravestone_doji;\npub mod cdl_hammer;\npub mod cdl_inverted_hammer;\npub mod cdl_long_shadow;\npub mod cdl_marubozu;\npub mod dema;\npub mod dx;\npub mod ecl;\npub mod ema;\npub mod macd;\npub mod medprice;\npub mod mfi;\npub mod midpoint;\npub mod midprice;\npub mod minus_di;\npub mod minus_dm;\npub mod mom;\npub mod natr;\npub mod obv;\npub mod plus_di;\npub mod plus_dm;\npub mod rma;\npub mod roc;\npub mod rocp;\npub mod rocr;\npub mod rocr100;\npub mod rsi;\npub mod sar;\npub mod sma;\npub mod stoch;\npub mod supertrend;\npub mod t3;\npub mod tema;\npub mod trange;\npub mod trima;\npub mod trix;\npub mod typprice;\npub mod vegas;\npub mod vwap;\npub mod wclprice;\npub mod willr;\npub mod wma;\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/mom.rs",
    "content": "use kand::{TAFloat, ohlcv::mom};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Momentum (MOM) over a NumPy array.\n///\n/// Momentum measures the change in price between the current price and the price n periods ago.\n///\n/// Args:\n///   data: Input data as a 1-D NumPy array of type `TAFloat`.\n///   period: Window size for momentum calculation. Must be positive and less than input length.\n///\n/// Returns:\n///   A new 1-D NumPy array containing the momentum values. The array has the same length as the input,\n///   with the first `period` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> data = np.array([2.0, 4.0, 6.0, 8.0, 10.0])\n///   >>> result = kand.mom(data, 2)\n///   >>> print(result)\n///   [nan, nan, 4.0, 4.0, 4.0]\n///   ```\n#[pyfunction]\n#[pyo3(name = \"mom\", signature = (data, period))]\npub fn mom_py(\n    py: Python,\n    data: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy array to a Rust slice.\n    let input = data.as_slice()?;\n    let len = input.len();\n\n    // Create a new output array using vec\n    let mut output = vec![0.0; len];\n\n    // Perform the momentum calculation while releasing the GIL to allow other Python threads to run.\n    py.allow_threads(|| mom::mom(input, period, output.as_mut_slice()))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Calculates the next Momentum (MOM) value incrementally.\n///\n/// This function provides an optimized way to calculate the latest momentum value\n/// when streaming data is available, without needing the full price history.\n///\n/// Args:\n///\n///   current_price: The current period's price value.\n///   old_price: The price value from n periods ago.\n///\n/// Returns:\n///   The calculated momentum value.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> momentum = kand.mom_inc(10.0, 6.0)\n///   >>> print(momentum)\n///   4.0\n///   ```\n#[pyfunction]\n#[pyo3(name = \"mom_inc\", signature = (current_price, old_price))]\npub fn mom_inc_py(py: Python, current_price: TAFloat, old_price: TAFloat) -> PyResult<TAFloat> {\n    // Perform the incremental momentum calculation while releasing the GIL\n    py.allow_threads(|| mom::mom_inc(current_price, old_price))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/natr.rs",
    "content": "use kand::{TAFloat, ohlcv::natr};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Normalized Average True Range (NATR) over NumPy arrays.\n///\n/// The NATR is a measure of volatility that accounts for the price level of the instrument.\n/// It expresses the ATR as a percentage of the closing price.\n///\n/// Args:\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Window size for NATR calculation. Must be positive and less than input length.\n///\n/// Returns:\n///   A new 1-D NumPy array containing the NATR values. The array has the same length as the input,\n///   with the first `period` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n///   >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n///   >>> close = np.array([9.0, 11.0, 14.0, 12.0, 11.0])\n///   >>> result = kand.natr(high, low, close, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"natr\", signature = (high, low, close, period))]\npub fn natr_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy arrays to Rust slices\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_high.len();\n\n    // Create a new output array using vec\n    let mut output = vec![0.0; len];\n\n    // Perform the NATR calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| {\n        natr::natr(\n            input_high,\n            input_low,\n            input_close,\n            period,\n            output.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Calculates the next NATR value incrementally.\n///\n/// This function provides an optimized way to calculate a single new NATR value\n/// using the previous ATR value and current price data, without recalculating the entire series.\n///\n/// Args:\n///\n///   high: Current period's high price.\n///   low: Current period's low price.\n///   close: Current period's closing price.\n///   prev_close: Previous period's closing price.\n///   prev_atr: Previous period's ATR value.\n///   period: Period for NATR calculation (must be >= 2).\n///\n/// Returns:\n///   The calculated NATR value.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> natr = kand.natr_inc(\n///   ...     15.0,  # high\n///   ...     11.0,  # low\n///   ...     14.0,  # close\n///   ...     12.0,  # prev_close\n///   ...     3.0,   # prev_atr\n///   ...     3      # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"natr_inc\", signature = (high, low, close, prev_close, prev_atr, period))]\npub fn natr_inc_py(\n    py: Python,\n    high: TAFloat,\n    low: TAFloat,\n    close: TAFloat,\n    prev_close: TAFloat,\n    prev_atr: TAFloat,\n    period: usize,\n) -> PyResult<TAFloat> {\n    // Perform the incremental NATR calculation while releasing the GIL\n    py.allow_threads(|| natr::natr_inc(high, low, close, prev_close, prev_atr, period))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/obv.rs",
    "content": "use kand::{TAFloat, ohlcv::obv};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the On Balance Volume (OBV) over NumPy arrays.\n///\n/// On Balance Volume (OBV) is a momentum indicator that uses volume flow to predict changes in stock price.\n/// When volume increases without a significant price change, the price will eventually jump upward.\n/// When volume decreases without a significant price change, the price will eventually jump downward.\n///\n/// Args:\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   volume: Volume data as a 1-D NumPy array of type `TAFloat`.\n///\n/// Returns:\n///   A new 1-D NumPy array containing the OBV values. The array has the same length as the input.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> close = np.array([10.0, 12.0, 11.0, 13.0])\n///   >>> volume = np.array([100.0, 150.0, 120.0, 200.0])\n///   >>> result = kand.obv(close, volume)\n///   >>> print(result)\n///   [100.0, 250.0, 130.0, 330.0]\n///   ```\n#[pyfunction]\n#[pyo3(name = \"obv\", signature = (close, volume))]\npub fn obv_py(\n    py: Python,\n    close: PyReadonlyArray1<TAFloat>,\n    volume: PyReadonlyArray1<TAFloat>,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy arrays to Rust slices\n    let input_close = close.as_slice()?;\n    let input_volume = volume.as_slice()?;\n    let len = input_close.len();\n\n    // Create a new output array using vec\n    let mut output = vec![0.0; len];\n\n    // Perform the OBV calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| obv::obv(input_close, input_volume, output.as_mut_slice()))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Calculates the next OBV value incrementally.\n///\n/// This function provides an optimized way to calculate a single new OBV value\n/// using the previous OBV value and current price/volume data.\n///\n/// Args:\n///   curr_close: Current closing price as `TAFloat`.\n///   prev_close: Previous closing price as `TAFloat`.\n///   volume: Current volume as `TAFloat`.\n///   prev_obv: Previous OBV value as `TAFloat`.\n///\n/// Returns:\n///   The calculated OBV value.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> curr_close = 12.0\n///   >>> prev_close = 10.0\n///   >>> volume = 150.0\n///   >>> prev_obv = 100.0\n///   >>> result = kand.obv_inc(curr_close, prev_close, volume, prev_obv)\n///   >>> print(result)\n///   250.0\n///   ```\n#[pyfunction]\n#[pyo3(name = \"obv_inc\", signature = (curr_close, prev_close, volume, prev_obv))]\npub fn obv_inc_py(\n    curr_close: TAFloat,\n    prev_close: TAFloat,\n    volume: TAFloat,\n    prev_obv: TAFloat,\n) -> PyResult<TAFloat> {\n    obv::obv_inc(curr_close, prev_close, volume, prev_obv)\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/plus_di.rs",
    "content": "use kand::{TAFloat, ohlcv::plus_di};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Plus Directional Indicator (+DI) over NumPy arrays.\n///\n/// +DI measures the presence and strength of an upward price trend. It is one component used in calculating\n/// the Average Directional Index (ADX), which helps determine trend strength.\n///\n/// Args:\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Window size for +DI calculation. Must be positive and less than input length.\n///\n/// Returns:\n///   A tuple of three 1-D NumPy arrays containing:\n///   - +DI values\n///   - Smoothed +DM values\n///   - Smoothed TR values\n///   Each array has the same length as the input, with the first `period` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([10.0, 12.0, 11.5, 11.0])\n///   >>> low = np.array([9.0, 10.0, 10.0, 9.5])\n///   >>> close = np.array([9.5, 11.0, 10.5, 10.0])\n///   >>> plus_di, smoothed_plus_dm, smoothed_tr = kand.plus_di(high, low, close, 2)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"plus_di\", signature = (high, low, close, period))]\npub fn plus_di_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    // Convert the input NumPy arrays to Rust slices\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_high.len();\n\n    // Create new output arrays using vec\n    let mut output_plus_di = vec![0.0; len];\n    let mut output_smoothed_plus_dm = vec![0.0; len];\n    let mut output_smoothed_tr = vec![0.0; len];\n\n    // Perform the +DI calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| {\n        plus_di::plus_di(\n            input_high,\n            input_low,\n            input_close,\n            period,\n            output_plus_di.as_mut_slice(),\n            output_smoothed_plus_dm.as_mut_slice(),\n            output_smoothed_tr.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output arrays to Python objects\n    Ok((\n        output_plus_di.into_pyarray(py).into(),\n        output_smoothed_plus_dm.into_pyarray(py).into(),\n        output_smoothed_tr.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculates the next +DI value incrementally using previous smoothed values.\n///\n/// This function enables real-time calculation of +DI by using the previous smoothed values\n/// and current price data, avoiding the need to recalculate the entire series.\n///\n/// Args:\n///   high: Current high price as `TAFloat`.\n///   low: Current low price as `TAFloat`.\n///   prev_high: Previous high price as `TAFloat`.\n///   prev_low: Previous low price as `TAFloat`.\n///   prev_close: Previous close price as `TAFloat`.\n///   prev_smoothed_plus_dm: Previous smoothed +DM value as `TAFloat`.\n///   prev_smoothed_tr: Previous smoothed TR value as `TAFloat`.\n///   period: Smoothing period (>= 2).\n///\n/// Returns:\n///   A tuple containing (latest +DI, new smoothed +DM, new smoothed TR).\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> plus_di, smoothed_plus_dm, smoothed_tr = kand.plus_di_inc(\n///   ...     10.5,  # high\n///   ...     9.5,   # low\n///   ...     10.0,  # prev_high\n///   ...     9.0,   # prev_low\n///   ...     9.5,   # prev_close\n///   ...     15.0,  # prev_smoothed_plus_dm\n///   ...     20.0,  # prev_smoothed_tr\n///   ...     14     # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"plus_di_inc\", signature = (high, low, prev_high, prev_low, prev_close, prev_smoothed_plus_dm, prev_smoothed_tr, period))]\npub fn plus_di_inc_py(\n    high: TAFloat,\n    low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    prev_close: TAFloat,\n    prev_smoothed_plus_dm: TAFloat,\n    prev_smoothed_tr: TAFloat,\n    period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat)> {\n    plus_di::plus_di_inc(\n        high,\n        low,\n        prev_high,\n        prev_low,\n        prev_close,\n        prev_smoothed_plus_dm,\n        prev_smoothed_tr,\n        period,\n    )\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/plus_dm.rs",
    "content": "use kand::{TAFloat, ohlcv::plus_dm};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Plus Directional Movement (+DM) over NumPy arrays.\n///\n/// Plus Directional Movement (+DM) measures upward price movement and is used as part of the\n/// Directional Movement System developed by J. Welles Wilder.\n///\n/// Args:\n///   high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Window size for +DM calculation. Must be positive and less than input length.\n///\n/// Returns:\n///   A new 1-D NumPy array containing the +DM values. The array has the same length as the input,\n///   with the first `period-1` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([35266.0, 35247.5, 35235.7, 35190.8, 35182.0])\n///   >>> low = np.array([35216.1, 35206.5, 35180.0, 35130.7, 35153.6])\n///   >>> result = kand.plus_dm(high, low, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"plus_dm\", signature = (high, low, period))]\npub fn plus_dm_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy arrays to Rust slices\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let len = input_high.len();\n\n    // Create a new output array using vec\n    let mut output = vec![0.0; len];\n\n    // Perform the Plus DM calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| plus_dm::plus_dm(input_high, input_low, period, output.as_mut_slice()))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Calculates the next Plus DM value incrementally using previous values.\n///\n/// This function enables real-time calculation of Plus DM by using the previous Plus DM value\n/// and current price data, avoiding the need to recalculate the entire series.\n///\n/// Args:\n///   high: Current high price as `TAFloat`.\n///   prev_high: Previous high price as `TAFloat`.\n///   low: Current low price as `TAFloat`.\n///   prev_low: Previous low price as `TAFloat`.\n///   prev_plus_dm: Previous Plus DM value as `TAFloat`.\n///   period: Smoothing period (>= 2).\n///\n/// Returns:\n///   The latest Plus DM value.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> new_plus_dm = kand.plus_dm_inc(\n///   ...     10.5,  # high\n///   ...     10.0,  # prev_high\n///   ...     9.8,   # low\n///   ...     9.5,   # prev_low\n///   ...     0.45,  # prev_plus_dm\n///   ...     14     # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"plus_dm_inc\", signature = (high, prev_high, low, prev_low, prev_plus_dm, period))]\npub fn plus_dm_inc_py(\n    high: TAFloat,\n    prev_high: TAFloat,\n    low: TAFloat,\n    prev_low: TAFloat,\n    prev_plus_dm: TAFloat,\n    period: usize,\n) -> PyResult<TAFloat> {\n    plus_dm::plus_dm_inc(high, prev_high, low, prev_low, prev_plus_dm, period)\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/rma.rs",
    "content": "use kand::{TAFloat, ohlcv::rma};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Running Moving Average (RMA) over a NumPy array.\n///\n/// The Running Moving Average is similar to an Exponential Moving Average (EMA) but uses a different\n/// smoothing factor. It is calculated using a weighted sum of the current value and previous RMA value,\n/// with weights determined by the period size.\n///\n/// Args:\n///   data: Input data as a 1-D NumPy array of type `TAFloat`.\n///   period: Window size for RMA calculation. Must be positive and less than input length.\n///\n/// Returns:\n///   A new 1-D NumPy array containing the RMA values. The array has the same length as the input,\n///   with the first `period-1` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n///   >>> result = kand.rma(data, 3)\n///   >>> print(result)\n///   [nan, nan, 2.0, 2.67, 3.44]\n///   ```\n#[pyfunction]\n#[pyo3(name = \"rma\", signature = (data, period))]\npub fn rma_py(\n    py: Python,\n    data: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy array to a Rust slice.\n    let input = data.as_slice()?;\n    let len = input.len();\n\n    // Create a new output array using vec\n    let mut output = vec![0.0; len];\n\n    // Perform the RMA calculation while releasing the GIL to allow other Python threads to run.\n    py.allow_threads(|| rma::rma(input, period, output.as_mut_slice()))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Calculates the next RMA value incrementally.\n///\n/// This function provides an optimized way to calculate the latest RMA value\n/// when streaming data is available, without needing the full price history.\n///\n/// Args:\n///   current_price: The current period's price value.\n///   prev_rma: The previous period's RMA value.\n///   period: The smoothing period (must be >= 2).\n///\n/// Returns:\n///   The calculated RMA value.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> new_rma = kand.rma_inc(10.0, 9.5, 14)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"rma_inc\", signature = (current_price, prev_rma, period))]\npub fn rma_inc_py(current_price: TAFloat, prev_rma: TAFloat, period: usize) -> PyResult<TAFloat> {\n    rma::rma_inc(current_price, prev_rma, period)\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/roc.rs",
    "content": "use kand::{TAFloat, ohlcv::roc};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Rate of Change (ROC) over a NumPy array.\n///\n/// The Rate of Change (ROC) is a momentum oscillator that measures the percentage change in price\n/// between the current price and the price n periods ago.\n///\n/// Args:\n///   data: Input price data as a 1-D NumPy array of type `TAFloat`.\n///   period: Number of periods to look back. Must be positive.\n///\n/// Returns:\n///   A new 1-D NumPy array containing the ROC values. The array has the same length as the input,\n///   with the first `period` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> data = np.array([10.0, 10.5, 11.2, 10.8, 11.5])\n///   >>> result = kand.roc(data, 2)\n///   >>> print(result)\n///   [nan, nan, 12.0, 2.86, 6.48]\n///   ```\n#[pyfunction]\n#[pyo3(name = \"roc\", signature = (data, period))]\npub fn roc_py(\n    py: Python,\n    data: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy array to a Rust slice\n    let input = data.as_slice()?;\n    let len = input.len();\n\n    // Create a new output array using vec\n    let mut output = vec![0.0; len];\n\n    // Perform the ROC calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| roc::roc(input, period, output.as_mut_slice()))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Calculates a single ROC value incrementally.\n///\n/// This function provides an optimized way to calculate the latest ROC value\n/// when streaming data is available, without needing the full price history.\n///\n/// Args:\n///   current_price: The current period's price value.\n///   prev_price: The price from n periods ago.\n///\n/// Returns:\n///   The calculated ROC value.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> roc = kand.roc_inc(11.5, 10.0)\n///   >>> print(roc)\n///   15.0\n///   ```\n#[pyfunction]\n#[pyo3(name = \"roc_inc\", signature = (current_price, prev_price))]\npub fn roc_inc_py(current_price: TAFloat, prev_price: TAFloat) -> PyResult<TAFloat> {\n    roc::roc_inc(current_price, prev_price)\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/rocp.rs",
    "content": "use kand::{TAFloat, ohlcv::rocp};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Rate of Change Percentage (ROCP) over a NumPy array.\n///\n/// The Rate of Change Percentage (ROCP) is a momentum indicator that measures the percentage change\n/// between the current price and the price n periods ago.\n///\n/// Args:\n///   data: Input price data as a 1-D NumPy array of type `TAFloat`.\n///   period: Number of periods to look back. Must be positive.\n///\n/// Returns:\n///   A new 1-D NumPy array containing the ROCP values. The array has the same length as the input,\n///   with the first `period` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> data = np.array([10.0, 10.5, 11.2, 10.8, 11.5])\n///   >>> result = kand.rocp(data, 2)\n///   >>> print(result)\n///   [nan, nan, 0.12, 0.0286, 0.0648]\n///   ```\n#[pyfunction]\n#[pyo3(name = \"rocp\", signature = (data, period))]\npub fn rocp_py(\n    py: Python,\n    data: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy array to a Rust slice\n    let input = data.as_slice()?;\n    let len = input.len();\n\n    // Create a new output array using vec\n    let mut output = vec![0.0; len];\n\n    // Perform the ROCP calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| rocp::rocp(input, period, output.as_mut_slice()))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Calculates a single ROCP value incrementally.\n///\n/// This function provides an optimized way to calculate the latest ROCP value\n/// when streaming data is available, without needing the full price history.\n///\n/// Args:\n///   current_price: The current period's price value.\n///   prev_price: The price from n periods ago.\n///\n/// Returns:\n///   The calculated ROCP value.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> rocp = kand.rocp_inc(11.5, 10.0)\n///   >>> print(rocp)\n///   0.15\n///   ```\n#[pyfunction]\n#[pyo3(name = \"rocp_inc\", signature = (current_price, prev_price))]\npub fn rocp_inc_py(current_price: TAFloat, prev_price: TAFloat) -> PyResult<TAFloat> {\n    rocp::rocp_inc(current_price, prev_price)\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/rocr.rs",
    "content": "use kand::{TAFloat, ohlcv::rocr};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Rate of Change Ratio (ROCR) over a NumPy array.\n///\n/// The Rate of Change Ratio (ROCR) is a momentum indicator that measures the ratio between\n/// the current price and the price n periods ago.\n///\n/// Args:\n///   data: Input price data as a 1-D NumPy array of type `TAFloat`.\n///   period: Number of periods to look back. Must be >= 2.\n///\n/// Returns:\n///   A new 1-D NumPy array containing the ROCR values. The array has the same length as the input,\n///   with the first `period` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> data = np.array([10.0, 10.5, 11.2, 10.8, 11.5])\n///   >>> result = kand.rocr(data, 2)\n///   >>> print(result)\n///   [nan, nan, 1.12, 1.0286, 1.0648]\n///   ```\n#[pyfunction]\n#[pyo3(name = \"rocr\", signature = (data, period))]\npub fn rocr_py(\n    py: Python,\n    data: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy array to a Rust slice\n    let input = data.as_slice()?;\n    let len = input.len();\n\n    // Create a new output array using vec\n    let mut output = vec![0.0; len];\n\n    // Perform the ROCR calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| rocr::rocr(input, period, output.as_mut_slice()))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Calculates a single ROCR value incrementally.\n///\n/// This function provides an optimized way to calculate the latest ROCR value\n/// when streaming data is available, without needing the full price history.\n///\n/// Args:\n///   current_price: The current period's price value.\n///   prev_price: The price from n periods ago.\n///\n/// Returns:\n///   The calculated ROCR value.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> rocr = kand.rocr_inc(11.5, 10.0)\n///   >>> print(rocr)\n///   1.15\n///   ```\n#[pyfunction]\n#[pyo3(name = \"rocr_inc\", signature = (current_price, prev_price))]\npub fn rocr_inc_py(current_price: TAFloat, prev_price: TAFloat) -> PyResult<TAFloat> {\n    rocr::rocr_inc(current_price, prev_price)\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/rocr100.rs",
    "content": "use kand::{TAFloat, ohlcv::rocr100};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Rate of Change Ratio * 100 (ROCR100) over a NumPy array.\n///\n/// ROCR100 is a momentum indicator that measures the percentage change in price over a specified period.\n/// It compares the current price to a past price and expresses the ratio as a percentage.\n/// Values above 100 indicate price increases, while values below 100 indicate price decreases.\n///\n/// Args:\n///   data: Input price data as a 1-D NumPy array of type `TAFloat`.\n///   period: Number of periods to look back. Must be >= 2.\n///\n/// Returns:\n///   A new 1-D NumPy array containing the ROCR100 values. The array has the same length as the input,\n///   with the first `period` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> data = np.array([10.0, 10.5, 11.2, 10.8, 11.5])\n///   >>> result = kand.rocr100(data, 2)\n///   >>> print(result)\n///   [nan, nan, 106.67, 102.86, 106.48]\n///   ```\n#[pyfunction]\n#[pyo3(name = \"rocr100\", signature = (data, period))]\npub fn rocr100_py(\n    py: Python,\n    data: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy array to a Rust slice\n    let input = data.as_slice()?;\n    let len = input.len();\n\n    // Create a new output array using vec\n    let mut output = vec![0.0; len];\n\n    // Perform the ROCR100 calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| rocr100::rocr100(input, period, output.as_mut_slice()))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Calculates a single ROCR100 value incrementally.\n///\n/// This function provides an optimized way to calculate the latest ROCR100 value\n/// when streaming data is available, without needing the full price history.\n///\n/// Args:\n///   current_price: The current period's price value.\n///   prev_price: The price from n periods ago.\n///\n/// Returns:\n///   The calculated ROCR100 value.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> rocr100 = kand.rocr100_inc(11.5, 10.0)\n///   >>> print(rocr100)\n///   115.0\n///   ```\n#[pyfunction]\n#[pyo3(name = \"rocr100_inc\", signature = (current_price, prev_price))]\npub fn rocr100_inc_py(current_price: TAFloat, prev_price: TAFloat) -> PyResult<TAFloat> {\n    rocr100::rocr100_inc(current_price, prev_price)\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/rsi.rs",
    "content": "use kand::{TAFloat, ohlcv::rsi};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Relative Strength Index (RSI) over NumPy arrays.\n///\n/// The RSI is a momentum oscillator that measures the speed and magnitude of recent price changes\n/// to evaluate overbought or oversold conditions.\n///\n/// Args:\n///   prices: Input prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Window size for RSI calculation. Must be positive and less than input length.\n///\n/// Returns:\n///   A tuple of three 1-D NumPy arrays containing:\n///   - RSI values\n///   - Average gain values\n///   - Average loss values\n///   Each array has the same length as the input, with the first `period` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> prices = np.array([44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42])\n///   >>> rsi, avg_gain, avg_loss = kand.rsi(prices, 5)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"rsi\", signature = (prices, period))]\npub fn rsi_py(\n    py: Python,\n    prices: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    // Convert the input NumPy array to a Rust slice\n    let input_prices = prices.as_slice()?;\n    let len = input_prices.len();\n\n    // Create new output arrays using vec\n    let mut output_rsi = vec![0.0; len];\n    let mut output_avg_gain = vec![0.0; len];\n    let mut output_avg_loss = vec![0.0; len];\n\n    // Perform the RSI calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| {\n        rsi::rsi(\n            input_prices,\n            period,\n            output_rsi.as_mut_slice(),\n            output_avg_gain.as_mut_slice(),\n            output_avg_loss.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output arrays to Python objects\n    Ok((\n        output_rsi.into_pyarray(py).into(),\n        output_avg_gain.into_pyarray(py).into(),\n        output_avg_loss.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculates a single RSI value incrementally.\n///\n/// This function provides an optimized way to calculate the latest RSI value\n/// when streaming data is available, without needing the full price history.\n///\n/// Args:\n///   current_price: The current period's price value.\n///   prev_price: The previous period's price value.\n///   prev_avg_gain: The previous period's average gain.\n///   prev_avg_loss: The previous period's average loss.\n///   period: The time period for RSI calculation.\n///\n/// Returns:\n///   A tuple containing (RSI value, new average gain, new average loss).\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> rsi, avg_gain, avg_loss = kand.rsi_inc(45.42, 45.10, 0.24, 0.14, 14)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"rsi_inc\", signature = (current_price, prev_price, prev_avg_gain, prev_avg_loss, period))]\npub fn rsi_inc_py(\n    current_price: TAFloat,\n    prev_price: TAFloat,\n    prev_avg_gain: TAFloat,\n    prev_avg_loss: TAFloat,\n    period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat)> {\n    rsi::rsi_inc(\n        current_price,\n        prev_price,\n        prev_avg_gain,\n        prev_avg_loss,\n        period,\n    )\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/sar.rs",
    "content": "use kand::{TAFloat, ohlcv::sar};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculates the Parabolic SAR (Stop And Reverse) indicator over NumPy arrays.\n///\n/// Args:\n///   high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n///   acceleration: Initial acceleration factor (e.g. 0.02).\n///   maximum: Maximum acceleration factor (e.g. 0.2).\n///\n/// Returns:\n///   A tuple of four 1-D NumPy arrays containing:\n///   - SAR values\n///   - Trend direction (true=long, false=short)\n///   - Acceleration factors\n///   - Extreme points\n///   Each array has the same length as the input, with the first element containing NaN.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n///   >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n///   >>> sar, is_long, af, ep = kand.sar(high, low, 0.02, 0.2)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"sar\", signature = (high, low, acceleration, maximum))]\npub fn sar_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    acceleration: TAFloat,\n    maximum: TAFloat,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<bool>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let len = input_high.len();\n\n    let mut output_sar = vec![0.0; len];\n    let mut output_is_long = vec![false; len];\n    let mut output_af = vec![0.0; len];\n    let mut output_ep = vec![0.0; len];\n\n    py.allow_threads(|| {\n        sar::sar(\n            input_high,\n            input_low,\n            acceleration,\n            maximum,\n            output_sar.as_mut_slice(),\n            output_is_long.as_mut_slice(),\n            output_af.as_mut_slice(),\n            output_ep.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_sar.into_pyarray(py).into(),\n        output_is_long.into_pyarray(py).into(),\n        output_af.into_pyarray(py).into(),\n        output_ep.into_pyarray(py).into(),\n    ))\n}\n\n/// Incrementally updates the Parabolic SAR with new price data.\n///\n/// Args:\n///   high: Current period's high price.\n///   low: Current period's low price.\n///   prev_high: Previous period's high price.\n///   prev_low: Previous period's low price.\n///   prev_sar: Previous period's SAR value.\n///   is_long: Current trend direction (true=long, false=short).\n///   af: Current acceleration factor.\n///   ep: Current extreme point.\n///   acceleration: Acceleration factor increment.\n///   maximum: Maximum acceleration factor.\n///\n/// Returns:\n///   A tuple containing (SAR value, trend direction, acceleration factor, extreme point).\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> sar, is_long, af, ep = kand.sar_inc(\n///   ...     15.0, 14.0, 14.5, 13.5, 13.0, True, 0.02, 14.5, 0.02, 0.2\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"sar_inc\", signature = (high, low, prev_high, prev_low, prev_sar, is_long, af, ep, acceleration, maximum))]\npub fn sar_inc_py(\n    high: TAFloat,\n    low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    prev_sar: TAFloat,\n    is_long: bool,\n    af: TAFloat,\n    ep: TAFloat,\n    acceleration: TAFloat,\n    maximum: TAFloat,\n) -> PyResult<(TAFloat, bool, TAFloat, TAFloat)> {\n    sar::sar_inc(\n        high,\n        low,\n        prev_high,\n        prev_low,\n        prev_sar,\n        is_long,\n        af,\n        ep,\n        acceleration,\n        maximum,\n    )\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/sma.rs",
    "content": "use kand::{TAFloat, ohlcv::sma};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Simple Moving Average (SMA) over a NumPy array.\n///\n/// The Simple Moving Average is calculated by taking the arithmetic mean of a window of values\n/// that moves across the input array. For each position, it sums the previous `period` values\n/// and divides by the period size.\n///\n/// Args:\n///     data: Input data as a 1-D NumPy array of type `TAFloat`.\n///     period: Window size for SMA calculation. Must be positive and less than input length.\n///\n/// Returns:\n///     A new 1-D NumPy array containing the SMA values. The array has the same length as the input,\n///     with the first `period-1` elements containing NaN values.\n///\n/// Examples:\n///     ```python\n///     >>> import numpy as np\n///     >>> import kand\n///     >>> data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n///     >>> result = kand.sma(data, 3)\n///     >>> print(result)\n///     [nan, nan, 2.0, 3.0, 4.0]\n///     ```\n#[pyfunction]\n#[pyo3(name = \"sma\", signature = (data, period))]\npub fn sma_py(\n    py: Python,\n    data: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy array to a Rust slice.\n    let input = data.as_slice()?;\n    let len = input.len();\n\n    // Create a new output array using vec\n    let mut output = vec![0.0; len];\n\n    // Perform the SMA calculation while releasing the GIL to allow other Python threads to run.\n    py.allow_threads(|| sma::sma(input, period, output.as_mut_slice()))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Incrementally calculates the next SMA value.\n///\n/// This function provides an optimized way to update an existing SMA value\n/// when new data arrives, without recalculating the entire series.\n///\n/// Args:\n///     prev_sma: Previous SMA value.\n///     new_price: New price to include in calculation.\n///     old_price: Oldest price to remove from calculation.\n///     period: The time period for SMA calculation (must be >= 2).\n///\n/// Returns:\n///     The next SMA value.\n///\n/// Examples:\n///     ```python\n///     >>> import kand\n///     >>> prev_sma = 4.0\n///     >>> new_price = 10.0\n///     >>> old_price = 2.0\n///     >>> period = 3\n///     >>> next_sma = kand.sma_inc(prev_sma, new_price, old_price, period)\n///     >>> print(next_sma)\n///     6.666666666666666\n///     ```\n#[pyfunction]\n#[pyo3(name = \"sma_inc\", signature = (prev_sma, new_price, old_price, period))]\npub fn sma_inc_py(\n    prev_sma: TAFloat,\n    new_price: TAFloat,\n    old_price: TAFloat,\n    period: usize,\n) -> PyResult<TAFloat> {\n    sma::sma_inc(prev_sma, new_price, old_price, period)\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/stoch.rs",
    "content": "use kand::{TAFloat, ohlcv::stoch};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Stochastic Oscillator indicator over NumPy arrays.\n///\n/// The Stochastic Oscillator is a momentum indicator that shows the location of the close\n/// relative to the high-low range over a set number of periods. The indicator consists of\n/// two lines: %K (the fast line) and %D (the slow line).\n///\n/// Args:\n///     high: High prices as a 1-D NumPy array of type `TAFloat`.\n///     low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///     close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///     k_period: Period for %K calculation. Must be >= 2.\n///     k_slow_period: Smoothing period for slow %K. Must be >= 2.\n///     d_period: Period for %D calculation. Must be >= 2.\n///\n/// Returns:\n///     A tuple of three 1-D NumPy arrays containing:\n///     - Fast %K values\n///     - Slow %K values\n///     - %D values\n///     Each array has the same length as the input, with initial values being NaN.\n///\n/// Examples:\n///     ```python\n///     >>> import numpy as np\n///     >>> import kand\n///     >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n///     >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n///     >>> close = np.array([9.0, 11.0, 14.0, 12.0, 11.0])\n///     >>> fast_k, k, d = kand.stoch(high, low, close, 3, 2, 2)\n///     ```\n#[pyfunction]\n#[pyo3(name = \"stoch\", signature = (high, low, close, k_period, k_slow_period, d_period))]\npub fn stoch_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    k_period: usize,\n    k_slow_period: usize,\n    d_period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let high_slice = high.as_slice()?;\n    let low_slice = low.as_slice()?;\n    let close_slice = close.as_slice()?;\n    let len = high_slice.len();\n\n    let mut output_fast_k = vec![0.0; len];\n    let mut output_k = vec![0.0; len];\n    let mut output_d = vec![0.0; len];\n\n    py.allow_threads(|| {\n        stoch::stoch(\n            high_slice,\n            low_slice,\n            close_slice,\n            k_period,\n            k_slow_period,\n            d_period,\n            output_fast_k.as_mut_slice(),\n            output_k.as_mut_slice(),\n            output_d.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_fast_k.into_pyarray(py).into(),\n        output_k.into_pyarray(py).into(),\n        output_d.into_pyarray(py).into(),\n    ))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/supertrend.rs",
    "content": "use kand::{TAFloat, TAInt, ohlcv::supertrend};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Supertrend indicator over NumPy arrays.\n///\n/// The Supertrend indicator is a trend-following indicator that combines Average True Range (ATR)\n/// with basic upper and lower bands to identify trend direction and potential reversal points.\n///\n/// Args:\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Period for ATR calculation (typically 7-14). Must be positive.\n///   multiplier: ATR multiplier (typically 2-4).\n///\n/// Returns:\n///   A tuple of five 1-D NumPy arrays:\n///   - trend: Array containing trend direction (1.0 for uptrend, -1.0 for downtrend)\n///   - supertrend: Array containing Supertrend values\n///   - atr: Array containing ATR values\n///   - upper: Array containing upper band values\n///   - lower: Array containing lower band values\n///   All arrays have the same length as the input, with the first `period-1` elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n///   >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n///   >>> close = np.array([9.0, 11.0, 14.0, 12.0, 11.0])\n///   >>> trend, supertrend, atr, upper, lower = kand.supertrend(high, low, close, 3, 3.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"supertrend\", signature = (high, low, close, period, multiplier))]\npub fn supertrend_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    period: usize,\n    multiplier: TAFloat,\n) -> PyResult<(\n    Py<PyArray1<TAInt>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    // Convert the input NumPy arrays to Rust slices\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_high.len();\n\n    // Create new output arrays using vec\n    let mut output_trend = vec![0; len];\n    let mut output_supertrend = vec![0.0; len];\n    let mut output_atr = vec![0.0; len];\n    let mut output_upper = vec![0.0; len];\n    let mut output_lower = vec![0.0; len];\n\n    // Perform the Supertrend calculation while releasing the GIL\n    py.allow_threads(|| {\n        supertrend::supertrend(\n            input_high,\n            input_low,\n            input_close,\n            period,\n            multiplier,\n            output_trend.as_mut_slice(),\n            output_supertrend.as_mut_slice(),\n            output_atr.as_mut_slice(),\n            output_upper.as_mut_slice(),\n            output_lower.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output arrays to Python objects\n    Ok((\n        output_trend.into_pyarray(py).into(),\n        output_supertrend.into_pyarray(py).into(),\n        output_atr.into_pyarray(py).into(),\n        output_upper.into_pyarray(py).into(),\n        output_lower.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculates a single Supertrend value incrementally.\n///\n/// This function provides an optimized way to calculate the latest Supertrend value\n/// using previous values, making it ideal for real-time calculations.\n///\n/// Args:\n///   high: Current period's high price\n///   low: Current period's low price\n///   close: Current period's close price\n///   prev_close: Previous period's close price\n///   prev_atr: Previous period's ATR value\n///   prev_trend: Previous period's trend direction (1 for uptrend, -1 for downtrend)\n///   prev_upper: Previous period's upper band\n///   prev_lower: Previous period's lower band\n///   period: ATR calculation period (typically 7-14)\n///   multiplier: ATR multiplier (typically 2-4)\n///\n/// Returns:\n///   A tuple containing:\n///   - trend: Current trend direction (1 for uptrend, -1 for downtrend)\n///   - supertrend: Current Supertrend value\n///   - atr: Current ATR value\n///   - upper: Current upper band\n///   - lower: Current lower band\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> trend, supertrend, atr, upper, lower = kand.supertrend_inc(\n///   ...     15.0,   # Current high\n///   ...     11.0,   # Current low\n///   ...     14.0,   # Current close\n///   ...     11.0,   # Previous close\n///   ...     2.0,    # Previous ATR\n///   ...     1,      # Previous trend\n///   ...     16.0,   # Previous upper band\n///   ...     10.0,   # Previous lower band\n///   ...     7,      # ATR period\n///   ...     3.0,    # Multiplier\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"supertrend_inc\", signature = (\n    high,\n    low,\n    close,\n    prev_close,\n    prev_atr,\n    prev_trend,\n    prev_upper,\n    prev_lower,\n    period,\n    multiplier\n))]\npub fn supertrend_inc_py(\n    high: TAFloat,\n    low: TAFloat,\n    close: TAFloat,\n    prev_close: TAFloat,\n    prev_atr: TAFloat,\n    prev_trend: TAInt,\n    prev_upper: TAFloat,\n    prev_lower: TAFloat,\n    period: usize,\n    multiplier: TAFloat,\n) -> PyResult<(TAInt, TAFloat, TAFloat, TAFloat, TAFloat)> {\n    supertrend::supertrend_inc(\n        high, low, close, prev_close, prev_atr, prev_trend, prev_upper, prev_lower, period,\n        multiplier,\n    )\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/t3.rs",
    "content": "use kand::{TAFloat, ohlcv::t3};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the T3 (Triple Exponential Moving Average) indicator over a NumPy array.\n///\n/// T3 is a sophisticated moving average developed by Tim Tillson that reduces lag while maintaining smoothness.\n/// It combines six EMAs with optimized weightings to produce a responsive yet smooth indicator.\n///\n/// Args:\n///     data: Input data as a 1-D NumPy array of type `TAFloat`.\n///     period: Smoothing period for EMAs (must be >= 2).\n///     vfactor: Volume factor controlling smoothing (typically 0-1).\n///\n/// Returns:\n///     A tuple of seven 1-D NumPy arrays containing:\n///     - T3 values\n///     - EMA1 values\n///     - EMA2 values\n///     - EMA3 values\n///     - EMA4 values\n///     - EMA5 values\n///     - EMA6 values\n///     Each array has the same length as the input, with initial values being NaN.\n///\n/// Examples:\n///     ```python\n///     >>> import numpy as np\n///     >>> import kand\n///     >>> data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n///     >>> t3, e1, e2, e3, e4, e5, e6 = kand.t3(data, 2, 0.7)\n///     ```\n#[pyfunction]\n#[pyo3(name = \"t3\", signature = (data, period, vfactor))]\npub fn t3_py(\n    py: Python,\n    data: PyReadonlyArray1<TAFloat>,\n    period: usize,\n    vfactor: TAFloat,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let input = data.as_slice()?;\n    let len = input.len();\n\n    let mut output = vec![0.0; len];\n    let mut ema1 = vec![0.0; len];\n    let mut ema2 = vec![0.0; len];\n    let mut ema3 = vec![0.0; len];\n    let mut ema4 = vec![0.0; len];\n    let mut ema5 = vec![0.0; len];\n    let mut ema6 = vec![0.0; len];\n\n    py.allow_threads(|| {\n        t3::t3(\n            input,\n            period,\n            vfactor,\n            output.as_mut_slice(),\n            ema1.as_mut_slice(),\n            ema2.as_mut_slice(),\n            ema3.as_mut_slice(),\n            ema4.as_mut_slice(),\n            ema5.as_mut_slice(),\n            ema6.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output.into_pyarray(py).into(),\n        ema1.into_pyarray(py).into(),\n        ema2.into_pyarray(py).into(),\n        ema3.into_pyarray(py).into(),\n        ema4.into_pyarray(py).into(),\n        ema5.into_pyarray(py).into(),\n        ema6.into_pyarray(py).into(),\n    ))\n}\n\n/// Incrementally calculates the next T3 value.\n///\n/// This function provides an optimized way to update T3 values in real-time by using\n/// previously calculated EMA values.\n///\n/// Args:\n///     price: Latest price value to calculate T3 from.\n///     prev_ema1: Previous EMA1 value.\n///     prev_ema2: Previous EMA2 value.\n///     prev_ema3: Previous EMA3 value.\n///     prev_ema4: Previous EMA4 value.\n///     prev_ema5: Previous EMA5 value.\n///     prev_ema6: Previous EMA6 value.\n///     period: Smoothing period for EMAs (must be >= 2).\n///     vfactor: Volume factor (typically 0-1).\n///\n/// Returns:\n///     A tuple containing:\n///     - Latest T3 value\n///     - Updated EMA1 value\n///     - Updated EMA2 value\n///     - Updated EMA3 value\n///     - Updated EMA4 value\n///     - Updated EMA5 value\n///     - Updated EMA6 value\n///\n/// Examples:\n///     ```python\n///     >>> import kand\n///     >>> t3, e1, e2, e3, e4, e5, e6 = kand.t3_inc(\n///     ...     100.0,  # New price\n///     ...     95.0,   # Previous EMA1\n///     ...     94.0,   # Previous EMA2\n///     ...     93.0,   # Previous EMA3\n///     ...     92.0,   # Previous EMA4\n///     ...     91.0,   # Previous EMA5\n///     ...     90.0,   # Previous EMA6\n///     ...     5,      # Period\n///     ...     0.7,    # Volume factor\n///     ... )\n///     ```\n#[pyfunction]\n#[pyo3(name = \"t3_inc\", signature = (price, prev_ema1, prev_ema2, prev_ema3, prev_ema4, prev_ema5, prev_ema6, period, vfactor))]\npub fn t3_inc_py(\n    price: TAFloat,\n    prev_ema1: TAFloat,\n    prev_ema2: TAFloat,\n    prev_ema3: TAFloat,\n    prev_ema4: TAFloat,\n    prev_ema5: TAFloat,\n    prev_ema6: TAFloat,\n    period: usize,\n    vfactor: TAFloat,\n) -> PyResult<(\n    TAFloat,\n    TAFloat,\n    TAFloat,\n    TAFloat,\n    TAFloat,\n    TAFloat,\n    TAFloat,\n)> {\n    t3::t3_inc(\n        price, prev_ema1, prev_ema2, prev_ema3, prev_ema4, prev_ema5, prev_ema6, period, vfactor,\n    )\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/tema.rs",
    "content": "use kand::{TAFloat, ohlcv::tema};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculate Triple Exponential Moving Average (TEMA) for a NumPy array.\n///\n/// TEMA is an enhanced moving average designed to reduce lag while maintaining smoothing properties.\n/// It applies triple exponential smoothing to put more weight on recent data and less on older data.\n///\n/// Args:\n///   prices: Input prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Smoothing period for calculations (must be >= 2).\n///\n/// Returns:\n///   A tuple of 4 1-D NumPy arrays containing:\n///   - TEMA values\n///   - First EMA values\n///   - Second EMA values\n///   - Third EMA values\n///   The first (3 * (period - 1)) elements of each array contain NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> prices = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])\n///   >>> tema, ema1, ema2, ema3 = kand.tema(prices, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"tema\", signature = (prices, period))]\npub fn tema_py(\n    py: Python,\n    prices: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let prices_slice = prices.as_slice()?;\n    let len = prices_slice.len();\n\n    let mut output_tema = vec![0.0; len];\n    let mut output_ema1 = vec![0.0; len];\n    let mut output_ema2 = vec![0.0; len];\n    let mut output_ema3 = vec![0.0; len];\n\n    py.allow_threads(|| {\n        tema::tema(\n            prices_slice,\n            period,\n            output_tema.as_mut_slice(),\n            output_ema1.as_mut_slice(),\n            output_ema2.as_mut_slice(),\n            output_ema3.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_tema.into_pyarray(py).into(),\n        output_ema1.into_pyarray(py).into(),\n        output_ema2.into_pyarray(py).into(),\n        output_ema3.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculate the next TEMA value incrementally.\n///\n/// Args:\n///   new_price: Latest price value to process.\n///   prev_ema1: Previous value of first EMA.\n///   prev_ema2: Previous value of second EMA.\n///   prev_ema3: Previous value of third EMA.\n///   period: Smoothing period for calculations (must be >= 2).\n///\n/// Returns:\n///   A tuple containing:\n///   - Current TEMA value\n///   - Updated first EMA\n///   - Updated second EMA\n///   - Updated third EMA\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> tema, ema1, ema2, ema3 = kand.tema_inc(\n///   ...     10.0,  # new_price\n///   ...     9.0,   # prev_ema1\n///   ...     8.0,   # prev_ema2\n///   ...     7.0,   # prev_ema3\n///   ...     3      # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"tema_inc\", signature = (new_price, prev_ema1, prev_ema2, prev_ema3, period))]\npub fn tema_inc_py(\n    new_price: TAFloat,\n    prev_ema1: TAFloat,\n    prev_ema2: TAFloat,\n    prev_ema3: TAFloat,\n    period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat, TAFloat)> {\n    tema::tema_inc(new_price, prev_ema1, prev_ema2, prev_ema3, period)\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/trange.rs",
    "content": "use kand::{TAFloat, ohlcv::trange};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the True Range (TR) over NumPy arrays.\n///\n/// True Range measures the market's volatility by considering the current high-low range\n/// and the previous close price.\n///\n/// Args:\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///\n/// Returns:\n///   A new 1-D NumPy array containing the TR values. The array has the same length as the input,\n///   with the first element containing NaN value.\n\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([10.0, 12.0, 15.0])\n///   >>> low = np.array([8.0, 9.0, 11.0])\n///   >>> close = np.array([9.0, 11.0, 14.0])\n///   >>> result = kand.trange(high, low, close)\n///   >>> print(result)\n///   [nan, 3.0, 4.0]\n///   ```\n#[pyfunction]\n#[pyo3(name = \"trange\", signature = (high, low, close))]\npub fn trange_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy arrays to Rust slices\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_high.len();\n\n    // Create a new output array using vec\n    let mut output = vec![0.0; len];\n\n    // Perform the TR calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| trange::trange(input_high, input_low, input_close, output.as_mut_slice()))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Calculates a single True Range value for the most recent period.\n///\n/// Args:\n///   high: Current period's high price.\n///   low: Current period's low price.\n///   prev_close: Previous period's closing price.\n///\n/// Returns:\n///   The calculated True Range value.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> tr = kand.trange_inc(12.0, 9.0, 11.0)\n///   >>> print(tr)\n///   3.0  # max(3, 1, 2)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"trange_inc\", signature = (high, low, prev_close))]\npub fn trange_inc_py(high: TAFloat, low: TAFloat, prev_close: TAFloat) -> PyResult<TAFloat> {\n    trange::trange_inc(high, low, prev_close)\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/trima.rs",
    "content": "use kand::{TAFloat, ohlcv::trima};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculate Triangular Moving Average (TRIMA) for a NumPy array.\n///\n/// TRIMA is a double-smoothed moving average that places more weight on the middle portion of the price series\n/// and less weight on the first and last portions. This results in a smoother moving average compared to a\n/// Simple Moving Average (SMA).\n///\n/// Args:\n///   prices: Input prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Smoothing period for calculations (must be >= 2).\n///\n/// Returns:\n///   A tuple of 2 1-D NumPy arrays containing:\n///   - First SMA values\n///   - Final TRIMA values\n///   The first (period - 1) elements of each array contain NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> prices = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n///   >>> sma1, trima = kand.trima(prices, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"trima\", signature = (prices, period))]\npub fn trima_py(\n    py: Python,\n    prices: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(Py<PyArray1<TAFloat>>, Py<PyArray1<TAFloat>>)> {\n    let prices_slice = prices.as_slice()?;\n    let len = prices_slice.len();\n\n    let mut output_sma1 = vec![0.0; len];\n    let mut output_sma2 = vec![0.0; len];\n\n    py.allow_threads(|| {\n        trima::trima(\n            prices_slice,\n            period,\n            output_sma1.as_mut_slice(),\n            output_sma2.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_sma1.into_pyarray(py).into(),\n        output_sma2.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculate the next TRIMA value incrementally.\n///\n/// Args:\n///   prev_sma1: Previous first SMA value.\n///   prev_sma2: Previous TRIMA value.\n///   new_price: Latest price to include in calculation.\n///   old_price: Price dropping out of first window.\n///   old_sma1: SMA1 value dropping out of second window.\n///   period: Smoothing period for calculations (must be >= 2).\n///\n/// Returns:\n///   A tuple containing:\n///   - Updated first SMA value\n///   - Updated TRIMA value\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> trima, sma1 = kand.trima_inc(\n///   ...     35.5,  # prev_sma1\n///   ...     35.2,  # prev_sma2\n///   ...     36.0,  # new_price\n///   ...     35.0,  # old_price\n///   ...     35.1,  # old_sma1\n///   ...     5      # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"trima_inc\", signature = (prev_sma1, prev_sma2, new_price, old_price, old_sma1, period))]\npub fn trima_inc_py(\n    prev_sma1: TAFloat,\n    prev_sma2: TAFloat,\n    new_price: TAFloat,\n    old_price: TAFloat,\n    old_sma1: TAFloat,\n    period: usize,\n) -> PyResult<(TAFloat, TAFloat)> {\n    trima::trima_inc(prev_sma1, prev_sma2, new_price, old_price, old_sma1, period)\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/trix.rs",
    "content": "use kand::{TAFloat, ohlcv::trix};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculates the Triple Exponential Moving Average Oscillator (TRIX) over a NumPy array.\n///\n/// TRIX is a momentum oscillator that measures the rate of change of a triple exponentially smoothed moving average.\n/// It helps identify oversold and overbought conditions and potential trend reversals through divergences.\n///\n/// Args:\n///   prices: Input prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Period for EMA calculations (must be >= 2).\n///\n/// Returns:\n///   A tuple of 4 1-D NumPy arrays containing:\n///   - TRIX values\n///   - First EMA values\n///   - Second EMA values\n///   - Third EMA values\n///   The first lookback elements of each array contain NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> prices = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n///   >>> trix, ema1, ema2, ema3 = kand.trix(prices, 2)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"trix\", signature = (prices, period))]\npub fn trix_py(\n    py: Python,\n    prices: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let prices_slice = prices.as_slice()?;\n    let len = prices_slice.len();\n\n    let mut output = vec![0.0; len];\n    let mut ema1 = vec![0.0; len];\n    let mut ema2 = vec![0.0; len];\n    let mut ema3 = vec![0.0; len];\n\n    py.allow_threads(|| {\n        trix::trix(\n            prices_slice,\n            period,\n            output.as_mut_slice(),\n            ema1.as_mut_slice(),\n            ema2.as_mut_slice(),\n            ema3.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output.into_pyarray(py).into(),\n        ema1.into_pyarray(py).into(),\n        ema2.into_pyarray(py).into(),\n        ema3.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculates a single new TRIX value incrementally.\n///\n/// Args:\n///   price: Current price value.\n///   prev_ema1: Previous first EMA value.\n///   prev_ema2: Previous second EMA value.\n///   prev_ema3: Previous third EMA value.\n///   period: Period for EMA calculations (must be >= 2).\n///\n/// Returns:\n///   A tuple containing:\n///   - TRIX value\n///   - Updated first EMA value\n///   - Updated second EMA value\n///   - Updated third EMA value\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> trix, ema1, ema2, ema3 = kand.trix_inc(\n///   ...     100.0,  # price\n///   ...     98.0,   # prev_ema1\n///   ...     97.0,   # prev_ema2\n///   ...     96.0,   # prev_ema3\n///   ...     14      # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"trix_inc\", signature = (price, prev_ema1, prev_ema2, prev_ema3, period))]\npub fn trix_inc_py(\n    price: TAFloat,\n    prev_ema1: TAFloat,\n    prev_ema2: TAFloat,\n    prev_ema3: TAFloat,\n    period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat, TAFloat)> {\n    trix::trix_inc(price, prev_ema1, prev_ema2, prev_ema3, period)\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/typprice.rs",
    "content": "use kand::{TAFloat, ohlcv::typprice};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Typical Price over NumPy arrays.\n///\n/// The Typical Price is calculated by taking the arithmetic mean of the high, low and close prices\n/// for each period.\n///\n/// Args:\n///   high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Input close prices as a 1-D NumPy array of type `TAFloat`.\n///\n/// Returns:\n///   A new 1-D NumPy array containing the Typical Price values. The array has the same length as the inputs.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([24.20, 24.07, 24.04])\n///   >>> low = np.array([23.85, 23.72, 23.64])\n///   >>> close = np.array([23.89, 23.95, 23.67])\n///   >>> result = kand.typprice(high, low, close)\n///   >>> print(result)\n///   [23.98, 23.91, 23.78]\n///   ```\n#[pyfunction]\n#[pyo3(name = \"typprice\", signature = (high, low, close))]\npub fn typprice_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert the input NumPy arrays to Rust slices\n    let input_high = high.as_slice()?;\n    let input_low = low.as_slice()?;\n    let input_close = close.as_slice()?;\n    let len = input_high.len();\n\n    // Create a new output array using vec\n    let mut output = vec![0.0; len];\n\n    // Perform the Typical Price calculation while releasing the GIL to allow other Python threads to run\n    py.allow_threads(|| {\n        typprice::typprice(input_high, input_low, input_close, output.as_mut_slice())\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output array to a Python object\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Calculates a single Typical Price value incrementally.\n///\n/// Args:\n///   high: Current period's high price.\n///   low: Current period's low price.\n///   close: Current period's close price.\n///\n/// Returns:\n///   The calculated Typical Price value.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> typ_price = kand.typprice_inc(24.20, 23.85, 23.89)\n///   >>> print(typ_price)\n///   23.98  # (24.20 + 23.85 + 23.89) / 3\n///   ```\n#[pyfunction]\n#[pyo3(name = \"typprice_inc\", signature = (high, low, close))]\npub fn typprice_inc_py(high: TAFloat, low: TAFloat, close: TAFloat) -> PyResult<TAFloat> {\n    typprice::typprice_inc(high, low, close)\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/vegas.rs",
    "content": "use kand::{TAFloat, ohlcv::vegas};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the VEGAS (Volume and EMA Guided Adaptive Scaling) indicator over NumPy arrays.\n///\n/// VEGAS is a trend following indicator that uses multiple EMAs to define channels and boundaries.\n///\n/// Args:\n///   prices: Input prices as a 1-D NumPy array of type `TAFloat`.\n///\n/// Returns:\n///   A tuple of four 1-D NumPy arrays containing:\n///   - Channel Upper (EMA 144)\n///   - Channel Lower (EMA 169)\n///   - Boundary Upper (EMA 576)\n///   - Boundary Lower (EMA 676)\n///   Each array has the same length as the input, with the first 675 elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> prices = np.array([44.34, 44.09, 44.15, 43.61, 44.33])\n///   >>> ch_upper, ch_lower, b_upper, b_lower = kand.vegas(prices)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"vegas\", signature = (prices))]\npub fn vegas_py(\n    py: Python,\n    prices: PyReadonlyArray1<TAFloat>,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    // Convert the input NumPy array to a Rust slice\n    let input_prices = prices.as_slice()?;\n    let len = input_prices.len();\n\n    // Create new output arrays using vec\n    let mut output_channel_upper = vec![0.0; len];\n    let mut output_channel_lower = vec![0.0; len];\n    let mut output_boundary_upper = vec![0.0; len];\n    let mut output_boundary_lower = vec![0.0; len];\n\n    // Perform the VEGAS calculation while releasing the GIL\n    py.allow_threads(|| {\n        vegas::vegas(\n            input_prices,\n            output_channel_upper.as_mut_slice(),\n            output_channel_lower.as_mut_slice(),\n            output_boundary_upper.as_mut_slice(),\n            output_boundary_lower.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert the output arrays to Python objects\n    Ok((\n        output_channel_upper.into_pyarray(py).into(),\n        output_channel_lower.into_pyarray(py).into(),\n        output_boundary_upper.into_pyarray(py).into(),\n        output_boundary_lower.into_pyarray(py).into(),\n    ))\n}\n\n/// Incrementally calculates the next VEGAS values.\n///\n/// Args:\n///   price: Current price value.\n///   prev_channel_upper: Previous EMA(144) value.\n///   prev_channel_lower: Previous EMA(169) value.\n///   prev_boundary_upper: Previous EMA(576) value.\n///   prev_boundary_lower: Previous EMA(676) value.\n///\n/// Returns:\n///   A tuple containing:\n///   - Updated Channel Upper value\n///   - Updated Channel Lower value\n///   - Updated Boundary Upper value\n///   - Updated Boundary Lower value\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> price = 100.0\n///   >>> prev_values = (98.0, 97.5, 96.0, 95.5)\n///   >>> ch_upper, ch_lower, b_upper, b_lower = kand.vegas_inc(\n///   ...     price,\n///   ...     prev_values[0],\n///   ...     prev_values[1],\n///   ...     prev_values[2],\n///   ...     prev_values[3]\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"vegas_inc\", signature = (price, prev_channel_upper, prev_channel_lower, prev_boundary_upper, prev_boundary_lower))]\npub fn vegas_inc_py(\n    price: TAFloat,\n    prev_channel_upper: TAFloat,\n    prev_channel_lower: TAFloat,\n    prev_boundary_upper: TAFloat,\n    prev_boundary_lower: TAFloat,\n) -> PyResult<(TAFloat, TAFloat, TAFloat, TAFloat)> {\n    vegas::vegas_inc(\n        price,\n        prev_channel_upper,\n        prev_channel_lower,\n        prev_boundary_upper,\n        prev_boundary_lower,\n    )\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/vwap.rs",
    "content": "use kand::{TAFloat, ohlcv::vwap};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculates Volume Weighted Average Price (VWAP) for a series of price data.\n///\n/// Args:\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///   volume: Volume data as a 1-D NumPy array of type `TAFloat`.\n///\n/// Returns:\n///   A tuple of three 1-D NumPy arrays containing:\n///   - VWAP values\n///   - Cumulative price-volume products\n///   - Cumulative volumes\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([10.0, 12.0, 15.0])\n///   >>> low = np.array([8.0, 9.0, 11.0])\n///   >>> close = np.array([9.0, 10.0, 12.0])\n///   >>> volume = np.array([100.0, 150.0, 200.0])\n///   >>> vwap, cum_pv, cum_vol = kand.vwap(high, low, close, volume)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"vwap\", signature = (high, low, close, volume))]\npub fn vwap_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    volume: PyReadonlyArray1<TAFloat>,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let high_slice = high.as_slice()?;\n    let low_slice = low.as_slice()?;\n    let close_slice = close.as_slice()?;\n    let volume_slice = volume.as_slice()?;\n    let len = high_slice.len();\n\n    let mut output_vwap = vec![0.0; len];\n    let mut output_cum_pv = vec![0.0; len];\n    let mut output_cum_vol = vec![0.0; len];\n\n    py.allow_threads(|| {\n        vwap::vwap(\n            high_slice,\n            low_slice,\n            close_slice,\n            volume_slice,\n            output_vwap.as_mut_slice(),\n            output_cum_pv.as_mut_slice(),\n            output_cum_vol.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_vwap.into_pyarray(py).into(),\n        output_cum_pv.into_pyarray(py).into(),\n        output_cum_vol.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculates a single VWAP value from the latest price and volume data.\n///\n/// Args:\n///   high: Latest high price value as `TAFloat`.\n///   low: Latest low price value as `TAFloat`.\n///   close: Latest close price value as `TAFloat`.\n///   volume: Latest volume value as `TAFloat`.\n///   prev_cum_pv: Previous cumulative price-volume product as `TAFloat`.\n///   prev_cum_vol: Previous cumulative volume as `TAFloat`.\n///\n/// Returns:\n///   A tuple containing (new cumulative PV, new cumulative volume, new VWAP).\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> new_cum_pv, new_cum_vol, vwap = kand.vwap_inc(15.0, 11.0, 14.0, 200.0, 1000.0, 150.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"vwap_inc\", signature = (high, low, close, volume, prev_cum_pv, prev_cum_vol))]\npub fn vwap_inc_py(\n    high: TAFloat,\n    low: TAFloat,\n    close: TAFloat,\n    volume: TAFloat,\n    prev_cum_pv: TAFloat,\n    prev_cum_vol: TAFloat,\n) -> PyResult<(TAFloat, TAFloat, TAFloat)> {\n    vwap::vwap_inc(high, low, close, volume, prev_cum_pv, prev_cum_vol)\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/wclprice.rs",
    "content": "use kand::{TAFloat, ohlcv::wclprice};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculates the Weighted Close Price (WCLPRICE) for a series of price data.\n///\n/// The Weighted Close Price is a price indicator that assigns more weight to the closing price\n/// compared to high and low prices. It provides a single value that reflects price action\n/// with emphasis on the closing price.\n///\n/// Args:\n///   high: High prices as a 1-D NumPy array of type `TAFloat`.\n///   low: Low prices as a 1-D NumPy array of type `TAFloat`.\n///   close: Close prices as a 1-D NumPy array of type `TAFloat`.\n///\n/// Returns:\n///   A 1-D NumPy array containing the WCLPRICE values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> high = np.array([10.0, 12.0, 15.0])\n///   >>> low = np.array([8.0, 9.0, 11.0])\n///   >>> close = np.array([9.0, 11.0, 14.0])\n///   >>> wclprice = kand.wclprice(high, low, close)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"wclprice\", signature = (high, low, close))]\npub fn wclprice_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    let high_slice = high.as_slice()?;\n    let low_slice = low.as_slice()?;\n    let close_slice = close.as_slice()?;\n    let len = high_slice.len();\n\n    let mut output = vec![0.0; len];\n\n    py.allow_threads(|| {\n        wclprice::wclprice(high_slice, low_slice, close_slice, output.as_mut_slice())\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Calculates a single Weighted Close Price (WCLPRICE) value from the latest price data.\n///\n/// Args:\n///   high: Latest high price value as `TAFloat`.\n///   low: Latest low price value as `TAFloat`.\n///   close: Latest close price value as `TAFloat`.\n///\n/// Returns:\n///   The calculated WCLPRICE value.\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> wclprice = kand.wclprice_inc(15.0, 11.0, 14.0)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"wclprice_inc\", signature = (high, low, close))]\npub fn wclprice_inc_py(high: TAFloat, low: TAFloat, close: TAFloat) -> PyResult<TAFloat> {\n    wclprice::wclprice_inc(high, low, close)\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/willr.rs",
    "content": "use kand::{TAFloat, ohlcv::willr};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculates Williams %R (Williams Percent Range) for a series of prices.\n///\n/// Williams %R is a momentum indicator that measures overbought and oversold levels by comparing\n/// the closing price to the high-low range over a specified period. The indicator oscillates\n/// between 0 and -100.\n///\n/// Args:\n///     high: Input high prices as a 1-D NumPy array of type `TAFloat`.\n///     low: Input low prices as a 1-D NumPy array of type `TAFloat`.\n///     close: Input closing prices as a 1-D NumPy array of type `TAFloat`.\n///     period: Lookback period for calculations. Must be >= 2.\n///\n/// Returns:\n///     A tuple of three 1-D NumPy arrays containing:\n///     - Williams %R values\n///     - Highest high values for each period\n///     - Lowest low values for each period\n///     Each array has the same length as the input, with the first `period-1` elements containing NaN values.\n///\n/// Examples:\n///     ```python\n///     >>> import numpy as np\n///     >>> import kand\n///     >>> high = np.array([10.0, 12.0, 15.0, 14.0, 13.0])\n///     >>> low = np.array([8.0, 9.0, 11.0, 10.0, 9.0])\n///     >>> close = np.array([9.0, 11.0, 14.0, 12.0, 11.0])\n///     >>> willr, highest, lowest = kand.willr(high, low, close, 3)\n///     ```\n#[pyfunction]\n#[pyo3(name = \"willr\", signature = (high, low, close, period))]\npub fn willr_py(\n    py: Python,\n    high: PyReadonlyArray1<TAFloat>,\n    low: PyReadonlyArray1<TAFloat>,\n    close: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let high_slice = high.as_slice()?;\n    let low_slice = low.as_slice()?;\n    let close_slice = close.as_slice()?;\n    let len = high_slice.len();\n\n    let mut output = vec![0.0; len];\n    let mut output_highest = vec![0.0; len];\n    let mut output_lowest = vec![0.0; len];\n\n    py.allow_threads(|| {\n        willr::willr(\n            high_slice,\n            low_slice,\n            close_slice,\n            period,\n            output.as_mut_slice(),\n            output_highest.as_mut_slice(),\n            output_lowest.as_mut_slice(),\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output.into_pyarray(py).into(),\n        output_highest.into_pyarray(py).into(),\n        output_lowest.into_pyarray(py).into(),\n    ))\n}\n\n/// Incrementally calculates Williams %R for the latest data point.\n///\n/// This function provides an optimized way to calculate the latest Williams %R value\n/// by using previously calculated highest high and lowest low values.\n///\n/// Args:\n///     prev_highest_high: Previous period's highest high value.\n///     prev_lowest_low: Previous period's lowest low value.\n///     prev_high: Previous period's high price.\n///     prev_low: Previous period's low price.\n///     close: Current period's closing price.\n///     high: Current period's high price.\n///     low: Current period's low price.\n///\n/// Returns:\n///     A tuple containing:\n///     - Current Williams %R value\n///     - New highest high\n///     - New lowest low\n///\n/// Examples:\n///     ```python\n///     >>> import kand\n///     >>> willr, high, low = kand.willr_inc(15.0, 10.0, 14.0, 11.0, 12.0, 13.0, 11.0)\n///     ```\n#[pyfunction]\n#[pyo3(\n    name = \"willr_inc\",\n    signature = (prev_highest_high, prev_lowest_low, prev_high, prev_low, close, high, low)\n)]\npub fn willr_inc_py(\n    prev_highest_high: TAFloat,\n    prev_lowest_low: TAFloat,\n    prev_high: TAFloat,\n    prev_low: TAFloat,\n    close: TAFloat,\n    high: TAFloat,\n    low: TAFloat,\n) -> PyResult<(TAFloat, TAFloat, TAFloat)> {\n    willr::willr_inc(\n        prev_highest_high,\n        prev_lowest_low,\n        prev_high,\n        prev_low,\n        close,\n        high,\n        low,\n    )\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/ohlcv/wma.rs",
    "content": "use kand::{TAFloat, ohlcv::wma};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Computes the Weighted Moving Average (WMA) over a NumPy array.\n///\n/// The Weighted Moving Average assigns linearly decreasing weights to each price in the period,\n/// giving more importance to recent prices and less to older ones.\n///\n/// Args:\n///     data: Input data as a 1-D NumPy array of type `TAFloat`.\n///     period: Window size for WMA calculation. Must be >= 2.\n///\n/// Returns:\n///     A new 1-D NumPy array containing the WMA values. The array has the same length as the input,\n///     with the first `period-1` elements containing NaN values.\n///\n/// Examples:\n///     ```python\n///     >>> import numpy as np\n///     >>> import kand\n///     >>> data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n///     >>> result = kand.wma(data, 3)\n///     >>> print(result)\n///     [nan, nan, 2.0, 3.0, 4.0]\n///     ```\n#[pyfunction]\n#[pyo3(name = \"wma\", signature = (data, period))]\npub fn wma_py(\n    py: Python,\n    data: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    let input = data.as_slice()?;\n    let len = input.len();\n    let mut output = vec![0.0; len];\n\n    py.allow_threads(|| wma::wma(input, period, output.as_mut_slice()))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok(output.into_pyarray(py).into())\n}\n\n/// Incrementally calculates the next WMA value.\n///\n/// This function provides an optimized way to calculate the latest WMA value\n/// by using a window of the most recent prices.\n///\n/// Args:\n///     input_window: Array of price values ordered from newest to oldest.\n///     period: The time period for WMA calculation (must be >= 2).\n///\n/// Returns:\n///     The next WMA value.\n///\n/// Examples:\n///     ```python\n///     >>> import kand\n///     >>> window = [5.0, 4.0, 3.0]  # newest to oldest\n///     >>> wma = kand.wma_inc(window, 3)\n///     >>> print(wma)\n///     4.333333333333333\n///     ```\n#[pyfunction]\n#[pyo3(name = \"wma_inc\", signature = (input_window, period))]\npub fn wma_inc_py(input_window: Vec<TAFloat>, period: usize) -> PyResult<TAFloat> {\n    wma::wma_inc(&input_window, period)\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/other/mod.rs",
    "content": ""
  },
  {
    "path": "kand-py/src/ta/stats/beta.rs",
    "content": ""
  },
  {
    "path": "kand-py/src/ta/stats/correl.rs",
    "content": "use kand::{TAFloat, stats::correl};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculate Pearson's Correlation Coefficient between two NumPy arrays\n///\n/// The Pearson Correlation Coefficient measures the linear correlation between two variables,\n/// returning a value between -1 and +1, where:\n/// - +1 indicates perfect positive correlation\n/// - -1 indicates perfect negative correlation\n/// - 0 indicates no linear correlation\n///\n/// Args:\n///   input0: First input series as a 1-D NumPy array of type `TAFloat`.\n///   input1: Second input series as a 1-D NumPy array of type `TAFloat`.\n///   period: Period for calculation (must be >= 2).\n///\n/// Returns:\n///   A tuple of six 1-D NumPy arrays containing:\n///   - Correlation coefficient values\n///   - Running sum of series 0\n///   - Running sum of series 1\n///   - Running sum of squares of series 0\n///   - Running sum of squares of series 1\n///   - Running sum of products\n///   Each array has the same length as the input, with the first (period-1) elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> series1 = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n///   >>> series2 = np.array([2.0, 4.0, 6.0, 8.0, 10.0])\n///   >>> correl, sum0, sum1, sum0_sq, sum1_sq, sum01 = kand.correl(series1, series2, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"correl\", signature = (input0, input1, period))]\npub fn correl_py(\n    py: Python,\n    input0: PyReadonlyArray1<TAFloat>,\n    input1: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let input0_array = input0.as_slice()?;\n    let input1_array = input1.as_slice()?;\n    let len = input0_array.len();\n\n    if len != input1_array.len() {\n        return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(\n            \"Input arrays must have the same length\",\n        ));\n    }\n\n    let mut output_correl = vec![0.0; len];\n    let mut output_sum_0 = vec![0.0; len];\n    let mut output_sum_1 = vec![0.0; len];\n    let mut output_sum_0_sq = vec![0.0; len];\n    let mut output_sum_1_sq = vec![0.0; len];\n    let mut output_sum_01 = vec![0.0; len];\n\n    py.allow_threads(|| {\n        correl::correl(\n            input0_array,\n            input1_array,\n            period,\n            &mut output_correl,\n            &mut output_sum_0,\n            &mut output_sum_1,\n            &mut output_sum_0_sq,\n            &mut output_sum_1_sq,\n            &mut output_sum_01,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_correl.into_pyarray(py).into(),\n        output_sum_0.into_pyarray(py).into(),\n        output_sum_1.into_pyarray(py).into(),\n        output_sum_0_sq.into_pyarray(py).into(),\n        output_sum_1_sq.into_pyarray(py).into(),\n        output_sum_01.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculate the latest Correlation value incrementally\n///\n/// This function provides an optimized way to update the Correlation value when new data arrives,\n/// avoiding full recalculation of the entire series.\n///\n/// Args:\n///   new0: The newest value from series 0 to add\n///   new1: The newest value from series 1 to add\n///   old0: The oldest value from series 0 to remove\n///   old1: The oldest value from series 1 to remove\n///   prev_sum0: Previous sum of series 0\n///   prev_sum1: Previous sum of series 1\n///   prev_sum0_sq: Previous sum of squares of series 0\n///   prev_sum1_sq: Previous sum of squares of series 1\n///   prev_sum01: Previous sum of products\n///   period: Period for calculation (must be >= 2)\n///\n/// Returns:\n///   A tuple containing:\n///   - New correlation value\n///   - New sum of series 0\n///   - New sum of series 1\n///   - New sum of squares of series 0\n///   - New sum of squares of series 1\n///   - New sum of products\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> correl, sum0, sum1, sum0_sq, sum1_sq, sum01 = kand.correl_inc(\n///   ...     4.0,    # new value for series 0\n///   ...     8.0,    # new value for series 1\n///   ...     1.0,    # old value for series 0\n///   ...     2.0,    # old value for series 1\n///   ...     6.0,    # previous sum of series 0\n///   ...     12.0,   # previous sum of series 1\n///   ...     14.0,   # previous sum of squares of series 0\n///   ...     56.0,   # previous sum of squares of series 1\n///   ...     28.0,   # previous sum of products\n///   ...     3       # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"correl_inc\")]\npub fn correl_inc_py(\n    py: Python,\n    new0: TAFloat,\n    new1: TAFloat,\n    old0: TAFloat,\n    old1: TAFloat,\n    prev_sum0: TAFloat,\n    prev_sum1: TAFloat,\n    prev_sum0_sq: TAFloat,\n    prev_sum1_sq: TAFloat,\n    prev_sum01: TAFloat,\n    period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat, TAFloat, TAFloat, TAFloat)> {\n    py.allow_threads(|| {\n        correl::correl_inc(\n            new0,\n            new1,\n            old0,\n            old1,\n            prev_sum0,\n            prev_sum1,\n            prev_sum0_sq,\n            prev_sum1_sq,\n            prev_sum01,\n            period,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/stats/fv.rs",
    "content": ""
  },
  {
    "path": "kand-py/src/ta/stats/max.rs",
    "content": "use kand::{TAFloat, stats::max};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculate Maximum Value for a NumPy array\n///\n/// Args:\n///   prices: Input prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Period for MAX calculation (must be >= 2).\n///\n/// Returns:\n///   A 1-D NumPy array containing MAX values. The first (period-1) elements contain NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> prices = np.array([1.0, 2.0, 3.0, 2.5, 4.0])\n///   >>> max_values = kand.max(prices, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"max\", signature = (prices, period))]\npub fn max_py(\n    py: Python,\n    prices: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert input NumPy array to Rust slice\n    let input_prices = prices.as_slice()?;\n    let len = input_prices.len();\n\n    // Create output array using vec\n    let mut output_max = vec![0.0; len];\n\n    // Perform MAX calculation while releasing the GIL\n    py.allow_threads(|| max::max(input_prices, period, &mut output_max))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert output array to Python object\n    Ok(output_max.into_pyarray(py).into())\n}\n\n/// Calculate the latest Maximum Value incrementally\n///\n/// Args:\n///   py: Python interpreter token\n///   price: Current period's price\n///   prev_max: Previous period's MAX value\n///   old_price: Price being removed from the period\n///   period: Period for MAX calculation (must be >= 2)\n///\n/// Returns:\n///   The new MAX value\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> new_max = kand.max_inc(10.5, 11.0, 9.0, 14)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"max_inc\")]\npub fn max_inc_py(\n    py: Python,\n    price: TAFloat,\n    prev_max: TAFloat,\n    old_price: TAFloat,\n    period: usize,\n) -> PyResult<TAFloat> {\n    // Perform incremental MAX calculation while releasing the GIL\n    py.allow_threads(|| max::max_inc(price, prev_max, old_price, period))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/stats/min.rs",
    "content": "use kand::{TAFloat, stats::min};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculate Minimum Value (MIN) for a NumPy array\n///\n/// The MIN indicator finds the lowest price value within a given time period.\n///\n/// Args:\n///   prices: Input prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Period for MIN calculation (must be >= 2).\n///\n/// Returns:\n///   A 1-D NumPy array containing MIN values. First (period-1) elements contain NaN.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> prices = np.array([10.0, 8.0, 6.0, 7.0, 9.0])\n///   >>> min_values = kand.min(prices, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"min\", signature = (prices, period))]\npub fn min_py(\n    py: Python,\n    prices: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert input NumPy array to Rust slice\n    let input_prices = prices.as_slice()?;\n    let len = input_prices.len();\n\n    // Create output array using vec\n    let mut output_min = vec![0.0; len];\n\n    // Perform MIN calculation while releasing the GIL\n    py.allow_threads(|| min::min(input_prices, period, &mut output_min))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert output array to Python object\n    Ok(output_min.into_pyarray(py).into())\n}\n\n/// Calculate the latest MIN value incrementally\n///\n/// Args:\n///   py: Python interpreter token\n///   price: Current period's price\n///   prev_min: Previous period's MIN value\n///   prev_price: Price value being removed from the period\n///   period: Period for MIN calculation (must be >= 2)\n///\n/// Returns:\n///   The new MIN value\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> new_min = kand.min_inc(15.0, 12.0, 14.0, 14)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"min_inc\")]\npub fn min_inc_py(\n    py: Python,\n    price: TAFloat,\n    prev_min: TAFloat,\n    prev_price: TAFloat,\n    period: usize,\n) -> PyResult<TAFloat> {\n    // Perform incremental MIN calculation while releasing the GIL\n    py.allow_threads(|| min::min_inc(price, prev_min, prev_price, period))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/stats/mod.rs",
    "content": "// pub mod beta;\npub mod correl;\npub mod max;\npub mod min;\npub mod stddev;\npub mod sum;\npub mod var;\n"
  },
  {
    "path": "kand-py/src/ta/stats/stddev.rs",
    "content": "use kand::{TAFloat, stats::stddev};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculate Standard Deviation for a NumPy array\n///\n/// Standard Deviation measures the dispersion of values from their mean over a specified period.\n/// It is calculated by taking the square root of the variance.\n///\n/// Args:\n///   input: Input values as a 1-D NumPy array of type `TAFloat`.\n///   period: Period for calculation (must be >= 2).\n///\n/// Returns:\n///   A tuple of three 1-D NumPy arrays containing:\n///   - Standard Deviation values\n///   - Running sum values\n///   - Running sum of squares values\n///   Each array has the same length as the input, with the first (period-1) elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> prices = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n///   >>> stddev, sum, sum_sq = kand.stddev(prices, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"stddev\", signature = (input, period))]\npub fn stddev_py(\n    py: Python,\n    input: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let input_array = input.as_slice()?;\n    let len = input_array.len();\n\n    let mut output_stddev = vec![0.0; len];\n    let mut output_sum = vec![0.0; len];\n    let mut output_sum_sq = vec![0.0; len];\n\n    py.allow_threads(|| {\n        stddev::stddev(\n            input_array,\n            period,\n            &mut output_stddev,\n            &mut output_sum,\n            &mut output_sum_sq,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_stddev.into_pyarray(py).into(),\n        output_sum.into_pyarray(py).into(),\n        output_sum_sq.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculate the latest Standard Deviation value incrementally\n///\n/// Args:\n///   py: Python interpreter token\n///   price: Current period's price\n///   prev_sum: Previous period's sum\n///   prev_sum_sq: Previous period's sum of squares\n///   old_price: Price being removed from the period\n///   period: Period for calculation (must be >= 2)\n///\n/// Returns:\n///   A tuple containing:\n///   - Latest Standard Deviation value\n///   - New sum\n///   - New sum of squares\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> stddev, sum, sum_sq = kand.stddev_inc(\n///   ...     10.0,   # current price\n///   ...     100.0,  # previous sum\n///   ...     1050.0, # previous sum of squares\n///   ...     8.0,    # old price\n///   ...     14      # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"stddev_inc\")]\npub fn stddev_inc_py(\n    py: Python,\n    price: TAFloat,\n    prev_sum: TAFloat,\n    prev_sum_sq: TAFloat,\n    old_price: TAFloat,\n    period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat)> {\n    py.allow_threads(|| stddev::stddev_inc(price, prev_sum, prev_sum_sq, old_price, period))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/stats/sum.rs",
    "content": "use kand::{TAFloat, stats::sum};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculate Sum for a NumPy array\n///\n/// Calculates the rolling sum of values over a specified period.\n///\n/// Args:\n///   input: Input values as a 1-D NumPy array of type `TAFloat`.\n///   period: Period for sum calculation (must be >= 2).\n///\n/// Returns:\n///   A 1-D NumPy array containing the sum values.\n///   The first (period-1) elements contain NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\n///   >>> sums = kand.sum(data, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"sum\", signature = (input, period))]\npub fn sum_py(\n    py: Python,\n    input: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<Py<PyArray1<TAFloat>>> {\n    // Convert input NumPy array to Rust slice\n    let input_data = input.as_slice()?;\n    let len = input_data.len();\n\n    // Create output array using vec\n    let mut output_sum = vec![0.0; len];\n\n    // Perform sum calculation while releasing the GIL\n    py.allow_threads(|| sum::sum(input_data, period, &mut output_sum))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    // Convert output array to Python object\n    Ok(output_sum.into_pyarray(py).into())\n}\n\n/// Calculate the latest sum value incrementally\n///\n/// Args:\n///   py: Python interpreter token\n///   new_price: The newest price value to add\n///   old_price: The oldest price value to remove\n///   prev_sum: The previous sum value\n///\n/// Returns:\n///   The new sum value\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> new_sum = kand.sum_inc(\n///   ...     5.0,    # new price\n///   ...     3.0,    # old price\n///   ...     10.0,   # previous sum\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"sum_inc\")]\npub fn sum_inc_py(\n    py: Python,\n    new_price: TAFloat,\n    old_price: TAFloat,\n    prev_sum: TAFloat,\n) -> PyResult<TAFloat> {\n    // Perform incremental sum calculation while releasing the GIL\n    py.allow_threads(|| sum::sum_inc(new_price, old_price, prev_sum))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-py/src/ta/stats/var.rs",
    "content": "use kand::{TAFloat, stats::var};\nuse numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};\nuse pyo3::prelude::*;\n\n/// Calculate Variance (VAR) for a NumPy array\n///\n/// Variance measures the average squared deviation of data points from their mean over a specified period.\n///\n/// Args:\n///   prices: Input prices as a 1-D NumPy array of type `TAFloat`.\n///   period: Period for Variance calculation (must be >= 2).\n///\n/// Returns:\n///   A tuple of three 1-D NumPy arrays containing:\n///   - Variance values\n///   - Running sum values\n///   - Running sum of squares values\n///   Each array has the same length as the input, with the first (period-1) elements containing NaN values.\n///\n/// Examples:\n///   ```python\n///   >>> import numpy as np\n///   >>> import kand\n///   >>> prices = np.array([2.0, 4.0, 6.0, 8.0, 10.0])\n///   >>> var, sum, sum_sq = kand.var(prices, 3)\n///   ```\n#[pyfunction]\n#[pyo3(name = \"var\", signature = (prices, period))]\npub fn var_py(\n    py: Python,\n    prices: PyReadonlyArray1<TAFloat>,\n    period: usize,\n) -> PyResult<(\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n    Py<PyArray1<TAFloat>>,\n)> {\n    let input_prices = prices.as_slice()?;\n    let len = input_prices.len();\n\n    let mut output_var = vec![0.0; len];\n    let mut output_sum = vec![0.0; len];\n    let mut output_sum_sq = vec![0.0; len];\n\n    py.allow_threads(|| {\n        var::var(\n            input_prices,\n            period,\n            &mut output_var,\n            &mut output_sum,\n            &mut output_sum_sq,\n        )\n    })\n    .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;\n\n    Ok((\n        output_var.into_pyarray(py).into(),\n        output_sum.into_pyarray(py).into(),\n        output_sum_sq.into_pyarray(py).into(),\n    ))\n}\n\n/// Calculate the latest Variance value incrementally\n///\n/// Args:\n///   py: Python interpreter token\n///   price: Current period's price\n///   prev_sum: Previous period's sum\n///   prev_sum_sq: Previous period's sum of squares\n///   old_price: Price being removed from the period\n///   period: Period for Variance calculation (must be >= 2)\n///\n/// Returns:\n///   A tuple containing:\n///   - Latest Variance value\n///   - New sum\n///   - New sum of squares\n///\n/// Examples:\n///   ```python\n///   >>> import kand\n///   >>> var, sum, sum_sq = kand.var_inc(\n///   ...     10.0,  # current price\n///   ...     25.0,  # previous sum\n///   ...     220.0, # previous sum of squares\n///   ...     5.0,   # price to remove\n///   ...     3      # period\n///   ... )\n///   ```\n#[pyfunction]\n#[pyo3(name = \"var_inc\")]\npub fn var_inc_py(\n    py: Python,\n    price: TAFloat,\n    prev_sum: TAFloat,\n    prev_sum_sq: TAFloat,\n    old_price: TAFloat,\n    period: usize,\n) -> PyResult<(TAFloat, TAFloat, TAFloat)> {\n    py.allow_threads(|| var::var_inc(price, prev_sum, prev_sum_sq, old_price, period))\n        .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))\n}\n"
  },
  {
    "path": "kand-wasm/Cargo.toml",
    "content": "[package]\nname = \"kand-wasm\"\nversion = \"0.1.1\"\nedition = \"2024\"\n\n[lib]\nname = \"kand\"\ncrate-type = [\"cdylib\", \"rlib\"]\n\n[dependencies]\nkand = { workspace = true }\nwasm-bindgen = { workspace = true }\n\n[package.metadata.wasm-pack.profile.release]\nwasm-opt = false\n\n[features]\ndefault = [\"f64\", \"i64\", \"check\"]\nf32 = [\"kand/f32\"]\nf64 = [\"kand/f64\"]\ni32 = [\"kand/i32\"]\ni64 = [\"kand/i64\"]\ncheck = [\"kand/check\"]\ncheck-nan = [\"kand/check-nan\"]\nallow-nan = [\"kand/allow-nan\"]\n"
  },
  {
    "path": "kand-wasm/src/lib.rs",
    "content": "pub mod ta;\n"
  },
  {
    "path": "kand-wasm/src/ta/mod.rs",
    "content": "pub mod ohlcv;\npub mod other;\npub mod stats;\n"
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/ad.rs",
    "content": "use kand::ta::ohlcv::ad;\nuse wasm_bindgen::prelude::*;\n\n/**\n * Returns the lookback period for A/D calculation.\n * @returns {number} The lookback period (always 0 for A/D).\n */\n#[wasm_bindgen(js_name = adLookback)]\npub fn ad_lookback_wasm() -> Result<usize, JsValue> {\n    ad::lookback().map_err(|e| JsValue::from_str(&e.to_string()))\n}\n\n/**\n * Calculates the Accumulation/Distribution (A/D) indicator.\n * @param {Float64Array} inputHigh - Array of high prices.\n * @param {Float64Array} inputLow - Array of low prices.\n * @param {Float64Array} inputClose - Array of close prices.\n * @param {Float64Array} inputVolume - Array of volume values.\n * @returns {Float64Array} An array of A/D values.\n * @throws {Error} If inputs are invalid or calculation fails.\n */\n#[wasm_bindgen(js_name = ad)]\npub fn ad_wasm(\n    input_high: Vec<f64>,\n    input_low: Vec<f64>,\n    input_close: Vec<f64>,\n    input_volume: Vec<f64>,\n) -> Result<Vec<f64>, JsValue> {\n    let mut output_ad = vec![0.0; input_high.len()];\n    ad::ad(\n        &input_high,\n        &input_low,\n        &input_close,\n        &input_volume,\n        &mut output_ad,\n    )\n    .map_err(|e| JsValue::from_str(&e.to_string()))?;\n    Ok(output_ad)\n}\n\n/**\n * Calculates the next A/D value incrementally.\n * @param {number} inputHigh - The current high price.\n * @param {number} inputLow - The current low price.\n * @param {number} inputClose - The current close price.\n * @param {number} inputVolume - The current volume.\n * @param {number} prevAd - The previous A/D value.\n * @returns {number} The new A/D value.\n * @throws {Error} If inputs are invalid or calculation fails.\n */\n#[wasm_bindgen(js_name = adInc)]\npub fn ad_inc_wasm(\n    input_high: f64,\n    input_low: f64,\n    input_close: f64,\n    input_volume: f64,\n    prev_ad: f64,\n) -> Result<f64, JsValue> {\n    ad::ad_inc(input_high, input_low, input_close, input_volume, prev_ad)\n        .map_err(|e| JsValue::from_str(&e.to_string()))\n}\n"
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/adosc.rs",
    "content": "use kand::ta::ohlcv::adosc;\nuse wasm_bindgen::prelude::*;\n\n/**\n * Returns the lookback period for ADOSC calculation.\n * @param {number} opt_fast_period - The time period for the fast EMA (must be >= 2).\n * @param {number} opt_slow_period - The time period for the slow EMA (must be >= fast_period).\n * @returns {number} The lookback period.\n * @throws {Error} If parameters are invalid.\n */\n#[wasm_bindgen(js_name = adoscLookback)]\npub fn adosc_lookback_wasm(\n    opt_fast_period: usize,\n    opt_slow_period: usize,\n) -> Result<usize, JsValue> {\n    adosc::lookback(opt_fast_period, opt_slow_period).map_err(|e| JsValue::from_str(&e.to_string()))\n}\n\n/**\n * Calculates the Chaikin A/D Oscillator (ADOSC).\n * @param {Float64Array} input_high - Array of high prices.\n * @param {Float64Array} input_low - Array of low prices.\n * @param {Float64Array} input_close - Array of close prices.\n * @param {Float64Array} input_volume - Array of volume values.\n * @param {number} opt_fast_period - The time period for the fast EMA (must be >= 2).\n * @param {number} opt_slow_period - The time period for the slow EMA (must be >= fast_period).\n * @returns {Float64Array} An array of ADOSC values.\n * @throws {Error} If inputs are invalid or calculation fails.\n */\n#[wasm_bindgen(js_name = adosc)]\npub fn adosc_wasm(\n    input_high: Vec<f64>,\n    input_low: Vec<f64>,\n    input_close: Vec<f64>,\n    input_volume: Vec<f64>,\n    opt_fast_period: usize,\n    opt_slow_period: usize,\n) -> Result<Vec<f64>, JsValue> {\n    let len = input_high.len();\n    let mut output_adosc = vec![0.0; len];\n    let mut output_ad = vec![0.0; len];\n    let mut output_ad_fast_ema = vec![0.0; len];\n    let mut output_ad_slow_ema = vec![0.0; len];\n\n    adosc::adosc(\n        &input_high,\n        &input_low,\n        &input_close,\n        &input_volume,\n        opt_fast_period,\n        opt_slow_period,\n        &mut output_adosc,\n        &mut output_ad,\n        &mut output_ad_fast_ema,\n        &mut output_ad_slow_ema,\n    )\n    .map_err(|e| JsValue::from_str(&e.to_string()))?;\n\n    Ok(output_adosc)\n}\n\n/**\n * Calculates the next ADOSC value incrementally.\n * @param {number} input_high - The current high price.\n * @param {number} input_low - The current low price.\n * @param {number} input_close - The current close price.\n * @param {number} input_volume - The current volume.\n * @param {number} opt_fast_period - The time period for the fast EMA.\n * @param {number} opt_slow_period - The time period for the slow EMA.\n * @param {number} prev_ad - The previous A/D value.\n * @param {number} prev_fast_ema - The previous fast EMA of the A/D line.\n * @param {number} prev_slow_ema - The previous slow EMA of the A/D line.\n * @returns {number} The new ADOSC value.\n * @throws {Error} If inputs are invalid or calculation fails.\n */\n#[wasm_bindgen(js_name = adoscInc)]\npub fn adosc_inc_wasm(\n    input_high: f64,\n    input_low: f64,\n    input_close: f64,\n    input_volume: f64,\n    opt_fast_period: usize,\n    opt_slow_period: usize,\n    prev_ad: f64,\n    prev_fast_ema: f64,\n    prev_slow_ema: f64,\n) -> Result<f64, JsValue> {\n    // For incremental calculation, we only need the final ADOSC value.\n    // The core `adosc_inc` function returns a tuple with intermediate values, which we can discard here.\n    let (output_adosc, _, _, _) = adosc::adosc_inc(\n        input_high,\n        input_low,\n        input_close,\n        input_volume,\n        prev_ad,\n        prev_fast_ema,\n        prev_slow_ema,\n        opt_fast_period,\n        opt_slow_period,\n    )\n    .map_err(|e| JsValue::from_str(&e.to_string()))?;\n\n    Ok(output_adosc)\n}\n"
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/adr.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/adx.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/adxr.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/apo.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/aroon.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/aroonosc.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/atr.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/bbands.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/bop.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/cci.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/cdl_doji.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/cdl_dragonfly_doji.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/cdl_gravestone_doji.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/cdl_hammer.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/cdl_inverted_hammer.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/cdl_long_shadow.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/cdl_marubozu.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/cmo.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/dema.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/dx.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/ecl.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/ema.rs",
    "content": "use kand::ta::ohlcv::ema;\nuse wasm_bindgen::prelude::*;\n\n/**\n * Returns the lookback period for EMA calculation.\n * @param {number} opt_period - The time period for EMA calculation (must be >= 2).\n * @returns {number} The lookback period.\n * @throws {Error} If the period is invalid.\n */\n#[wasm_bindgen(js_name = emaLookback)]\npub fn ema_lookback_wasm(opt_period: usize) -> Result<usize, JsValue> {\n    ema::lookback(opt_period).map_err(|e| JsValue::from_str(&e.to_string()))\n}\n\n/**\n * Calculates Exponential Moving Average (EMA) for a price series.\n * @param {Float64Array} input_prices - Array of price values to calculate EMA from.\n * @param {number} opt_period - The time period for EMA calculation (must be >= 2).\n * @param {number | null | undefined} opt_k - Optional custom smoothing factor. If not provided, it defaults to `2 / (period + 1)`.\n * @returns {Float64Array} An array of EMA values with the same length as the input.\n * @throws {Error} If the input is invalid or the calculation fails.\n */\n#[wasm_bindgen(js_name = ema)]\npub fn ema_wasm(\n    input_prices: Vec<f64>,\n    opt_period: usize,\n    opt_k: Option<f64>,\n) -> Result<Vec<f64>, JsValue> {\n    let mut output_ema = vec![0.0; input_prices.len()];\n\n    ema::ema(&input_prices, opt_period, opt_k, &mut output_ema)\n        .map_err(|e| JsValue::from_str(&e.to_string()))?;\n\n    Ok(output_ema)\n}\n\n/**\n * Calculates a single EMA value incrementally using the previous EMA.\n * @param {number} input_price - The current period's price value.\n * @param {number} prev_ema - The previous period's EMA value.\n * @param {number} opt_period - The time period for EMA calculation (must be >= 2).\n * @param {number | null | undefined} opt_k - Optional custom smoothing factor. If not provided, it defaults to `2 / (period + 1)`.\n * @returns {number} The new EMA value.\n * @throws {Error} If the parameters are invalid or the calculation fails.\n */\n#[wasm_bindgen(js_name = emaInc)]\npub fn ema_inc_wasm(\n    input_price: f64,\n    prev_ema: f64,\n    opt_period: usize,\n    opt_k: Option<f64>,\n) -> Result<f64, JsValue> {\n    ema::ema_inc(input_price, prev_ema, opt_period, opt_k)\n        .map_err(|e| JsValue::from_str(&e.to_string()))\n}\n"
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/ha.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/ht_dcperiod.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/ht_dcphase.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/ht_phasor.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/ht_sine.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/ht_trendline.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/ht_trendmode.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/kama.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/linearreg.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/linearreg_angle.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/linearreg_intercept.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/linearreg_slope.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/macd.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/macdext.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/mama.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/medprice.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/mfi.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/midpoint.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/midprice.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/minus_di.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/minus_dm.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/mod.rs",
    "content": "pub mod ad;\npub mod adosc;\npub mod adr;\npub mod adx;\npub mod adxr;\n// pub mod apo;\npub mod aroon;\npub mod aroonosc;\npub mod atr;\npub mod bbands;\npub mod bop;\npub mod cci;\npub mod cdl_doji;\npub mod cdl_dragonfly_doji;\npub mod cdl_gravestone_doji;\npub mod cdl_hammer;\npub mod cdl_inverted_hammer;\npub mod cdl_long_shadow;\npub mod cdl_marubozu;\n// pub mod cdl_spinning_top;\n// pub mod cmo;\npub mod dema;\npub mod dx;\npub mod ecl;\npub mod ema;\npub mod ha;\n// pub mod ht_dcperiod;\n// pub mod ht_dcphase;\n// pub mod ht_phasor;\n// pub mod ht_sine;\n// pub mod ht_trendline;\n// pub mod ht_trendmode;\n// pub mod kama;\n// pub mod linearreg;\n// pub mod linearreg_angle;\n// pub mod linearreg_intercept;\n// pub mod linearreg_slope;\npub mod macd;\n// pub mod macdext;\n// pub mod mama;\npub mod medprice;\npub mod mfi;\npub mod midpoint;\npub mod midprice;\npub mod minus_di;\npub mod minus_dm;\npub mod mom;\npub mod natr;\npub mod obv;\npub mod plus_di;\npub mod plus_dm;\n// pub mod ppo;\npub mod rma;\npub mod roc;\npub mod rocp;\npub mod rocr;\npub mod rocr100;\npub mod rsi;\npub mod sar;\n// pub mod sarext;\npub mod sma;\npub mod stoch;\n// pub mod stochf;\n// pub mod stochrsi;\npub mod supertrend;\npub mod t3;\npub mod tema;\npub mod trange;\npub mod trima;\npub mod trix;\n// pub mod tsf;\npub mod typprice;\n// pub mod ultosc;\npub mod vegas;\npub mod vwap;\npub mod wclprice;\npub mod willr;\npub mod wma;\n"
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/mom.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/natr.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/obv.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/plus_di.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/plus_dm.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/ppo.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/renko.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/rma.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/roc.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/rocp.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/rocr.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/rocr100.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/rsi.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/sar.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/sarext.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/sma.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/stoch.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/stochf.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/stochrsi.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/supertrend.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/t3.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/tema.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/trange.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/trima.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/trix.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/tsf.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/typprice.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/ultosc.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/vegas.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/vwap.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/wclprice.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/willr.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/ohlcv/wma.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/other/mod.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/alpha.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/beta.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/calmar.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/correl.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/drawdown.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/fv.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/kelly.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/max.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/min.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/mod.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/nper.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/ret.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/sharpe.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/sortino.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/stddev.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/sum.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/var.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/stats/winrate.rs",
    "content": ""
  },
  {
    "path": "kand-wasm/src/ta/types.rs",
    "content": ""
  },
  {
    "path": "mkdocs.yml",
    "content": "# Project information\nsite_name: Kand - Blazingly Fast Technical Analysis Library\nsite_url: https://kand-ta.github.io/kand/\nsite_author: CtrlX\nsite_description: >-\n  kand - Pure Rust reimplementation of TA-Lib for fast financial analysis.\n\n# Repository\nrepo_url: https://github.com/kand-ta/kand\nrepo_name: kand-ta/kand\n\n# Copyright\ncopyright: >\n  Copyright &copy; 2024 - 2025 CtrlX –\n  <a href=\"#__consent\">Change cookie settings</a>\n\n# Configuration\ntheme:\n  name: material\n  logo: assets/logo.png\n  favicon: assets/logo.png\n  language: en\n  palette:\n\n    # Palette toggle for automatic mode\n    - media: \"(prefers-color-scheme)\"\n      toggle:\n        icon: material/brightness-auto\n        name: Switch to light mode\n\n    # Palette toggle for light mode\n    - media: \"(prefers-color-scheme: light)\"\n      primary: orange\n      scheme: default\n      toggle:\n        icon: material/brightness-7\n        name: Switch to dark mode\n\n    # Palette toggle for dark mode\n    - media: \"(prefers-color-scheme: dark)\"\n      primary: black\n      scheme: slate\n      toggle:\n        icon: material/brightness-4\n        name: Switch to system preference\n\n  features:\n    - navigation.instant\n    - navigation.instant.progress\n    - navigation.tracking\n    - navigation.tabs\n    - content.tabs.link\n    - navigation.sections\n    - navigation.expand\n    - navigation.path\n    - content.code.copy\n    - toc.follow\n    - search.suggest\n    - search.highlight\n    - search.share\n    - header.autohide\n    - announce.dismiss\n    - navigation.footer\n    - navigation.indexes\n    - content.code.select\n    - content.code.annotate\n\n# Additional configuration\nextra:\n  consent:\n    title: Cookie Usage Statement\n    description: >-\n      We use cookies to record your behavior and preferences across multiple visits, as well as to evaluate the effectiveness of our documentation and whether users can find the information they need.\n      With your consent, you will help us optimize our documentation content.\n    consent:\n    actions:\n      - accept\n      - reject\n      - manage\n\n  social:\n    - icon: fontawesome/brands/github\n      link: https://github.com/rust-ta\n    - icon: fontawesome/brands/x-twitter\n      link: https://x.com/GitCtrlX\n\n  generator: false\n\n# Plugins\nplugins:\n  - search\n  - mkdocstrings:\n      handlers:\n            python:\n              options:\n                show_root_heading: true\n                show_source: false\n                docstring_style: google\n                show_if_no_docstring: false\n                merge_init_into_class: true\n  - minify:\n      minify_html: true\n      minify_js: true\n      minify_css: true\n      htmlmin_opts:\n          remove_comments: true\n      cache_safe: true\n\nmarkdown_extensions:\n  - pymdownx.tasklist:\n      custom_checkbox: true\n  - pymdownx.highlight:\n      anchor_linenums: true\n  - pymdownx.superfences\n  - pymdownx.tabbed:\n      alternate_style: true\n  - admonition\n  - pymdownx.details\n  - attr_list\n  - md_in_html\n  - tables\n\n# Page tree\nnav:\n  - Home: \"index.md\"\n  - Install:\n      - Quick Start: \"install.md\"\n      - Advanced Setup: \"advance.md\"\n  - API: \"api.md\"\n  - Changelog: \"changelog.md\"\n  - Performance: \"performance.md\"\n  - About: \"about.md\"\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"maturin>=1.7,<2.0\"]\nbuild-backend = \"maturin\"\n\n[project]\nname = \"kand\"\ndescription = \"A high-performance technical analysis library written in Rust with Python bindings.\"\nauthors = [{ name = \"CtrlX\", email = \"gitctrlx@gmail.com\" }]\nrequires-python = \">=3.8\"\nkeywords = [\n  \"ta\", \"ta-lib\", \"finance\", \"quant\", \"indicator\", \"technical-analysis\"\n]\nclassifiers = [\n    \"License :: OSI Approved :: MIT License\",\n    \"License :: OSI Approved :: Apache Software License\",\n    \"Programming Language :: Rust\",\n    \"Programming Language :: Python :: Implementation :: CPython\",\n    \"Programming Language :: Python :: Implementation :: PyPy\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3.8\",\n    \"Programming Language :: Python :: 3.9\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3 :: Only\",\n]\ndynamic = [\"version\"]\nreadme = \"README.md\"\ndependencies = [\n    \"numpy>=1.24.0,<1.26.0; python_version<'3.9'\",\n    \"numpy>=1.26.0; python_version>='3.9'\",\n]\n\n[project.urls]\nRepository = \"https://github.com/rust-ta/kand\"\nDocumentation = \"https://docs.rs/kand\"\nChangelog = \"https://github.com/rust-ta/kand/blob/main/CHANGELOG.md\"\nReleases = \"https://github.com/rust-ta/kand/releases\"\n\n[tool.maturin]\nbindings = \"pyo3\"\nmanifest-path = \"kand-py/Cargo.toml\"\nmodule-name = \"kand._kand\"\npython-source = \"kand-py/python\"\nfeatures = [\"pyo3/extension-module\", \"f64\", \"i64\", \"check\"]\ninclude = [\n    { path = \"rust-toolchain.toml\", format = [\"sdist\", \"wheel\"] },\n    { path = \"LICENSE-APACHE\", format = \"sdist\" },\n    { path = \"LICENSE-MIT\", format = \"sdist\" },\n]\n"
  },
  {
    "path": "rust-toolchain.toml",
    "content": "[toolchain]\nchannel = \"stable\"\n"
  },
  {
    "path": "scripts/gen_stub.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport sys\nimport inspect\nimport importlib\n\n\ndef generate_stub_file(package_name: str, output_path: str):\n    \"\"\"Generates a .pyi stub file for a package.\n\n    The .pyi stub file contains function signatures and docstrings to enable\n    IDE autocompletion and type hints.\n\n    Args:\n        package_name: Name of the package to process (e.g. 'kand')\n        output_path: Target path for the .pyi file (must be writable)\n    \"\"\"\n    # Dynamically import the package\n    pkg = importlib.import_module(package_name)\n\n    # Get all top-level names that don't start and end with double underscores\n    public_names = [\n        name for name in dir(pkg) if not (name.startswith(\"__\") and name.endswith(\"__\"))\n    ]\n\n    # Generate stub file content\n    pyi_lines = []\n    pyi_lines.append(f\"# Auto-generated stub file for {package_name}\")\n    pyi_lines.append('\"\"\"')\n    pyi_lines.append(\n        \"Type hints and function signatures stub file for IDE autocompletion.\"\n    )\n    pyi_lines.append(\n        \"Auto-generated to avoid manual maintenance. Can be enhanced with more precise type annotations.\"\n    )\n    pyi_lines.append('\"\"\"')\n    pyi_lines.append(\"\")\n\n    # Output signatures and docs for functions/builtins\n    for name in public_names:\n        attr = getattr(pkg, name)\n        # Check if callable (function, builtin, etc)\n        if inspect.isfunction(attr) or inspect.isbuiltin(attr):\n            # Get function signature (may fail for builtins)\n            try:\n                sig = str(inspect.signature(attr))\n            except ValueError:\n                sig = \"(...)\"\n\n            # Get function docstring\n            doc = inspect.getdoc(attr) or \"\"\n            doc_lines = doc.splitlines() if doc else []\n\n            # Write declaration\n            pyi_lines.append(f\"def {name}{sig}:\")\n\n            if doc_lines:\n                # Write docstring as proper Python docstring\n                pyi_lines.append('    \"\"\"')\n                for line in doc_lines:\n                    pyi_lines.append(f\"    {line}\")\n                pyi_lines.append('    \"\"\"')\n            else:\n                pyi_lines.append('    \"\"\"No docstring available.\"\"\"')\n\n            # Add ellipsis for function body\n            pyi_lines.append(\"    ...\")\n            pyi_lines.append(\"\")\n\n    # Create output directory if it doesn't exist\n    os.makedirs(os.path.dirname(output_path), exist_ok=True)\n\n    # Write the stub file\n    with open(output_path, \"w\", encoding=\"utf-8\") as f:\n        f.write(\"\\n\".join(pyi_lines))\n    print(f\"Generated: {output_path}\")\n\n\nif __name__ == \"__main__\":\n    \"\"\"Example usage:\n\n    To generate a stub file for the kand package:\n        python gen_stub.py kand path/to/output.pyi\n    \"\"\"\n    if len(sys.argv) < 3:\n        print(\"Usage: python gen_stub.py <package_name> <output_path>\")\n        print(\"Example: python gen_stub.py kand python/kand/_kand.pyi\")\n        sys.exit(1)\n\n    pkg_name = sys.argv[1]\n    output_path = sys.argv[2]\n    generate_stub_file(pkg_name, output_path)\n"
  }
]