Indicators

Indicators

Overview & lifecycle

What an indicator is in TradeStrike, the contract you implement, and the exact order in which the framework calls your code. Read this first — every other indicator page builds on it.

The indicator model

An indicator is a small, stateful object that reads a bar series and produces output — usually one or more plotted lines, but it can also be a headless filter or an alert. You implement it by subclassing IndicatorBase, the SDK's ready-made implementation of the IIndicator contract (both live in the TradeStrike.Pipeline.Indicators namespace). The base class gives you empty virtual lifecycle hooks, the plot and line lists, child-indicator management and idempotent disposal, so you override only the few methods you actually need.

The single most important property of an indicator instance is that it is single-threaded with respect to its callbacks. The runtime guarantees that no two lifecycle methods ever run at the same time for the same instance. That means you can store rolling state — running sums, ring buffers, the previous bar's value — in plain instance fields, with no locks and no volatile. This is what makes indicator code so clean compared to general multithreaded C#.

The IIndicator contract

You almost never implement IIndicator directly — you subclass IndicatorBase. But it is worth seeing the shape, because every page in this chapter maps back to a member here. These are the real signatures from the contract.

IIndicator.cs (excerpt)public interface IIndicator : IDisposable
{
    string ContentKey { get; }                  // stable identity for dedup + persistence
    CalculationMode Calculate { get; }          // OnBarClose (default) / OnPriceChange / OnEachTick
    bool ProcessesMarketData { get; }           // opt into raw OnMarketData ticks
    bool ProcessesMarketDepth => false;         // opt into depth / MBO callbacks

    IReadOnlyList<IPlot> Plots { get; }          // renderable outputs
    IReadOnlyList<IIndicatorLine> Lines => Array.Empty<IIndicatorLine>();  // threshold lines

    void OnInit(IIndicatorContext ctx);
    void OnDataLoaded(IIndicatorContext ctx);
    void OnBarUpdate(IIndicatorContext ctx);
    void OnMarketData(in Tick tick, IIndicatorContext ctx);
    void OnDataSeriesReady(int seriesIndex, IIndicatorContext ctx);
    void OnDispose();

    ImmutableArray<IIndicator> Children { get; }  // parent-owned child tree
    void AttachChild(IIndicator child);
    void DetachChild(IIndicator child);
}

IndicatorBase implements all of these. It exposes two source-series properties you read from — Input (an ISeries<double>, defaults to Bars(0).Close) and Bars (an IBarSeries, defaults to Bars(0)) — plus the helpers AddPlot(...) and AddLine(...). Everything you reach through a callback — series, instrument metadata, logging, alerts — comes from the IIndicatorContext passed to each hook (and cached as the protected Context property).

Lifecycle hooks

The framework drives your indicator through a fixed sequence of callbacks. You override the ones you care about and leave the rest as the base class's no-ops. Understanding the order matters, because it is what lets you compute history in one efficient pass and then react to live bars one at a time.

Hook When it fires What you do in it
OnInit(ctx) Once, immediately after construction. Validate parameters and allocate state (buffers, rolling windows). Call base.OnInit(ctx) first so Input and Bars are defaulted before your code reads them.
OnDataLoaded(ctx) Once, after the full history is loaded into every subscribed series. Compute every historical bar in a single pass. This replaces NinjaTrader's per-historical-bar loop.
OnBarUpdate(ctx) Live, at the cadence set by Calculate. Compute the newest value. Never fires during backfill, regardless of mode.
OnMarketData(in tick, ctx) Per live tick — only if ProcessesMarketData is true. Raw trade/quote handling for VWAP, orderflow, etc.
OnMarketDepth / OnMarketByOrder Per depth / MBO event — only if ProcessesMarketDepth is true. DOM ladders, heatmaps. Default no-ops.
OnDataSeriesReady(i, ctx) When a series added via ctx.AddDataSeries finishes backfilling. Start reading the secondary series at index i.
OnReset(ctx) When a parameter change forces a full recompute. Drop your accumulators. The base impl already clears plot series and disposes children — call base.OnReset(ctx), then clear your own extra state.
OnDispose() Once, at teardown. Release unmanaged resources. Child indicators are disposed for you.
Backfill versus live, in one sentence. OnDataLoaded hands you the entire history at once so you can loop over it efficiently; OnBarUpdate then delivers each new live bar individually. If you have side effects that must never fire while replaying history — sounds, alerts, broker calls — gate them on ctx.IsLive (or its inverse, ctx.IsHistorical).

The lifecycle in one picture

graph TD;
      A["new MyIndicator()"] --> B["OnInit(ctx)"];
      B --> C["OnDataLoaded(ctx) — full history, one pass"];
      C --> D{"live event"};
      D -->|"bar closes / ticks (per Calculate)"| E["OnBarUpdate(ctx)"];
      D -->|"raw tick (if ProcessesMarketData)"| F["OnMarketData(in tick, ctx)"];
      D -->|"added series ready"| G["OnDataSeriesReady(i, ctx)"];
      E --> D; F --> D; G --> D;
      D -->|"chart closed / disposed"| H["OnDispose()"];
    

Opt-in capabilities

The core surface above is deliberately small. Everything else an indicator can do — custom painting, recolouring candles, a toolbar dropdown, multi-timeframe data, orderflow — is an opt-in you reach by implementing an extra interface or setting a property. You never pay for what you don't use: an indicator that doesn't implement IChartCustomRender never enters the custom-render pass; one that leaves ProcessesMarketData at false is skipped entirely on the tick path. Each of these has its own page in this chapter.

Where to go next

The rest of this chapter takes one subject at a time. A good path is to read them roughly in order, but each page stands alone if you only need a specific feature.

Page Covers
Your first indicator A complete SMA, line by line — the template for most line indicators.
Reading bars & series Bars-ago indexing, OHLC access and the cached price projections.
Parameters User-tunable inputs, editors, groups and the content key.
Plots & styles Lines, histograms, areas, candles and runtime restyling.
Panel placement Overlay the price panel or open a subpanel.
Threshold lines Reference levels like RSI 30/70.
Calculation & market data Per-bar vs per-tick, raw trades, depth, alerts and sessions.
Child indicators Compose indicators (MACD = EMA − EMA).
Multi-timeframe Add secondary series and read higher timeframes.
Orderflow Per-price-level bid/ask volume for footprint tools.
Custom rendering Paint anything on the chart, recolour candles, handle clicks.
Repaint requests Drive smooth, coalesced repaints on your own schedule.
Chart toolbar menu Add a toolbar dropdown and a right-click context menu.