Skip to content

Migration from v2 to v3 ​

This guide provides a comprehensive migration path from v2 to v3 of the Stock Indicators library. It includes all technical changes to the public API, syntax changes, and specific examples of deprecated and breaking changes.

Yes, there are minor breaking changes, but ...

Most of the deprecated v2 syntax has been shimmed in library version 3.0 with [Obsolete] code analysis warning flags to aid migrations. These shims are (or will be) removed in version 3.1, so start with 3.0.1 or later before upgrading further.

Why 3.0.1 and not 3.0.0? — defects in the v2 shims

3.0.0 shipped several [Obsolete] shims that did not faithfully reproduce the v2 methods they stand in for. All are corrected in 3.0.1. None of them affect the ToX() methods, only the deprecated GetX() names.

Two returned different values than v2 did. If you ran either on 3.0.0, stored results — and any thresholds tuned against them — need revalidating:

  • GetPrs() returned the inverted ratio. It passed its two series in the wrong order, returning base / eval, the reciprocal of the ratio documented for PRS. 3.0.1 returns eval / base, matching ToPrs() and v2.
  • GetPvo() used the wrong default periods. It declared fastPeriods: 9, slowPeriods: 12 where v2 and ToPvo() declare 12 and 26, so an argument-less call silently computed a differently-parameterized PVO. Callers who passed explicit periods were unaffected.

The rest rejected calls that v2 accepted — these failed loudly at build or run time, never with quietly wrong values:

  • GetKeltner() rejected a fractional multiplier. It declared int multiplier where v2 declares double, so quotes.GetKeltner(20, 2.5, 10) would not compile. Do not round the multiplier to work around it; that silently changes your channel widths. Upgrade to 3.0.1, or move to ToKeltner(), which always accepted these values.
  • GetStdDevChannels() rejected a null lookback through its tuple overload, declaring int lookbackPeriods where v2 declares int?, whose null means "use the whole series". Do not substitute a fixed lookback; that computes a different channel. The bar overload of the same method still accepts null — and its own deprecation message points you there — so the two forms disagreed. Upgrade to 3.0.1, or move to ToStdDevChannels().
  • GetPrs() with no lookbackPeriods threw. The unspecified lookback mapped to 0, which validation rejects, so the shim's own default raised ArgumentOutOfRangeException. 3.0.1 computes with a null PrsPercent, as v2 did.
  • GetPrs() and GetRoc() tuple overloads silently ignored smaPeriods. They still accept and discard the argument — v3 has no PrsSma/RocSma field — but as of 3.0.1 their deprecation messages say so.

None of these fixes reach you by dropping the 3.0.1 assembly next to code compiled against 3.0.0 — recompile. GetKeltner() and GetStdDevChannels() changed parameter types, so existing call sites no longer bind to a method that exists. And GetPvo()'s corrected defaults are baked into the caller's IL at compile time, so an argument-less call keeps computing 9/12 until it is rebuilt. Recompiling from source requires no edits.

Summary of breaking changes ​

We've renamed the package from Skender.Stock.Indicators to FacioQuo.Stock.Indicators, so you'll have to search and replace your usings statements.

API method naming ​

All static time-series API methods: Renamed from GetX() to ToX()

Example: GetSma(...) → ToSma(...)

Market-data type renames (Quote → Bar) ​

To align with industry-standard terminology (an OHLCV aggregate is universally a bar; a quote is a bid/ask snapshot), the core market-data types were renamed in v3. The old names remain as deprecated aliases during a migration window, so existing code keeps working — update to the new names as the deprecation warnings guide you:

  • Quote → Bar (and the built-in Quote record → Bar record)
  • IQuote → IBar — custom market-data types now implement IBar
  • PeriodSize → BarInterval (aggregation interval enum; member names unchanged)

The old Quote, IQuote, PeriodSize, IReusableResult, and BasicData names remain as warning-level [Obsolete] aliases of the new types, so existing code keeps compiling and running while deprecation warnings guide each rename. Quote/IQuote flow through the new generic API directly, and PeriodSize keeps working via obsolete Aggregate(PeriodSize)/GetPivotPoints(PeriodSize) forwarding overloads. Migrate at your own pace — and finish related member renames such as Date → Timestamp to clear all warnings. These shims will be removed in a future major version.

New: BarInterval now has a bidirectional string-code map — interval.ToCode() (e.g. BarInterval.FiveMinutes → "5m") and "5m".ToBarInterval() (case-insensitive, with aliases like "5min"/"1day").

