Chart trader panel

Strategies

Chart trader panel INTERFACE

Give your strategy the control panel traders expect: long-only or short-only, run or pause, one-shot or continuous, flatten now, and a live status readout, as its own tab on the chart.

What it is

Implement IChartPanelProvider and your strategy contributes its own tab to the chart trader panel, beside Trade, Wallet and the AI assistants. This is where a strategy gets the control panel traders expect from it: long-only or short-only, run or pause, one-shot or continuous, flatten now, plus a live status readout. You describe the controls; the host renders them natively, so there is no UI framework anywhere in your plugin.

When the panel appears

A strategy runs in the dark. Starting one does not create a panel, because a run with no chart has nowhere to put it. The moment the user attaches the strategy to a chart, through the chart's "Strategies on this chart" dialog or the picker on the Strategies tab, the host calls CreateChartPanel() and the tab appears. Detach it, or stop the run, and the tab is removed again and OnChartPanelReleased tells you so.

One strategy owns one panel, however many charts show it. Attach the same strategy to three charts and all three show the SAME live panel, which is what makes it a control surface for the run instead of three copies drifting apart. CreateChartPanel can be called again after a release, so build a fresh panel each time rather than caching one.

Building the panel

Construct the elements you want, hand them to a ChartPanel with a tab title, and keep references to the ones you intend to update later.

GridStrategy.cs (control panel)using SabrTrader.Pipeline.Panels;
using SabrTrader.Pipeline.Strategies;

public sealed class GridStrategy : Strategy, IChartPanelProvider
{
    private ChartPanelLabel  _status;
    private ChartPanelButton _flatten;
    private ChartPanelToggle _paused;

    private int  _direction;        // 0 both, 1 long only, 2 short only
    private bool _oneShot;

    public ChartPanel CreateChartPanel()
    {
        _status  = new ChartPanelLabel("Position", "Flat");
        _paused  = new ChartPanelToggle("Pause", on => _isPaused = on);
        _flatten = new ChartPanelButton("Flatten", Flatten)
        {
            Accent = ChartPanelAccent.Negative,
            IsEnabled = false,
        };

        return new ChartPanel("Grid",
            new ChartPanelChoice("Direction",
                new[] { "Both", "Long only", "Short only" },
                i => _direction = i),
            new ChartPanelChoice("Mode",
                new[] { "Continuous", "One-shot" },
                i => _oneShot = i == 1),
            new ChartPanelNumber("Quantity",
                v => _quantity = (int)v,
                value: 1, minimum: 1, maximum: 20, step: 1),
            new ChartPanelSeparator(),
            new ChartPanelRow(
                new ChartPanelButton("Buy", () => EnterLong(_quantity))
                    { Accent = ChartPanelAccent.Positive },
                new ChartPanelButton("Sell", () => EnterShort(_quantity))
                    { Accent = ChartPanelAccent.Negative }),
            _paused,
            _flatten,
            new ChartPanelSeparator(),
            _status);
    }

    public void OnChartPanelReleased(ChartPanel panel) => _status = null;

    protected override void OnBar()
    {
        if (_status is null) return;              // not on a chart right now
        _status.Text = Position is null ? "Flat" : Position.Quantity.ToString("+0;-0");
        _flatten.IsEnabled = Position is not null;
    }
}

Return null from CreateChartPanel to contribute no tab after all, for a configuration where the panel would be pointless. The tab title is fixed for the panel's life, so put anything that changes into a ChartPanelLabel rather than into the title.

The element kinds

Element What it is
ChartPanelButton A push button. Its callback runs when the user presses it.
ChartPanelToggle A two-state button that stays pressed while it is on: Run / Pause, Long only, Arm.
ChartPanelCheckBox A labelled checkbox, for an option the user ticks rather than a button they press.
ChartPanelChoice One of a fixed set of options: Both / Long only / Short only, One-shot / Continuous.
ChartPanelNumber A numeric field. The host clamps and rounds every edit to your declared range before your callback sees it, and the mouse wheel steps it by your step.
ChartPanelText A single-line text field. You get the raw string and may write a normalised value back.
ChartPanelLabel A read-only caption and value. This is where a live status belongs.
ChartPanelRow Lays its children side by side on one line, sharing the width evenly.
ChartPanelGroup A captioned block of elements, for when a panel grows past a handful of controls.
ChartPanelSeparator A horizontal rule between two stretches of the panel.

Every element carries IsEnabled, IsVisible and an Accent. The accent is a semantic role, not a colour: Neutral, Positive, Negative, Warning, Info. The host resolves it against whichever theme the user is running, so a panel keeps matching the platform when they switch theme and you never hardcode a brush.

Keeping it live

The elements are live objects, not a snapshot you re-emit. Write to them whenever you have something new and the tab follows:

updating the panel_status.Text   = position is null ? "Flat" : $"{position.Quantity:+0;-0} @ {position.AveragePrice}";
_status.Accent = pnl >= 0 ? ChartPanelAccent.Positive : ChartPanelAccent.Negative;
_flatten.IsEnabled = position is not null;

Nothing is rebuilt when you do that. The structure of a panel is fixed the moment you construct it, and only the value you changed is re-read, so the user never loses focus, a scroll position or a half-typed value because an update landed while they were working.

Updating on every tick is fine. A burst of writes collapses into one repaint showing the newest value, so a status readout costs nothing per tick and a fast market cannot flood the UI.

Writing a value from your own code never raises your own callback. That is what lets you reflect a state you changed yourself, a strategy pausing on a risk stop for example, without re-entering the handler that would have made the change.

Threading

Element callbacks run on your strategy's own serialised thread, the same one that runs OnBar and OnFill. They never run concurrently with those hooks and never nest inside one. That is the whole point: a button handler may read and write your fields and place orders exactly like any other strategy code, with no locks and no risk of landing halfway through a bar.

Writing to elements works from anywhere. Updating a label from OnBar is the normal case and needs no marshalling of any kind.

A handler that throws is treated as strategy code throwing, exactly as a throw from OnBar would be: the run faults and the Strategies tab shows why. Handle what you expect to fail.

Where to go next

An indicator can contribute a tab the same way, with the same elements and the same live updates; see Indicators › Chart trader panel for the differences in its lifecycle. For a member-level reference of the panel contracts, see Reference › Indicators.