Registration & discovery

Market Analyzer columns

Registration & discovery

You don't register a column by hand. The host discovers your definition type by scanning the plugin DLLs in its plugin folder, the same scan that finds indicators, strategies and bar types. This chapter explains how that discovery works, how the host resolves a saved column id back into a live definition, and what your column must satisfy.

Discovery

A column takes part in discovery by being a public, concrete IColumnDefinition with a zero-arg-callable constructor (a parameterless constructor, or one whose parameters all have default values). The host scans your DLL, finds the type, creates one instance to read its Id, and registers it. The [MarketAnalyzerColumn] attribute controls how the column is presented in the picker (display name, category, description); a column without it appears under its Header with an empty category.

MidPriceColumn.csusing System;
using System.Threading;
using System.Threading.Tasks;
using SabrTrader.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 row's quote.")]
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(context);
}

internal sealed class MidPriceSource : IColumnValueSource
{
    private readonly IColumnContext _context;
    private IDisposable? _subscription;

    public MidPriceSource(IColumnContext context) => _context = context;

    public Task StartAsync(IColumnValueSink sink, CancellationToken cancellationToken)
    {
        var feed = _context.GetService<IInstrumentFeed>();
        if (feed is null) { sink.Publish(CellValue.Error("No market data for this row.")); return Task.CompletedTask; }

        _subscription = feed.OnQuote(change =>
        {
            var q = change.Quote;
            if (q.HasBid && q.HasAsk) sink.Publish(CellValue.Ready((q.Bid + q.Ask) / 2));
        });
        return Task.CompletedTask;
    }

    public ValueTask DisposeAsync()
    {
        _subscription?.Dispose();
        return ValueTask.CompletedTask;
    }
}
Same pipeline as the other plugin kinds. Build a class library that references the SabrTrader.Pipeline.Contracts SDK, implement the contract interface, and put 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 opens) the host turns that id back into a live IColumnDefinition through its column catalog. The host chains its built-in columns with the plugin columns it discovered, so the dialog, the restore flow and expressions see built-in and plugin columns the same way. Your plugin supplies IColumnDefinition types; the host owns the catalog.

Unknown ids are dropped, not fatal. If a saved workspace references a column id that no installed column reports (an uninstalled plugin, a changed id), the host drops that one column and restores the rest. That is why a shipped Id is a permanent contract.

The column picker

The Columns dialog's "Available" list shows one entry per column id. For a plugin column the host builds that entry at install time from your [MarketAnalyzerColumn] attribute and your [ColumnParameter] properties, or derives a minimal one from the definition when the attribute is absent. Each part of the entry comes from your code like this:

Entry part Sourced from Shown as
Display name [MarketAnalyzerColumn] display name (else Header) The entry's label in "Available".
Category attribute Category The grouping header the entry sits under.
Description attribute Description Tooltip / detail text.
Settings presence of [ColumnParameter] properties Whether a Properties pane appears for the entry.
Editable set by the host for its built-in Notes column; false for plugin columns Whether the user can type into cells.

Deployment

  1. Create a .NET class library that references the SabrTrader.Pipeline.Contracts SDK package.
  2. Add your IColumnDefinition classes (and their sources), each public, concrete and constructible without arguments, decorated with [MarketAnalyzerColumn].
  3. Build the DLL and place it in the host's plugin folder (the same location as your indicators; see Where to deploy).
  4. Start 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 host validates your column when it installs the plugin, so mistakes surface immediately rather than at workspace restore:

  • Public, non-abstract class implementing IColumnDefinition with a zero-arg-callable constructor.
  • Id non-blank and globally unique. A plugin type claiming an id another plugin type already registered is refused 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 stays) and writes a warning to the activity log.

Styling

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

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. Cell-condition alerts reach the user through the platform's alert pipeline, described in Alerts & channels.