Quote/Bar interface details ​

  • Bar type (formerly Quote): Immutable record type
  • IBar.Date property: Renamed to IBar.Timestamp. Date remains as an [Obsolete] alias in v3.x for backward compatibility and will be removed in v3.1 — update consumers to Timestamp now.
  • IBar interface (formerly IQuote): Now a reusable (chainable) type
  • Custom bar types: Must implement the IReusable interface
  • IReusableResult: Renamed to IReusable
  • IReusable.Value property: Changed to non-nullable, returns double.NaN instead of null

Indicator return types ​

  • All indicator results: Changed from sealed class to immutable record types
  • BasicData type: Renamed to TimeValue
  • AtrStopResult values: Changed from decimal to double
  • UlcerIndexResult.UI property: Renamed to UlcerIndex
  • SmaAnalysis model: Renamed to SmaAnalysisResult

Removed features ​

  • GetBaseQuote() indicator: Removed — use the Use(CandlePart) utility instead
  • SyncSeries() utility: Removed along with SyncType enum
  • Find() and FindIndex() utilities: Deprecated
  • ToTupleCollection() utility: Deprecated
  • ToCollection() utility: Deprecated
  • PrsResult.PrsSma and RocResult.RocSma: Removed. v2 returned a moving average alongside these indicators; v3 has no such field. Chain results.ToSma(smaPeriods) onto ToPrs() or ToRoc() instead.

Other changes ​

  • Indicator method parameters: v2 generic signatures like GetSma<TQuote>(this IEnumerable<TQuote>) are now interface-typed, like ToSma(this IReadOnlyList<IReusable>) and ToFractal(this IReadOnlyList<IBar>). Concrete lists (e.g. List<Bar>) convert automatically; your own generic wrapper methods need a class constraint (see Step 4)
  • Use() method: candlePart parameter now required (no default)
  • Use() return type: Now returns chainable TimeValue instead of tuple
  • Numerix class: Renamed to Numerical
  • Internal signals: Deprecated for several indicators
  • GetX tuple interfaces: Deprecated
  • Minor ADXR calculation correction to fix a one-period shift in the lookback period

Quick reference table ​

v2 APIv3 APINotes
quotes.GetSma(20)quotes.ToSma(20)Method prefix changed
QuoteBarType renamed (OHLCV bar)
IQuoteIBarInterface renamed
PeriodSizeBarIntervalEnum renamed
IBar.DateIBar.TimestampProperty renamed
quotes.Use()quotes.Use(CandlePart.Close)Parameter now required
result.Value == nulldouble.IsNaN(result.Value)Null handling changed
NumerixNumericalClass renamed
BasicDataTimeValueType renamed
SmaAnalysisSmaAnalysisResultType renamed
UlcerIndexResult.UIUlcerIndexResult.UlcerIndexProperty renamed
SyncSeries()(removed)Use manual alignment
Find() / FindIndex()LINQ methodsUse .FirstOrDefault() etc.
GetBaseQuote()Use(CandlePart)Use utility instead

Migration steps ​

This section is for those of you who need a more detailed walk-through for migration.

Step 1: Update method names ​

Replace all GetX() method calls with ToX():

csharp
var smaResults = quotes.GetSma(20); 
var smaResults = quotes.ToSma(20);  

Step 2: Update IBar property names ​

Rename Date to Timestamp in all custom bar classes (and implement IBar in place of the obsolete IQuote):

csharp
public class MyBar : IQuote
public class MyBar : IBar
{
    public DateTime Date { get; set; }      
    public DateTime Timestamp { get; set; } 
    public decimal Open { get; set; }
    public decimal High { get; set; }
    public decimal Low { get; set; }
    public decimal Close { get; set; }
    public decimal Volume { get; set; }
}

Step 3: Update custom bar types ​

If you had a custom quote type, change it to a record type and derive from IBar to replace IQuote.

csharp
// v2
public class MyQuote : IQuote
{
    // properties...

    [JsonIgnore]
    public double Value => (double)Close;
}

// v3 - option 1: use record
public record MyBar : IBar
{
    // properties...
}

// v3 - option 2: implement value-based equality
public class MyBar : IBar, IEquatable<MyBar>
{
    // properties...
    
    // implement value-based equality
    public override bool Equals(object obj) { /* ... */ }
    public override int GetHashCode() { /* ... */ }
}

Step 4: Update generic extension methods ​

v2 indicator methods were generic, e.g. GetSma<TQuote>(...) where TQuote : IQuote. In v3, they instead accept interface-typed lists — IReadOnlyList<IBar> for bar-based indicators and IReadOnlyList<IReusable> for chainable ones. Calls on concrete collections like List<Bar> work unchanged through covariance.

If you wrote your own generic extension methods over bars, add a class constraint. Covariance (IReadOnlyList<TBar> → IReadOnlyList<IBar>) only applies when the compiler knows TBar is a reference type:

