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.
On this page
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. |
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. |