Skip to content

Creating custom indicators

At some point in your journey, you may want to create your own custom indicators. The following guide shows you how to create custom indicators that work seamlessly with this library.

🚩 Series (batch) style only

Custom indicators are currently supported for the Batch (Series) style only. Creating custom Buffer list or Stream hub indicators is not yet supported as a first-class extension point; support is planned for a future release (buffer lists: #2096, stream hubs: #2097). To integrate custom logic with streaming data today, see Custom observers.

✨ Working example code is available in the CustomIndicatorsLibrary project.

Creating a custom indicator

Step 1: Create the result class

Create your results class by implementing the IReusable interface or inheriting from existing result patterns. This allows your custom indicator to be chainable with other indicators.

csharp
using FacioQuo.Stock.Indicators;

namespace Custom.Indicators;

// Custom results class
public record AtrWmaResult : IReusable
{
    public DateTime Timestamp { get; init; }
    public double? AtrWma { get; init; }
    
    // Identify value to propagate in chains
    double IReusable.Value => AtrWma.Null2NaN();
}

Step 2: Create your custom indicator

Create your custom algorithm following the same patterns as the main library.

csharp
using FacioQuo.Stock.Indicators;

namespace Custom.Indicators;

public static class CustomIndicators
{
    /// <summary>
    /// ATR-weighted moving average (custom indicator example)
    /// </summary>
    /// <param name="bars">Historical price bars</param>
    /// <param name="lookbackPeriods">Lookback period</param>
    /// <returns>Collection of AtrWmaResult</returns>
    public static IReadOnlyList<AtrWmaResult> ToAtrWma(
        this IReadOnlyList<IBar> bars,
        int lookbackPeriods)
    {
        // Validate parameters
        ArgumentNullException.ThrowIfNull(bars);
        
        if (lookbackPeriods <= 0)
        {
            throw new ArgumentOutOfRangeException(
                nameof(lookbackPeriods),
                "Lookback periods must be greater than 0.");
        }

        // sort price bars (optional)
        List<IBar> barsList = bars
            .OrderBy(x => x.Timestamp)
            .ToList();

        // initialize results
        List<AtrWmaResult> results = new(barsList.Count);

        // get pre-requisite ATR values
        List<AtrResult> atrResults = barsList
            .ToAtr(lookbackPeriods)
            .ToList();

        // roll through source values
        for (int i = 0; i < barsList.Count; i++)
        {
            IBar b = barsList[i];
            double atrWma = double.NaN;

            // only do calculations after uncalculable periods
            if (i >= lookbackPeriods - 1)
            {
                double sumWma = 0;
                double sumAtr = 0;

                for (int p = i - lookbackPeriods + 1; p <= i; p++)
                {
                    double close = (double)barsList[p].Close;
                    double atr = atrResults[p].Atr ?? double.NaN;

                    sumWma += atr * close;
                    sumAtr += atr;
                }

                atrWma = sumWma / sumAtr;
            }

            // add record to results
            results.Add(new AtrWmaResult(
                Timestamp: b.Timestamp,
                AtrWma: atrWma.NaN2Null()));
        }

        return results;
    }
}

Step 3: Use your custom indicator

Use your custom indicator just like the built-in indicators:

csharp
using FacioQuo.Stock.Indicators;
using Custom.Indicators;

// Get historical price bars
IReadOnlyList<Bar> bars = GetBarsFromFeed("MSFT");

// Calculate custom indicator
IReadOnlyList<AtrWmaResult> results = bars.ToAtrWma(10);

// Use results
foreach (AtrWmaResult r in results)
{
    Console.WriteLine(
        $"ATR WMA on {r.Timestamp:d} was {r.AtrWma:N4}");
}

Advanced patterns

Chainable indicators

By implementing IReusable, your custom indicator can be chained with other indicators:

csharp
// Chain your custom indicator with RSI
var rsiOfAtrWma = bars
    .ToAtrWma(10)
    .ToRsi(14);

Using multiple indicator styles

You can also implement your custom indicator in other styles:

  • Buffer list style - For incremental processing
  • Stream hub style - For real-time data feeds

See the Guide for more information about different indicator styles.

Best practices

When creating custom indicators:

  1. Validate inputs - Always validate parameters and bars
  2. Handle edge cases - Check for insufficient data, null values
  3. Follow naming conventions - Use To{IndicatorName} pattern
  4. Implement IReusable - Enable chaining with other indicators
  5. Add XML documentation - Document parameters and return values
  6. Test thoroughly - Verify calculations against reference data

Example projects

For complete working examples, see:

See also