C# Core API
Not yet released
The C# API is not yet released. Estimated release: Q1 2027.
3.1 Batch Processing
3.2 Output Size and Lookback
3.3 Display Shift
3.4 Errors
4.1 Abstract API
4.2 Numerical Stability
4.3 Candlestick Settings
4.4 Input Type: float vs. double
4.5 Index Range
4.6 Threading
4.7 Trimming and NativeAOT
1.0 Introduction
The .NET library is a native port of TA-Lib in the TALib namespace — no P/Invoke, no native dependency, pure managed C# targeting net10.0. Every indicator is a method on a Core instance, takes its series as spans, and is bit-identical to the reference C library over the same inputs, with one exception beyond TA-Lib's control: where a calculation calls a transcendental math function (such as exp, log, sin or atan), .NET's math library can round the result differently from the C library in the last bit. That difference can grow through the rest of the calculation. Details: Versions and Determinism.
The Core API provides:
- The
Coretype and the builder that configures it. - The settings each
Corecarries: unstable period and candlestick settings. MultipleCoreinstances can safely co-exist (say for different settings). - Every TA function, each processing a whole array of data at once.
- An optional Abstract API for calling those functions dynamically.
To process a live feed one bar at a time instead, see the companion C# Streaming API.
There is no initialization step and nothing to shut down. Where C requires TA_Initialize before any call and TA_Shutdown at exit, C# has Core.Default (or a configured Core.Builder()...Build()) ready immediately; a Core owns only managed state, so an unreferenced one is simply garbage-collected.
2.0 Add it to your project
The package is not on NuGet yet. Until it is, reference the TALib project directly:
<ItemGroup>
<ProjectReference Include="path/to/ta_codegen/output/csharp/library/TALib.csproj" />
</ItemGroup>It targets net10.0.
3.0 Calling into TA-Lib
Every indicator is exposed as a method on Core, taking the same startIdx/endIdx/inputs/optional-parameters/outputs shape as the C function it mirrors.
3.1 Batch Processing
Every function follows the same simple pattern: it reads its inputs from spans you pass in and writes its results into spans you allocate.
A function never writes more elements than you request, so the output span only needs to cover the startIdx-to-endIdx range.
As an example, let's walk through SMA, a method to calculate a moving average.
public OutRange Sma( int startIdx, int endIdx, ReadOnlySpan<double> inReal, int optInTimePeriod, Span<double> outReal )
All TA methods use the same calling pattern, divided into four groups:
- The output will be calculated only for the range specified by startIdx and endIdx. These are zero-based indices into the input spans.
- One or more input spans are then specified. Typically, these are the "price" data. In this example there is only one input. All input parameter names start with "in".
- Zero or more optional inputs are then specified. In this example there is one optional input. These parameters give finer control specific to each function. Passing
int.MinValuefor an integer parameter, the real-default sentinel-4e37for adoubleparameter, orMAType.DEFAULTfor an MA-type parameter selects that parameter's documented default. - One or more output spans come last. In this example there is only one output (outReal). Where the values landed is the return value, not a parameter: on success you get an
OutRange.
This calling pattern takes some getting used to, but it lets your app spend time and memory only on the data it actually needs.
For example, here is how to calculate a 30-day simple moving average (SMA) of daily closing prices:
using TALib;
var core = Core.Default;
double[] close = [ /* ...your closing prices... */ ];
var outReal = new double[close.Length];
OutRange r = core.Sma(
0, close.Length - 1,
close,
30,
outReal );
// outReal[0 .. r.Count - 1] holds the SMA; outReal[i] is input bar r.BegIdx + i.
for (int i = 0; i < r.Count; i++)
{
Console.WriteLine($"bar {r.BegIdx + i} = {outReal[i]}");
}
After the call, read r to learn what was produced. Even though we requested the whole range (0 to close.Length - 1), a 30-day average is not defined until the 30th day. Consequently r.BegIdx will be 29 (zero-based) and r.Count will be close.Length - 29. In other words, only that many elements of outReal are written, corresponding to input elements 29 through the end.
Arrays convert to spans implicitly, so the call above and a call passed a slice of a larger buffer (close.AsSpan(start, count)) are both ordinary code — no copy either way. startIdx and endIdx index the span you pass, not the array it came from: a slice holds no bars before its first element, so Sma(0, n - 1, close.AsSpan(100, n), ...) takes its lookback from inside the slice and produces fewer, possibly different, values than Sma(100, 100 + n - 1, close, ...). Both calls succeed. A span is never null, so passing null arrives as an empty span and is rejected by the length check as one (ArgumentException naming the parameter). Every input an indicator declares is checked, including the OHLC series a few candlestick patterns never read.
If the range ends before the lookback, so no value can be calculated, the call still succeeds and r.Count is 0 (r.IsEmpty).
OutRange is a readonly record struct with two components — BegIdx and Count — plus the conveniences IsEmpty and Empty. They are C's outBegIdx / outNBElement, Java's begIdx / count and Rust's beg_idx / count.
Every indicator also has a ReadOnlySpan<float> overload — see 4.4.
3.2 Output Size and Lookback
An indicator consumes a number of leading bars — its lookback — before it can produce anything. Query it with the matching *Lookback method:
int lookback = core.SmaLookback(30); // 29Output is written only where the indicator is defined: outReal[0] corresponds to input bar r.BegIdx. The library never pads with NaN. A range that ends before the lookback is a success with no values (r.Count == 0), not an error.
3.3 Display Shift
A few indicators are conventionally drawn at another bar than the one that computes them. The values are never moved: the matching <Name>DisplayShift method gives the number of bars to shift one output when charting it, positive ahead and negative behind. It is 0 for almost every function.
int shift = core.DpoDisplayShift(20, 0); // period 20, first output: -11The same answer comes from the Abstract API. The rules are in the specification.
3.4 Errors
The public methods throw rather than return a status code:
| Condition | Exception |
|---|---|
startIdx/endIdx negative, above Core.IndexMax, or endIdx < startIdx | TALibArgumentOutOfRangeException |
| An optional parameter outside its documented range | TALibArgumentException |
An input span that does not reach endIdx, or an output span shorter than the values produced | TALibArgumentException naming the span |
| Two outputs that are the same buffer (rB6) | TALibArgumentException |
| An inconsistency in the library's own state: a bug, please report it | TALibInvalidOperationException carrying RetCode.InternalError |
Each extends the framework type you would reach for and implements ITALibFailure, so catch (ArgumentException) still works and the RetCode is there when you want it.
Computing wholly in place is allowed and stays supported — passing the same buffer as both an input and an output is how several indicators are meant to be used. Avoid partial overlap, which only spans can express: what it does is unspecified (specification).
A NaN or ±Inf inside an input series is not detected, and nothing is promised about the output: a running sum or a recursion carries it into every later value, not only the bars whose window holds it. Clean or split the series before calling.
A batch call may allocate managed scratch, sized by a period (MFI, ULTOSC) or, for some functions built from other functions such as STOCHRSI, by the range. An allocation of 85,000 bytes or more (about 10,600 doubles) lands on the large object heap.
4.0 Advanced Features
4.1 Abstract API
TALib.Metadata.FunctionCatalog describes every function at run time and calls it without naming it at compile time. A span cannot be boxed, so the API cannot be invoked through MethodInfo.Invoke: calling a function chosen at run time needs this typed path instead of reflection. See the Abstract API page.
Core.Functions implements IReadOnlyList<FuncInfo>, so it is enumerable, LINQ-able, and indexable by position or by name. A misused holder (an index out of range, a mismatched type, an unbound input or output at call time) throws TALibArgumentException, an ArgumentException that carries a return code (rA3).
4.2 Numerical Stability
Your value changed when you fed the same bar more history? That is by design: recursive functions converge as history accumulates. See Unstable Period for how to mitigate that.
Rounding is a separate axis: floating-point error accumulates over a very long series, which is one reason a call is capped at Core.IndexMax.
Every function documentation page carries a numerical-stability property: how much the value at a given bar depends on where the series you passed in begins.
4.3 Candlestick Settings
The CDL* pattern methods judge each candle against tunable thresholds. See candlestick settings for the full list and defaults; the builder sets them the same way:
var core = Core.Builder()
.CandleSetting(CandleSettingType.BodyDoji, RangeType.HighLow, 10, 0.1)
.Build();Each setter throws ArgumentOutOfRangeException immediately if an argument is out of range, so the rejection names the call that caused it. Build() cannot fail. Rust is the one backend that defers: a setter there cannot throw, so build() returns a Result.
4.4 Input Type: float vs. double
Every indicator also has a ReadOnlySpan<float> overload (float[] converts implicitly), for callers who store series at single precision; the arithmetic is double either way, so both overloads produce the same output, bit-for-bit.
4.5 Index Range
Core.IndexMax is the largest value startIdx or endIdx may take: 100,000,000. It's a sanity bound. Past it, a call is more likely a caller bug than a real need, and it's also untested territory for overflow and rounding error.
4.6 Threading
A Core is immutable once built, so it is safe to share read-only across threads and call any indicator concurrently — no locking, and no setup ordering to respect. Core.Default is the all-defaults instance. To change a setting, build another Core with Core.Builder().
4.7 Trimming and NativeAOT
The library is annotated IsAotCompatible, uses no reflection, and publishes clean under PublishAot with TrimMode=full.
One publishing note worth knowing: at ILC's default instruction-set baseline, Math.FusedMultiplyAdd is compiled to a library call rather than the hardware FMA instruction. Values are unaffected — output is bit-identical across the JIT and both AOT baselines — but the indicators that lean on it are measurably slower (TRIX ~3.7x, DEMA ~2.3x, EMA ~1.5x, with SMA flat as a control). If you publish AOT and care about throughput, raise the baseline:
<IlcInstructionSet>x86-64-v3</IlcInstructionSet>5.0 Documentation
Every function ships XML doc comments rendered from the same canonical description as every other backend's docs, so your IDE's tooltips and IntelliSense are populated without a separate doc build. GenerateDocumentationFile is on and CS1591 (a public member missing its doc comment) is an error, so the assembly can never ship undocumented.