Indicators
Indicators
The contract surface for building indicators: the IIndicator lifecycle, the
IIndicatorContext runtime view, the IndicatorBase base class, the declaration
attributes, the cadence enums, the optional render / interaction capability interfaces, and the chart trader
panel controls. Namespaces: SabrTrader.Pipeline.Indicators and its
.Search and .Statistics sub-namespaces, plus SabrTrader.Pipeline.Panels.
Trading hours and sessions (.TradingHours, .Sessions) are in the
Alerts & sessions reference; contract rollovers
(.ContractRollover) and the series types are in Bars & series;
PlotColors (.Builtin) is in Plots & rendering.
On this page
IIndicator INTERFACE
The lifecycle contract every indicator implements (it extends IDisposable). The runtime
calls the hooks; indicators communicate through child indicators and the shared bar/series storage, and
never call each other's hooks. Callbacks are single-threaded per instance: no two hooks fire in parallel
for the same indicator.
Lifecycle ordering: OnInit once → OnDataLoaded once (with all
historical bars already in storage) → OnBarUpdate per closed/intra-bar event at the
Calculate cadence → OnMarketData per live tick (with
ProcessesMarketData) → OnDataSeriesReady when an added series finishes
backfill → OnDispose once. Live hooks fire only after backfill.
Properties
| Member | Description |
|---|---|
string ContentKey { get; } |
Stable identity across runs, derived from (Type, ParameterValues, InputSeriesSpecs). Two requests with the same key resolve to the same instance (diamond-dependency dedup). |
CalculationMode Calculate { get; } |
Cadence the runtime fires OnBarUpdate on the primary series. Must be stable for the indicator's lifetime. Default OnBarClose. |
bool ProcessesMarketData { get; } |
Opt into raw OnMarketData tick delivery. Default false, which keeps per-tick dispatch cost at zero for the indicator. Independent of Calculate. |
bool ProcessesMarketDepth { get; } |
Opt into the order-book callbacks: OnMarketDepth, OnMarketByOrder, OnMarketByOrderLoss and OnOrderBookLifecycle. Default-interface member, default false. |
IReadOnlyList<IPlot> Plots { get; } |
Renderable outputs: one IPlot per line (SMA = 1, MACD = 3). Stable list reference; allocate plots in the ctor/OnInit and never resize. Plot properties are mutable at runtime. |
IReadOnlyList<IIndicatorLine> Lines { get; } |
Horizontal threshold lines (RSI 30/70, ADX 20/40). Default-interface member returns empty. |
bool ShowPriceMarkers { get; } |
Master toggle overriding every plot's ShowPriceMarker. Default true (per-plot setting wins). |
int Displacement { get; } |
Purely visual horizontal shift of plots in bars (positive = right/future). The computed value series is never altered. Default 0. |
ImmutableArray<IIndicator> Children { get; } |
Live snapshot of attached child indicators in registration order. Lock-free (atomic swap). |
Lifecycle & child-tree methods
| Signature | Description |
|---|---|
void OnInit(IIndicatorContext ctx) |
Once after construction. Declare secondary series, instantiate known children, read parameters. ctx.Bars(0) is available. |
void OnDataLoaded(IIndicatorContext ctx) |
Once after all historical bars are loaded. Compute historical state in a single (typically vectorised) pass. |
void OnBarUpdate(IIndicatorContext ctx) |
A live bar closed/updated on one of the indicator's series; ctx.BarsInProgress identifies which. |
void OnMarketData(in Tick tick, IIndicatorContext ctx) |
Per live tick, only when ProcessesMarketData is true. |
void OnMarketDepth(in DepthUpdate update, IIndicatorContext ctx) |
Per aggregated L2 depth event (ProcessesMarketDepth). The book is applied before this fires. Default no-op. |
void OnMarketByOrder(in MboEvent evt, IIndicatorContext ctx) |
One Market-By-Order event (ProcessesMarketDepth). The book is applied before this fires. Sampled on a live chart: the host hands over the newest queued event per worker pass and announces the rest through OnMarketByOrderLoss. Default no-op. See delivery contract. |
void OnMarketByOrderLoss(long lostEvents, IIndicatorContext ctx) |
MBO events addressed to this indicator were not delivered; fires before the first event that follows the gap. Exact for the events the host's latest-per-pass delivery skipped; ring overflow is added to the same count. The shared book is unaffected. Default no-op. |
void OnOrderBookLifecycle(OrderBookLifecycleEvent evt, IIndicatorContext ctx) |
The shared book was reset, cleared for a venue re-image, finished that re-image, or changed its OrderBookDepthMode. The book already reflects the transition. Never concurrent with the other order-book callbacks. Default no-op. |
void OnDataConnectionChanged(DataConnectionStatus status, IIndicatorContext ctx) |
The live market-data connection feeding the chart changed state: it dropped, is reconnecting, came back, or a connect attempt failed. status equals ctx.DataConnection for the duration of the call, and ctx.IsLive is true, so an alert raised here fires. Rapid transitions are coalesced; you always end on the current status. Needs no opt-in. Default no-op. See Data connection. |
void OnDataSeriesReady(int seriesIndex, IIndicatorContext ctx) |
An AddDataSeries series finished backfilling; ctx.Bars(seriesIndex) now returns real bars. |
void OnDispose() |
Once on teardown. Release non-managed resources; children are disposed automatically. |
void AttachChild(IIndicator child) |
Registers a child; thread-safe. The child's own constructor calls it (through IndicatorBase), so indicator code has no reason to. |
void DetachChild(IIndicator child) |
Detach without disposing (call child.Dispose() instead, which routes here). No-op if not attached. |
public interface IStrategy : IIndicator marks a strategy, so consumers can
type-check "indicator vs strategy". See the Strategies reference.IIndicatorContext INTERFACE
The runtime view an indicator has of its environment, passed to every lifecycle hook. Single-threaded with respect to the indicator. Do not cache it across threads or persist it past a callback. Many members are default-interface methods, so a test stub or third-party host implements only the core members.
Series & per-callback metadata
| Member | Description |
|---|---|
IBarSeries Bars(int seriesIndex) |
Bars for a series; 0 = primary. Added series live at the indices returned by AddDataSeries. |
int SeriesCount { get; } |
Number of subscribed series (primary + added). |
int CurrentBar { get; } |
Last-bar index on the firing series (Bars(BarsInProgress).Count - 1); -1 in warmup. |
CurrentBarsAccessor CurrentBars { get; } |
Per-series last-bar indexer (ctx.CurrentBars[1]) for MTF indicators. Zero-allocation struct. |
int BarsInProgress { get; } |
Which series this callback is firing for (0 = primary; always 0 on OnDataLoaded). |
bool IsFirstTickOfBar { get; } |
True when the current tick opened a new bar on BarsInProgress. |
bool IsBarClosed { get; } |
True for a closed-bar OnBarUpdate; false intra-bar. Default true. |
long SequenceNumber { get; } |
Monotonic event sequence for deterministic logging/replay. |
bool IsHistorical { get; } |
True during backfill/catch-up (!IsLive). Gate alerts/broker calls on this. |
bool IsLive { get; } |
True during realtime dispatch. Default false (conservative). |
DataConnectionStatus DataConnection { get; } |
State of the live market-data connection feeding this chart. Unlike IsLive, which describes the current callback, this describes the connection and can be read from any hook. Default NotApplicable. See Data connection. |
bool IsDataConnected { get; } |
True when DataConnection is Connected. False while disconnected, connecting, lost or reconnecting, and also on hosts with no live connection; check DataConnection for NotApplicable when that difference matters. |
Resources, orderflow & instrument info
| Member | Description |
|---|---|
Task<int> AddDataSeries(BarSpecification spec) |
Subscribe an additional series, loaded as bars: its historical order flow is empty. Idempotent for an already-subscribed spec; OnDataSeriesReady fires when bars are available. |
Task<int> AddDataSeries(BarSpecification spec, OrderFlowDetail orderFlow) |
The same, for an indicator that reads OrderFlow of the series it adds. OrderFlowDetail.Totals: per-bar totals only, loaded from the venue's own bars where they carry the buy/sell split. OrderFlowDetail.PriceLevels: the full footprint. The host honours it on a chart that loads order flow, which [RequiresOrderFlow] makes it do. A request never shares a series that was loaded with less, so read the index this call returns. |
Task<IReadOnlyList<Bar>> LoadHistoricalBars(BarSpecification spec, DateTime fromUtc, DateTime toUtcExclusive, CancellationToken ct = default) |
One-shot read of closed bars in [fromUtc, toUtcExclusive), independent of the chart's own history: it subscribes nothing and creates no series slot. Resolves spec like AddDataSeries (same instrument inherits the primary's trading hours and continuous root). Completes on another thread: fold the result in through IRecalculateNotifier, never from the continuation. Default: faulted with NotSupportedException. |
void RemoveDataSeries(int seriesIndex) |
Drop an added series. Index 0 throws InvalidOperationException. |
IIndicatorContext CreateChildContext(IBarSeries childPrimary, string childIdentity) |
Build a per-child context (called by AttachChild). Default returns this. |
void BindOwner(IIndicator owner) |
Host plumbing: tells the context which indicator it serves. IndicatorBase calls it. |
IDecisionClient Decisions { get; } |
The decision-model client (SabrTrader.Pipeline.Decisions). DecisionClient.None until a chart host wires one. IndicatorBase exposes it as the protected Decisions property and delivers answers to OnDecision. See Decision models. |
IAiClient Ai { get; } |
The AI client (SabrTrader.Pipeline.AiRequests). AiClient.None where the host does not ask the AI. IndicatorBase exposes it as the protected Ai property. See AI requests. |
INewsClient News { get; } |
The news client (SabrTrader.Pipeline.News). NewsClient.None where the host has no news: backtests, the Strategy Analyzer and the Market Analyzer. On a live chart it also works during the historical pass: you can request past headlines, and live headlines arrive once the chart is live. IndicatorBase exposes it as the protected News property. See News and Reference › News. |
IIndicatorResolver? Indicators { get; } |
Host catalog of instantiable indicator types (built-ins + plugins), or null. Tolerate null. |
ISeries<OrderFlowBar> OrderFlow(int seriesIndex) |
Per-bar orderflow. Only indicators marked [RequiresOrderFlow] may call it (else throws). |
ISeries<double> OpenInterest(int seriesIndex) |
Per-bar OI scalar series (NaN where unreported). Default empty series; read defensively. |
IOrderBook? OrderBook { get; } |
Live shared depth book, or null. Applied before depth/MBO callbacks. Default null. |
void ReadOrderBook(Action<IOrderBook> reader) |
Invoke reader against the live depth book under the host's read guard (null-safe no-op when no book). Prefer this over caching OrderBook across a callback. |
bool TryReadOrderBook(Action<IOrderBook> reader) |
The same read, but it tells you whether it happened: false means there was no book and reader was never invoked. Use it wherever "never ran" and "ran against an empty book" mean different things (seeding state from the book, reporting a fault); ReadOrderBook treats a cold start with no depth feed and a broken host the same way. |
IRecordedDepthCatalog? RecordedDepth { get; } |
Read access to the L2 / MBO tapes the platform's recorder wrote, with their reset, re-image and gap markers, or null when this host records none. Unlike the sampled callbacks, it holds every event. See IRecordedDepthCatalog. |
double? TickSize { get; } |
Minimum price increment, or null. Default null. |
double? PointValue { get; } |
Currency value of one full point, or null. Default null. |
double? QuantityStep { get; } |
Smallest tradable quantity increment for the chart's instrument (1 contract, 1 share, 0.0001 BTC), or null when the host could not resolve one. Default null. |
IOptionChainReader? OptionChains { get; } |
The read-only options data surface (SabrTrader.Pipeline.Options): chains, per-contract quotes and greeks, routed to whichever connected venue serves the underlying. Null when the host serves no options data. Availability changes as venues connect, so keep the reader and watch IsAvailable / AvailabilityChanged. Default reads OptionChainReaderAmbient.Reader. See the Options reference. |
TimeZoneInfo? DisplayTimeZone { get; } |
The chart's display time zone. Bar.StartUtc / EndUtc stay true venue UTC; the chart timeline is Bar.Start / End. Convert an external UTC instant with ChartDisplayTime.FromUtc(utc, DisplayTimeZone). Default null (treat as already on the bar timeline). |
Trading hours, rollovers & diagnostics
| Member | Description |
|---|---|
TradingHoursTemplate? TradingHours { get; } |
Session/holiday calendar for the instrument, or null (treat as 24/7). |
ISessionIterator? CreateSessionIterator(TradingHoursTemplate? template = null) |
A fresh stateful session iterator over template (the instrument's own when null). Default null. |
bool IsFirstBarOfSession { get; } |
True when the current OnBarUpdate bar opens a new session. Default false. |
IReadOnlyList<DateTime> GetContractRolloversUtc(DateTime fromUtc, DateTime toUtc) |
Futures rollover instants as true venue UTC (same clock as Bar.StartUtc). Default empty. |
void Print(string message) |
Dev trace to the host log window (like NinjaTrader's Print()). |
void LogWarning(string message) |
Non-fatal anomaly; indicator keeps running. |
void LogError(string message, Exception? exception = null) |
Fatal-for-this-callback condition; runtime keeps dispatching. |
void Alert(string message) |
Raise a user-facing alert (live only). Message doubles as the rearm id. |
void Alert(string id, string message, string? soundPath = null, AlertSeverity severity = Info, double rearmSeconds = 0) |
Full-control alert. The same id fires at most once per rearmSeconds. Default no-op. |
Data connection
An indicator can tell whether the chart it runs on has a live data connection, and react when that changes.
Read ctx.DataConnection (or the shortcut ctx.IsDataConnected) from any hook, and
override OnDataConnectionChanged to hear about transitions. The status follows the chart's
data venue as a whole, so it keeps tracking the right connection when the platform replaces a venue
session after a reconnect. For a practical walk-through, see
Showing the connection state.
DataConnectionStatus.csnamespace SabrTrader.Pipeline.Providers;
public enum DataConnectionStatus
{
NotApplicable = 0,
Disconnected = 1,
Connecting = 2,
Connected = 3,
ConnectionLost = 4,
Reconnecting = 5,
Failed = 6,
}
| Value | Meaning |
|---|---|
NotApplicable |
This host has no live connection to report. Treat it as "the question does not apply" rather than as a disconnect. |
Disconnected |
No connection and none in progress: the venue is idle or not configured. |
Connecting |
A first connect attempt is in progress. |
Connected |
Connected. Live data can flow. |
ConnectionLost |
Was connected, and the link dropped unexpectedly. No reconnect is running yet. |
Reconnecting |
Was connected, the link dropped, and a reconnect attempt is in progress. |
Failed |
The most recent connect attempt failed and the venue has not connected since. A rejected login stays here until the user fixes the connection. |
Where you get NotApplicable
Only an indicator on a chart follows a data venue. Everywhere else the context reports
NotApplicable and OnDataConnectionChanged never fires: backtests and optimizer passes,
historical-only analysis, indicators hosted inside a strategy (the strategy reads its own
DataConnection), SuperDOM and Market Analyzer column hosts, test rigs, and any third-party
IIndicatorContext that keeps the default. Treat NotApplicable like "connected" for
display purposes: a "disconnected" badge in a backtest would be wrong.
Delivery rules
-
OnDataConnectionChangedruns on the same serialised path as the other callbacks, never concurrently with them, withctx.IsLivetrue, soctx.Alert(...)fires from it. - Inside the callback,
ctx.DataConnectionalready returns the new status. - Rapid transitions are coalesced: you always end on the current status, but you may not see every
intermediate one (a quick drop and reconnect can arrive as just
Connected). - It fires on transitions only. Read
ctx.DataConnectioninOnInitfor the starting status; the usual pattern is one field set in both places. - No ticks arrive while the connection is down, so an indicator that draws the status implements
IRepaintNotifierand raisesRepaintRequestedfrom the callback. - No opt-in attribute is needed. A child indicator hosted by a composite reads
DataConnectionfrom its own context; the callback goes to the indicators the host dispatches to.
IndicatorBase BASE CLASS
The class plugin authors derive from. It implements IIndicator,
IIndicatorHost (host-side plumbing, see below),
IOrderFlowResolutionAware and IInputSourceAware, and provides empty virtual
lifecycle hooks (override only what you need), lazy ContentKey, idempotent
Dispose, the parent-owned child tree, and the plot/line registration helpers.
SimpleAverage.csusing System;
using SabrTrader.Pipeline.Indicators;
using SabrTrader.Pipeline.Plots;
using SabrTrader.Pipeline.Series;
[IndicatorDescription("Simple moving average of the input series.")]
[IndicatorCategory(IndicatorCategory.Trend)]
[IndicatorInput(IndicatorInputKind.Scalar)]
[IndicatorPlot("SMA", IsOverlay = true, Kind = PlotKind.Overlay, Unit = PlotValueUnit.Price)]
public sealed class SimpleAverage : IndicatorBase
{
private readonly ChunkedSeries<double> _sma = new();
[IndicatorParameter(DisplayName = "Period", MinValue = 1, MaxValue = 500, Step = 1)]
public int Period { get; set; } = 14;
public SimpleAverage(IIndicator? parent = null, ISeries<double>? input = null)
: base(parent, input)
{
AddPlot(new Plot("SMA", _sma, PlotStyle.Line, new ChartColor(33, 150, 243), 1.5));
}
public override void OnDataLoaded(IIndicatorContext ctx)
{
for (int barsAgo = Input.Count - 1; barsAgo >= 0; barsAgo--)
_sma.Append(Average(barsAgo));
}
public override void OnBarUpdate(IIndicatorContext ctx) => _sma.Append(Average(0));
private double Average(int barsAgo)
{
int n = Math.Min(Period, Input.Count - barsAgo);
double sum = 0;
for (int i = 0; i < n; i++) sum += Input[barsAgo + i];
return sum / n;
}
}
Construction
Every subclass exposes one ctor of the shape Xxx(IIndicator? parent = null,
ISeries<double>? input = null, /* params */) : base(parent, input). The base ctor stores
Input/Bars and, when parent is non-null, registers as a child.
The child's own OnInit/OnDataLoaded are deferred to the first dispatch event so
the derived ctor body sets Period, plots, etc. before lifecycle math runs.
protected IndicatorBase(IIndicator? parent = null, ISeries<double>? input = null, IBarSeries? bars = null);
Public & protected members
| Member | Description |
|---|---|
ISeries<double> Input { get; set; } |
Scalar source for SMA/EMA/RSI-style indicators; null defaults to primary Bars(0).Close at init. |
IBarSeries Bars { get; set; } |
Bar source for OHLCV indicators (ATR/ADX/VWAP); null defaults to primary Bars(0). |
InputSourceDescriptor InputSource { get; set; } |
The universal "Input series" parameter backing store (surfaces in the editor only for Scalar input kind). |
OrderFlowResolution OrderFlowResolution { get; set; } |
The universal orderflow "Resolution" backing store (surfaces only for [RequiresOrderFlow] indicators). Default Auto. |
bool ShowPriceMarkers { get; set; } |
Master price-marker toggle (attributed parameter, group "Display"). |
int Displacement { get; set; } |
Visual bar shift (attributed parameter, ContentKeyDefault = 0). |
IReadOnlyList<IPlot> Plots { get; } / Lines { get; }
|
The registered plots and threshold lines. |
IIndicatorContext Context { get; } |
Protected. The most recent context the runtime supplied. Do not cache across threads. |
bool TryGetPlot(string name, out IPlot plot) |
Find a registered plot by its exact name. False when this indicator has no plot with that name. |
IDecisionClient Decisions { get; } |
Protected. The decision-model client from Context.Decisions; DecisionClient.None until a chart host wires one. See Decision models. |
IAiClient Ai { get; } |
Protected. The AI client from Context.Ai: send the AI instructions with bars, plots, your own data or the chart picture, and get the answer back as your own type. AiClient.None where the host does not ask the AI. See AI requests. |
INewsClient News { get; } |
Protected. The news client from Context.News: live and past headlines and story text, with handlers on the indicator's thread. NewsClient.None where the host has no news. See News and Reference › News. |
const int MaxChildDepth = 16 |
Maximum parent→child chain depth (cycle + depth guarded by AttachChild). |
protected void AddPlot(IPlot plot) |
Register a renderable output (append-only; earlier entries render behind later ones). |
protected void AddLine(IIndicatorLine line) |
Register a horizontal threshold line. |
protected void AddLine(string name, double value, ChartColor color, double lineWidthPx = 1.0, PlotStyle style = Line, bool participatesInAutoScale = true) |
Convenience overload constructing the default IndicatorLine. |
protected void AddLine(string name, double value, ChartColor color, double lineWidthPx, PlotStyle style, bool participatesInAutoScale, bool isParameterDriven) |
The same, for a line whose level comes from one of the indicator's parameters (IIndicatorLine.IsParameterDriven). Every argument is required. |
ImmutableArray<IIndicator> Children { get; } |
The attached child indicators, in registration order. |
void AttachChild(IIndicator child) / void DetachChild(IIndicator child)
|
The IIndicator child-tree members. The base constructor attaches a child to its parent; Dispose() on a child detaches it. |
void Dispose() |
Idempotent teardown: disposes the children (newest first), runs OnDispose, then detaches from the parent. |
protected void AdvanceChildren(IIndicatorContext ctx) |
Advance attached children for the current live bar (call at the top of OnBarUpdate when you read a child's Output). |
protected virtual void OnReset(IIndicatorContext ctx) |
Reset hook for recalc: clears clearable plot series + disposes children. Override to drop custom accumulators (call base first). |
Overridable virtuals
| Signature | Description |
|---|---|
virtual void OnInit(IIndicatorContext ctx) |
Base seeds Context, resolves InputSource, and defaults Input/Bars. Call base.OnInit(ctx) first. |
virtual void OnDataLoaded(IIndicatorContext ctx) |
No-op by default. Vectorised backfill pass. |
virtual void OnBarUpdate(IIndicatorContext ctx) |
No-op by default. BIP filtering is handled by the framework wrapper. |
virtual void OnMarketData(in Tick, …) / OnMarketDepth / OnMarketByOrder / OnMarketByOrderLoss / OnOrderBookLifecycle / OnDataSeriesReady / OnDispose
|
All no-op by default; override what you need. |
virtual void OnDataConnectionChanged(DataConnectionStatus status, IIndicatorContext ctx) |
Public virtual, no-op by default. Override to react when the chart's data connection changes; see Data connection. |
protected virtual void OnDecision(DecisionAnswer answer) |
No-op by default. Receives a completed Decisions.Ask. The chart host calls it on the chart's session worker, serialised with bar dispatch and recalculation, never on the thread the answer arrived on. |
virtual string ContentKey { get; } |
Lazily computed via IndicatorMetadata.For(GetType()).ComputeContentKey(this). |
virtual CalculationMode Calculate { get; protected set; } |
Default OnBarClose. Set in your ctor to opt into per-tick updates. |
virtual bool ProcessesMarketData { get; protected set; } / ProcessesMarketDepth
|
Default false; set in your ctor to receive tick / depth callbacks. |
CalculationMode ENUM
Frequency at which the runtime dispatches OnBarUpdate on the primary series (the same three
modes as NinjaTrader). It controls only the bar-callback cadence; raw-tick delivery is the separate
ProcessesMarketData opt-in.
| Value | Meaning |
|---|---|
OnBarClose = 0 |
Fire once per closed bar (default). Lowest CPU; the right choice for EMA/SMA/RSI. |
OnPriceChange = 1 |
Fire whenever the bar's Close changes; skips repeat-price quotes. |
OnEachTick = 2 |
Fire on every tick. Highest CPU; for tick-precision VWAP, footprint and volume-reactive indicators. |
CalculationCadence STATIC HELPER
Decides whether a callback fires for a given CalculationMode and update. The indicator
runtime and the strategy runner both use it, and a custom host can too.
public static bool ShouldFire(CalculationMode mode, bool isClosed, bool closeChanged);
// OnBarClose => isClosed
// OnPriceChange => isClosed || closeChanged
// OnEachTick => true
Attributes
Class- and property-level attributes the framework reads by reflection to compute content keys, render
the parameter editor, persist values, and route data. All are Inherited = true except
[IndicatorHotkey].
IndicatorParameter (property)
Marks a property as a user-tunable parameter. Constraints are advisory (the editor enforces them; programmatic sets are not clamped).
| Member | Description |
|---|---|
string? DisplayName |
Editor label (defaults to the property name, humanised). |
string? Description |
Tooltip text. |
string? Group |
Logical group; the editor renders a group together. |
int Order |
Display order within the group (lower first; ties break by declaration order). |
object? MinValue, MaxValue, Step |
Advisory numeric bounds + spinner increment. |
object? ContentKeyDefault |
Omit-when-default sentinel: the parameter contributes no content-key term while equal to this value, so adding a parameter leaves the keys of saved instances unchanged. |
ParameterEditorKind Editor |
Editor control hint. Default Auto. |
string? FileFilter |
File-dialog filter for FilePicker, in the standard "Name (*.ext)|*.ext|All files (*.*)|*.*" form. Null shows all files; other editor kinds ignore it. |
bool Primary |
When any parameter is marked primary, AI schema projection surfaces only primary knobs (plus the universal Input / Resolution parameters). The editor, codec and content-key behaviour are unchanged. |
string? VisibleWhen |
Conditional editor visibility: "Param=Value" or "Param=Val1|Val2". Null/empty = always visible. Editor-only and fails open (unknown left-hand side keeps the row visible). |
bool Bold |
Draws the setting's label bold in the settings list. Default false. Editor-only, like the two fields below: the value, persistence and the content key are unchanged. |
ParameterLabelSize LabelSize |
The size the setting's label is drawn at: Normal (default) or Large. |
bool SeparatorBefore |
Draws a separator line above the setting. Default false. |
ParameterLabelSize enum: Normal = 0, Large = 1.
ParameterFormat is the readonly record struct the metadata carries the three formatting fields in:
ParameterFormat(bool Bold = false, ParameterLabelSize LabelSize = Normal, bool SeparatorBefore = false),
with bool IsLarge. Its default value is the plain row. Strategy parameters use the same type.
ParameterEditorKind enum: Auto = 0 (host picks from the CLR type),
FilePicker = 1 (path field + Browse), SymbolPicker = 2 (shared instrument picker
for a benchmark/comparison symbol), Hidden = 3 (no settings-dialog row; the value still
persists and still contributes to the content key), FontPicker = 4 (on a string
holding a font family name: the same font list the chart properties use; the box stays editable so a family
this machine lacks can still be typed, an empty value keeps the host's default font, and a host without a
font list shows a plain text box).
IndicatorPlot (class, repeatable)
Declares a named plot the indicator produces and what its values mean. Declare one per output (MACD line,
signal and histogram each get one). IndicatorMetadata.Plots exposes them as
PlotDescriptors, with the semantics in PlotDescriptor.Semantics.
| Member | Description |
|---|---|
string Name (ctor) |
The plot name. |
string? Description |
Optional description. |
bool IsOverlay |
Overlay on the price panel vs a separate panel. |
PlotKind Kind |
Shape (defaults to Overlay when IsOverlay, else Unspecified). |
double RangeMin, RangeMax |
Inclusive value bounds; double.NaN = unbounded (mapped to null). |
PlotValueUnit Unit |
Value unit. |
PlotBullishDirection BullishDirection |
Whether rising = bullish/bearish/neither. |
string? Reference |
Notable levels, e.g. "30/70 overbought/oversold". |
Class-level marker & metadata attributes
| Attribute | What it declares |
|---|---|
[IndicatorDescription(string)] |
Human-readable description shown in the Add Indicator and settings dialogs. The ctor throws ArgumentException on a null or blank string. |
[IndicatorCategory(IndicatorCategory)] |
The indicator's family (see enum below). |
[IndicatorDisplayName(string)] |
The name the user sees, instead of the one derived from the class name (type name, trailing "Indicator" trimmed, PascalCase split into words). Use it when the class name is not what a trader calls the indicator. The class name stays the indicator's identity in workspaces, templates and the AI catalog, so the on-screen name can change without breaking anything saved. The declared name is the English fallback: a translation under Indicator.{TypeName}.Name wins when present. Blank is ignored. (Class: IndicatorDisplayNameAttribute.) |
[IndicatorFolder(string)] |
Lists the indicator under a folder of your choosing in the indicator pickers ("My Scalping Tools") instead of under its category folder. Only where it is listed changes: IndicatorCategory still says what kind of indicator it is, so declare both. Indicators naming the same folder (compared case-insensitively) share it; the name is shown as written, is not translated, and is one level only (a "/" is part of the name). Blank is ignored. (Class: IndicatorFolderAttribute.) |
[IndicatorInput(IndicatorInputKind)] |
How the indicator consumes data: Scalar (surfaces the Input-series picker), Bars, or None. |
[RequiresOrderFlow], [RequiresOrderFlow(OrderFlowDetail.Totals)]
|
Opt into pre-aggregated orderflow delivery + the synthesized "Resolution" parameter. Without an argument: the full footprint. With OrderFlowDetail.Totals: the indicator reads per-bar totals only (volume, buy volume, sell volume, trade count, delta), and the host loads the venue's own bars where they carry the buy/sell split. |
[RequiresOpenInterest] |
Opt into the per-bar Open-Interest channel (routing hint only; the channel is always reachable, all-NaN without an OI provider). |
[SingleInstancePerChart] |
At most one instance of this concrete type per chart; the chart ignores a second add. The limit applies per concrete type, so tagging a base class caps each derived type separately. |
[ParameterContainer(Type)] |
Delegate parameters to a separate settings object of type ContainerType. By default only its [IndicatorParameter] properties are parameters; AllProperties = true surfaces every public read/write property of a persistable type (bool, enum, numeric, string, ChartColor, time/date). The indicator must also implement IParameterContainerProvider. |
Enums: IndicatorCategory = Unspecified, Trend, Momentum, Volume,
Volatility, OrderFlow, Structure, Custom. IndicatorInputKind = Scalar = 0,
Bars = 1, None = 2.
Keyboard shortcuts
An indicator declares the shortcuts it wants, one attribute per action, and implements
IIndicatorHotkeyTarget to receive them. The declarations sit on the type, not an instance, so
the host reads them from the catalog at start-up and the action can be bound before any chart has the
indicator on it, the same way drawing-tool shortcuts work.
hotkeysusing System.Collections.Generic;
using SabrTrader.Pipeline.Indicators;
// Ids are dotted lower-case and PERSISTED in the user's binding file - never rename one.
[IndicatorHotkey("myzones.toggle", "Toggle zones", defaultGesture: "Ctrl+Alt+Z")]
[IndicatorHotkey("myzones.clear", "Clear zones")] // no default: unbound until the user assigns one
public sealed class MyZones : IndicatorBase, IIndicatorHotkeyTarget
{
private readonly List<double> _zones = new();
private bool _show = true;
public bool InvokeHotkey(string hotkeyId)
{
switch (hotkeyId)
{
case "myzones.toggle": _show = !_show; return true;
case "myzones.clear": _zones.Clear(); return true;
default: return false; // not mine
}
}
}
IndicatorHotkeyAttribute(string id, string displayName, string? defaultGesture = null)
(AllowMultiple = true, Inherited = false), with read-only Id,
DisplayName and DefaultGesture. DisplayName is what the Keyboard
Shortcuts dialog shows, and DefaultGesture uses the host's chord syntax
(Ctrl+Alt+B); null leaves the action unbound until the user assigns one. When the key is
pressed, every indicator on the active chart that implements IIndicatorHotkeyTarget is offered
the id, so return false for an id that is not yours and expect other listeners.
Metadata & identity
Reflection-extracted, per-type cached data driving content keys, the parameter editor, and discovery.
| Member | Description |
|---|---|
IndicatorMetadata.For(Type) / For<T>()
|
Cached metadata for a type. Synthesizes the "Input series" and orderflow "Resolution" parameters when applicable. |
string ComputeContentKey(object instance) |
Deterministic, locale-invariant content key from name-sorted parameter values (honours ContentKeyDefault). |
static object ResolveParameterTarget(object indicator, ParameterDescriptor) |
The object a descriptor's get/set targets (the indicator itself, or its [ParameterContainer] object). |
IReadOnlyList<ParameterDescriptor> Parameters / IReadOnlyList<PlotDescriptor> Plots
|
Declared parameters and plots. |
Type IndicatorType, string Description, IndicatorCategory Category
|
Type and descriptive metadata. |
string DisplayName / string Folder
|
The invariant user-facing name (from [IndicatorDisplayName], else derived from the type name) and the folder from [IndicatorFolder] (empty when none is declared, in which case pickers file the indicator under Category). Both are local facts of the type and never come from a cloud spec; identity stays on the CLR type name. |
bool RequiresOrderFlow, RequiresOpenInterest, SingleInstancePerChart, IsInert |
Structural flags. IsInert is true for a premium indicator whose definition is withheld (empty editor; it still constructs and can be removed). |
IndicatorInputKind? InputKind |
From [IndicatorInput]; null when the type declares none. |
Records: ParameterDescriptor(PropertyInfo Property, string Name, string DisplayName,
string Description, string Group, int Order, object? MinValue, object? MaxValue, object? Step,
ParameterEditorKind Editor = Auto, string FileFilter = "") with init-only
ContentKeyDefault, bool IsPrimary, string? VisibleWhen,
ParameterFormat Format;
PlotDescriptor(string Name, string Description, bool IsOverlay) with init-only
Semantics. Both are sealed records the metadata builds; you read them, you never construct them.
IParameterContainerProvider: object ParameterContainer { get; } returns the live settings
instance for a [ParameterContainer] indicator; never null once the indicator is constructed.
PlotSemantics
The machine-readable meaning of a plot's value, so a consumer can interpret RSI = 71.6 vs
EMA = 5012 without recognising the indicator (a sealed record). PlotSemantics.Default
has everything unspecified.
public sealed record PlotSemantics(
PlotKind Kind, double? RangeMin, double? RangeMax,
PlotValueUnit Unit, PlotBullishDirection BullishDirection, string? Reference);
// bool HasRange => RangeMin.HasValue && RangeMax.HasValue;
| Enum | Values |
|---|---|
PlotKind |
Unspecified, Overlay, Oscillator, Histogram, Signal, Band |
PlotValueUnit |
Unspecified, Price, Percent, Ratio, Ticks, Raw |
PlotBullishDirection |
None, Up, Down |
The reflected definition
What the host sees when it inspects an indicator type without instantiating it: the settings dialog, the indicator picker and the AI builder read this rather than your fields. It is produced from your attributes (or served as a premium spec, see below), so indicator code only reads it.
| Interface | Members |
|---|---|
IIndicatorDefinitionSpec |
IndicatorCategory Category, string Description, IndicatorInputKind? InputKind, bool RequiresOrderFlow, bool RequiresOpenInterest, IReadOnlyList<IIndicatorSpecParameter> Parameters, IReadOnlyList<IIndicatorSpecPlot> Plots. |
IIndicatorSpecParameter |
Name, DisplayName, Description, Group, Order, ParameterEditorKind Editor, MinValue / MaxValue / Step (boxed), FileFilter, VisibleWhen: one [IndicatorParameter] as the dialog renders it. |
IIndicatorSpecPlot |
Name, Description, bool IsOverlay, PlotSemantics Semantics: one declared plot, with the semantics you gave it. |
IIndicatorHost |
The host side of an indicator instance: void SetContext(IIndicatorContext), bool EnsureInitialized(), void ResetForRecalc(), void DeliverDecision(DecisionAnswer) (runs the indicator's OnDecision on the chart's session worker, under the gate that serialises bar dispatch and recalculation), bool IsInitialized, IIndicatorContext? InstanceContext, IndicatorBase? InputChainChild, bool HostWalksChildren { get; set; }. IndicatorBase implements it; only a host calls it. |
Input sources
IInputSourceAware / IOrderFlowResolutionAware: backing-store contracts
(InputSourceDescriptor InputSource { get; set; } and
OrderFlowResolution OrderFlowResolution { get; set; }) that IndicatorBase
implements so the editor/codec descriptors are synthesized only for the right indicators.
InputPriceType enum (the standard price types plus the volume and open-interest channels):
Close = 0, Open = 1, High = 2, Low = 3, Median = 4, Typical = 5, Weighted = 6, OHLC4 = 7, Volume = 8,
OpenInterest = 9.
InputSourceDescriptor: a serializable value type (readonly struct) describing where the scalar Input
comes from: a price type, or another indicator's plot (a private configured copy). Its canonical string
is a frozen, persisted grammar; structural equality is semantic equality.
| Member | Description |
|---|---|
static InputSourceDescriptor Close { get; } |
The default (equals default(InputSourceDescriptor)). |
static InputSourceDescriptor Price(InputPriceType type) |
A price-source descriptor. |
static InputSourceDescriptor Indicator(string catalogKey, IReadOnlyDictionary<string,string>? parameters = null, string? plotName = null) |
An indicator-plot source (parameters canonicalized). |
InputPriceType PriceType · string? IndicatorKey, IndicatorParametersText, PlotName
|
The descriptor's components. |
bool IsPrice, IsDefault |
Classification helpers. |
IReadOnlyDictionary<string,string> GetParameterMap() |
The source indicator's overrides as a name→value map. |
string ToString() / ToDisplayString()
|
Canonical (persisted) form / human-readable form. |
static InputSourceDescriptor Parse(string) / bool TryParse(string?, out …)
|
Parse from string (throwing / non-throwing). |
Optional capability interfaces
An indicator opts into extra host integration by ALSO implementing one of these alongside
IIndicator, so only the indicators that need a capability take the dependency.
| Interface | Opts into |
|---|---|
IIndicatorPanelHint |
Default panel placement via PanelPlacement DefaultPanel { get; } (PriceOverlay or NewSubpanel). Non-implementers default to PriceOverlay. |
IChartCustomRender |
A custom paint pass on the indicator's own panel: void OnCustomRender(IIndicatorRenderContext), plus CustomRenderLayer Layer, bool SuppressDefaultBarRendering (default false; polled every frame) and int ReservedRightPx (default 0: pixels the overlay occupies against the price axis; the chart reserves the widest value so candles end at the overlay's left edge). |
IChartBackgroundRender |
An extra below-bars paint pass: void OnRenderBackground(IIndicatorRenderContext), for dual-layer studies (volume profile backdrop plus foreground). |
IPricePanelOverlayRender |
Painting onto the main price panel even from a sub-panel: void OnRenderPricePanel(IIndicatorRenderContext) + CustomRenderLayer PricePanelLayer (like NinjaTrader's DrawOnPricePanel). |
IBarColorSource |
Per-bar candle recolouring (like NinjaTrader paint bars): bool PaintBarsEnabled { get; } and bool TryGetBarColor(int barIndex, in Bar bar, out ChartColor color). Last enabled source wins. |
IChartClickReceiver |
React to a left-click in the plot area: bool OnChartClick(ChartClickContext ctx) (return true to consume). Pairs with a custom render that drew the control. |
IChartMouseMoveReceiver |
Hover-driven decorations: void OnChartMouseMove(ChartMouseMoveContext ctx) (a NaN position = mouse left the chart). |
IRepaintNotifier |
Request a frame off-cadence (timer/async): event Action? RepaintRequested + void RequestRepaint(). The chart coalesces N requests to one paint. |
IRecalculateNotifier |
Ask the host to re-run history (not just repaint) when math-affecting state changed: event Action? RecalculateRequested. |
IIndicatorToolbarMenu |
Contribute a chart-toolbar dropdown: string MenuTitle, IReadOnlyList<ToolbarMenuItem> BuildMenu(), event Action? MenuChanged. |
IIndicatorContextMenu |
Contribute right-click menu items at a click location: IReadOnlyList<ToolbarMenuItem> BuildContextMenu(IndicatorContextMenuLocation) + event Action? ContextMenuChanged. |
IDynamicOrderFlowRequirement |
Runtime twin of [RequiresOrderFlow] when the need depends on live config: bool RequiresOrderFlow { get; } + event Action? RequirementChanged. |
IIndicatorCellSearch |
Make the indicator's own values searchable from the chart's search box: string SearchTitle, IReadOnlyList<IndicatorSearchField> SearchFields (what the user can search on) and IReadOnlyList<IndicatorSearchHit> Search(IndicatorSearchQuery query). The indicator decides what a hit means for its study; the host lists the hits and navigates to the one the user picks. |
IIndicatorBarStatistics |
Contribute rows to the per-bar statistics panel: string StatisticsTitle + IReadOnlyList<IndicatorStatisticRow> GetBarStatistics(int absBarIndex). Called for the bar under the cursor, so keep it allocation-light and side-effect free. |
IIndicatorHotkeyTarget |
Receive the keyboard shortcuts the indicator declared with [IndicatorHotkey]: bool InvokeHotkey(string hotkeyId) (return true when handled). The id is the attribute's, so one method switches over them. |
ISubpanelAutoScaleSource |
Custom-render subpanel contributes to Y auto-fit: bool TryGetAutoScaleRange(TimeSpan visibleSpan, out double min, out double max). Pass TimeSpan.Zero to fit over everything. |
Search types (SabrTrader.Pipeline.Indicators.Search):
IIndicatorCellSearch (above);
IndicatorSearchField(string Id, string Name), one searchable field;
IndicatorSearchQuery(string FieldId, double MinValue, IndicatorSearchLocation Location = Anywhere,
IndicatorSearchDirection Direction = Any, int LookbackBars = 0, int MaxHits = 500), the user's
search (LookbackBars = 0 searches all bars);
IndicatorSearchHit(int AbsBarIndex, DateTime TimeUtc, double Price, double Value, string Summary),
where Summary is what the user reads in the result list.
IndicatorSearchLocation enum = Anywhere = 0, High = 1, Low = 2, Body = 3, Wick = 4;
IndicatorSearchDirection enum = Any = 0, Up = 1, Down = 2. All are readonly record structs
except the enums and the interface.
Statistics types (SabrTrader.Pipeline.Indicators.Statistics):
IIndicatorBarStatistics (above);
IndicatorStatisticRow(string Id, string Name, string Value, IndicatorStatisticTone Tone), where
Value is pre-formatted text so the indicator owns its units;
IndicatorStatisticTone enum = Neutral = 0, Positive = 1, Negative = 2 (sets the colour only).
Context structs: ChartClickContext(double X, double Y);
ChartMouseMoveContext(double X, double Y) with IsInside + Outside;
IndicatorContextMenuLocation(int BarIndex, double Price, DateTime Time, double PixelX, double PixelY).
CustomRenderLayer enum = BelowBars = 0, AboveBars = 1 (Layer and
PricePanelLayer default to AboveBars).
PanelPlacement enum = PriceOverlay = 0, NewSubpanel = 1.
IIndicatorRenderContext & menu items
The per-frame context passed to the render capability methods. Single-threaded (the chart's UI thread); do not cache it across frames.
| Member | Description |
|---|---|
IChartRenderer Renderer { get; } |
Active renderer (DrawLine/DrawText/FillRect/…). |
ChartViewport Viewport { get; } |
Bar↔pixel + price↔pixel transforms for the current paint. |
IBarSeries? Bars { get; } |
Primary bar series being rendered (null on an empty tab). |
DateTime Now { get; } |
Wall-clock at frame start, on the chart timeline (same clock as Bar.End; for BarTimer-style countdowns; pinnable in tests). |
PlotAreaRect PlotArea { get; } |
Pixel rect of the candle pane (excludes axis strips). PlotAreaRect(X, Y, Width, Height) with Right/Bottom. |
double HeaderInsetPx { get; } / double ContentTopPx { get; }
|
Top chrome reserved by the legend; safe starting Y for top-anchored content. Default 0. |
TimeZoneInfo? DisplayTimeZone { get; } |
The chart's display zone. Time-of-day shading uses Bar.Start / Bar.End (already stamped); convert an external UTC source with ChartDisplayTime.FromUtc. Default null. |
IReadOnlyList<IDrawingTool> Drawings { get; } |
User-placed drawing tools (read-only). Default empty. |
double? SizeMultiplier { get; } |
Units per one raw volume/size unit for the chart's instrument, or null when sizes are already whole (futures, equities). Crypto sizes are carried as scaled integers (Bar.Volume and Tick.Size are long), so a render pass that prints a volume multiplies by this. Default null. |
PerfScope BeginScope(string category, string? detail = null) |
Nested perf sub-scope for the Task Manager. Default no-op. |
ToolbarMenuItem hierarchy
A closed record hierarchy (the host pattern-matches the concrete types) reused by both
IIndicatorToolbarMenu and IIndicatorContextMenu. The host asks for a fresh list
each time the menu opens, so build it on demand and do not cache it.
| Record | Use |
|---|---|
ToolbarMenuItem(string Label) |
Abstract base. |
ToolbarCommand(Label, Action Invoke, bool IsEnabled = true) |
Click-to-invoke action. |
ToolbarCheckItem(Label, Func<bool> IsChecked, Action<bool> Toggle, bool IsEnabled = true) |
Two-state toggle (re-queried each open). |
ToolbarSubmenu(Label, IReadOnlyList<ToolbarMenuItem> Items, Func<bool>? IsChecked = null) |
Nested submenu. |
ToolbarSeparator() |
Visual divider. |
ToolbarNumberInput(Label, Func<double> GetValue, Action<double> SetValue, double Min, double Max, int Decimals = 0, bool IsEnabled = true) |
Inline numeric field (clamped, rounded). |
ToolbarSlider(…same shape as ToolbarNumberInput…) |
Inline draggable slider. |
ToolbarTextInput(Label, Func<string> GetText, Action<string> SetText, bool IsEnabled = true) |
Inline free-text field (raw string pushed; indicator validates). |
ToolbarDescription(string Text) |
Greyed-out help line. |
ToolbarRadioGroup(Label, IReadOnlyList<ToolbarRadioOption> Options) |
Mutually-exclusive option group. ToolbarRadioOption(Label, Func<bool> IsSelected, Action Select, bool IsEnabled = true). |
ToolbarColorPicker(Label, Func<ChartColor> GetColor, Action<ChartColor> SetColor, bool IsEnabled = true) |
Reversible colour-picker row. |
Helpers
| Type | Description |
|---|---|
CurrentBarsAccessor |
Zero-allocation struct returned by ctx.CurrentBars (public ctor CurrentBarsAccessor(IIndicatorContext ctx)); this[int seriesIndex] is the last-bar index on that local series (-1 if empty). |
IntraBarRefreshGate |
Throttle for expensive intra-bar refresh work: IntraBarRefreshGate(int intervalMs = 250, Func<long>? clockMs = null), bool ShouldRefresh(bool force = false), const int DefaultIntervalMs = 250. Single-threaded; pass clockMs to pin the clock in tests. |
BarTimeProjection (static) |
Time-axis math for custom rendering: double FractionalBarIndex(DateTime t, IReadOnlyList<DateTime> barStarts, TimeSpan barDuration), TimeSpan EstimateBarDuration(IReadOnlyList<DateTime> barStarts, int sample = 64), and the nested BarTimeProjection.BarStartList(IBarSeries bars), a zero-copy IReadOnlyList<DateTime> view of a series' bar start times. |
IndicatorOutputSeries (SabrTrader.Pipeline.Series) |
Wraps an indicator's output series with the bar series it was computed from: new IndicatorOutputSeries(ISeries<double> inner, IBarSeries sourceBars). A downstream indicator that takes the wrapped output as its input reads SourceBars (IBarSourcedSeries) to seed its own primary series. See Child indicators. |
Resolver & ambients
| Type | Members |
|---|---|
IIndicatorResolver |
Type-keyed catalog (built-ins + plugins): IReadOnlyList<Type> AvailableTypes, IIndicator? Create(Type, IReadOnlyDictionary<string,object>? overrides = null) (returns null when unknown). Reachable via ctx.Indicators or IndicatorResolverAmbient.Resolver. |
IndicatorResolverAmbient (static) |
Process-wide resolver the host installs: IIndicatorResolver? Resolver, SetResolver(IIndicatorResolver?), and the scoped IDisposable Use(IIndicatorResolver?) for tests. Resolver is null until a host installs one. |
OptionChainReaderAmbient (static) |
Process-wide options data reader behind ctx.OptionChains: IOptionChainReader? Reader, SetReader(IOptionChainReader?), scoped IDisposable Use(IOptionChainReader?). The host installs it; indicators read ctx.OptionChains. |
Premium definition seam
Lets the host serve a premium indicator's editor metadata from a cloud spec, or withhold it, per entitlement. A process that installs no source sees local reflection for every type. It is host-side only, and content keys are the same under every verdict.
| Type | Members |
|---|---|
IIndicatorDefinitionSource |
DefinitionResult Resolve(Type indicatorType) and long Generation (bumped whenever a verdict can change, so IndicatorMetadata rebuilds its cached entry). Implementations read in-memory state only; no I/O on the call. |
DefinitionVerdict (enum) |
Reflection = 0 (metadata from your attributes), Spec = 1 (metadata from a served IIndicatorDefinitionSpec), Withheld = 2 (empty editor; the indicator still constructs, see IndicatorMetadata.IsInert). |
DefinitionResult (readonly record struct) |
DefinitionResult(DefinitionVerdict Verdict, IIndicatorDefinitionSpec? Spec, long Generation) with the factories Reflection(long generation), FromSpec(IIndicatorDefinitionSpec spec, long generation) and Withheld(long generation). |
IndicatorDefinitionAmbient (static) |
IIndicatorDefinitionSource? Source, SetSource(…), scoped IDisposable Use(…), event Action? Changed and RaiseChanged(). The host raises it after an entitlement or spec change so charts re-evaluate indicators already placed; it can fire on a background thread. |
Alerts
An indicator raises a user-facing alert with ctx.Alert(…), passing an id, a message, an
optional sound, an AlertSeverity and a re-arm window. The call is live-only and returns at once;
the host routes the alert and delivers it to the channels the user has enabled. Guide:
Alerts & channels; types:
Alerts & sessions.
Chart trader panels
An indicator or strategy that implements IChartPanelProvider contributes its own
tab to the chart trader panel. The contracts live in SabrTrader.Pipeline.Panels. Guides:
Indicators › Chart trader panel and
Strategies › Chart trader panel.
public interface IChartPanelProvider
{
ChartPanel? CreateChartPanel(); // null = contribute no tab
void OnChartPanelReleased(ChartPanel panel) { } // the tab is gone; drop your element refs
}
public abstract class PanelSurface // shared root of ChartPanel and AddOnView
{
public IReadOnlyList<ChartPanelElement> Elements { get; } // structure, fixed for its life
public event Action<ChartPanelElement>? ElementChanged; // host-facing
public void Release(); // host-facing
}
public sealed class ChartPanel : PanelSurface
{
public ChartPanel(string title, params ChartPanelElement[] elements);
public string Title { get; } // tab header, fixed for its life
}
public abstract class ChartPanelElement
{
public bool IsEnabled { get; set; } // greyed out and non-interactive while false
public bool IsVisible { get; set; } // out of the layout entirely while false
public ChartPanelAccent Accent { get; set; } // Neutral | Positive | Negative | Warning | Info
protected void Invalidate(); // tell the host to re-read this element
}
public abstract class ChartPanelContainer : ChartPanelElement // base of Row and Group
{
public IReadOnlyList<ChartPanelElement> Children { get; } // fixed at construction
}
ElementChanged is raised on whichever thread wrote the element; the host marshals and
coalesces. Release() is called by the host once the tab has left the screen: it detaches every
element (so you can reuse the instances in the next panel you build) and stops ElementChanged.
ChartPanelAccent values: Neutral = 0, Positive = 1, Negative = 2, Warning = 3,
Info = 4, resolved against the user's theme. Hiding a container hides its children.
The element kinds, all of them writable at any time from any thread:
new ChartPanelButton(string text, Action onClick);
new ChartPanelToggle(string text, Action<bool> onToggled, bool isOn = false);
new ChartPanelCheckBox(string text, Action<bool> onChanged, bool isChecked = false);
new ChartPanelLabel(string caption, string text = "");
new ChartPanelChoice(string caption, IReadOnlyList<string> options, Action<int> onSelected,
int selectedIndex = 0);
new ChartPanelNumber(string caption, Action<double> onChanged, double value = 0,
double minimum = 0, double maximum = double.MaxValue,
double step = 1, int decimals = 0);
new ChartPanelText(string caption, Action<string> onChanged, string text = "");
new ChartPanelSeparator();
new ChartPanelRow(params ChartPanelElement[] children);
new ChartPanelGroup(string caption, params ChartPanelElement[] children);
Setting a value property (Text, IsOn, IsChecked,
SelectedIndex, Value) updates the control without raising your callback,
so reflecting a state you changed yourself cannot re-enter your own handler. Read-only members:
Caption on the captioned kinds, ChartPanelChoice.Options and
SelectedOption (the selected text), and ChartPanelNumber.Minimum /
Maximum / Step / Decimals.
The host applies user input through ChartPanelButton.Click(),
ChartPanelToggle.Toggle(bool), ChartPanelCheckBox.Check(bool),
ChartPanelChoice.Select(int), ChartPanelNumber.Commit(double) (clamps and rounds
first) and ChartPanelText.Commit(string). Each stores the value and raises your callback on the
plugin's callback thread; plugin code uses the value properties instead. An element belongs to one panel at a
time: passing it to a second panel throws until the first is released.