Chart trader panel
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.
On this page
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 your plugin needs no UI framework.
When the panel appears
A panel belongs to a chart, so it appears when the user attaches the running strategy to one,
through the chart's "Strategies on this chart" dialog or the picker on the Strategies tab. At
that moment the host calls CreateChartPanel() and the tab appears. Detach the
strategy, or stop the run, and the host removes the tab and calls
OnChartPanelReleased.
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;
using SabrTrader.Pipeline.Venues.Trading;
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;
private bool _isPaused;
private int _quantity = 1;
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;
_flatten = null;
_paused = null;
}
protected override void OnBar()
{
if (_status is null || _flatten is null) return; // not on a chart right now
Position? position = Position;
_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:
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, with no colours in your code.
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.AverageEntryPrice}";
_status.Accent = pnl >= 0 ? ChartPanelAccent.Positive : ChartPanelAccent.Negative;
_flatten.IsEnabled = position is not null;
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.
Element callbacks fire for user edits only. 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.