---
url: /migration/v3.md
description: >-
  Guide for migrating from Stock Indicators v2 to v3, including breaking
  changes, API updates, and new streaming capabilities.
package: FacioQuo.Stock.Indicators
docs_version: v3
canonical: https://dotnet.stockindicators.dev/migration/v3
generated: 2026-09-30T04:41:06.055Z
commit: ae1f432eaa0b03ae9f39e032c69434b557efa4a2
---

# 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.

> [!NOTE]
> **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](/indicators/prs.md). `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](#step-4-update-generic-extension-methods))
* **`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 API | v3 API | Notes |
| ------ | ------ | ----- |
| `quotes.GetSma(20)` | `quotes.ToSma(20)` | Method prefix changed |
| `Quote` | `Bar` | Type renamed (OHLCV bar) |
| `IQuote` | `IBar` | Interface renamed |
| `PeriodSize` | `BarInterval` | Enum renamed |
| `IBar.Date` | `IBar.Timestamp` | Property renamed |
| `quotes.Use()` | `quotes.Use(CandlePart.Close)` | Parameter now required |
| `result.Value == null` | `double.IsNaN(result.Value)` | Null handling changed |
| `Numerix` | `Numerical` | Class renamed |
| `BasicData` | `TimeValue` | Type renamed |
| `SmaAnalysis` | `SmaAnalysisResult` | Type renamed |
| `UlcerIndexResult.UI` | `UlcerIndexResult.UlcerIndex` | Property renamed |
| `SyncSeries()` | (removed) | Use manual alignment |
| `Find()` / `FindIndex()` | LINQ methods | Use `.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); // removed
var smaResults = quotes.ToSma(20);  // added
```

### 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                 // removed
public class MyBar : IBar                   // added
{
    public DateTime Date { get; set; }      // removed
    public DateTime Timestamp { get; set; } // added
    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         // removed
    where TBar : class, IBar  // added
{
    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](/guide/styles.md) for a full feature comparison and detailed usage. The examples below show the v2→v3 transition for each style.

> [!CAUTION]
> **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:

* Moving Averages: [SMA](/indicators/sma.md), [EMA](/indicators/ema.md), [WMA](/indicators/wma.md)
* Oscillators: [RSI](/indicators/rsi.md), [MACD](/indicators/macd.md), [Stochastic](/indicators/stoch.md)
* Channels: [Bollinger Bands](/indicators/bollinger-bands.md), [Keltner](/indicators/keltner.md)

## Need help?

* [Guide and Pro tips](/guide/getting-started.md) - Getting started with v3
* [Indicators](/indicators.md) - Indicator-specific documentation
* [GitHub Discussions](https://github.com/facioquo/stock-indicators-dotnet/discussions) - Ask questions and share ideas
* [GitHub Issues](https://github.com/facioquo/stock-indicators-dotnet/issues) - Report bugs or request features
