CandleIndicators 1.2.0
dotnet add package CandleIndicators --version 1.2.0
NuGet\Install-Package CandleIndicators -Version 1.2.0
<PackageReference Include="CandleIndicators" Version="1.2.0" />
<PackageVersion Include="CandleIndicators" Version="1.2.0" />
<PackageReference Include="CandleIndicators" />
paket add CandleIndicators --version 1.2.0
#r "nuget: CandleIndicators, 1.2.0"
#:package CandleIndicators@1.2.0
#addin nuget:?package=CandleIndicators&version=1.2.0
#tool nuget:?package=CandleIndicators&version=1.2.0
CandleIndicators
A focused, dependency-free .NET library of technical indicators computed from plain OHLCV candles — trend, momentum, volatility, volume, and candlestick-pattern indicators.
It deliberately does one thing: correct, well-tested arithmetic. It does not hand you a pre-opinionated buy/sell score, a confidence weight, or a "this indicator is better than that one" verdict. Every indicator returns the real numbers it computes — the actual MACD line and signal line, not just the histogram; the actual upper/middle/lower Bollinger Bands, not just a bandwidth percentage — so you can build whatever signal logic fits your own strategy on top, instead of inheriting someone else's untested thresholds baked into the library.
It has no dependencies beyond the .NET base class library. Reference it as a normal package, or copy the files you need directly into your project.
Design
Every Compute method is a static call that returns a nullable result. No classes to
construct, no interfaces to implement, no DI container required — Rsi.Compute(candles) either
gives you a value or null when there isn't enough data yet. null always means "not enough
data," never a disguised zero.
Multi-part indicators expose every real part. MACD returns its line, signal, and histogram. Bollinger Bands returns the upper, middle, and lower bands plus %B and bandwidth. ADX returns +DI and -DI alongside ADX itself. Stochastic returns %K and %D. If the underlying calculation produces a number, you can get it.
No directional score, no weight, no "this indicator matters more than that one." Turning a raw indicator value into a trading signal — what threshold counts as a signal, how much to trust it, how to combine it with other indicators — is a strategy decision, not a fact about the indicator. Different strategies, instruments, and timeframes will reasonably disagree about it. This library stays out of that decision entirely and gives you the honest numbers instead.
Install
Reference the project directly, or build and reference the package:
dotnet add reference path/to/CandleIndicators.csproj
or pack it and add it as a local NuGet feed:
dotnet pack src/CandleIndicators -o ./nupkg
dotnet add package CandleIndicators --source ./nupkg
Usage
All indicators take an IReadOnlyList<Candle> in chronological order:
using CandleIndicators;
public record Candle(
string Symbol, DateTime Timestamp,
decimal Open, decimal High, decimal Low, decimal Close, long Volume,
string Timeframe = "1D"); // this is the exact shape CandleIndicators.Candle already is
Trend
using CandleIndicators.Trend;
double? sma20 = Sma.Compute(candles, period: 20);
double? ema20 = Ema.Compute(candles, period: 20);
// The whole EMA series, aligned one-to-one with the candles and null through warm-up.
// Useful when an EMA is an input to something else rather than an output in its own right.
IReadOnlyList<double?>? emaSeries = Ema.ComputeSeries(candles, period: 20);
IReadOnlyList<double?>? ofAnything = Ema.ComputeSeries(mySeriesOfDoubles, period: 9);
MacdResult? macd = Macd.Compute(candles); // fastPeriod=12, slowPeriod=26, signalPeriod=9 by default
if (macd is not null)
Console.WriteLine($"MACD={macd.MacdLine:F2} Signal={macd.Signal:F2} Histogram={macd.Histogram:F2}");
AdxResult? adx = Adx.Compute(candles, period: 14);
bool trending = adx is { Adx: > 25 };
SupertrendResult? st = Supertrend.Compute(candles, period: 10, multiplier: 3.0);
bool bullish = st?.IsBullish ?? false;
ParabolicSarResult? psar = ParabolicSar.Compute(candles, step: 0.02, maxStep: 0.2);
// Senkou Span A/B are traditionally plotted `kijunPeriod` candles ahead, and Chikou Span the
// same number of candles behind — that display shift is left to you; this returns the current
// computed value of each line.
IchimokuResult? ichimoku = Ichimoku.Compute(candles, tenkanPeriod: 9, kijunPeriod: 26, senkouBPeriod: 52);
Momentum
using CandleIndicators.Momentum;
double? rsi = Rsi.Compute(candles, period: 14); // 0-100
double? cci = Cci.Compute(candles, period: 20); // unbounded, ±100 is a common reference
double? mfi = Mfi.Compute(candles, period: 14); // 0-100, volume-weighted RSI
double? roc = Roc.Compute(candles, period: 12); // % change over `period` bars
StochasticResult? stoch = Stochastic.Compute(candles, kPeriod: 14, dPeriod: 3);
double? williamsR = WilliamsR.Compute(candles, period: 14); // -100 to 0
Volatility
using CandleIndicators.Volatility;
double? atr = Atr.Compute(candles, period: 14); // absolute price units — handy for stop distances
BollingerBandsResult? bb = BollingerBands.Compute(candles, period: 20, stdDevMultiplier: 2.0);
if (bb is not null)
Console.WriteLine($"Upper={bb.Upper:F2} Mid={bb.Middle:F2} Lower={bb.Lower:F2} %B={bb.PercentB:F2}");
double? hv = HistoricalVolatility.Compute(candles, period: 20); // annualized %, 252 trading days/year by default
// Keltner Channels: an EMA middle line with ATR-based bands — composed directly from
// Ema and Atr above, so it's built on the same tested primitives, not a re-implementation.
KeltnerChannelsResult? kc = KeltnerChannels.Compute(candles, emaPeriod: 20, atrPeriod: 10, multiplier: 2.0);
Volume
using CandleIndicators.Volume;
double? cmf = Cmf.Compute(candles, period: 20); // -1 to +1
double? volRatio = VolumeRatio.Compute(candles, period: 20); // 1.0 = exactly average volume
double? vwap = Vwap.Compute(candles); // session VWAP; null on daily candles
// OBV and the A/D Line are both running series, not point values — you get the
// whole thing and derive your own slope, moving average, or price divergence check.
IReadOnlyList<double>? obv = Obv.Compute(candles);
double? obvSlope = obv is { Count: >= 20 }
? (obv[^1] - obv[^20]) / 20.0
: null;
IReadOnlyList<double>? adLine = AccumulationDistribution.Compute(candles);
Patterns and structure
using CandleIndicators.Pattern;
using CandleIndicators.Structure;
PatternMatch? pattern = CandlestickPatterns.Compute(candles);
if (pattern is not null)
Console.WriteLine($"{pattern.Name} ({(pattern.Direction > 0 ? "bullish" : "bearish")})");
PivotPointsResult? pivots = PivotPoints.Compute(candles); // standard method, from the prior trading day's OHLC
DonchianBreakoutResult? donchian = DonchianBreakout.Compute(candles, period: 20);
if (donchian is { IsBreakoutAbove: true })
Console.WriteLine($"Broke above {donchian.UpperChannel:F2} on {donchian.VolumeRatio:F1}x average volume");
Overriding the built-in thresholds
Some numbers in an indicator come from a published definition; others were simply picked by whoever wrote the code. This library exposes the picked ones so you are never stuck with a judgement call someone else made.
Candlestick pattern shapes. Patterns are defined qualitatively — "a small body with a long lower
wick" — so every implementation has to invent numbers for "small" and "long". Ours are in
PatternThresholds, and traders legitimately disagree about how strict to be:
using CandleIndicators.Pattern;
// Only accept a hammer whose lower wick is at least three times the body.
var strict = PatternThresholds.Default with { WickToBodyRatio = 3.0 };
PatternMatch? match = CandlestickPatterns.Compute(candles, strict);
| Threshold | Default | What it controls |
|---|---|---|
WickToBodyRatio |
2.0 | How many times the body a wick must exceed to count as "long" (Hammer, Shooting Star) |
EngulfingBodyRatio |
1.1 | How much bigger the current body must be than the prior one to engulf it |
StarBodyToRangeRatio |
0.3 | Largest body-to-range fraction the middle Morning Star candle may have |
DojiBodyToRangeRatio |
0.1 | Largest body-to-range fraction that still counts as a Doji |
DojiWickAsymmetryRatio |
2.0 | How lopsided a Doji's wicks must be to read as Gravestone/Dragonfly rather than symmetric |
CCI's scaling constant. Lambert's 0.015 exists only to put CCI on a scale where roughly 70–80% of readings fall inside the conventional ±100 band. Changing it rescales the output, which also moves where those familiar levels sit:
double? cci = Cci.Compute(candles, period: 20, scalingConstant: 0.015);
VWAP's daily-timeframe labels. Timeframe strings are a vendor convention with no standard, so
the built-in list can never be exhaustive. A feed that labels daily bars "EOD" would otherwise get
a number that looks like a VWAP but is really just one bar's typical price. Extend the defaults
rather than replacing them:
var labels = new HashSet<string>(Vwap.DailyOrCoarserTimeframes, StringComparer.OrdinalIgnoreCase)
{
"EOD", "1Dy",
};
double? vwap = Vwap.Compute(candles, labels);
Comparison is case-insensitive whatever comparer your own set uses, and passing an empty set opts out of the timeframe check entirely.
Three things that apply to all of the above:
- Existing calls are unaffected. These are overloads and optional parameters with unchanged defaults, so code written against the earlier API compiles and returns the identical value. There are tests asserting that equivalence.
- Thresholds are validated on use. A non-finite value would make every comparison against it false and silently disable the patterns that depend on it, so it throws instead. A body-to-range ratio above 1 also throws — a body cannot exceed its own candle's range, and such a threshold would classify every candle as a Doji.
Defaultis immutable and there is no settable global. A mutable static would let one part of a program silently change what another part measures. Hold your ownstatic readonlyfield if you want one configuration application-wide, and pass it.
What is deliberately not configurable
Not every number is a knob, and exposing the rest would imply a choice where none exists.
PivotPoints' 2 × is the standard formula (R1 = 2·PP − L); the halving in Ichimoku and
Supertrend is the definition of a midpoint; BollingerBands' 0.5 is a degenerate-window
convention. Periods, standard-deviation multipliers, ATR multipliers and Parabolic SAR's
step/maximum were already ordinary parameters.
How much of this has actually been verified
Correct arithmetic is the only thing this library claims, so it seems fair to say how far that claim has been checked rather than asserting it.
As of 1.2.0, 24 of the 25 indicators have been compared against independently written reference
implementations of their published definitions — 7,420 computed values, evaluated at every
prefix length of a 140-bar series so warm-up boundaries were checked alongside the arithmetic.
Every indicator listed above now agrees exactly (to 1e-9) with the canonical definition, including
the two the pass found wrong and fixed (Macd carried a second, differently seeded EMA;
Supertrend's bands could never reset). The changelog has the measured detail.
Two things that pass did not settle, stated plainly:
CandlestickPatternsis not verified this way. Qualitative shape rules ("a small body with a long lower wick") have no reference implementation to diff against. It has targeted tests from 1.0.1; it does not have this.- Multi-field indicators withhold data they already have. Where a result record has several
fields, all of them wait for the slowest — the fields are non-nullable, so a partial result
would have to invent the missing values.
Ichimokuis the expensive case: nothing is returned before bar 52, though Tenkan-sen is computable at bar 9 and Chikou Span (just the current close) at bar 1. That is a deliberate trade, documented in each class, not an accident — but if you need Ichimoku's fast lines early you will need to compute the midpoints yourself.
What this library does not do
- It does not turn any of these numbers into a trading signal. No thresholds, no scoring, no "buy when RSI crosses 30" logic — that judgment call belongs to your strategy, not this library.
- It does not include anything that isn't computable from plain OHLCV candles. Indicators that need external data feeds — index-VIX levels, put/call ratios from an options chain, FII/DII institutional flow data, exchange-published delivery percentages — aren't in scope here, because they can't be computed from candles alone and the data-source plumbing they'd need is inherently specific to whichever market and vendor you're pulling it from.
- It does not fetch data, cache anything, or know about symbols, sessions, or exchanges beyond
what's needed for VWAP's and Pivot Points' session/prior-day logic, which operates purely on the
Timestampvalues you provide. - It is not a ranking of which indicators are "good." Some of these (Williams %R vs. Stochastic, for instance) are near-duplicates of each other by construction. That's included deliberately — which one is more useful for a given purpose depends on the purpose.
Provenance
Most of this library was extracted and redesigned from a private trading-research platform's
internal indicator library. The original returned each indicator wrapped in that platform's own
composite-scoring opinions — a hand-tuned weight and a pre-baked directional score specific to
its own strategy. None of that traveled here: every indicator was rebuilt around its plain,
correct arithmetic, several were extended to expose real values the original silently discarded
(MACD's signal line, Bollinger's actual bands, ADX's ±DI), and every indicator has real unit
tests with hand-verified worked arithmetic behind it — most of them had none before this package
existed. SMA, Keltner Channels, the Accumulation/Distribution Line, Parabolic SAR, and Ichimoku
Kinko Hyo were not part of the original platform and were written directly for this package, to
round out gaps in the standard indicator set (Keltner Channels is composed directly from this
library's own Ema and Atr).
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.