Registration & discovery

Market Analyzer columns

Registration & discovery

You don't register a column by hand. The host discovers your definition type the same way it discovers indicators, drawing tools and bar builders — by scanning a dropped-in plugin DLL. This chapter explains how that discovery works, how the catalog resolves a saved column id back into a live definition, and what you must satisfy for it all to succeed.

Attribute-based discovery

A column participates in discovery by being a public, concrete IColumnDefinition with a zero-arg-callable constructor — the same convention every other plugin kind uses. The host scans your DLL, finds the type, and registers it. The [MarketAnalyzerColumn] attribute is not required for discovery itself; it enriches how the column is presented in the picker (display name, category, description). Without it the column still loads, but appears under its Header with an empty category.

MidPriceColumn.csusing TradeStrike.Pipeline.MarketAnalyzer;

namespace MyPlugin.Columns;

// Public, concrete, parameterless ctor + the attribute = discovered and categorized.
[MarketAnalyzerColumn("Mid price",
    Category    = "My plugin",
    Description = "(Bid + Ask) / 2, live from the venue's quote stream.")]
public sealed class MidPriceColumn : IColumnDefinition
{
    public string Id => "myplugin.midprice";
    public string Header => "Mid";
    public ColumnDataType DataType => ColumnDataType.Price;

    public IColumnValueSource CreateSource(InstrumentRef instrument, IColumnContext context)
        => new MidPriceSource(instrument, context);
}
Same spine as indicators. If you've shipped an indicator or drawing-tool plugin, there is nothing new to learn here: build a class library that references the TradeStrike.Pipeline.Contracts SDK, implement the contract interface, drop the DLL where the host scans. See Building & deploying for the shared discovery pipeline.

The catalog: resolving ids

A workspace snapshot stores a column by its Id string, never by type. On restore (and when the Columns dialog is opened) the host turns that id back into a live IColumnDefinition through an IColumnDefinitionCatalog. The UI and the restore flow both code against this interface, never against any concrete registry — that is what lets the host chain built-ins, your plugin columns and future column kinds behind one seam.

IColumnDefinitionCatalog.csnamespace TradeStrike.Pipeline.MarketAnalyzer;

public interface IColumnDefinitionCatalog
{
    // Resolve a column id back into its definition, or null when this
    // catalog doesn't know the id. A chained catalog delegates further;
    // an id no catalog knows (e.g. a column type a newer build shipped)
    // is dropped rather than failing the whole workspace.
    IColumnDefinition? Resolve(string columnId);

    // All ids this catalog can resolve, for the column-picker dialog.
    IReadOnlyList KnownIds { get; }

    // Display metadata for one known id. Default: derived from the
    // resolved definition (header as display name, empty category /
    // description) so catalogs that predate descriptors keep working.
    ColumnDescriptor? Describe(string columnId) { /* default impl */ }
}

You rarely implement this interface yourself — the host's plugin registry already exposes your column through a catalog and merges it with the built-in catalog. Implement IColumnDefinitionCatalog directly only when you ship a family of columns generated at runtime (for example, one column per item in an external schema) rather than a fixed set of attributed classes.

Unknown ids are dropped, not fatal. If a saved workspace references a column id that no longer resolves — an uninstalled plugin, a renamed id — the host drops that one column and restores the rest. This is why a shipped Id must be treated as a permanent contract.

ColumnDescriptor & the picker

The Columns dialog's "Available" list is populated from ColumnDescriptors — pure display metadata for one column id. When you supply [MarketAnalyzerColumn], the host builds the descriptor from your attribute; otherwise it derives a minimal one from the definition.

ColumnDescriptor.csnamespace TradeStrike.Pipeline.MarketAnalyzer;

public sealed record ColumnDescriptor(
    string Id,                      // the stable column id
    string DisplayName,             // name shown in the Available list
    string Category,                // picker grouping ("Market data", "Session", ...)
    string Description,             // one-line tooltip / detail text
    ColumnDataType DataType,        // the cell payload type
    bool HasSettings = false,       // true when the column exposes [ColumnParameter]s
    bool IsEditable  = false);      // true when the user can type into the cell (e.g. Notes)

The descriptor fields map straight to what the dialog shows:

Field Sourced from Shown as
DisplayName [MarketAnalyzerColumn] display name The entry's label in "Available".
Category attribute Category The grouping header the entry sits under.
Description attribute Description Tooltip / detail text.
HasSettings presence of [ColumnParameter] properties Drives whether a Properties pane appears for the entry.
IsEditable your column kind Whether the user can type into cells (mirrors NinjaTrader's editable-column capability).

Deployment

  1. Create a .NET class library that references the TradeStrike.Pipeline.Contracts SDK package.
  2. Add your IColumnDefinition classes (and their sources), each public, concrete and parameterless-constructable, decorated with [MarketAnalyzerColumn].
  3. Build the DLL and place it in the host's plugin folder (the same location your indicators and drawing tools deploy to — see Where to deploy).
  4. Restart the host. Your columns appear in the Market Analyzer's Columns… dialog, categorized and searchable, and resolve on workspace restore.
One DLL, many plugin kinds. A single plugin assembly can carry indicators, drawing tools, bar builders and Market Analyzer columns side by side — the host's discovery scan registers each kind into its own catalog from the one DLL.

What the host enforces (fail-loud at install)

The plugin loader validates your column when it loads the DLL — not silently at workspace restore — so mistakes surface immediately:

  • Public, non-abstract class implementing IColumnDefinition with a public parameterless constructor.
  • Id non-blank and globally unique. A collision with another plugin or a built-in refuses registration with a descriptive error.
  • Every [ColumnParameter] property has a public getter and setter and one of the supported types (bool, int, long, double, decimal, string, TimeSpan, DateTime, enums). Anything else fails at install.

What the host does for you

Discovery

Finds your definition types, registers them, and merges them with the built-ins through a composite catalog — so your column appears in the Columns dialog and resolves on restore.

Persistence

Stores your column Id plus every [ColumnParameter] value in the workspace and re-applies them on restore. A malformed saved value skips just that parameter (your default survives) and warns in the activity log.

Styling

Header override, colors, visibility and decimal places from the Columns dialog apply to your column exactly like a built-in. You do nothing.

Conditions & sort

Numeric cells join conditional formatting, alerts and sorting automatically — publishing a CellValue.Ready(double) from a numeric DataType is all it takes.

That completes the Market Analyzer column chapter. To put cell values to work — firing on a threshold, routing to a channel — continue to Alerts & channels.