C# Core API
Not yet released
The C# API is not yet released. Estimated release: Q1 2027.
4.1 Abstraction Layer
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.
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 abstraction layer 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 new Core() (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
NuGet packaging (PackageId, versioning) lands with the release milestone — until then, dotnet pack deliberately produces nothing. 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 = new Core();
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. 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 you do not provide enough data to calculate even one value, the call still succeeds and r.Count is 0 (r.IsEmpty).
OutRange is a readonly struct with two components — BegIdx and Count — plus the conveniences IsEmpty and Empty. The component names match the C, Rust and Java surfaces (outBegIdx / outNBElement), so the same concept reads the same way in every backend.
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.SMA_Lookback(30); // 29Output is written only where the indicator is defined: outReal[0] corresponds to input bar r.BegIdx, and nothing outside 0 .. r.Count - 1 is touched. The library never pads with NaN. A range shorter than the lookback is a success with no values (r.Count == 0), not an error.
3.3 Errors
The public methods throw rather than return a status code:
| Condition | Exception |
|---|---|
startIdx/endIdx negative, above Core.MAX_INDEX, or endIdx < startIdx | ArgumentOutOfRangeException |
| An optional parameter outside its documented range | ArgumentException |
| Two outputs overlapping, or an output partially overlapping an input | ArgumentException |
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. What is rejected is partial overlap, which only spans can express: two views of the same memory at different offsets make a body write through what it is still reading, and the result would be silently wrong rather than merely surprising.
4.0 Advanced Features
4.1 Abstraction Layer
TALib.Metadata.FunctionCatalog describes every function at run time and calls it without naming it at compile time — the C# equivalent of C's abstraction layer. It exists because a span cannot be boxed: the API cannot be invoked through MethodInfo.Invoke, so calling a function chosen at run time needs a typed path instead of reflection — which is also faster.
using TALib.Metadata;
foreach (var f in Core.Functions.Where(f => f.Flags.HasFlag(FunctionFlags.Candlestick)))
{
Console.WriteLine($"{f.Name}: {f.Hint}");
}Core.Functions (an alias for FunctionCatalog.Default) implements IReadOnlyList<FunctionInfo>, so it is directly enumerable and LINQ-able, and is indexable by position or by name (Core.Functions["SMA"]). The name is matched with StringComparer.OrdinalIgnoreCase, so "SMA", "sma" and "Sma" all resolve to the same function; FunctionInfo.Name stays the canonical "SMA". Streamable functions carry FunctionFlags.Stream.
Binding arguments at run time goes through a FunctionCall, obtained from FunctionInfo.CreateCall():
var f = Core.Functions["SMA"];
var range = f.CreateCall()
.SetInput(0, close)
.SetOption(0, 30)
.SetOutput(0, outReal)
.Invoke(0, close.Length - 1);An index out of range, a type that does not match the declared parameter, or an unbound input or output at call time throws ArgumentException. Optional parameters left unbound take their documented defaults. A FunctionCall is not thread-safe: confine one to one thread, or build one per call. The FunctionCatalog it comes from is immutable and shared freely.
4.2 Numerical Stability
Some indicators are recursive, so their earliest values depend on how much history precedes them. The unstable period setting controls how many of those warm-up bars are discarded. It lives on Core and is set through the builder:
var core = Core.Builder()
.UnstablePeriod(FuncUnstId.RSI, 10)
.Build();The setters chain, so they cannot report a rejection at the point it happens; the first one is latched and surfaced by Build(), which throws ArgumentOutOfRangeException.
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();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.MAX_INDEX 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. 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.