source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install maturin numpy pytest
maturin develop --release # Build and install in editable mode
```
---
## Adding a New Candlestick Pattern
All candlestick patterns live in **`src/pattern/`** (Rust: `mod.rs` plus one `.rs` file per pattern) and **`python/ferro_ta/indicators/pattern.py`** (Python wrapper).
### Step 1 — Implement the Rust function
Add a new file `src/pattern/cdl_mypattern.rs` with your `#[pyfunction]`, or add the function to an existing pattern file. Register it in **`src/pattern/mod.rs`**. Open `src/pattern/mod.rs` to see how other patterns are declared and registered (e.g. `mod cdl_doji;` and `self::cdl_doji::cdl_doji` in `register()`). Then implement the logic in your new file (e.g. open `src/pattern/cdl_doji.rs` as a template) and add a new `#[pyfunction]` using the shared helper functions already available at the top of the file:
| `candle_range(high, low)` | Full candle range (high − low) |
| `is_bullish(open, close)` | `true` when close ≥ open |
| `is_bearish(open, close)` | `true` when close < open |
**Template for a single-candle pattern** (save as `src/pattern/cdl_mypattern.rs` and add `mod cdl_mypattern;` plus the register call in `src/pattern/mod.rs`):
```rust
#[pyfunction]
pubfncdl_mypattern<'py>(
py: Python<'py>,
open: PyReadonlyArray1<'py,f64>,
high: PyReadonlyArray1<'py,f64>,
low: PyReadonlyArray1<'py,f64>,
close: PyReadonlyArray1<'py,f64>,
)-> PyResult<Bound<'py,PyArray1<i32>>>{
letopens=open.as_slice()?;
lethighs=high.as_slice()?;
letlows=low.as_slice()?;
letcloses=close.as_slice()?;
letn=opens.len();
ifn!=highs.len()||n!=lows.len()||n!=closes.len(){
returnErr(PyValueError::new_err("arrays must have the same length"));
In **`src/pattern/mod.rs`**, add `mod cdl_mypattern;` at the top with the other pattern modules, and in the `register()` function add `self::cdl_mypattern::cdl_mypattern` to the list of registered functions.
### Step 3 — Add the Python wrapper
Open `python/ferro_ta/indicators/pattern.py` and:
1. Import the Rust function at the top:
```python
from ferro_ta._ferro_ta import cdl_mypattern as _cdl_mypattern
Each module has a `pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()>` at the bottom — add your `wrap_pyfunction!` call there.
---
## Code Style
- Rust: follow `rustfmt` defaults (`cargo fmt`).
- Python: follow PEP 8; use Ruff for lint and format (`ruff check`, `ruff format`).
- Every public Rust function needs a docstring above the `#[pyfunction]` attribute.
- Every Python wrapper must have a NumPy-style docstring with `Parameters` and `Returns` sections.
## Validation and tests for new indicators
All new indicators **must**:
- Use the validation helpers in `ferro_ta.exceptions`: call `check_timeperiod()` for every period parameter and `check_equal_length()` for multi-array inputs (OHLCV) before calling the Rust extension. Wrap the Rust call in `try/except ValueError` and re-raise with `_normalize_rust_error(e)`.
- Have tests in `tests/unit/` (including at least one test for invalid parameters or edge cases where applicable).
- Update docstrings and type stubs (`python/ferro_ta/__init__.pyi`) when adding or changing the public API.
## Running the Full Test Suite
```bash
pytest tests/unit/ tests/integration/ -v
```
CI runs on Python 3.10–3.13 across Linux, macOS, and Windows. Please make sure your change passes on all targets locally before opening a pull request.
Each module is a directory with `mod.rs` and one or more `.rs` files (e.g. `src/overlap/mod.rs`, `src/overlap/sma.rs`).
**Modular layout:** Pattern recognition is split into `src/pattern/mod.rs` plus one file per pattern (e.g. `src/pattern/cdl_doji.rs`, `src/pattern/cdl_engulfing.rs`). Overlap and momentum use a similar directory layout. To add a new pattern: add a new `src/pattern/cdl_*.rs` file with your `#[pyfunction]` and register it in `src/pattern/mod.rs`.
---
## Batch API
The `ferro_ta.batch` module provides `batch_sma`, `batch_ema`, and `batch_rsi`
(Rust 2-D implementations) plus the generic `batch_apply(data, fn, **kwargs)`.
For a new indicator that does not have a dedicated Rust batch function, use
`batch_apply(data, YOUR_INDICATOR)`.
---
## Running Rust Benchmarks
```bash
# Compile benchmarks only (fast, used in CI)
cargo bench --no-run
# Run benchmarks and get timings
cargo bench
```
Benchmarks are in `benches/indicators.rs` using [Criterion](https://github.com/bheisler/criterion.rs).
---
## Rust Coverage
```bash
# Install cargo-tarpaulin (one-time)
cargo install cargo-tarpaulin
# Collect coverage for the core crate
cargo tarpaulin -p ferro_ta_core --out Html
# Open htmlcov/index.html
```
---
## Type Checking (mypy)
```bash
# Install mypy (one-time)
pip install mypy numpy
# Run type checking
mypy python/ferro_ta --ignore-missing-imports
# No errors should be reported.
```
Type stubs live in `python/ferro_ta/__init__.pyi`. Update them whenever you add a new
public function.
---
## Release Process
See [RELEASE.md](RELEASE.md) for the full step-by-step release playbook and
[PACKAGING.md](PACKAGING.md) for conda-forge submission and feedstock maintenance. (version bump →
changelog → tag → CI builds wheels → publish to PyPI).
See [VERSIONING.md](VERSIONING.md) for the versioning policy (MAJOR/MINOR/PATCH rules,
supported Python version policy, and changelog maintenance requirements).
### Changelog requirement
Every PR that touches `src/`, `python/`, or `wasm/` **must** add an entry to the
`[Unreleased]` section of [CHANGELOG.md](CHANGELOG.md). Use the
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format
`Cargo.toml` and `pyproject.toml` must always carry the same `version` string.
CI enforces this with the `version-check` job — a PR that changes one but not the
other will fail CI.
### Dependency audits
CI runs **cargo audit** (Rust) and **pip-audit** (Python) in the `audit` job. PRs must
not introduce critical or high-severity vulnerabilities. If a dependency cannot be
updated immediately, document the accepted risk in the PR or in SECURITY.md. See
[SECURITY.md](SECURITY.md) for the full policy.
### Fuzzing (robustness)
Fuzz targets live in `fuzz/` (cargo-fuzz). To run fuzzing locally:
```bash
# Install cargo-fuzz (one-time)
cargo install cargo-fuzz
# Run the SMA fuzz target for 60 seconds
cargo fuzz run fuzz_sma -- -max_total_time=60
# Run the RSI fuzz target
cargo fuzz run fuzz_rsi -- -max_total_time=60
```
Any crash found by the fuzzer is saved to `fuzz/artifacts/<target>/`. Open a bug report
with the reproducing input and the panic message.
---
## Getting Help
If you have a question, found a bug, or want to suggest a new indicator:
- **GitHub Discussions** — For questions, ideas, and general discussion, use our [Discussions](https://github.com/pratikbhadane24/ferro-ta/discussions) space:
- **Q&A** — Ask usage or API questions
- **Ideas** — Propose new features or indicators
- **Show & Tell** — Share strategies and projects built with ferro-ta
- **Announcements** — Follow for release notes and important updates
- **GitHub Issues** — For confirmed bugs and actionable feature requests, open an [issue](https://github.com/pratikbhadane24/ferro-ta/issues).
- **Security issues** — See [SECURITY.md](SECURITY.md) for responsible disclosure instructions.