Registration & discovery
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.
On this page
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;
}
}
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.
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
- Create a .NET class library that references the
SabrTrader.Pipeline.ContractsSDK package. - Add your
IColumnDefinitionclasses (and their sources), each public, concrete and constructible without arguments, decorated with[MarketAnalyzerColumn]. - Build the DLL and place it in the host's plugin folder (the same location as your indicators; see Where to deploy).
- Start the host. Your columns appear in the Market Analyzer's Columns… dialog, categorized and searchable, and resolve on workspace restore.
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
IColumnDefinitionwith a zero-arg-callable constructor. -
Idnon-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.