Files
QuanTAlib/python/SPEC.md
T

1220 lines
43 KiB
Markdown
Raw Normal View History

2026-02-28 14:14:35 -08:00
# quantalib Python NativeAOT Wrapper — Specification v2
## 1) Objective
Deliver a Python package named **`quantalib`** that exposes the entire QuanTAlib
indicator library through a stable, versioned C ABI compiled via .NET NativeAOT.
Python ergonomics follow pandas-ta conventions where practical.
### Primary goals
- Near-zero-copy interop via `ctypes` + contiguous NumPy buffers (zero-copy when inputs are already C-contiguous `float64`; otherwise one normalization copy is allowed)
- Stable C ABI surface (`qtl_*`) with explicit major-version evolution policy for future non-Python consumers
- Full library coverage target: all supported `Batch` methods exported using a generated manifest and validated rollout gates
- pandas-ta-compatible function signatures for common indicators
### Non-goals (initial release)
- Rewriting indicator math in Python
- pybind11 / Cython extension modules
- GPU acceleration
- Streaming / stateful Python classes per indicator
- Alternate numeric dtypes (`float32`, decimal)
- Full pandas-ta edge-case parity on day one
---
## 2) Scope
### In scope
- `python/` directory at repository root
- NativeAOT shared library (`quantalib.dll` / `quantalib.so` / `quantalib.dylib`)
- Single `Exports.cs` mapping all `Batch` methods to `[UnmanagedCallersOnly]` exports
- Python package `quantalib` with loader, bridge, and indicator wrappers
- Compatibility matrix documenting pandas-ta parity levels
- CI matrix for `win-x64`, `linux-x64`, `osx-arm64`, `osx-x64`
### Out of scope
- Windows ARM64 packaging
- Async / streaming Python APIs
- TBarSeries / BatchInputs overloads (use flat span-based overloads only)
---
## 3) Design requirements
1. **ABI correctness** — pointer, length, and parameter contracts are explicit and validated on both sides of the boundary.
2. **Performance** — call QuanTAlib `Batch` methods directly; no managed allocations in the export shim beyond span construction.
3. **ABI stability** — export names are prefixed (`qtl_*`) and versioned; existing symbols never change within a major version.
4. **Deterministic behavior** — warmup slots filled with `NaN`; invalid inputs produce defined status codes, never exceptions.
5. **pandas-ta compatibility** — Python names and signatures mirror pandas-ta where feasible, with documented deltas.
6. **Thread safety** — exports are designed to be stateless and reentrant; concurrent calls are supported when each call uses distinct buffers. This must be validated by stress tests in CI.
---
## 4) Repository layout
```text
python/
SPEC.md # this file
python.csproj # NativeAOT shared lib project
Directory.Build.props # local build overrides
publish.ps1 # multi-RID publish + stage script
pyproject.toml # Python package metadata
README.md # usage docs + indicator list
src/
StatusCodes.cs # QTL_OK / QTL_ERR_* constants
ArrayBridge.cs # unsafe ptr→span helpers + validation
Exports.cs # ALL [UnmanagedCallersOnly] entry points
quantalib/
__init__.py # re-export indicators; __version__
_loader.py # platform+arch dispatch; ctypes.CDLL
_bridge.py # ctypes argtypes/restype declarations
indicators.py # pandas-ta-compatible wrappers
_compat.py # pandas-ta name aliases + helpers
py.typed # PEP 561 marker
native/
win_amd64/.gitkeep
linux_x86_64/.gitkeep
macosx_arm64/.gitkeep
macosx_x86_64/.gitkeep
tests/
test_smoke.py # import + load + call one indicator
test_shapes.py # output length == input length
test_status_codes.py # null ptr, bad length, bad params
test_golden.py # golden-value vs managed QuanTAlib
test_compat.py # pandas-ta parity subset
```
**Design note:** `Exports.cs` and `indicators.py` stay as single flat files.
Internal splitting is allowed later without changing any public API or ABI.
---
## 5) Native ABI contract
### 5.1 Calling convention
- All exports use `[UnmanagedCallersOnly(EntryPoint = "qtl_<name>")]`.
- All exports return `int` status code.
- All pointer parameters use C ABI-compatible primitive types only.
- No managed exceptions ever cross the ABI boundary. Every export wraps its body
in a `try/catch` that returns `QTL_ERR_INTERNAL` on unhandled exceptions.
### 5.2 Status codes
| Code | Name | Meaning |
|------|------|---------|
| `0` | `QTL_OK` | Success |
| `1` | `QTL_ERR_NULL_PTR` | Required pointer is null |
| `2` | `QTL_ERR_INVALID_LENGTH` | `n <= 0` or output length mismatch |
| `3` | `QTL_ERR_INVALID_PARAM` | Parameter out of valid range |
| `4` | `QTL_ERR_INTERNAL` | Unhandled exception in managed code |
Failure guarantees:
- For validation failures (`QTL_ERR_NULL_PTR`, `QTL_ERR_INVALID_LENGTH`, `QTL_ERR_INVALID_PARAM`), outputs are untouched by contract.
- For `QTL_ERR_INTERNAL`, output buffers are unspecified and must be treated as invalid by the caller.
### 5.3 ABI signature patterns
The ~290 `Batch` methods in QuanTAlib follow 9 distinct signature patterns.
Each pattern maps to a specific C ABI template:
#### Pattern A: Single-input, single-output + scalar params
**C# source:** `Sma.Batch(ReadOnlySpan<double> source, Span<double> output, int period)`
**C ABI:**
```c
int qtl_sma(double* src, int n, double* out, int period);
```
**Indicators (~130):** SMA, EMA, DEMA, TEMA, HMA, WMA, RSI, ROC, MOM, CMO,
TSI, APO, PPO, DPO, TRIX, Fisher, Inertia, Zscore, StdDev, Variance, etc.
#### Pattern B: Multi-input HLCV, single-output
**C# source:** `Mfi.Batch(ReadOnlySpan<double> high, ..low, ..close, ..volume, Span<double> output, int period)`
**C ABI:**
```c
int qtl_mfi(double* high, double* low, double* close, double* volume,
int n, double* out, int period);
```
**Indicators (~25):** MFI, ADL, CMF, VWAP, VWAD, ADOsc, KVO, III, WAD, VA, EOM.
#### Pattern C: Multi-input OHLC (no volume), single-output
**C# source:** `Bop.Batch(ReadOnlySpan<double> open, ..high, ..low, ..close, Span<double> destination)`
**C ABI:**
```c
int qtl_bop(double* open, double* high, double* low, double* close,
int n, double* out);
```
**Indicators (~20):** BOP, ASI, BRAR, Avgprice, Typprice, Wclprice, Midbody,
HA, ReverseEMA variants requiring OHLC, RVGI, DEM, etc.
#### Pattern D: Multi-input HL, single-output
**C# source:** `Ao.Batch(ReadOnlySpan<double> high, ReadOnlySpan<double> low, Span<double> destination, int fast, int slow)`
**C ABI:**
```c
int qtl_ao(double* high, double* low, int n, double* out,
int fast_period, int slow_period);
```
**Indicators (~10):** AO, AC, Medprice, Frama (HL overload), WillR, etc.
#### Pattern E: Multi-input HLC (no volume), single-output
**C# source:** `Tr.Batch(ReadOnlySpan<double> high, ..low, ..close, Span<double> output)`
**C ABI:**
```c
int qtl_tr(double* high, double* low, double* close, int n, double* out);
```
**Indicators (~15):** TR, ATR, Etherm, GHLA, PGO, RWMA, etc.
#### Pattern F: Dual-input (actual + predicted), single-output
**C# source:** `Mse.Batch(ReadOnlySpan<double> actual, ..predicted, Span<double> output, int period)`
**C ABI:**
```c
int qtl_mse(double* actual, double* predicted, int n, double* out, int period);
```
**Indicators (~25):** MSE, RMSE, MAE, MAPE, SMAPE, RMSLE, Rsquared, Huber,
PseudoHuber, TheilU, WMAPE, LogCosh, QuantileLoss, TukeyBiweight, etc.
#### Pattern G: Dual-input (source + volume), single-output
**C# source:** `Obv.Batch(ReadOnlySpan<double> close, ReadOnlySpan<double> volume, Span<double> output)`
**C ABI:**
```c
int qtl_obv(double* close, double* volume, int n, double* out);
```
**Indicators (~12):** OBV, PVT, PVR, VF, EFI, NVI, PVI, VWMA, EVWMA, TVI, PVD.
#### Pattern H: Dual-input statistics (seriesX + seriesY)
**C# source:** `Correlation.Batch(ReadOnlySpan<double> seriesX, ..seriesY, Span<double> output, int period)`
**C ABI:**
```c
int qtl_correlation(double* x, double* y, int n, double* out, int period);
```
**Indicators (~6):** Correlation, Covariance, Spearman, Kendall, Granger,
Cointegration.
#### Pattern I: Multi-output indicators
**C# source (example):** `Pvo.Batch(ReadOnlySpan<double> volume, Span<double> output, Span<double> signal, Span<double> histogram, ...)`
**C ABI:** Each output gets its own pointer parameter:
```c
int qtl_pvo(double* volume, int n,
double* out_pvo, double* out_signal, double* out_hist,
int fast, int slow, int signal_period);
```
**Multi-output indicators:**
| Indicator | Outputs | Names |
|-----------|---------|-------|
| PVO | 3 | pvo, signal, histogram |
| PMA | 2 | pma, trigger |
| MAMA | 2 | mama, fama |
| HtPhasor | 2 | inPhase, quadrature |
| HtSine | 2 | sine, leadSine |
| AOBV | 2 | fast, slow |
| AMAT | 2 | trend, strength |
| Stoch | 2 | K, D |
| Stochf | 2 | K, D |
| KDJ | 3 | K, D, J |
| SMI | 2+ | smi, signal |
| BBands | 3 | upper, middle, lower |
| KChannel | 3 | upper, middle, lower |
| DCChannel | 3 | upper, middle, lower |
| AccBands | 3 | upper, middle, lower |
| AtrBands | 3 | upper, middle, lower |
| All channel indicators | 3 | upper, middle, lower |
| Vortex | 2 | viPlus, viMinus |
| Aroon | 1* | combined |
### 5.4 Special parameter mappings
Several `Batch` methods use types that cannot cross the C ABI directly.
Each requires a specific mapping:
| C# type | C ABI type | Strategy |
|---------|-----------|----------|
| `enum StcSmoothing` | `int` | 0 = SMA, 1 = EMA |
| `enum WindowType` (Afirma) | `int` | Map enum ordinals |
| `bool` (isPopulation, annualize, etc.) | `int` | 0 = false, 1 = true |
| `int[]? lengths` (Cfb) | `int* lengths, int lengths_count` | Null-safe; null → use defaults |
| `double[] kernel` (Conv) | `double* kernel, int kernel_len` | Caller allocates |
| `ReadOnlySpan<long>` (Solar, Lunar) | `long* timestamps` | Direct pointer cast |
| `TBarSeries` overloads | **Skip** | Use flat span-based overload instead |
| `BatchInputs`/`BatchOutput` struct overloads | **Skip** | Use flat span-based overload instead |
### 5.5 Dual overloads (period vs alpha)
Many IIR filters expose both `Batch(src, out, int period)` and
`Batch(src, out, double alpha)`. Export strategy:
- Primary export: `qtl_ema` — takes `int period`
- Alpha export: `qtl_ema_alpha` — takes `double alpha`
Both are exported; Python wrapper defaults to the period variant.
### 5.6 Validation in each export
Every export function validates before calling the inner `Batch`:
1. Null checks for all required pointer parameters → `QTL_ERR_NULL_PTR`
2. `n > 0``QTL_ERR_INVALID_LENGTH`
3. Range checks on scalar parameters (e.g., `period > 0`) → `QTL_ERR_INVALID_PARAM`
4. Semantic checks (e.g., `fastPeriod < slowPeriod` where required) → `QTL_ERR_INVALID_PARAM`
5. `try/catch` around `Batch` call → `QTL_ERR_INTERNAL` on any managed exception
### 5.7 Memory ownership rules
- Input pointers are **read-only** by contract (cast to `ReadOnlySpan<double>`).
- Caller allocates **all** output buffers with length `n`.
- Native layer writes exactly `n` values per output buffer on success.
- No ownership transfer across boundary. No heap allocation in the shim.
### 5.8 ABI versioning policy
- All exports use the flat `qtl_` prefix for v1.
- Existing published symbols are immutable within ABI major v1.
- New optional parameters require new symbol names (e.g. `qtl_ema_alpha`).
- Deprecation policy: old symbols remain exported for at least one minor release after replacement and are marked deprecated in Python wrappers.
- First breaking ABI revision introduces `qtl2_` prefix and parallel support window.
### 5.9 Export manifest (authoritative source)
To reduce drift across `Exports.cs`, `_bridge.py`, and `indicators.py`, maintain one machine-readable manifest (e.g., JSON/YAML) containing:
- indicator name and export symbol
- input pattern (AI or special)
- parameter schema (name, type, defaults, constraints)
- output schema (count, names, order)
Generated artifacts from this manifest are preferred over hand-maintained parallel lists.
### 5.10 Concrete export example
```csharp
// StatusCodes.cs
file static class StatusCodes
{
public const int QTL_OK = 0;
public const int QTL_ERR_NULL_PTR = 1;
public const int QTL_ERR_INVALID_LENGTH = 2;
public const int QTL_ERR_INVALID_PARAM = 3;
public const int QTL_ERR_INTERNAL = 4;
}
// Exports.cs (excerpt)
[UnmanagedCallersOnly(EntryPoint = "qtl_sma")]
public static unsafe int Sma(double* src, int n, double* output, int period)
{
if (src == null || output == null) return StatusCodes.QTL_ERR_NULL_PTR;
if (n <= 0) return StatusCodes.QTL_ERR_INVALID_LENGTH;
if (period <= 0) return StatusCodes.QTL_ERR_INVALID_PARAM;
try
{
var srcSpan = new ReadOnlySpan<double>(src, n);
var outSpan = new Span<double>(output, n);
QuanTAlib.Sma.Batch(srcSpan, outSpan, period);
return StatusCodes.QTL_OK;
}
catch { return StatusCodes.QTL_ERR_INTERNAL; }
}
[UnmanagedCallersOnly(EntryPoint = "qtl_bbands")]
public static unsafe int Bbands(
double* src, int n,
double* outUpper, double* outMiddle, double* outLower,
int period, double multiplier)
{
if (src == null || outUpper == null || outMiddle == null || outLower == null)
return StatusCodes.QTL_ERR_NULL_PTR;
if (n <= 0) return StatusCodes.QTL_ERR_INVALID_LENGTH;
if (period <= 0) return StatusCodes.QTL_ERR_INVALID_PARAM;
try
{
var srcSpan = new ReadOnlySpan<double>(src, n);
var upper = new Span<double>(outUpper, n);
var middle = new Span<double>(outMiddle, n);
var lower = new Span<double>(outLower, n);
QuanTAlib.Bbands.Batch(srcSpan, upper, middle, lower, period, multiplier);
return StatusCodes.QTL_OK;
}
catch { return StatusCodes.QTL_ERR_INTERNAL; }
}
```
---
## 6) Python contract
### 6.1 Package identity
| Property | Value |
|----------|-------|
| PyPI name | `quantalib` |
| Import name | `quantalib` |
| AssemblyName | `quantalib` |
### 6.2 Loader (`_loader.py`)
- Detect OS + architecture at import time.
- Load bundled shared library from `quantalib/native/<platform>/`.
- Platform directory mapping:
| Platform | Directory |
|----------|-----------|
| Windows x64 | `native/win_amd64/quantalib.dll` |
| Linux x64 | `native/linux_x86_64/quantalib.so` |
| macOS ARM64 | `native/macosx_arm64/quantalib.dylib` |
| macOS x64 | `native/macosx_x86_64/quantalib.dylib` |
- On failure: raise `OSError` with actionable message listing expected path
and detected platform.
- Use explicit package-path loading; never rely on system library search order.
### 6.3 Bridge (`_bridge.py`)
- Declare `argtypes` and `restype` for every native function at module level.
- Validate function presence at import time (graceful `AttributeError` → skip).
- Input normalization: `np.ascontiguousarray(arr, dtype=np.float64)`.
- Status code checking: helper raises Python exception mapped from return code:
```python
class QtlError(Exception):
"""Base exception for quantalib native errors."""
class QtlNullPointerError(QtlError): ... # status 1
class QtlInvalidLengthError(QtlError): ... # status 2
class QtlInvalidParamError(QtlError): ... # status 3
class QtlInternalError(QtlError): ... # status 4
```
### 6.4 Indicator wrappers (`indicators.py`)
- Expose pandas-ta-compatible function signatures where practical.
- Return types:
| Input type | Output (single) | Output (multi) |
|------------|-----------------|----------------|
| `pd.Series` | `pd.Series` | `pd.DataFrame` |
| `pd.DataFrame` | `pd.Series` | `pd.DataFrame` |
| `np.ndarray` | `np.ndarray` | tuple of `np.ndarray` |
- Series names follow pandas-ta conventions: `SMA_14`, `BBU_20_2.0`, etc.
- DataFrame columns for multi-output: `BBU_20_2.0`, `BBM_20_2.0`, `BBL_20_2.0`.
### 6.5 Concrete Python wrapper example
```python
# indicators.py (excerpt)
import numpy as np
import pandas as pd
from quantalib._bridge import _lib, _check
def sma(close, length=None, offset=None, **kwargs):
"""Simple Moving Average."""
length = int(length) if length else 10
offset = int(offset) if offset else 0
# Extract numpy array
index = None
if isinstance(close, pd.Series):
index = close.index
close = close.to_numpy(dtype=np.float64, copy=False)
close = np.ascontiguousarray(close, dtype=np.float64)
n = len(close)
out = np.empty(n, dtype=np.float64)
_check(_lib.qtl_sma(
close.ctypes.data_as(c_double_p), n,
out.ctypes.data_as(c_double_p), length
))
if offset:
out = np.roll(out, offset)
out[:offset] = np.nan
if index is not None:
result = pd.Series(out, index=index, name=f"SMA_{length}")
result.category = "trend"
return result
return out
```
### 6.6 NaN / warmup behavior
- Warmup slots are filled with `NaN` by the C# `Batch` methods.
- This matches pandas-ta's convention for most indicators.
- Known deltas from pandas-ta warmup lengths are documented in the
compatibility table in `README.md`.
### 6.7 pandas fallback policy
When pandas is not installed:
- Functions accept and return `np.ndarray` only.
- `pd.Series` / `pd.DataFrame` input raises `ImportError` with message
`"pandas required for Series/DataFrame input"`.
- `numpy` is a hard dependency; `pandas` is an optional extra.
---
## 7) Build and packaging
### 7.1 .NET project (`python.csproj`)
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Library</OutputType>
<PublishAot>true</PublishAot>
<AssemblyName>quantalib</AssemblyName>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<InvariantGlobalization>true</InvariantGlobalization>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<Compile Include="src\**\*.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\lib\quantalib.csproj" />
</ItemGroup>
</Project>
```
### 7.2 Local build props (`Directory.Build.props`)
```xml
<Project>
<PropertyGroup>
<GitVersionSkip>true</GitVersionSkip>
<IlcOptimizationPreference>Speed</IlcOptimizationPreference>
<RunAnalyzers>false</RunAnalyzers>
<ErrorReport>none</ErrorReport>
</PropertyGroup>
</Project>
```
### 7.3 Publish script (`publish.ps1`)
Requirements:
- Publish for RIDs: `win-x64`, `linux-x64`, `osx-arm64`, `osx-x64`
- Clean target directory before each publish
- Verify expected artifact exists per RID (`.dll`, `.so`, `.dylib`)
- Copy artifact into `quantalib/native/<platform>/`
- Exit non-zero on any missing artifact
### 7.4 Python packaging (`pyproject.toml`)
```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "quantalib"
version = "0.1.0"
description = "High-performance technical analysis via QuanTAlib NativeAOT"
requires-python = ">=3.10"
dependencies = ["numpy>=1.24"]
[project.optional-dependencies]
pandas = ["pandas>=1.5"]
dev = ["pytest", "pandas>=1.5"]
[tool.hatch.build.targets.wheel]
packages = ["quantalib"]
```
### 7.5 NativeAOT considerations
- **Trimming:** NativeAOT trims unreachable code. All `Batch` methods are
reachable via explicit calls in `Exports.cs`, so no `rd.xml` needed.
- **Reflection:** QuanTAlib indicators do not use reflection in `Batch` paths.
- **SIMD:** NativeAOT preserves hardware intrinsics. `Avx2.IsSupported` works
at runtime in the compiled native binary.
- **Startup:** NativeAOT has near-zero startup overhead. No JIT warmup.
---
## 8) Full indicator catalog by category
### 8.1 Core (7 indicators)
| Indicator | C# Class | Pattern | Params |
|-----------|----------|---------|--------|
| avgprice | `Avgprice` | C (OHLC) | — |
| ha | `Ha` | C (OHLC) | — |
| medprice | `Medprice` | D (HL) | — |
| midbody | `Midbody` | C (OC) | — |
| midpoint | `Midpoint` | A | period |
| midprice | `Midprice` | D (HL) | period |
| typprice | `Typprice` | C (OHLC) | — |
| wclprice | `Wclprice` | E (HLC) | — |
### 8.2 Momentum (18 indicators)
| Indicator | C# Class | Pattern | Params |
|-----------|----------|---------|--------|
| asi | `Asi` | C (OHLC) | period, limitMove |
| bias | `Bias` | A | period |
| bop | `Bop` | C (OHLC) | — |
| cfb | `Cfb` | A + int[]* | lengths |
| cmo | `Cmo` | A | period |
| macd | `Macd` | A | fastPeriod, slowPeriod |
| mom | `Mom` | A | period |
| pmo | `Pmo` | A | timePeriods, smoothPeriods, signalPeriods |
| ppo | `Ppo` | A | fastPeriod, slowPeriod |
| prs | `Prs` | H (dual) | period |
| roc | `Roc` | A | period |
| rocp | `Rocp` | A | period |
| rocr | `Rocr` | A | period |
| rsi | `Rsi` | A | period |
| rsx | `Rsx` | A | period |
| sam | `Sam` | A | alpha, cutoff |
| tsi | `Tsi` | A | longPeriod, shortPeriod |
| vel | `Vel` | A | period |
### 8.3 Oscillators (~35 indicators)
| Indicator | C# Class | Pattern | Key params |
|-----------|----------|---------|------------|
| ac | `Ac` | D (HL) | fastPeriod, slowPeriod, acPeriod |
| ao | `Ao` | D (HL) | fastPeriod, slowPeriod |
| apo | `Apo` | A | fastPeriod, slowPeriod |
| bbb | `Bbb` | A | period, multiplier |
| bbi | `Bbi` | A | periods... |
| bbs | `Bbs` | E (HLC) | multi-output |
| brar | `Brar` | C (OHLC) | period |
| cfo | `Cfo` | A | period |
| coppock | `Coppock` | A | roc1, roc2, wma |
| crsi | `Crsi` | A | rsiPeriod, streakPeriod, rankPeriod |
| cti | `Cti` | A | period |
| deco | `Deco` | A | shortPeriod, longPeriod |
| dem | `Dem` | E (HLC) | period |
| dpo | `Dpo` | A | period |
| dosc | `Dosc` | A | rsiPeriod, ema1Period, ema2Period, sigPeriod |
| dymoi | `Dymoi` | A | basePeriod, shortPeriod, longPeriod... |
| er | `Er` | A | period |
| fisher | `Fisher` | A | period, alpha |
| fisher04 | `Fisher04` | A | period |
| gator | `Gator` | A | jawPeriod... |
| imi | `Imi` | C (OHLC) | period |
| inertia | `Inertia` | A | period |
| kdj | `Kdj` | I (HLC→3) | period, kSmooth, dSmooth |
| kri | `Kri` | A | period |
| kst | `Kst` | A | multiple roc/ma periods |
| marketfi | `Marketfi` | D (HL) + vol | — |
| mstoch | `Mstoch` | A | period |
| pgo | `Pgo` | E (HLC) | period |
| psl | `Psl` | A | period |
| qqe | `Qqe` | A | rsiPeriod, smoothFactor, qqeFactor |
| reflex | `Reflex` | A | period |
| reverseema | `ReverseEma` | A | period |
| rvgi | `Rvgi` | C (OHLC) | period |
| smi | `Smi` | I (HLC→2+) | period, smoothK, smoothD |
| squeeze | `Squeeze` | I (HLC→multi) | bbPeriod, kcPeriod... |
| stc | `Stc` | A + enum | kPeriod, dPeriod, fastLen, slowLen, smoothing(int) |
| stoch | `Stoch` | I (HLC→2) | kPeriod, kSmooth, dSmooth |
| stochf | `Stochf` | I (HLC→2) | kPeriod, dPeriod |
| stochrsi | `Stochrsi` | A | rsiLen, stochLen, kSmooth, dSmooth |
| td_seq | `Td_seq` | C (OHLC) | — |
| trendflex | `Trendflex` | A | period |
| trix | `Trix` | A | period |
| ttm_wave | `TtmWave` | I (HLC→multi) | — |
| ultosc | `Ultosc` | E (HLC) | period1, period2, period3 |
| willr | `Willr` | E (HLC) | period |
### 8.4 Trends — FIR (~30 indicators)
| Indicator | C# Class | Pattern | Key params |
|-----------|----------|---------|------------|
| alma | `Alma` | A | period, offset, sigma |
| blma | `Blma` | A | period |
| bwma | `Bwma` | A | period, order |
| conv | `Conv` | A + double[]* | kernel |
| crma | `Crma` | A | period |
| dwma | `Dwma` | A | period |
| fwma | `Fwma` | A | period |
| gwma | `Gwma` | A | period, sigma |
| hamma | `Hamma` | A | period |
| hanma | `Hanma` | A | period |
| hend | `Hend` | A | period |
| hma | `Hma` | A | period |
| ilrs | `Ilrs` | A | period |
| kaiser | `Kaiser` | A | period, beta |
| lanczos | `Lanczos` | A | period |
| lsma | `Lsma` | A | period, offset |
| nlma | `Nlma` | A | period |
| nyqma | `Nyqma` | A | period, nyquistPeriod |
| parzen | `Parzen` | A | period |
| pma | `Pma` | A / I (2) | period |
| pwma | `Pwma` | A | period |
| qrma | `Qrma` | A | period |
| rain | `Rain` | A | period |
| rwma | `Rwma` | D (CHL) | period |
| sgma | `Sgma` | A | period, degree |
| sinema | `Sinema` | A | period |
| sma | `Sma` | A | period |
| sp15 | `Sp15` | A | — |
| swma | `Swma` | A | period |
| trima | `Trima` | A | period |
| tsf | `Tsf` | A | period |
| tukey_w | `Tukey_w` | A | period, alpha |
| wma | `Wma` | A | period |
### 8.5 Trends — IIR (~30 indicators)
| Indicator | C# Class | Pattern | Key params |
|-----------|----------|---------|------------|
| ahrens | `Ahrens` | A | period |
| coral | `Coral` | A | period, cd |
| decycler | `Decycler` | A | period |
| dema | `Dema` | A (+ alpha) | period |
| dsma | `Dsma` | A | period |
| ema | `Ema` | A (+ alpha) | period |
| frama | `Frama` | A / D (HL) | period |
| gdema | `Gdema` | A | period, vfactor |
| hema | `Hema` | A | period |
| holt | `Holt` | A | period, gamma |
| htit | `Htit` | A | — |
| hwma | `Hwma` | A | period |
| jma | `Jma` | A | period, phase, power |
| kama | `Kama` | A | period, fastPeriod, slowPeriod |
| lema | `Lema` | A (+ alpha) | period |
| ltma | `Ltma` | A | period |
| mama | `Mama` | I (2) | fastLimit, slowLimit |
| mavp | `Mavp` | H (src+periods) | minPeriod, maxPeriod |
| mcnma | `Mcnma` | A (+ alpha) | period |
| mgdi | `Mgdi` | A | period, k |
| mma | `Mma` | A | period |
| nma | `Nma` | A | period |
| qema | `Qema` | A | period |
| rema | `Rema` | A | period, lambda |
| rgma | `Rgma` | A | period, passes |
| rma | `Rma` | A | period |
| t3 | `T3` | A | period, vfactor |
| tema | `Tema` | A (+ alpha) | period |
| trama | `Trama` | A | period |
| vama | `Vama` | A | period |
| vidya | `Vidya` | A | period |
| yzvama | `Yzvama` | A | period |
| zldema | `Zldema` | A (+ alpha) | period |
| zlema | `Zlema` | A (+ alpha) | period |
| zltema | `Zltema` | A (+ alpha) | period |
### 8.6 Channels (~20 indicators)
All channel indicators output 3 spans: upper, middle, lower (Pattern I).
| Indicator | C# Class | Input pattern | Key params |
|-----------|----------|---------------|------------|
| abber | `Abber` | A | period |
| accbands | `AccBands` | B (HLC) | period |
| apchannel | `Apchannel` | D (HL) | period |
| apz | `Apz` | B (HLC) | period |
| atrbands | `AtrBands` | B (HLC) | period, multiplier |
| bbands | `Bbands` | A | period, multiplier |
| dchannel | `Dchannel` | D (HL) | period |
| decaychannel | `Decaychannel` | B (HLC) | period |
| fcb | `Fcb` | D (HL) | period |
| jbands | `Jbands` | A | period, phase, power |
| kchannel | `Kchannel` | B (HLC) | period, multiplier |
| maenv | `Maenv` | A | period, pct |
| mmchannel | `Mmchannel` | D (HL) | period |
| pchannel | `Pchannel` | D (HL) | period |
| regchannel | `Regchannel` | A | period |
| sdchannel | `Sdchannel` | A | period |
| starchannel | `Starchannel` | B (HLC) | period |
| stbands | `Stbands` | B (HLC) | period |
| ttm_lrc | `TtmLrc` | A | period |
| ubands | `Ubands` | A | period |
| uchannel | `Uchannel` | D (HL) | period |
| vwapbands | `Vwapbands` | A + vol | period |
| vwapsd | `Vwapsd` | A + vol | period |
### 8.7 Volatility (~20 indicators)
| Indicator | C# Class | Pattern | Key params |
|-----------|----------|---------|------------|
| bbw | `Bbw` | A | period, multiplier |
| bbwn | `Bbwn` | A | period, multiplier, lookback |
| bbwp | `Bbwp` | A | period, multiplier, lookback |
| ccv | `Ccv` | A | period, method |
| cv | `Cv` | A | period, alpha, beta |
| cvi | `Cvi` | A | rocLength, smoothLength |
| etherm | `Etherm` | E (HLC) | period |
| ewma | `Ewma` | A | period, annualize, annualPeriods |
| gkv | `Gkv` | C (OHLC) | period, annualPeriods |
| hlv | `Hlv` | D (HL) | period, annualPeriods |
| hv | `Hv` | A | period, annualPeriods |
| jvolty | `Jvolty` | A | period, phase, power |
| jvoltyn | `Jvoltyn` | A | period, phase, power |
| massi | `Massi` | A | emaLength, sumLength |
| rsv | `Rsv` | C (OHLC) | period, annualPeriods |
| rv | `Rv` | A | period, annualPeriods |
| rvi | `Rvi` | A | period |
| tr | `Tr` | E (HLC) | — |
| ui | `Ui` | A | period |
| vov | `Vov` | A | period, vovPeriod |
| vr | `Vr` | E (HLC) | period |
| yzv | `Yzv` | C (OHLC) | period, annualPeriods |
### 8.8 Volume (~30 indicators)
| Indicator | C# Class | Pattern | Key params |
|-----------|----------|---------|------------|
| adl | `Adl` | B (HLCV) | — |
| adosc | `Adosc` | B (HLCV) | fastPeriod, slowPeriod |
| aobv | `Aobv` | I (CV→2) | — |
| cmf | `Cmf` | B (HLCV) | period |
| efi | `Efi` | G (CV) | period |
| eom | `Eom` | B (HLV) | period, volumeScale |
| evwma | `Evwma` | G (SV) | period |
| iii | `Iii` | B (HLCV) | period |
| kvo | `Kvo` | B (HLCV) | fastPeriod, slowPeriod |
| mfi | `Mfi` | B (HLCV) | period |
| nvi | `Nvi` | G (CV) | startValue |
| obv | `Obv` | G (CV) | — |
| pvd | `Pvd` | G (CV) | pricePeriod, volumePeriod, smoothingPeriod |
| pvi | `Pvi` | G (CV) | startValue |
| pvo | `Pvo` | I (V→3) | fastPeriod, slowPeriod, signalPeriod |
| pvr | `Pvr` | G (PV) | — |
| pvt | `Pvt` | G (CV) | — |
| tvi | `Tvi` | G (PV) | minTick |
| twap | `Twap` | A | period |
| va | `Va` | B (HLCV) | — |
| vf | `Vf` | G (CV) | period |
| vo | `Vo` | A (volume) | shortPeriod, longPeriod |
| vroc | `Vroc` | A (volume) | period, usePercent |
| vwad | `Vwad` | B (HLCV) | period |
| vwap | `Vwap` | B (HLCV) | period |
| vwma | `Vwma` | G (SV) | period |
| wad | `Wad` | B (HLCV) | — |
### 8.9 Statistics (~30 indicators)
| Indicator | C# Class | Pattern | Key params |
|-----------|----------|---------|------------|
| acf | `Acf` | A | period, lag |
| cma | `Cma` | A | — |
| correlation | `Correlation` | H | period |
| covariance | `Covariance` | H | period, isPopulation |
| entropy | `Entropy` | A | period |
| geomean | `Geomean` | A | period |
| granger | `Granger` | H | period, maxlag |
| harmean | `Harmean` | A | period |
| hurst | `Hurst` | A | period |
| iqr | `Iqr` | A | period |
| jb | `Jb` | A | period |
| kendall | `Kendall` | H | period |
| kurtosis | `Kurtosis` | A | period, isPopulation |
| linreg | `LinReg` | A | period, offset |
| meandev | `MeanDev` | A | period |
| median | `Median` | A | period |
| mode | `Mode` | A | period |
| pacf | `Pacf` | A | period, lag |
| percentile | `Percentile` | A | period, percent |
| polyfit | `Polyfit` | A | period, degree |
| quantile | `Quantile` | A | period, quantileLevel |
| skew | `Skew` | A | period, isPopulation |
| spearman | `Spearman` | H | period |
| stddev | `StdDev` | A | period, isPopulation |
| stderr | `Stderr` | A | period |
| sum | `Sum` | A | period |
| theil | `Theil` | A | period |
| trim | `Trim` | A | period, trimPct |
| variance | `Variance` | A | period, isPopulation |
| wavg | `Wavg` | A | period |
| wins | `Wins` | A | period, winPct |
| zscore | `Zscore` | A | period |
| ztest | `Ztest` | A | period, mu0 |
| cointegration | `Cointegration` | H | — |
### 8.10 Errors (~25 indicators)
| Indicator | C# Class | Pattern | Key params |
|-----------|----------|---------|------------|
| huber | `Huber` | F | period, delta |
| logcosh | `LogCosh` | F | period |
| mae | `Mae` | F | period |
| maape | `Maape` | F | period |
| mape | `Mape` | F | period |
| mapd | `Mapd` | F | period |
| mase | `Mase` | F | period |
| mdae | `Mdae` | F | period |
| mdape | `Mdape` | F | period |
| me | `Me` | F | period |
| mpe | `Mpe` | F | period |
| mrae | `Mrae` | F | period |
| mse | `Mse` | F | period |
| msle | `Msle` | F | period |
| pseudohuber | `PseudoHuber` | F | period, delta |
| quantileloss | `QuantileLoss` | F | period, quantile |
| rae | `Rae` | F | period |
| rmse | `Rmse` | F | period |
| rmsle | `Rmsle` | F | period |
| rse | `Rse` | F | period |
| rsquared | `Rsquared` | F | period |
| smape | `Smape` | F | period |
| theilu | `TheilU` | F | period |
| tukeybiweight | `TukeyBiweight` | F | period, c |
| wmape | `Wmape` | F | period |
| wrmse | `Wrmse` | F | period (+ weighted variant) |
### 8.11 Filters (~30 indicators)
| Indicator | C# Class | Pattern | Key params |
|-----------|----------|---------|------------|
| agc | `Agc` | A | decay |
| alaguerre | `ALaguerre` | A | length, medianLength |
| baxterking | `BaxterKing` | A | pLow, pHigh, k |
| bessel | `Bessel` | A | length |
| bilateral | `Bilateral` | A | period, sigmaSRatio, sigmaRMult |
| bpf | `Bpf` | A | lowerPeriod, upperPeriod |
| butter2 | `Butter2` | A | period |
| butter3 | `Butter3` | A | period |
| cfitz | `Cfitz` | A | pLow, pHigh |
| cheby1 | `Cheby1` | A | period, ripple |
| cheby2 | `Cheby2` | A | period, attenuation |
| edcf | `Edcf` | A | length |
| elliptic | `Elliptic` | A | period |
| gauss | `Gauss` | A | sigma |
| hann | `Hann` | A | length |
| hp | `Hp` | A | lambda |
| hpf | `Hpf` | A | length |
| kalman | `Kalman` | A | q, r (or period, gain) |
| laguerre | `Laguerre` | A | gamma |
| lms | `Lms` | A | order, mu |
| loess | `Loess` | A | period |
| modf | `Modf` | A | period, beta, feedback, fbWeight |
| notch | `Notch` | A | period, q |
| nw | `Nw` | A | period, bandwidth |
| oneeuro | `OneEuro` | A | minCutoff, beta, dCutoff |
| rls | `Rls` | A | order, lambda |
| rmed | `Rmed` | A | period |
| roofing | `Roofing` | A | hpLength, ssLength |
| sgf | `Sgf` | A | period, polyOrder |
| spbf | `Spbf` | A | shortPeriod, longPeriod, rmsPeriod |
| ssf2 | `Ssf2` | A | period |
| ssf3 | `Ssf3` | A | period |
| usf | `Usf` | A | period |
| voss | `Voss` | A | period, predict, bandwidth |
| wavelet | `Wavelet` | A | levels, threshMult |
| wiener | `Wiener` | A | period, smoothPeriod |
### 8.12 Cycles (~14 indicators)
| Indicator | C# Class | Pattern | Key params |
|-----------|----------|---------|------------|
| ccor | `Ccor` | A | period, threshold |
| ccyc | `Ccyc` | A | alpha |
| cg | `Cg` | A | period |
| dsp | `Dsp` | A | period |
| eacp | `Eacp` | A | minPeriod, maxPeriod, avgLength, enhance |
| ebsw | `Ebsw` | A | hpLength, ssfLength |
| homod | `Homod` | A | minPeriod, maxPeriod |
| ht_dcperiod | `HtDcperiod` | A | — |
| ht_dcphase | `HtDcphase` | A | — |
| ht_phasor | `HtPhasor` | I (→2) | — |
| ht_sine | `HtSine` | I (→2) | — |
| lunar | `Lunar` | Special (long*) | — |
| solar | `Solar` | Special (long*) | — |
| ssfdsp | `Ssfdsp` | A | period |
### 8.13 Dynamics (~10 indicators)
| Indicator | C# Class | Pattern | Key params |
|-----------|----------|---------|------------|
| amat | `Amat` | I (→2) | fastPeriod, slowPeriod |
| aroon | `Aroon` | D (HL) | period |
| ghla | `Ghla` | E (HLC) | period |
| ht_trendmode | `HtTrendmode` | A | — |
| pfe | `Pfe` | A | period, smoothPeriod |
| ravi | `Ravi` | A | shortPeriod, longPeriod |
| vhf | `Vhf` | A | period |
| vortex | `Vortex` | I (HLC→2) | period |
| qstick | `Qstick` | C (OC) | period |
### 8.14 Reversals (~10 indicators)
| Indicator | C# Class | Pattern | Key params |
|-----------|----------|---------|------------|
| chandelier | `Chandelier` | C (OHLC→2) | period, multiplier |
| ckstop | `Ckstop` | C (OHLC→2) | period, multiplier |
| fractals | `Fractals` | D (HL→2) | period |
| pivot | `Pivot` | D (HL→multi) | — |
| pivotcam | `Pivotcam` | E (HLC→multi) | — |
| pivotdem | `Pivotdem` | C (OHLC→multi) | — |
| pivotext | `Pivotext` | E (HLC→multi) | — |
| pivotfib | `Pivotfib` | E (HLC→multi) | — |
| pivotwood | `Pivotwood` | E (HLC→multi) | — |
| psar | `Psar` | C (OHLC) | accelStart, accelMax |
| swings | `Swings` | D (HL→multi) | period |
| ttm_scalper | `TtmScalper` | D (HL→multi) | period |
### 8.15 Numerics (~15 indicators)
| Indicator | C# Class | Pattern | Key params |
|-----------|----------|---------|------------|
| accel | `Accel` | A | — |
| betadist | `Betadist` | A | alpha, beta params |
| binomdist | `Binomdist` | A | n, p |
| change | `Change` | A | period |
| dwt | `Dwt` | A | levels |
| expdist | `Expdist` | A | lambda |
| exptrans | `Exptrans` | A | — |
| fdist | `Fdist` | A | df1, df2 |
| fft | `Fft` | A | — |
| gammadist | `Gammadist` | A | alpha, beta |
| highest | `Highest` | A | period |
| ifft | `Ifft` | A | — |
| jerk | `Jerk` | A | — |
| lineartrans | `Lineartrans` | A | slope, intercept |
| lognormdist | `Lognormdist` | A | mu, sigma |
| logtrans | `Logtrans` | A | — |
| lowest | `Lowest` | A | period |
| normdist | `Normdist` | A | mu, sigma |
| normalize | `Normalize` | A | period |
| poissondist | `Poissondist` | A | lambda |
| relu | `Relu` | A | — |
| sigmoid | `Sigmoid` | A | k, x0 |
| slope | `Slope` | A | — |
| sqrttrans | `Sqrttrans` | A | — |
| tdist | `Tdist` | A | df |
| weibulldist | `Weibulldist` | A | k, lambda |
### 8.16 Forecasts (1 indicator)
| Indicator | C# Class | Pattern | Key params |
|-----------|----------|---------|------------|
| afirma | `Afirma` | A + enum | period, window(int), leastSquares(bool→int) |
---
## 9) Compatibility strategy with pandas-ta
### 9.1 Compatibility levels
| Level | Description |
|-------|-------------|
| **L1** | Signature-compatible: same parameter names and defaults |
| **L2** | Return-compatible: same return container shape and type |
| **L3** | Behavior-compatible: matching warmup NaN count, naming, core semantics |
### 9.2 pandas-ta overlap indicators
These indicators exist in both pandas-ta and QuanTAlib. All target L2 minimum,
L3 where QuanTAlib's algorithm matches:
**Trend:** SMA, EMA, DEMA, TEMA, HMA, WMA, KAMA, FWMA, T3, TRIMA, ALMA, LSMA,
SINEMA, VIDYA, ZLEMA, FRAMA, HWMA
**Momentum:** RSI, ROC, MOM, CMO, MACD, PPO, TSI, RSX
**Oscillators:** STOCH, STOCHF, STOCHRSI, WILLR, AO, APO, TRIX, FISHER, DPO,
ULTOSC, KDJ, QQE, SQUEEZE, STC, RVGI
**Volatility:** ATR, TR, BBANDS, BBW, MASSI, UI, EWMA (realized vol)
**Volume:** OBV, ADL, ADOSC, CMF, MFI, NVI, PVI, PVT, VWAP, VWMA, EFI, KVO
**Statistics:** ZSCORE, VARIANCE, STDDEV, SKEW, KURTOSIS, MEDIAN, ENTROPY,
LINREG
### 9.3 Known deltas
- pandas-ta uses `talib=True` to delegate to TA-Lib C library; quantalib uses
its own C# implementations.
- Warmup NaN counts may differ for recursive indicators (EMA, RSI) due to
different convergence thresholds.
- `offset` parameter: quantalib supports it via `np.roll` in the Python layer.
- `fillna`/`fill_method` kwargs: supported in Python layer, not in native.
---
## 10) Testing protocol
### 10.1 Native ABI tests (C# xUnit in lib project)
- Status-code contract tests for all exports
- Null pointer, zero length, negative period → correct error codes
- Known golden values vs managed `Batch` calls
### 10.2 Python tests (pytest)
| Suite | Description |
|-------|-------------|
| `test_smoke.py` | Import, load native lib, call SMA, check output is ndarray |
| `test_shapes.py` | `len(output) == len(input)` for all single-output indicators |
| `test_status_codes.py` | Null ptr, bad length, bad params raise correct exceptions |
| `test_golden.py` | Compare outputs vs known golden values from managed QuanTAlib |
| `test_compat.py` | pandas-ta parity for overlap indicators (Series/DataFrame) |
### 10.3 Performance tests
Track per-indicator:
- Python wrapper overhead (µs per call for 10k bars)
- Throughput (rows/sec)
- Compare vs pandas-ta for overlap indicators
Acceptance (initial baseline):
- Median wrapper overhead does not exceed pandas-ta by more than 20% on overlap indicators where algorithms are directly comparable.
- No unbounded memory growth across repeated runs.
- Performance report is produced per release candidate and checked into CI artifacts.
---
## 11) CI and release
### 11.1 Build matrix
| RID | Runner | Artifact |
|-----|--------|----------|
| `win-x64` | `windows-latest` | `quantalib.dll` |
| `linux-x64` | `ubuntu-latest` | `quantalib.so` |
| `osx-arm64` | `macos-14` | `quantalib.dylib` |
| `osx-x64` | `macos-13` | `quantalib.dylib` |
### 11.2 Pipeline stages
1. `dotnet publish -r <rid> -c Release` per RID
2. Stage artifacts into `quantalib/native/<platform>/`
3. `python -m pytest tests/` per platform
4. Build wheel with `python -m build`
5. Upload to PyPI (manual trigger for releases)
### 11.3 Release checklist
- [ ] All RID artifacts present and loadable
- [ ] All pytest suites pass on all platforms
- [ ] Performance benchmark logged (no regressions)
- [ ] `CHANGELOG.md` updated
- [ ] Version bumped in `pyproject.toml`
---
## 12) Risk register
| # | Risk | Impact | Mitigation |
|---|------|--------|------------|
| 1 | ABI breakage on update | High | Versioned symbols (`qtl_*`) + ABI contract tests |
| 2 | NativeAOT trims used code | High | Explicit calls in `Exports.cs` guarantee reachability |
| 3 | Platform loader failures | Medium | Explicit package-path loading + startup diagnostics |
| 4 | Behavior drift vs pandas-ta | Medium | Compatibility suite + documented deltas |
| 5 | Enum/array params over ABI | Medium | int/ptr mapping with documented contracts |
| 6 | Large export surface (~290) | Medium | Code generation considered for `Exports.cs` if manual is too error-prone |
| 7 | Cross-compilation failures | Medium | CI matrix catches per-platform issues early |
| 8 | Thread safety | Low | `Batch` methods are stateless; no shared mutable state |
---
## 13) Review checklist
- [ ] Naming accepted: PyPI/import/binary all `quantalib`
- [ ] Versioned ABI prefix accepted (`qtl_*`)
- [ ] Flat structure accepted (`Exports.cs`, `indicators.py`)
- [ ] Status-code model accepted (04)
- [ ] 9 ABI signature patterns accepted
- [ ] Special parameter mappings accepted (enum→int, bool→int, array→ptr+len)
- [ ] Full indicator catalog reviewed
- [ ] Compatibility levels accepted (L1/L2/L3)
- [ ] RID matrix accepted (4 platforms)
- [ ] NaN/warmup policy accepted
- [ ] Thread safety model accepted
- [ ] CI pipeline accepted
---
## 14) Implementation order
1. **Skeleton:** `python.csproj`, `Directory.Build.props`, package layout,
`_loader.py`, `_bridge.py` scaffolding, `pyproject.toml`
2. **ABI layer:** `StatusCodes.cs`, `ArrayBridge.cs`, `Exports.cs` (all ~290)
3. **Python layer:** `indicators.py` (all wrappers), `_compat.py` (aliases)
4. **Tests:** `test_smoke.py`, `test_shapes.py`, `test_status_codes.py`,
`test_golden.py`, `test_compat.py`
5. **Publish:** `publish.ps1`, CI workflow, wheel packaging
6. **Docs:** `README.md` with full indicator list and compatibility table
---
## 15) Open decisions
1. **Error mapping granularity:** `ValueError` for bad params vs separate
exception classes per status code (current spec uses separate classes).
2. **Alpha overloads:** Export as `qtl_ema_alpha` or defer to v2?
3. **Code generation:** Generate `Exports.cs` + `_bridge.py` + `indicators.py`
from a manifest file, or write manually?
4. **Linux wheel tag:** `manylinux2014_x86_64` vs `manylinux_2_17_x86_64`?
5. **Version sync:** Should `quantalib` Python version track QuanTAlib NuGet
version, or version independently?
## 16) Review findings and prioritized refinements
### High priority
1. **Finalize ABI evolution policy**
- Keep `qtl_` immutable for v1 and reserve `qtl2_` for first break.
2. **Adopt manifest-driven generation**
- Eliminate manual drift risk across native exports, ctypes bridge, and wrappers.
3. **Clarify output validity on internal failures**
- Treat outputs as invalid on `QTL_ERR_INTERNAL`.
### Medium priority
4. **Lock pandas fallback behavior**
- NumPy-only mode should be deterministic and explicitly tested.
5. **Define parity subset for L3**
- Freeze exact Wave 1 L3 indicator list before coding starts.
6. **Set measurable perf gates**
- Keep CI benchmarks with comparable overlap indicators and stable datasets.
### Low priority
7. **Catalog automation**
- Generate indicator catalog tables from manifest to avoid stale docs.
8. **Release metadata policy**
- Define whether Python package version mirrors or decouples from NuGet version.