TaLibStandard is a C# 14 / .NET 10 port of TA-Lib. It ships 98 technical indicators and 61 candlestick patterns as pure, allocation-light static methods with no native dependency.
This guide covers installation, the one concept you must understand before you trust a single
number the library returns (BegIdx / NBElement), and the traps that bite everybody once.
Every code block below was compiled and executed against the source in this repository. The numbers printed in the worked examples are real program output, not illustrations.
Atypical.TechnicalAnalysis is a meta-package: it contains no code of its own, it just references
Atypical.TechnicalAnalysis.Functions and Atypical.TechnicalAnalysis.Candles (which in turn pull
in Atypical.TechnicalAnalysis.Common).
dotnet add package Atypical.TechnicalAnalysisIf you only need indicators, or only need candlestick patterns, reference just what you use:
dotnet add package Atypical.TechnicalAnalysis.Functions # TAMath / TAFunc - 98 indicators
dotnet add package Atypical.TechnicalAnalysis.Candles # TACandle - 61 patternsBoth depend on Atypical.TechnicalAnalysis.Common, which carries the shared types (RetCode,
MAType, IndicatorResult and friends). You never need to reference it explicitly.
There are four, and they are not all the same shape as the package ids:
| Type | Namespace | Package |
|---|---|---|
TAMath, TAFunc, every *Result record |
TechnicalAnalysis.Functions |
Atypical.TechnicalAnalysis.Functions |
TACandle |
TechnicalAnalysis.Candles |
Atypical.TechnicalAnalysis.Candles |
RetCode, MAType, FuncUnstId, IndicatorResult, CandleIndicatorResult, CandleSetting, CandleSettingType, RangeType, Compatibility, GlobalsType, ValidationHelper |
TechnicalAnalysis.Common |
Atypical.TechnicalAnalysis.Common |
TACore (the global settings holder) |
TechnicalAnalysis |
Atypical.TechnicalAnalysis.Common |
Note the last row: TACore lives in the bare TechnicalAnalysis namespace, not in
TechnicalAnalysis.Common. A typical file starts with:
using TechnicalAnalysis; // TACore
using TechnicalAnalysis.Candles; // TACandle
using TechnicalAnalysis.Common; // RetCode, MAType, FuncUnstId, ...
using TechnicalAnalysis.Functions; // TAMath, TAFunc, SmaResult, ...The packages target net10.0 only. LangVersion is 14, nullable reference types are enabled, and
the candlestick API uses generic math (System.Numerics.IFloatingPoint<T>), which requires .NET 7
or later at minimum.
using TechnicalAnalysis.Functions;
double[] close = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
SmaResult sma = TAMath.Sma(0, close.Length - 1, close, timePeriod: 5);
Console.WriteLine($"RetCode={sma.RetCode} BegIdx={sma.BegIdx} NBElement={sma.NBElement}");
Console.WriteLine(string.Join(", ", sma.Real));Real output:
RetCode=Success BegIdx=4 NBElement=6
12, 13, 14, 15, 16, 17, 0, 0, 0, 0
Look at that second line carefully. sma.Real has 10 elements — the same length as the input —
but only the first 6 are real SMA values, and the first of them belongs to input bar 4, not
bar 0. The trailing four zeros are uninitialised slack.
That is the whole game. Read the next section before writing any trading logic.
Every TAMath.* and TACandle.* method returns a record deriving from
TechnicalAnalysis.Common.IndicatorResult:
public abstract record IndicatorResult
{
public RetCode RetCode { get; } // Success, BadParam, OutOfRangeStartIndex, OutOfRangeEndIndex, InternalError
public int BegIdx { get; } // index, in the INPUT array, of the first valid output element
public int NBElement { get; } // how many output elements are valid (note: capital B)
}SingleOutputResultaddspublic double[] Real { get; }.DualOutputResult/TripleOutputResultkeepReal0/Real1/Real2protected; the concrete record exposes named properties (MacdValue,RealUpperBand,SlowK, ...).CandleIndicatorResultaddspublic int[] Integers { get; }with values0,+100or-100.
TAMath allocates every output array as new double[endIdx - startIdx + 1] and fills it from
index 0. So:
Output element
kcorresponds to input barBegIdx + k, forkin[0, NBElement). Everything at index>= NBElementis meaningless.
Getting this wrong silently shifts every signal in time. It is the single most common correctness bug when consuming this library, and it never throws.
Eight daily bars, a 3-period SMA:
using TechnicalAnalysis.Functions;
DateTime[] dates =
[
new(2024, 1, 2), new(2024, 1, 3), new(2024, 1, 4), new(2024, 1, 5),
new(2024, 1, 8), new(2024, 1, 9), new(2024, 1, 10), new(2024, 1, 11)
];
double[] close = [100.0, 102.0, 101.0, 105.0, 107.0, 106.0, 110.0, 111.0];
SmaResult sma = TAMath.Sma(0, close.Length - 1, close, timePeriod: 3);
Console.WriteLine($"Real.Length={sma.Real.Length} RetCode={sma.RetCode} BegIdx={sma.BegIdx} NBElement={sma.NBElement}");
for (int k = 0; k < sma.NBElement; k++)
{
int bar = sma.BegIdx + k;
Console.WriteLine($"k={k} bar={bar} date={dates[bar]:yyyy-MM-dd} close={close[bar]} sma={sma.Real[k]}");
}Real output:
Real.Length=8 RetCode=Success BegIdx=2 NBElement=6
k=0 bar=2 date=2024-01-04 close=101 sma=101
k=1 bar=3 date=2024-01-05 close=105 sma=102.66666666666667
k=2 bar=4 date=2024-01-08 close=107 sma=104.33333333333333
k=3 bar=5 date=2024-01-09 close=106 sma=106
k=4 bar=6 date=2024-01-10 close=110 sma=107.66666666666667
k=5 bar=7 date=2024-01-11 close=111 sma=109
Cross-check the arithmetic by hand:
| Bar | Date | Close | Window | SMA(3) | Output slot |
|---|---|---|---|---|---|
| 0 | 2024-01-02 | 100 | — | warm-up | — |
| 1 | 2024-01-03 | 102 | — | warm-up | — |
| 2 | 2024-01-04 | 101 | (100 + 102 + 101) / 3 | 101 | Real[0] |
| 3 | 2024-01-05 | 105 | (102 + 101 + 105) / 3 | 102.666… | Real[1] |
| 4 | 2024-01-08 | 107 | (101 + 105 + 107) / 3 | 104.333… | Real[2] |
| 5 | 2024-01-09 | 106 | (105 + 107 + 106) / 3 | 106 | Real[3] |
| 6 | 2024-01-10 | 110 | (107 + 106 + 110) / 3 | 107.666… | Real[4] |
| 7 | 2024-01-11 | 111 | (106 + 110 + 111) / 3 | 109 | Real[5] |
The raw array is:
[101, 102.66666666666667, 104.33333333333333, 106, 107.66666666666667, 109, 0, 0]
|<------------------- NBElement = 6 valid elements, bars 2..7 ----------->| |<-->|
index >= NBElement:
padding, not data
BegIdx == 2 because a 3-period SMA needs 2 prior bars. You can ask for that number up front:
int lookback = TAFunc.SmaLookback(3); // => 2Every indicator has a matching TAFunc.<Name>Lookback(...). Measured values:
| Call | Lookback |
|---|---|
TAFunc.SmaLookback(3) |
2 |
TAFunc.EmaLookback(30) |
29 |
TAFunc.RsiLookback(14) |
14 |
TAFunc.AtrLookback(14) |
14 |
TAFunc.AdxLookback(14) |
27 |
TAFunc.MacdLookback(12, 26, 9) |
33 |
TAFunc.BollingerBandsLookback(20, MAType.Sma) |
19 |
TAFunc.StochLookback(5, 3, MAType.Sma, 3, MAType.Sma) |
8 |
TAFunc.CciLookback(20) |
19 |
TAFunc.ObvLookback() |
0 |
Charting libraries, joins and backtests all want an array parallel to the bar array. Write the projection once and never do it inline again:
using TechnicalAnalysis.Common;
using TechnicalAnalysis.Functions;
public static class IndicatorAlignment
{
/// <summary>
/// Projects a single-output indicator back onto the original bar index space,
/// padding the warm-up region with <see cref="double.NaN"/>.
/// </summary>
public static double[] AlignTo(this SingleOutputResult result, int barCount)
{
ArgumentNullException.ThrowIfNull(result);
double[] aligned = new double[barCount];
Array.Fill(aligned, double.NaN);
if (result.RetCode != RetCode.Success)
{
return aligned;
}
for (int k = 0; k < result.NBElement; k++)
{
aligned[result.BegIdx + k] = result.Real[k];
}
return aligned;
}
}double[] aligned = TAMath.Sma(0, close.Length - 1, close, timePeriod: 3).AlignTo(close.Length);
Console.WriteLine(string.Join(", ", aligned.Select(v => double.IsNaN(v) ? "NaN" : v.ToString("0.####"))));Real output:
NaN, NaN, 101, 102.6667, 104.3333, 106, 107.6667, 109
Now aligned[i] and close[i] refer to the same bar, and double.NaN marks "not computable yet"
so it can never be mistaken for a legitimate 0.0 reading — which matters enormously for
oscillators such as MACD or CCI that genuinely cross zero.
Prefer NaN padding over 0.0 padding; prefer both over silently reusing Real as if it were
bar-aligned.
This surprises everyone. startIdx is where you want output to begin, not where the data begins.
The function still reads bars before startIdx when they exist:
SmaResult subset = TAMath.Sma(2, 7, close, timePeriod: 3);
Console.WriteLine($"Real.Length={subset.Real.Length} BegIdx={subset.BegIdx} NBElement={subset.NBElement}");
Console.WriteLine(string.Join(", ", subset.Real));Real output:
Real.Length=6 BegIdx=2 NBElement=6
101, 102.66666666666667, 104.33333333333333, 106, 107.66666666666667, 109
Six outputs starting at bar 2 — including bar 2 itself, whose SMA needed bars 0 and 1. Contrast
with TAMath.Sma(0, 7, close, 3) from §3.2, which also produced BegIdx = 2: with startIdx = 0
the engine clamps startIdx up to the lookback; with startIdx = 2 it already has enough history.
Two consequences:
- Do not slice your price array to "save work". Slicing destroys the history the indicator
needs and moves
BegIdx. Pass the full array and setstartIdx/endIdxinstead. Real.Length == endIdx - startIdx + 1, always, whateverNBElementturns out to be.BegIdx == max(startIdx, lookback). It is an index into the input array, never an offset relative tostartIdx, and never less thanstartIdx.
SmaResult tooShort = TAMath.Sma(0, 1, [1.0, 2.0], timePeriod: 30);
Console.WriteLine($"{tooShort.RetCode} BegIdx={tooShort.BegIdx} NBElement={tooShort.NBElement}");
SmaResult badPeriod = TAMath.Sma(0, 7, close, timePeriod: 1);
Console.WriteLine($"{badPeriod.RetCode} NBElement={badPeriod.NBElement}");
SmaResult badStart = TAMath.Sma(-1, 5, close, timePeriod: 3);
Console.WriteLine(badStart.RetCode);Real output:
Success BegIdx=0 NBElement=0
BadParam NBElement=0
OutOfRangeStartIndex
Not enough data is Success with NBElement == 0, not an error. Always guard on both:
if (result.RetCode != RetCode.Success || result.NBElement == 0)
{
return; // nothing usable
}The full RetCode enum is small:
| Value | Numeric | Meaning |
|---|---|---|
RetCode.Success |
0 | Call completed. NBElement may still be 0. |
RetCode.BadParam |
2 | A period is out of range (valid range is 2..100000 for most), or an input array is null. |
RetCode.OutOfRangeStartIndex |
12 | startIdx < 0. |
RetCode.OutOfRangeEndIndex |
13 | endIdx < 0 or endIdx < startIdx. |
RetCode.InternalError |
5000 | Should not happen; report it. |
Real0 / Real1 / Real2 are protected. Use the named properties:
using TechnicalAnalysis.Common;
using TechnicalAnalysis.Functions;
double[] high = /* ... */;
double[] low = /* ... */;
double[] close = /* ... */;
int last = close.Length - 1;
MacdResult macd = TAMath.Macd(0, last, close,
optInFastPeriod: 12, optInSlowPeriod: 26, optInSignalPeriod: 9);
// macd.MacdValue, macd.MacdSignal, macd.MacdHist
BollingerBandsResult bb = TAMath.BollingerBands(0, last, close,
timePeriod: 20, nbDevUp: 2.0, nbDevDn: 2.0, maType: MAType.Sma);
// bb.RealUpperBand, bb.RealMiddleBand, bb.RealLowerBand
StochResult stoch = TAMath.Stoch(0, last, high, low, close,
fastKPeriod: 5, slowKPeriod: 3, slowKMAType: MAType.Sma,
slowDPeriod: 3, slowDMAType: MAType.Sma);
// stoch.SlowK, stoch.SlowDAll output arrays of one result share the same BegIdx and NBElement. You align them once.
A few results are int[] rather than double[]: MaxIndexResult.Integers, MinIndexResult.Integers,
HtTrendModeResult.Integers, and MinMaxIndexResult.MinIdx / .MaxIdx. See the
indicator reference for the exact output property names of all 159 entry
points.
Almost every TAMath method is declared twice — once with double[] inputs, once with float[]:
public static SmaResult Sma(int startIdx, int endIdx, double[] real, int timePeriod = 30);
public static SmaResult Sma(int startIdx, int endIdx, float[] real, int timePeriod = 30);The float[] overload is a thin adapter: it widens each input array to a freshly allocated
double[] and calls the double[] implementation. Therefore:
- Computation is always in
double. There is no reduced-precision code path. - Outputs are always
double[](orint[]), whatever the input type.SmaResult.Realisdouble[]even when you called thefloat[]overload. - The
float[]overload costs one extra array allocation and copy per input array.
Rules of thumb:
- Store and pass
double[]unless something outside your control hands youfloat[]. - Never round-trip
double[] -> float[] -> TAMathto "save memory"; you pay an allocation and lose precision for nothing. - If your source of truth is
decimal(an exchange API, a ledger), convert todoubleonce at the boundary forTAMath. ForTACandle, you do not need to convert at all — see the next section.
The float[] overload is not a compatibility shim you should feel bad about using; it exists so
that pipelines already holding float[] (GPU buffers, interop, compact caches) do not need a manual
conversion step. It just is not faster.
One sharp edge: because both overloads exist,
TAMath.Sma(0, 7, null, 3)does not compile —CS0121: the call is ambiguous. Cast thenull, or better, do not passnull(it returnsRetCode.BadParamanyway).
TACandle is generic over T : IFloatingPoint<T>:
public static CandleIndicatorResult CdlDoji<T>(
int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)
where T : IFloatingPoint<T>;double, float and decimal all satisfy that constraint (as do Half and NFloat), so a
decimal[] price series — the natural representation for exchange data — runs through pattern
recognition with no conversion:
using TechnicalAnalysis.Candles;
using TechnicalAnalysis.Common;
decimal[] open = [100m, 100m, 100m, 100m, 100m, 100m, 100m, 100m, 100m, 100m, 100m, 105m, 98m, 100m];
decimal[] high = [101m, 101m, 101m, 101m, 101m, 101m, 101m, 101m, 101m, 101m, 101m, 106m, 107m, 101m];
decimal[] low = [ 99m, 99m, 99m, 99m, 99m, 99m, 99m, 99m, 99m, 99m, 99m, 99m, 97m, 99m];
decimal[] close = [101m, 101m, 101m, 101m, 101m, 101m, 101m, 101m, 101m, 101m, 101m, 99m, 106m, 100m];
CandleIndicatorResult engulfing = TACandle.CdlEngulfing(0, close.Length - 1, open, high, low, close);
Console.WriteLine($"RetCode={engulfing.RetCode} BegIdx={engulfing.BegIdx} NBElement={engulfing.NBElement}");
for (int k = 0; k < engulfing.NBElement; k++)
{
if (engulfing.Integers[k] != 0)
{
Console.WriteLine($"bar {engulfing.BegIdx + k}: {engulfing.Integers[k]}");
}
}Real output:
RetCode=Success BegIdx=2 NBElement=12
bar 11: -100
bar 12: 100
Bar 11 is a bearish engulfing (-100), bar 12 a bullish one (+100). Running the identical series
as double[] and as float[] produced byte-identical Integers arrays.
Key facts:
-
CandleIndicatorResult.Integersisint[], and it obeys the sameBegIdx/NBElementcontract asReal.Integers.LengthisendIdx - startIdx + 1; onlyNBElemententries are valid, starting at input barBegIdx. -
0= pattern absent,+100= bullish,-100= bearish. Only the direction-aware patterns emit both signs —CdlEngulfingderives the sign from the candle colour. Single-direction patterns emit one sign only:Cdl2Crowsemits0/-100,CdlHammeremits0/+100, and non-directional patterns such asCdlDojiemit0/+100where+100just means "present". Test!= 0when you only care about presence. -
Seven patterns take an extra
T penetrationargument and also expose a no-penetration overload:CdlAbandonedBaby,CdlDarkCloudCover,CdlEveningDojiStar,CdlEveningStar,CdlMatHold,CdlMorningDojiStar,CdlMorningStar. Because the parameter isT, you write0.3mfordecimal[]inputs and0.3fordouble[]inputs:CandleIndicatorResult ms = TACandle.CdlMorningStar(0, close.Length - 1, open, high, low, close, 0.3m); CandleIndicatorResult msDefault = TACandle.CdlMorningStar(0, close.Length - 1, open, high, low, close);
-
Pattern lookbacks are driven by the global candle settings, not only by the bar geometry. A
CdlDojion the 14-bar series above returnedBegIdx = 10, becauseCandleSettingType.BodyDojiaverages the body over 10 bars by default. Short series legitimately produceNBElement == 0.
TACore (namespace TechnicalAnalysis) exposes a single process-wide GlobalsType instance with
three knobs.
Recursive indicators (EMA, RSI, ADX, ATR, the Hilbert Transform family, …) never fully forget their seed value. TA-Lib lets you discard an extra warm-up window so results converge to the "steady-state" value regardless of where your data starts.
using TechnicalAnalysis;
using TechnicalAnalysis.Common;
using TechnicalAnalysis.Functions;
Console.WriteLine(TACore.Globals.UnstablePeriod[FuncUnstId.Ema]); // 0 by default
Console.WriteLine(TAFunc.EmaLookback(30)); // 29
TACore.Globals.UnstablePeriod[FuncUnstId.Ema] = 100;
Console.WriteLine(TAFunc.EmaLookback(30)); // 129On a 200-bar series, TAMath.Ema(0, 199, prices, timePeriod: 30) moved from
BegIdx = 29, NBElement = 171 to BegIdx = 129, NBElement = 71 — the first 100 outputs were
discarded, and the ones that remain no longer depend on the arbitrary seed.
UnstablePeriod is a Dictionary<FuncUnstId, long> pre-populated with 0 for all 23 ids:
Adx, Adxr, Atr, Cmo, Dx, Ema, HtDcPeriod, HtDcPhase, HtPhasor, HtSine,
HtTrendline, HtTrendMode, Kama, Mama, Mfi, MinusDI, MinusDM, Natr, PlusDI,
PlusDM, Rsi, StochRsi, T3 (plus the sentinels FuncUnstNone = -1 and FuncUnstAll = 23,
which are not usable as keys — there is no "set them all" helper; loop over the ids you care
about).
When to raise it: when you compare against a chart whose history starts much earlier than yours, or
when you want two runs over different date ranges to agree on the overlap. Typical values are
50–250. The cost is a shorter output series.
TACore.Globals.Compatibility = Compatibility.Metastock; // default is Compatibility.DefaultCompatibility.Metastock seeds EMA with the first input value and shifts the RSI/CMO lookback by
one. Only switch if you are explicitly reproducing MetaStock output.
Compatibility.Default is meant to seed EMA with the simple average of the first timePeriod
values, as the C library does. It does not: TA_INT_EMA sums timePeriod - 1 values, divides by
timePeriod, and then applies one extra smoothing step, so
TAMath.Ema(0, 7, [100, 102, 101, 105, 107, 106, 110, 111], timePeriod: 3) returns Real[0] = 84.166667 where the C library returns 101. See §10.
Seeding differences decay geometrically but never vanish — see the TradingView parity caveats.
Candlestick detection thresholds are data, not constants:
using TechnicalAnalysis;
using TechnicalAnalysis.Common;
CandleSetting bodyDoji = TACore.Globals.CandleSettings[CandleSettingType.BodyDoji];
// RangeType=HighLow, AvgPeriod=10, Factor=0.1
TACore.Globals.CandleSettings[CandleSettingType.BodyDoji] =
new CandleSetting(CandleSettingType.BodyDoji, RangeType.HighLow, 10, 0.05);The eleven tunable settings are BodyLong, BodyVeryLong, BodyShort, BodyDoji, ShadowLong,
ShadowVeryLong, ShadowShort, ShadowVeryShort, Near, Far, Equal
(CandleSettingType.AllCandleSettings is a sentinel, not a key). CandleSetting.GetDefaultByType
and the CandleSetting.Default* static properties give you the stock values back.
There is no locking and no scoping. Set it once during start-up, before any indicator runs, and treat it as immutable afterwards. Mutating it from a request handler while other requests compute indicators produces non-deterministic results. If you need per-tenant settings, run separate processes.
TAMath is a friendly wrapper. TAFunc is the direct port, and it lets you own the output buffer —
useful in hot loops where you want to reuse arrays instead of allocating one per call:
using TechnicalAnalysis.Common;
using TechnicalAnalysis.Functions;
double[] close = [100.0, 102.0, 101.0, 105.0, 107.0, 106.0, 110.0, 111.0];
int outBegIdx = 0;
int outNBElement = 0;
double[] outReal = new double[close.Length];
RetCode rc = TAFunc.Sma(0, close.Length - 1, close, 3, ref outBegIdx, ref outNBElement, ref outReal);
Console.WriteLine($"rc={rc} outBegIdx={outBegIdx} outNBElement={outNBElement} outReal[0]={outReal[0]}");Real output:
rc=Success outBegIdx=2 outNBElement=6 outReal[0]=101
The in/ref signature is TAFunc.Sma(int startIdx, int endIdx, in double[] inReal, in int optInTimePeriod, ref int outBegIdx, ref int outNBElement, ref double[] outReal).
The same BegIdx / NBElement contract applies — outReal[k] is bar outBegIdx + k.
TAFunc also owns the *Lookback helpers used in §3.2.
1. Treating Real[i] as bar i. It is bar BegIdx + i. Always project through BegIdx.
2. Iterating to Real.Length instead of NBElement. The tail is zeros, and 0.0 is a perfectly
plausible value for MACD, CCI, ROC or a correlation. Loop to NBElement.
3. Treating RetCode.Success as "I have data". Insufficient history returns Success with
NBElement == 0. Guard on both.
4. Passing endIdx >= array.Length. The library validates index ordering and array nullness,
but not endIdx against the array length. TAMath.Sma(0, 100, eightBars, 3) throws
IndexOutOfRangeException. Always pass endIdx: prices.Length - 1.
5. Passing startIdx > endIdx. TAMath computes new double[endIdx - startIdx + 1], so a
reversed range throws OverflowException before any validation runs. Check your range first.
6. Slicing the price array to the window you care about. That deletes the lookback history and
changes the answer. Pass the whole array with a narrower startIdx.
7. Assuming float[] is cheaper. It allocates a double[] copy of every input array and
computes in double regardless.
8. Forgetting that unstable periods change BegIdx. After raising
TACore.Globals.UnstablePeriod[...], cached Lookback values and any hard-coded warm-up offsets are
wrong. Re-read BegIdx from the result.
9. Mutating TACore.Globals concurrently. See §7.4.
10. Comparing against a chart without matching the history window. Recursive indicators depend on how far back the data starts. Two "RSI(14)" series computed from different start dates differ slightly forever; that is mathematics, not a bug. See the TradingView parity caveats.
11. Reusing a result record after mutating the input array. Results hold references to arrays they allocated, but the inputs are read at call time. Recompute after any input change; there is no incremental/streaming API.
12. Assuming candlestick patterns work on tiny series. Pattern lookbacks include the
CandleSettings averaging window (10 bars for BodyDoji, for example). Feed at least ~30 bars.
13. Assuming every indicator is correct. Three of them are not, and they fail quietly rather
than loudly: TAMath.Atr diverges to +∞, the EMA family seeds low, and TAMath.Rsi returns
NaN for a flat series — all with RetCode.Success. See §10 before
you build on any of them.
14. Charting a value without checking double.IsFinite. Success and a non-zero NBElement
do not imply a finite number. One NaN or ∞ in a series poisons every aggregate computed from it,
and the two defects above both produce one.
These are bugs in src/TechnicalAnalysis.Functions, not in your code. They are listed here because
they are reachable from the five-line example at the top of this guide, and because a wrong number
that looks plausible costs more than a crash.
| Defect | What you get | Location |
|---|---|---|
Atr never divides its running average |
TAMath.Atr grows by a factor of period - 1 on every bar after the second output. On a series whose true range is exactly 2.0 every bar, Atr(…, 14) returns 2, 2, 26.142857, 340, 4420.142857, 57462, …; on a 1 500-bar series it is +∞ from bar 300 onwards. Natr is not affected |
src/TechnicalAnalysis.Functions/Atr/TAFunc.cs, main output loop |
TA_INT_EMA seeds itself low |
The seed sums period - 1 values and divides by period, then smooths once more, so TAMath.Ema over a constant series of 100 with timePeriod: 20 returns 95.476190 instead of 100. The error decays with the smoothing factor, so it distorts the bars just after warm-up rather than the steady state. Affects Ema, Macd, MacdExt, MacdFix, Dema, Tema, T3, Apo, Ppo, Trix |
src/TechnicalAnalysis.Functions/TAFunc.cs, TA_INT_EMA seed loop |
Rsi returns NaN on a perfectly flat series |
RetCode.Success, a non-zero NBElement, and every element NaN — there is no zero guard on prevGain + prevLoss. Trigger: a halted instrument, or any window with no price change |
src/TechnicalAnalysis.Functions/Rsi/TAFunc.cs |
Two habits make these survivable, and they are worth having regardless:
// 1. Treat a non-finite output as "no value", never as a price.
if (!double.IsFinite(result.Real[k]))
{
continue;
}For ATR specifically, Wilder smoothing over TAMath.TrueRange is a dozen lines and is correct
today; the TradingView guide has the
snippet. Raising the unstable period (§7.1) discards the bars the EMA seed contaminates, but does
not help ATR — the divergence is unbounded, not transient.
The backtesting, real-time streaming and TradingView guides show what each one does to a running system.
- Complete indicator reference — all 159 entry points with signatures, parameters, defaults, outputs and links to the generated API pages.
- TradingView integration — Pine Script mapping, parity caveats, serving series to a chart, consuming webhook alerts.
Atypical.TechnicalAnalysis.FunctionsAPI referenceAtypical.TechnicalAnalysis.CandlesAPI referenceAtypical.TechnicalAnalysis.CommonAPI reference