Build and Validate the C TA-Lib Library with PRIK¶
TA-Lib is a C library for technical-analysis calculations. You give it aligned arrays of historical prices or volume—one array element per time step—and it calculates derived series such as moving averages, momentum, volatility, regression values, price transforms, and candlestick-pattern signals. It does not download market data, choose a trading strategy, or place trades.
This maintained example wraps TA-Lib v0.7.1's complete numerical indicator API with NumPy arrays: all 161 double-input and all 161 float-input functions.
The checked-in contract contains those 322 numerical functions plus initialization and shutdown. The 198 excluded public functions are 161 lookbacks, six global settings, and 31 optional abstraction or metadata functions. The returned beginning and count replace the need for public lookbacks; calculations use TA-Lib's default global settings.
C integration boundary¶
PRIK does not wrap TA-Lib by reading or modifying its implementation .c
files. This example has two native inputs:
| Input | Role |
|---|---|
public C header ta_libc.h |
Supplies the declarations used to audit TA-Lib's complete public function inventory |
compiled libta-lib library |
Supplies the native function implementations that the generated Python extension calls |
The checked-in semantic .pyi is the reviewed contract between those C
declarations and Python. The example's native helper downloads and compiles
the pinned TA-Lib release only to create a reproducible installed header and
library for the test environment. TA-Lib's C implementation files are never
passed to PRIK as wrapper inputs.
The shape of a TA-Lib indicator¶
The 322 numerical functions are not 322 unrelated APIs. They combine the same small set of parts:
TA_<indicator>(
start index, end index,
one or more input arrays,
zero or more scalar options,
output beginning, output count,
one or more caller-owned output arrays,
) -> status
startIdx and endIdx select an inclusive range of the input arrays. Options
control the calculation—for example, the moving-average period. TA-Lib returns
a status code and writes the calculated values into storage supplied by the
caller.
Every successful indicator also writes two pieces of alignment metadata:
outBegIdxis the input index represented by the first calculated value;outNBElementis the number of values written into each output array.
TA-Lib writes the first result at output[0], not at output[outBegIdx].
Therefore output[:outNBElement] corresponds to the input range
outBegIdx:outBegIdx + outNBElement. Early input values may have no result
because an indicator needs earlier observations. A three-period moving average,
for example, cannot produce its first value until input index 2.
Recurring parameters and results¶
| Contract name | Meaning in the indicator API |
|---|---|
startIdx, endIdx |
Inclusive input range to calculate |
inReal, inReal0, inReal1 |
One or more generic numerical series |
inOpen, inHigh, inLow, inClose, inVolume |
Aligned market-data series required by price and volume indicators |
inPeriods |
A period value for each input element, used by variable-period moving averages |
optIn... |
Explicit scalar calculation options such as a period, deviation, or moving-average kind |
outReal... |
Caller-owned floating-point result arrays |
outInteger... |
Caller-owned integer results, such as indexes or candlestick signals |
outBegIdx, outNBElement |
Native scalar outputs projected by this contract into begin and count |
| native return value | TA-Lib status: zero on success and a nonzero error code otherwise |
“Optional input” is TA-Lib's C naming convention for a parameter with a
documented default. The generated wrapper still makes every selected optIn...
value explicit in the Python call.
Different indicators vary only in how many of those pieces they use:
| Example | Inputs | Options | Output arrays |
|---|---|---|---|
TA_SMA |
one value series | period | one floating-point average |
TA_ADD |
two value series | none | one floating-point series |
TA_AVGPRICE |
open, high, low, close | none | one floating-point price series |
TA_BBANDS |
one value series | period, two deviations, average kind | upper, middle, and lower floating-point bands |
TA_STOCH |
high, low, close | five period/kind options | two floating-point oscillator series |
TA_MINMAXINDEX |
one value series | period | two integer index arrays |
TA_CDLENGULFING |
open, high, low, close | none | one integer pattern-signal array |
TA_MAVP |
value and per-element period series | minimum period, maximum period, average kind | one floating-point average |
For every TA_NAME function accepting Float64[:] inputs, this release also
provides a TA_S_NAME variant accepting Float32[:] inputs. The reviewed
contract keeps floating-point output arrays as Float64[:] for both variants.
The two lifecycle functions, TA_Initialize() and TA_Shutdown(), sit outside
this indicator pattern.
How the checked-in .pyi describes that shape¶
The semantic .pyi is the reviewed source of truth for the wrapper. Its
function signature describes what Python supplies and receives; its
@native_call decorator describes the complete native argument order.
This is the complete TA_SMA declaration from the fixture:
@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Return("outBegIdx", 1), Return("outNBElement", 2), Arg(4)])
def TA_SMA(
startIdx: Int, endIdx: Int, inReal: Float64[:], optInTimePeriod: Int, outReal: Float64[:]
) -> tuple[Int, Returns["outBegIdx", Int], Returns["outNBElement", Int]]: ...
Read the declaration in two layers:
startIdx,endIdx,inReal,optInTimePeriod, andoutRealare the five Python-visible arguments.Float64[:]means a one-dimensional, C-contiguous NumPy array using the target's compatible double storage.- Python receives
(status, begin, count). The plain firstIntis the native function result. The two namedReturns[...]entries expose native output parameters as Python results. Arg(0)throughArg(3)place the first four visible arguments into native positions 0 through 3.Return("outBegIdx", 1)andReturn("outNBElement", 2)ask PRIK to create two hidden writable integer slots, pass their addresses to TA-Lib, then put their values into Python result positions 1 and 2.Arg(4)places the caller's visible output array after those two hidden native pointers, matching TA-Lib's real argument order.
Conceptually, the native and Python views are:
native: status = TA_SMA(start, end, input, period, &begin, &count, output)
Python: status, begin, count = talib.TA_SMA(start, end, input, period, output)
The output array remains visible because TA-Lib writes potentially many values into caller-owned storage. Only the two scalar bookkeeping outputs are hidden and returned. The same pattern is repeated across the other 321 indicators, with additional input, option, and output arrays as required.
What this example proves¶
- PRIK can build a large, array-centered C API against the compiled library without ABI conversion.
- Generated C
int[],float[], anddouble[]contracts use the compiler- probed NumPy storage for the active target. - TA-Lib's caller-owned output buffers remain caller-owned NumPy arrays.
- The edited contract hides
outBegIdxandoutNBElementas returned metadata, while retaining TA-Lib's status code as the first result. - A fail-closed inventory accounts for every public function in the pinned header and reference-checks every numerical entrypoint.
Versions and requirements¶
| Component | Version / source |
|---|---|
| PRIK | current repository checkout |
| TA-Lib | v0.7.1, commit 2247d599bddf37ed37e3a709371517e46efc66f6 |
| Native language | C |
| PRIK declaration input | public ta_libc.h through ta_lib_probe.h, plus the reviewed semantic .pyi |
| Link input | compiled libta-lib |
| Native build | CMake with TA-Lib's regression tools enabled |
| Python | 3.12 in Real Libraries Portability CI |
Install Git, CMake, a C compiler, Python development headers, NumPy, and pytest. The build helper clones the pinned TA-Lib tag into the user cache, verifies the exact commit, and installs it into a target-and-compiler-specific cache directory. No TA-Lib source is vendored in PRIK.
Build and test¶
Run from the repository root:
source examples/c/ta_lib/build_all.sh
python3 -m pytest -q examples/c/ta_lib/tests
build_all.sh leaves the generated extension on PYTHONPATH and the
TA-Lib shared library on the platform runtime-library path for the current
shell.
Set PRIK_TALIB_CC to select a compiler. Set
PRIK_TALIB_CACHE_DIR to choose the native source and build cache.
Calculate a moving average¶
TA-Lib writes results into storage supplied by the caller. Allocate an output array with enough capacity, then use the returned count to select the values that were written:
import numpy as np
import prik_reference_talib as talib
prices = np.arange(1.0, 11.0, dtype=np.float64)
output = np.empty_like(prices)
status, begin, count = talib.TA_SMA(
np.intc(0),
np.intc(prices.size - 1),
prices,
np.intc(3),
output,
)
if status != 0:
raise RuntimeError(f"TA_SMA failed with status {status}")
print(begin)
print(output[:count])
2
[2. 3. 4. 5. 6. 7. 8. 9.]
begin is the input index corresponding to the first output value.
count is the number of values written from the start of output.
Multi-output indicators use one caller-owned array per output.
What the build creates¶
build_all.sh prepares the test environment; it does not run pytest itself.
It performs three checked operations:
- Fetch, verify, and compile the pinned TA-Lib release, including TA-Lib's regression runner and a direct native reference server.
- Ask PRIK to generate a target-specific inventory from the complete public
ta_libc.humbrella header. This inventory is used to detect a missing, added, or unreviewed public function; it is not the wrapper contract. - Build the reviewed
324-function semantic contract.
That contract contains 322 indicators plus initialization and shutdown. For
every indicator it projects the two scalar output pointers into
(status, begin, count)and keeps the native output arrays visible.
The separate pytest command then audits the inventory and exercises the built extension.
Where the expected results come from¶
TA-Lib supplies a program named ta_regtest with two relevant kinds of
validation:
- its native C regression mode checks the indicator implementation using TA-Lib's own C test cases;
- its code-generation mode checks another implementation or language boundary against a direct native TA-Lib reference.
This example deliberately reuses the second mode because its question is
whether PRIK preserves the C API correctly. The test invokes
ta_regtest --codegen-only, so it does not rerun TA-Lib's separate native C
regression suite. It asks TA-Lib's runner to validate the generated PRIK
boundary instead.
There is no file in PRIK containing 322 arrays of expected numbers. The expected values are calculated during the test:
TA-Lib metadata and built-in input data
|
one identical request
+-------------+-------------+
| |
v v
direct native reference PRIK test adapter
`ta_ref_serve` |
| checked-in `.pyi`
| |
| NumPy values
| |
| PRIK-generated module
| |
+----------+ +------------+
| |
v v
compare status, begin, count,
and every output value
The build creates two routes from the same pinned TA-Lib v0.7.1 source. The
reference server is linked directly with the native library and does not use
PRIK. The generated Python extension links the shared native library and can be
reached only through the semantic .pyi and PRIK's generated boundary. The
reference route produces the expected result; the wrapper route produces the
actual result being tested.
That arrangement is a differential wrapper test. It does not require 322 handwritten sets of expected numbers. TA-Lib's runner creates a request once and sends that same request through both paths:
| Path | What it calls | Purpose |
|---|---|---|
| Direct reference | TA-Lib's reference server calls the pinned C API directly, without PRIK | Produce the expected native result |
| PRIK wrapper | The Python adapter calls the PRIK-generated module, which calls the same pinned C API | Produce the result a Python user receives |
Use the moving-average call above as the mental model. The reference path calls
native TA_SMA directly with the price data, period, and output buffer. The
wrapper path calls talib.TA_SMA with equivalent NumPy values. If the direct
path returns begin=2, count=8, and the eight displayed averages, the
wrapper path must return the same metadata and write the same eight values.
The runner automates that pattern for the entire indicator surface.
For each indicator, the sequence is:
- TA-Lib's runner selects its built-in price and volume data, input range, and option values. No market data is downloaded.
- The runner serializes those inputs into one request. It sends the identical
request first to the direct reference server and then to
reference_adapter.py. - The direct server calls TA-Lib natively and returns the native status,
outBegIdx,outNBElement, and output arrays. - The adapter reads the checked-in semantic
.pyias its call schema, converts the same inputs to the required NumPy dtypes, allocates the caller-owned output arrays, and invokes the generated PRIK function. - The runner compares the two statuses, beginning indexes, result counts, and every output value using TA-Lib's comparison tolerances. This comparison is designed to expose a wrong symbol, argument order, scalar conversion, array dtype, output projection, or written value.
- The adapter records every indicator that actually crossed the generated wrapper. After the run, pytest requires that record to equal the exact set of 322 reviewed indicator names. A silently skipped function fails even if every function that did run produced correct values.
Both paths ultimately execute the same TA-Lib v0.7.1 indicator logic. If TA-Lib itself calculated an indicator incorrectly in both paths, this comparison would not detect that shared error. What it establishes is that crossing the Python/NumPy/PRIK boundary does not change the result produced by the pinned native library. TA-Lib's own C regression suite is the upstream evidence for the indicator implementation; the PRIK suite is evidence for the wrapper.
What the complete pytest suite checks¶
The suite has four complementary layers:
- Public-surface accounting: the 522 public header functions must equal the 324 reviewed exports plus the 198 named exclusions. The edited contract and built Python module must expose exactly the same 324 names.
- Entrypoint reachability: every one of the 322 indicator functions is called through the generated Python module with a deliberately invalid start index and must return TA-Lib's expected error status. This catches a missing or uncallable binding independently of numerical comparison.
- Numerical parity: TA-Lib's runner covers all 161 double-input functions
and all 161
TA_S_*float-input functions through the two-path comparison above. The suite also repeatsTA_MAVPandTA_S_MAVPwith an explicit period-series request as a focused, readable check of that two-array input. - Lifecycle and readable examples: initialization and shutdown must both succeed. Focused tests for moving averages, Bollinger Bands, and integer index outputs make the expected Python calling pattern easy to inspect.
TA-Lib's runner performs abstraction-protocol self-checks before the indicator comparisons. That API is outside this example, so those setup requests are forwarded directly to the native reference server. They are not counted as PRIK calls and cannot satisfy the required 322-name coverage set.
Tested platforms¶
The Real Libraries Portability workflow builds and runs the complete surface audit and 322-indicator numerical comparison with Python 3.12 on:
| Operating system | Architectures | C compiler |
|---|---|---|
| Linux | x86-64, ARM64 | GCC 13 |
| macOS | Intel, ARM64 | Apple Clang |
Each compiler and target combination exercises the compiler-specific native cache, generated inventory, semantic lowering, extension build, and runtime comparison. TA-Lib therefore covers both GCC and Clang families across the matrix, but unlike libm it does not run both compiler families on every target. Native Windows/MSVC remains outside PRIK's current POSIX C build support. See the complete portability matrix.
Support boundary¶
The excluded abstraction API is useful to programs that discover indicators and parameter schemas dynamically. It requires aggregate records, callbacks, multi-level pointers, and library-owned pointer results, which are outside the current supported C subset. The maintained example therefore uses TA-Lib's ordinary typed batch functions, which are the numerical API Python callers need.
See the copyable example README for cache, toolchain, and troubleshooting details.