csharp
public static double MyVolumeSma<TBar>(
    this IReadOnlyList<TBar> bars,
    int lookbackPeriods)
    where TBar : IBar
    where TBar : class, IBar
{
    IReadOnlyList<SmaResult> results = bars
        .Use(CandlePart.Volume)
        .ToSma(lookbackPeriods);
    // ...
}

Additional interface constraints still compose normally, e.g. where TBar : class, IBar, IMyOther. Since all practical IBar implementations are reference types (record or class), the added constraint does not limit usage.

Step 5: Update Use() method calls ​

Add explicit candlePart parameter:

csharp
// v2 - candlePart defaulted to Close
var quoteParts = quotes.Use();

// v3 - candlePart required
var barParts = bars.Use(CandlePart.Close);

Handle new TimeValue return type:

csharp
// v2
var (timestamp, value) = quotes.Use(CandlePart.Close);

// v3
IReadOnlyList<TimeValue> barParts = bars.Use(CandlePart.Close);

Step 6: Value is now non-nullable ​

The chainable IReusable.Value property changed from double? to double, returning double.NaN instead of null. This property exists mainly for internal chaining, so most code is unaffected — named result properties such as SmaResult.Sma remain nullable double?. If you do read .Value directly, replace result.Value == null checks with double.IsNaN(result.Value).

Step 7: Update class references ​

  • Numerix → Numerical
  • BasicData → TimeValue
  • SmaAnalysis → SmaAnalysisResult
  • UlcerIndexResult.UI → UlcerIndexResult.UlcerIndex

Step 8: Remove deprecated utilities ​

Replace or remove calls to:

  • SyncSeries() - manually align data instead
  • Find() - use LINQ .FirstOrDefault()
  • FindIndex() - use LINQ .Select((item, index) => ...) with .FirstOrDefault()
  • GetBaseQuote() - use Use(CandlePart) utility instead

Step 9: Update ADXR expectations ​

If using ADXR, expect slight changes in values and timing:

csharp
// v2 - first ADXR at index 40 (for lookbackPeriods=14)
var adxResults = quotes.GetAdx(14);
var firstAdxr = adxResults[40].Adxr; // not null

// v3 - first ADXR at index 41
var adxResults = bars.ToAdx(14);
var firstAdxr = adxResults[41].Adxr; // not null
// adxResults[40].Adxr is now null

New v3 feature: streaming capabilities ​

v3 adds incremental and real-time processing. Alongside the v2 Series (batch) style, most indicators now also support two streaming styles:

  • BufferList — self-managed incremental updates, ideal for growing datasets.
  • StreamHub — reactive, observable hubs with cascading updates for live feeds.

See the Indicator styles guide for a full feature comparison and detailed usage. The examples below show the v2→v3 transition for each style.

Optional migrations

These migrations to streaming style indicators are only appropriate if you have advanced incremental or live-streaming uses cases.

Our time series (batch) style indicators are still the best choice for processing of complete historical OHLCV aggregate price datasets, and are functionally unchanged from v2.

Migration examples ​

From v2 Series to v3 BufferList ​

If you were building up results incrementally in v2, you can now use BufferList for better performance:

csharp
// v2 approach (inefficient for incremental updates)
List<Quote> quotes = new();
foreach (Quote newQuote in stream)
{
    quotes.Add(newQuote);
    var results = quotes.ToSma(20);  // Recalculates everything!
    // Use results...
}

// v3 BufferList (efficient incremental updates)
SmaList smaList = new(20);
foreach (Bar newBar in stream)
{
    smaList.Add(newBar);
    
    // Note: smaList[^1] throws ArgumentOutOfRangeException if empty
    if (smaList.Count > 0)
    {
        SmaResult latest = smaList[^1];
        // Use latest...
    }
}

From v2 Series to v3 StreamHub ​

If you need to coordinate multiple indicators with live data:

csharp
// v2 approach (requires maintaining separate lists)
List<Quote> quotes = new();
foreach (Quote newQuote in stream)
{
    quotes.Add(newQuote);
    var smaResults = quotes.ToSma(20);
    var rsiResults = quotes.ToRsi(14);
    var macdResults = quotes.ToMacd();
    // Process results...
}

// v3 StreamHub (coordinated real-time updates)
BarHub barHub = new();
SmaHub smaHub = barHub.ToSmaHub(20);
RsiHub rsiHub = barHub.ToRsiHub(14);
MacdHub macdHub = barHub.ToMacdHub();

foreach (Bar newBar in stream)
{
    barHub.Add(newBar);  // Single update propagates to all observers
    // Access latest results from each hub
}

Streaming documentation ​

For indicator-specific streaming examples, see the documentation for each indicator. Indicators with streaming support include a "Streaming" section with BufferList and StreamHub examples.

Popular indicators with complete streaming documentation:

Need help? ​