Files
ferro-ta/docs/architecture.md
2026-03-23 23:34:28 +05:30

177 lines
8.6 KiB
Markdown

# Architecture
This document describes the internal layout of **ferro-ta** — how the Rust and
Python layers are organised, how they communicate, and what each component is
responsible for.
---
## Repository Layout
```
ferro-ta/
├── src/ # Root PyO3 crate (Python extension, _ferro_ta)
│ ├── lib.rs # Module registration — assembles all sub-modules
│ ├── overlap/ # SMA, EMA, WMA, DEMA, TEMA, KAMA, BBANDS, …
│ ├── momentum/ # RSI, STOCH, ADX, CCI, AROON, WILLR, MFI, …
│ ├── volatility/ # ATR, NATR, TRANGE
│ ├── volume/ # AD, ADOSC, OBV
│ ├── statistic/ # STDDEV, VAR, LINEARREG, BETA, CORREL, …
│ ├── price_transform/ # AVGPRICE, MEDPRICE, TYPPRICE, WCLPRICE
│ ├── pattern/ # 61 CDL candlestick patterns
│ ├── cycle/ # HT_TRENDLINE, HT_DCPERIOD, HT_DCPHASE, …
│ └── common.rs # Shared helpers (Wilder smoothing, etc.)
├── crates/
│ └── ferro_ta_core/ # Pure-Rust library (no PyO3 / numpy)
│ └── src/ # Used by fuzz targets and WASM binding
├── python/
│ └── ferro_ta/ # Python package
│ ├── __init__.py # Public API — re-exports + pandas/polars wraps
│ ├── _utils.py # _to_f64, pandas_wrap, polars_wrap, get_ohlcv
│ ├── overlap.py # Thin wrappers around _ferro_ta overlap functions
│ ├── momentum.py # … momentum
│ ├── volatility.py # … volatility
│ ├── volume.py # … volume
│ ├── statistic.py # … statistic
│ ├── price_transform.py # … price_transform
│ ├── pattern.py # … pattern (61 CDL functions)
│ ├── cycle.py # … cycle
│ ├── math_ops.py # ADD, SUB, MULT, DIV, SUM, MAX, MIN, math transforms
│ ├── extended.py # Extended indicators (VWAP, SUPERTREND, ICHIMOKU, …)
│ ├── streaming.py # Stateful streaming classes (StreamingSMA, …)
│ ├── batch.py # Batch execution API (batch_sma, batch_ema, …)
│ ├── pipeline.py # Pipeline / make_pipeline
│ ├── config.py # set_default / Config
│ ├── registry.py # Indicator registry (list_indicators, run)
│ ├── backtest.py # Simple backtest helpers
│ ├── gpu.py # CuPy-backed GPU PoC (SMA, EMA, RSI)
│ ├── exceptions.py # FerroTAError, FerroTAValueError, FerroTAInputError
│ ├── utils.py # Public re-export of get_ohlcv
│ └── py.typed # PEP 561 marker
├── fuzz/ # cargo-fuzz targets (fuzz_sma, fuzz_rsi, …)
├── wasm/ # wasm-pack / wasm-bindgen binding (uses ferro_ta_core)
├── benches/ # Rust criterion benchmarks
├── benchmarks/ # Python pytest-benchmark benchmarks
├── docs/ # Sphinx documentation source
└── tests/ # Python pytest test suite
```
---
## Two Rust Crates
ferro-ta has **two** Rust crates that serve different purposes:
### 1. Root crate (`src/`) — Python extension (`_ferro_ta`)
| Property | Value |
|----------------|---------------------------------------------------|
| Crate type | `cdylib` (compiled to a `.so` / `.pyd` file) |
| PyO3 / numpy | Yes — depends on `pyo3` and `numpy` |
| Depends on | `ta` crate (provides TA-Lib-compatible algorithms)|
| Used by | Python extension (`ferro_ta._ferro_ta`) |
Each category module (`src/overlap/`, `src/momentum/`, …) registers
`#[pyfunction]`s that accept `numpy` arrays (via `PyReadonlyArray1<f64>`)
and return `Vec<f64>` which PyO3 converts to a Python list/ndarray.
### 2. `crates/ferro_ta_core/` — Pure Rust library
| Property | Value |
|----------------|-------------------------------------------------------------------|
| Crate type | `lib` (not a Python extension) |
| PyO3 / numpy | No — pure Rust, no Python dependency |
| Depends on | Nothing outside `std` |
| Used by | `fuzz/` targets and `wasm/` binding |
`ferro_ta_core` provides the same indicator categories with a `&[f64]` API,
making it usable from WASM and fuzz targets without pulling in PyO3 or numpy.
> **Note:** The root crate and `ferro_ta_core` are *independent* implementations.
> They are not merged by design — merging them would require careful testing of
> both the Python and WASM/fuzz surfaces. If you want to share code, the
> recommended path is to make the root crate depend on `ferro_ta_core` and wrap
> its `&[f64]` API with PyO3 `#[pyfunction]`s; that is a future refactor.
---
## Python Binding Flow
```
User code
├── from ferro_ta import SMA # __init__.py re-export
│ │
│ └── python/ferro_ta/overlap.py::SMA
│ │
│ ├── _utils._to_f64(close) # convert to float64 ndarray
│ ├── check_timeperiod(n) # validate parameters
│ └── _ferro_ta.sma(arr, n) # call Rust extension
│ │
│ └── src/overlap/sma.rs # pure Rust computation
├── SMA(pd.Series(...)) # pandas_wrap intercepts first
│ │
│ ├── extracts .to_numpy(dtype=float64)
│ ├── calls SMA(ndarray)
│ └── wraps result in pd.Series(result, index=original_index)
└── SMA(pl.Series(...)) # polars_wrap intercepts first
├── extracts .cast(Float64).to_numpy()
├── calls SMA(ndarray)
└── wraps result in pl.Series(name, np.asarray(result))
```
Both `pandas_wrap` and `polars_wrap` are applied to every public name in
`__init__.py` so the same function transparently handles numpy arrays,
pandas Series, and polars Series.
---
## Extended Indicators, Streaming, and Batch
| Module | Implementation | Notes |
|---------------|-----------------------------|-------------------------------------------------------------|
| `extended.py` | Rust (`src/extended/`) | VWAP, SUPERTREND, ICHIMOKU, DONCHIAN, PIVOT_POINTS, … |
| `streaming.py`| Rust re-export | Stateful classes (StreamingSMA, StreamingEMA, …) from `_ferro_ta`; no Python fallback |
| `batch.py` | Rust for 2-D SMA/EMA/RSI | `batch_sma`, `batch_ema`, `batch_rsi` call Rust batch functions; `batch_apply` is a Python loop for other indicators |
Streaming and batch 2-D paths are implemented in Rust for maximum performance.
The generic `batch_apply` remains for indicators that do not have a dedicated
Rust batch implementation (see `docs/performance.md`).
---
## Packaging and Build
- **Build backend:** [maturin](https://www.maturin.rs/) — compiles the root
crate and packages it alongside the Python source into a wheel.
- **`python-source = "python"`** in `pyproject.toml` tells maturin where the
Python package lives.
- **`module-name = "ferro_ta._ferro_ta"`** tells maturin to place the compiled
`.so` at `ferro_ta/_ferro_ta.so` inside the wheel.
- Wheels are built for Linux (manylinux), Windows, and macOS via CI on release.
---
## Where Validation Lives
Currently most validation (array length checks, `timeperiod` range checks) is
done in Python wrappers before the Rust call. A future improvement is to move
these checks into the `#[pyfunction]`s so that callers using the raw
`_ferro_ta` extension directly also get clear errors.
---
## Related Documents
- [`docs/performance.md`](performance.md) — when to use raw numpy vs pandas/polars,
how to avoid unnecessary conversion, batch performance notes.
- [`CONTRIBUTING.md`](../CONTRIBUTING.md) — development workflow, running tests,
adding a new indicator.
- [`CHANGELOG.md`](../CHANGELOG.md) — version history.