A complete add-on
A complete add-on WORKED EXAMPLE
Every file of a working add-on: the project, the manifest, the window, its declared view and its workspace state.
What it does
An Account Summary window: pick an account, see its balance and its open positions, and get the same account back when you reopen the workspace. It is small on purpose, and it exercises the whole surface: a manifest, a menu entry, a declared view, live platform state, and workspace state.
Four files. Nothing else, and no reference to any UI framework.
The project
SabrTrader.Examples.SampleAddOn.csproj<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<AssemblyName>SabrTrader.Examples.SampleAddOn</AssemblyName>
<!-- Drop the built DLL into the host's Plugins folder on every build. -->
<DeployToAlgoStudioPlugins>true</DeployToAlgoStudioPlugins>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SabrTrader.Sdk" />
</ItemGroup>
</Project>
The add-on
The whole required surface: a manifest, and one window put in the New menu.
AccountSummaryAddOn.csusing SabrTrader.Pipeline.AddOns;
public sealed class AccountSummaryAddOn : IAddOn
{
private IAddOnHost? _host;
public AddOnManifest Manifest { get; } = new(
id: "sabrtrader.samples.account-summary",
name: "Account Summary",
vendor: "SabrTrader Samples",
version: "1.0.0")
{
Description = "Shows balance and open positions for one account.",
};
public AddOnContributions Initialize(IAddOnHost host)
{
_host = host;
host.Log.Info("Account Summary is ready.");
return new AddOnContributions(
windows: new[]
{
new AddOnWindowKind("summary", () => new AccountSummaryWindow(host))
{
MenuTitle = "Account Summary",
Description = "Balance and open positions for one account.",
MenuSection = AddOnMenuSection.New,
},
});
}
public void Shutdown() => _host?.Log.Info("Account Summary stopped.");
}
The state record
AccountSummaryState.cspublic sealed class AccountSummaryState
{
/// <summary>The account the window was showing, or empty for the first connected account.</summary>
public string? Account { get; set; }
}
The window
Elements are fields, so the view reads as a layout and every update is a plain property
write. Refresh runs on the window's own thread, so it touches those fields
directly.
AccountSummaryWindow.csusing System;
using System.Text;
using SabrTrader.Pipeline.AddOns;
using SabrTrader.Pipeline.Panels;
using SabrTrader.Pipeline.Venues.Trading;
public sealed class AccountSummaryWindow : IAddOnWindow, IAddOnWindowState<AccountSummaryState>
{
private readonly IAddOnHost _host;
private readonly ChartPanelText _account = new("Account", _ => { });
private readonly ChartPanelLabel _balance = new("Balance", "-");
private readonly ChartPanelLabel _positions = new("Positions", "-");
private readonly ChartPanelLabel _status = new("", "Choose an account and press Refresh.");
public AccountSummaryWindow(IAddOnHost host) => _host = host;
public string Title => "Account Summary";
public int DefaultWidth => 420;
public int DefaultHeight => 320;
public AddOnView CreateView() => new(
_account,
new ChartPanelRow(new ChartPanelButton("Refresh", Refresh)),
new ChartPanelSeparator(),
_balance,
_positions,
_status);
public void OnOpened() => Refresh();
public AccountSummaryState Save() => new() { Account = _account.Text };
public void Restore(AccountSummaryState state) => _account.Text = state.Account ?? "";
private void Refresh()
{
var trading = _host.Trading;
if (trading is null)
{
_status.Text = "This installation has no trading connection.";
_status.Accent = ChartPanelAccent.Warning;
return;
}
string wanted = _account.Text.Trim();
foreach (var account in trading.Accounts)
{
if (wanted.Length != 0 &&
!string.Equals(account.DisplayName, wanted, StringComparison.OrdinalIgnoreCase))
continue;
_account.Text = account.DisplayName;
_balance.Text = account.CashValue.ToString("N2");
_positions.Text = DescribePositions(trading, account.Id);
_status.Text = $"Updated {DateTime.Now:HH:mm:ss}";
_status.Accent = ChartPanelAccent.Neutral;
return;
}
_status.Text = wanted.Length == 0 ? "No accounts connected." : $"No account '{wanted}'.";
_status.Accent = ChartPanelAccent.Warning;
}
private static string DescribePositions(ITradingService trading, AccountId account)
{
var text = new StringBuilder();
foreach (var position in trading.Positions)
{
if (!position.AccountId.Equals(account) || position.Quantity == 0) continue;
if (text.Length > 0) text.Append(", ");
text.Append(position.Instrument).Append(' ').Append(position.Quantity);
}
return text.Length == 0 ? "Flat" : text.ToString();
}
}
Build it and run it
-
Build in Release. With
DeployToAlgoStudioPluginson, the DLL lands in your plugins folder automatically. - Open the menu. New › Add-ons › Account Summary. If the platform was already running, the rebuild hot-reloads and the entry is there without a restart.
- Save a workspace with the window open, switch to another workspace and back. The window returns on the same account, in the same place.
- Check the log. The Control Center's Log tab shows "Account Summary is ready." filed under your add-on's name. Anything your add-on throws shows up there too.
Examples/SampleAddOn.