Custom modules
Note
Requires ATAS 8.0.15 or later: ATAS 7.0.9 has no custom module API.
A custom module is a panel of your own: a window of the classic ATAS (docked or floating, like the Market Pressure widget) and a widget of ATAS X, whose whole client area your code draws. One class serves both platforms: the module draws through the platform drawing API (Drawing), never through WPF or Avalonia controls, so the same DLL looks and behaves the same in ATAS and in ATAS X. A module:
- draws its client area with
RenderContextand asks for a new frame when its picture changes; - receives the trades, best bid and ask changes, order book changes and exchange statistics of the instrument selected in the panel, and reads the latest state of that data through its context;
- can ask for the same data of other instruments, up to 16 next to the selected one;
- can put buttons of its own into the header of the panel and hide some of the standard ones;
- gets the size changes of the panel, mouse input (move, press, release, click, wheel), keyboard input and a timer;
- has settings: public properties edited in the settings window of the panel and saved with the workspace.
A module reads market data and draws. It cannot place, modify or cancel orders and has no access to accounts or positions.
What you build
A public class derived from PlatformModule with a public parameterless constructor,
marked with [PlatformModule(id)]: the stable module id (see below) and, optionally, DisplayName, the name in the
menu and in the panel title. OnRender is the only required override.
The user opens a module in the classic ATAS from the Widgets tab of the main window ribbon, button Custom modules, and in ATAS X from the + button of the workspace tabs, item Custom modules: one item per installed module, shown only while at least one module is installed. Like the built-in widgets, a new panel first asks for the instrument; the panel has the usual instrument selector in its header, can be linked to other panels with the group color, and has a settings button. Several panels of the same module can be open at the same time, each with its own instrument and settings.
Lifecycle
| Method | Called |
|---|---|
| constructor | when the panel is opened or loaded with the workspace (and the tariff allows custom modules); the saved settings are applied right after it |
OnInitialize() |
once per instance, when it is attached to the panel; Context is available from here on |
OnSettingsChanged() |
after OnInitialize, and after the user changed a setting |
OnInstrumentChanged() |
when the panel's instrument is known (and every time the user selects another one); Context.Instrument is the new instrument |
OnSummaryChanged(summary) |
right after OnInstrumentChanged when the feed has already reported the exchange statistics of the instrument, and every time they change |
OnNewTrades(trades) |
new trades of the instrument (MarketDataArg, DataType == Trade), oldest first |
OnBestBidAskChanged(bestPrice) |
the best bid or ask changed (DataType is Bid or Ask) |
OnMarketDepthChanged(levels) |
order book levels changed (DataType is Bid or Ask; a zero volume removes the level); the first call after OnInstrumentChanged carries the whole book of the new instrument |
OnInstrumentStateChanged(instrument) |
the state of an additional instrument changed |
OnNewTrades(instrument, trades), OnBestBidAskChanged(instrument, bestPrice), OnMarketDepthChanged(instrument, levels), OnSummaryChanged(instrument, summary) |
the same data callbacks for an additional instrument: the overloads whose first argument is the instrument |
OnButtonClick(button) |
the user clicked a header button of the module |
OnTimer() |
about ten times per second while the panel is open, also while it is hidden behind another tab |
OnSizeChanged(size) |
before the first frame and whenever the panel is resized |
OnRender(context, bounds) |
to draw a frame |
OnMouseMove, OnMouseDown, OnMouseUp, OnMouseClick, OnMouseWheel, OnMouseLeave |
mouse input over the module |
OnKeyDown, OnKeyUp |
keyboard input while the module has the focus (after the user clicked it) |
OnDispose() |
once, when the platform stops using the instance (see Errors and OnDispose) |
Drawing
OnRender(context, bounds) draws the whole client area: bounds starts at (0, 0) and has the size of the panel.
Before the call the platform has filled it with Context.BackgroundColor (a dark or a light background, following
the platform theme) and clipped the drawing to it. Use Context.TextColor to match it and Context.Font for text;
Context.Font is a fixed font (Segoe UI, 10), not a font of the theme.
The platform does not redraw the module continuously: a frame is drawn when the panel is shown or resized, when
the theme changes, and when your module calls Redraw() (calls are merged; the frame follows shortly
afterwards). Call Redraw() from a data callback or from OnTimer whenever the picture changed; for an animation,
from OnTimer (about ten frames per second). OnRender is called for every frame: keep it cheap, compute in the
data callbacks and in OnTimer, draw from fields.
Input
The mouse callbacks receive RenderControlMouseEventArgs (namespace OFT.Rendering.Control): the position in the
client area (X, Y), Button, Clicks, Delta for the wheel and the modifier keys (Alt, Control, Shift).
Set Handled = true in OnMouseClick to suppress the panel context menu of a right click. A left click on a module
stopped by an error restarts it and is not passed to the module.
The keyboard callbacks receive PlatformModuleKeyEventArgs: Key is the name of the key as in the Key
enumerations of WPF (classic ATAS) and Avalonia (ATAS X): "A"..."Z", "D0"..."D9" for the digits of the main
keyboard, "NumPad0"..., "F1"..., "Left", "Right", "Up", "Down", "Return" for the Enter key, "Escape",
"Space", "Back", "Delete", ..., plus the modifier keys. Most names are the same in both platforms, but not all:
Page Down is "Next" in the classic ATAS and "PageDown" in ATAS X, Caps Lock is "Capital" and "CapsLock";
accept both names for such keys. Set Handled = true if the module used the key. On macOS the Cmd key is reported
as Control.
The context
Context (IPlatformModuleContext) is available from OnInitialize on:
| Member | Meaning |
|---|---|
Instrument |
the instrument of the panel (IInstrumentInfo: name, exchange, tick size, TimeZone / TimeZoneOffset), or null while none is selected |
BestBid, BestAsk |
the latest best prices delivered to the module, or null |
LastTrade |
the latest trade delivered to the module, or null |
Summary |
the latest exchange statistics (InstrumentDailySummary: day open, high, low, volume, previous close, ...), or null |
GetDepth(isAsk) |
the bid or ask levels of the order book as of the delivered depth changes, best first (copies) |
Size |
the size of the client area |
BackgroundColor, TextColor, Font |
a dark or a light pair of colors following the platform theme, and the font for text |
FormatPrice(price) |
the price with the decimals of the instrument |
Redraw() |
the same as the module's Redraw() |
Buttons, MaxButtons, HiddenStandardButtons |
the header buttons of the module and the standard buttons it hides |
AdditionalInstruments, MaxAdditionalInstruments, AddInstrument(codeOrId, exchange, withDepth), RemoveInstrument(instrument), FindInstruments(text, maxCount) |
the instruments the module added to the selected one |
The data members return the state as of the last data delivered to your module: they agree with the callbacks you have already received, and they do not change while one of your methods runs.
Header buttons
A module can put buttons of its own into the header of its panel: in ATAS next to the standard header buttons of the window, in ATAS X in the toolbar of the widget, before the settings button. The buttons are described in the module, without WPF or Avalonia types, so one DLL shows them in both platforms.
private readonly PlatformModuleButton _pause = new("pause")
{
ToolTip = "Pause counting",
Icon = PlatformModuleIcons.Pause,
IsCheckable = true
};
protected override void OnInitialize()
{
_pause.Click = () =>
{
// The platform has already flipped the toggle; the icon and the hint follow.
_pause.Icon = _pause.IsChecked ? PlatformModuleIcons.Play : PlatformModuleIcons.Pause;
_pause.ToolTip = _pause.IsChecked ? "Continue counting" : "Pause counting";
};
Context.Buttons.Add(_pause);
Context.Buttons.Add(new PlatformModuleButton("reset", "Reset", Reset) { ToolTip = "Reset the counters" });
}
Member of PlatformModuleButton |
Meaning |
|---|---|
Id |
the id of the button, unique among the buttons of the module |
Text |
the caption; null for a button with an icon only. Captions longer than 32 characters are cut |
ToolTip |
the hint |
Icon |
the icon as path geometry data in a 16×16 box, the syntax WPF and Avalonia share ("M4,2 L14,8 L4,14 Z"). The platform scales it to the size of the header icons and fills it with the color of the header. PlatformModuleIcons has ready-made ones (Play, Pause, Stop, Plus, Minus, Check, Cross, Refresh, ArrowUp, ArrowDown). Data the platform cannot read or draw (not path data, coordinates that are not finite numbers, more than 4096 characters) is ignored: the button shows its caption or its id |
IsCheckable, IsChecked |
a toggle: every click of the user flips IsChecked before the click is delivered; a checked button is highlighted |
IsEnabled |
false greys the button out; it is not clicked |
IsVisible |
false hides the button without removing it from the list |
Click |
called when the user clicks the button |
A button with neither a caption nor an icon shows its id.
Clicks. A click calls OnButtonClick(button) of the module, whose default implementation calls button.Click.
Override OnButtonClick to handle all buttons in one place, or give every button its Click: both are ordinary
module callbacks, never concurrent with the other callbacks, with the same time limit, and an exception thrown there
stops the module.
Changing the buttons. Add, remove and change the buttons at any time from the callbacks of the module: the
header follows shortly afterwards. A module can have at most Context.MaxButtons (16) buttons; adding more, or two
buttons with one id, throws. The buttons stand in the order of Context.Buttons, left to right, to the left of the
standard buttons. In a narrow panel the buttons that do not fit move into the overflow menu of the header, the
leftmost first, so declare the button that must stay at hand last. In the menu a button is named by its caption,
its hint or its id: give icon-only buttons a ToolTip.
Standard buttons. Context.HiddenStandardButtons hides standard header buttons the module does not need; set it
to None to show them again:
PlatformModuleStandardButtons |
ATAS | ATAS X |
|---|---|---|
InstrumentSelector |
hidden | hidden |
Settings |
hidden, but only for a module that has no settings: a module with at least one setting shown in the settings window always keeps the button | the same |
OnTop |
hidden | hidden |
Clone |
hidden | ignored (the widget header has no such button) |
Layouts |
hidden (the button that docks the window and saves window layouts) | ignored |
Hiding a button removes the button only: the instrument can still be changed through the group link, and the panel
context menu keeps its commands. The module cannot remove the close button, the group link selector and the
maximize button. The buttons belong to the running module instance: when the module is stopped by an error, is not
available on the tariff or its DLL is missing, its buttons disappear and the standard buttons come back; a restarted
module declares its buttons again in OnInitialize.
Additional instruments
The module gets the data of the instrument selected in the panel without asking. It can also ask for the data of
other instruments with Context.AddInstrument:
protected override void OnSettingsChanged()
{
// The list is a setting: saved with the workspace, applied at start and after every change by the user.
if (Instruments == _subscribed)
return;
_subscribed = Instruments;
foreach (var instrument in Context.AdditionalInstruments)
Context.RemoveInstrument(instrument);
_counts.Clear();
foreach (var code in Instruments.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
// null: the platform does not know the instrument, or the module already has too many.
if (Context.AddInstrument(code) is { } instrument)
_counts[instrument] = 0;
}
Redraw();
}
Naming an instrument. Both ways go into the first argument of AddInstrument:
- By code, what a user types: the code of the instrument as the platform shows it in the instrument selector
(
Context.Instrument.Instrumentis such a code) and, optionally, the exchange, its code or its name asIInstrumentInfo.Exchangeshows it;"code@exchange"in the first argument works as well. Pass the exchange where a code exists on several (the same crypto pair on several exchanges). A code does not always name one instrument even with the exchange: a delayed data source lists the same contract next to the real-time one. Of such namesakes the platform takes the one of the same kind as the instrument selected in the panel, and otherwise the real-time one. - By identifier, what a program keeps: every instrument has an
Id, an opaque text that names exactly this instrument and stays the same between runs of the platform.FindInstruments(text, maxCount)searches the instruments the platform knows by code and name, best matches first (at most 100), and returnsPlatformModuleInstrumentInfoitems (Id,Code,Exchange,Name,TickSize,IsDelayed) to show to the user; an added instrument has the sameId. Do not parse an identifier and do not show it.
The result of AddInstrument is null when the platform does not know the instrument right now, when the module
already has Context.MaxAdditionalInstruments (16) instruments, or when the argument is empty. The list of
instruments is loaded after the logon and the instruments of a data source appear when it connects, so an instrument
that is unknown at start may become known later: a module that restores its instruments asks again for the missing
ones, for example from OnTimer. Adding an instrument the module already has returns the same object.
What you get. AddInstrument returns an IPlatformModuleInstrument: keep it and compare it in the callbacks.
The data of an additional instrument comes through the overloads whose first argument is the instrument, with the
rules of the selected instrument: the same queue and order of arrival for all instruments together, the same time
slice and time limit, the same error policy. The order book is delivered only for an instrument added with
withDepth: true, because it is the most expensive part: ask for it only for the instruments whose book you draw.
Member of IPlatformModuleInstrument |
Meaning |
|---|---|
Id |
the identifier of the instrument: save it and pass it to AddInstrument again |
Instrument |
name, exchange, tick size and time zone of the instrument |
IsDelayed |
true for an instrument of a delayed data source |
State |
see below |
BestBid, BestAsk, LastTrade, Summary |
the latest state delivered to the module, or null |
HasDepth, GetDepth(isAsk) |
whether the order book was asked for, and its levels as of the delivered changes, best first |
FormatPrice(price) |
the price with the decimals of the instrument |
State. An added instrument starts as Connecting; the platform subscribes to its feed shortly afterwards and
reports every change with OnInstrumentStateChanged(instrument):
PlatformModuleInstrumentState |
Meaning |
|---|---|
Connecting |
just added; no data yet |
Active |
the data is delivered. Right after this state change the module gets the current statistics (OnSummaryChanged) and, if asked for, the whole order book as the first OnMarketDepthChanged; BestBid and BestAsk already show the current prices |
NoData |
the platform has no live data of this instrument to show now: no connected data source provides it, the data permissions of the account do not include it, a market replay is running, or the feed subscription failed (it is tried again). The data comes back by itself |
NotAllowed |
the tariff does not allow it: the market data of other instruments is restricted for custom code (Tariffs), or the tariff's limit of simultaneously open instruments is reached. It becomes Active by itself when the restriction is lifted |
Removed |
the module removed it, or the module instance was released |
An instrument that simply does not trade stays Active and gets no callbacks. Draw the state: a module that shows
«NotAllowed» or «NoData» next to the instrument explains itself to the user. When an instrument becomes Active
again, it starts like a new one: the current statistics and, if asked for, the whole order book come first.
Lifetime. The instruments belong to the running module instance. RemoveInstrument gives the feed subscription
back at once, together with the instrument's place in the tariff's limit of open instruments, and no callback of a
removed instrument follows. Every way a module instance ends gives the subscriptions back the same way; you do not
unsubscribe in OnDispose. A new instance starts with no additional instruments and adds them again.
Saving the list. The platform does not save the additional instruments: keep them in a setting (a public
property with the codes the user typed, or with the Id of each instrument when the module picked them itself; mark
such a property [Browsable(false)] to keep it out of the settings window) and add them in OnInitialize or in
OnSettingsChanged, as the quote board below does. Changing the instrument of the panel does not touch the
additional instruments.
Not included. No market-by-order and no options data, no history (the module sees an instrument from the moment
it became Active), no trading. During a market replay the additional instruments are NoData.
Threading
The calls of all the extension kinds side by side: Call model.
- The platform never calls one instance concurrently: data callbacks (of the selected and of the additional
instruments), the timer, input, button clicks and drawing are serialized, so your fields need no locks. Do not
call
Contextfrom threads of your own. - The data callbacks are not called on the data feed thread: the platform copies the data and delivers it shortly afterwards (within about 0.05 s) on a background thread. Drawing and input are called on the thread of the panel. The lists passed to you belong to the call: do not keep them.
- The start is not waited for. The constructor and
OnInitializeare called on the same background thread as the data, not on the thread of the panel, and the platform does not wait for them: while a module is starting its panel shows its empty background, takes no input and has no header buttons yet. UntilOnInitializereturns the module gets no data and draws nothing, so keep it short and move long work (reading a file, a network call) to a task of your own, whose result the module picks up inOnTimer. - Trades and order book changes are delivered in arrival order. The best prices and the exchange statistics are a current state: if they change several times between two deliveries, your module gets only the latest value.
- Delivery is time-sliced: one pass hands your module what was waiting when it started and stops after about 8 ms; the rest follows on the next pass. A module that is slower than its data falls behind, and when too much data is waiting newer data is skipped.
- When the user selects another instrument, data of the previous instrument that was not delivered yet is
discarded, and
OnInstrumentChangedcomes before the data of the new one. The additional instruments are not affected. - A click on a header button is delivered on the thread of the panel, like the mouse input. A click that finds another callback of the module running waits for it about 0.05 s and is then dropped (a toggle is not flipped) rather than delivered later.
- Time limit. A single call (a frame included) that takes longer than 100 ms is written to the log once. If that happens 5 times within 10 seconds, the platform stops the module like a module that threw an exception.
Settings
Public properties with a getter and a setter are the module settings (Settings). They
are shown in the settings window of the panel, saved with the workspace and restored right after the constructor,
before OnInitialize. The first time the panel is opened, the values your constructor sets are the defaults. After
the user changed a setting the module gets OnSettingsChanged on the next delivery. Settings must be of concrete
serializable types (numbers, strings, bool, enums, colors); a setting that cannot be restored is skipped and keeps
its default value. Read-only properties are not settings.
The same settings are what an AI agent connected through ATAS Connect sees and changes (the settings scope module
of a custom module): it reads the names of [Display] and the limits of [Range], so describe your settings with
them. A change made by an agent reaches the module exactly like a change made in the settings window.
Errors and OnDispose
If one of your methods throws, or the module repeatedly exceeds the time limit, the platform writes the reason to the log, stops the module, and the panel shows «Error» in its title and a message in its client area; its header buttons disappear and the subscriptions of its additional instruments are released; the rest of the platform keeps working. The module starts again, a new instance with the settings of the stopped one, when the user clicks the panel, opens its settings, or loads the workspace again. Do not rely on this as a control flow: check your inputs instead.
OnDispose() is called exactly once for every instance whose constructor completed, when the platform stops
using it: the panel is closed (also when the user switches the workspace or exits the platform), the module was
stopped by an error, the tariff no longer allows custom modules, the settings were replaced as a whole (a template,
a cancelled settings dialog), or the DLL was reloaded. Release there whatever the module created: timers, files,
subscriptions to your own services, native resources. No other method is called afterwards; an exception thrown by
OnDispose is logged and ignored.
Example: one instrument
The tape speed, the number of trades of the selected instrument during the last second, as a horizontal bar. The
module keeps its state in plain fields (the platform serializes the calls), counts in OnTimer, asks for a frame
only when the number changed and draws from fields. TradesPerSecond has no setter, so it is not a setting. The
colors are System.Drawing.Color, so the same DLL runs in ATAS and ATAS X.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Drawing;
using ATAS.Indicators;
using ATAS.Indicators.Modules;
using OFT.Rendering.Context;
using OFT.Rendering.Tools;
namespace ATAS.Docs.Samples.Modules;
/// <summary>The tape speed, trades per second of the selected instrument, drawn as a horizontal bar.</summary>
[PlatformModule("mycompany.tape-speed", DisplayName = "Tape speed")]
public sealed class TapeSpeedModule : PlatformModule
{
private readonly Queue<DateTime> _arrivals = new();
private int _tradesPerSecond;
[Display(Name = "Full scale, trades per second", Order = 10)]
public int FullScale { get; set; } = 50;
[Display(Name = "Bar color", Order = 20)]
public Color BarColor { get; set; } = Color.DodgerBlue;
/// <summary>Trades received during the last second.</summary>
public int TradesPerSecond => _tradesPerSecond;
protected override void OnInstrumentChanged()
{
_arrivals.Clear();
_tradesPerSecond = 0;
Redraw();
}
protected override void OnNewTrades(IReadOnlyList<MarketDataArg> trades)
{
var now = DateTime.UtcNow;
for (var i = 0; i < trades.Count; i++)
_arrivals.Enqueue(now);
}
protected override void OnTimer()
{
// Called about ten times per second: drop the arrivals older than one second.
var since = DateTime.UtcNow.AddSeconds(-1);
while (_arrivals.Count > 0 && _arrivals.Peek() < since)
_arrivals.Dequeue();
if (_arrivals.Count == _tradesPerSecond)
return;
_tradesPerSecond = _arrivals.Count;
Redraw();
}
protected override void OnRender(RenderContext context, Rectangle bounds)
{
var share = FullScale > 0 ? Math.Min(1.0, (double)_tradesPerSecond / FullScale) : 0;
var track = Rectangle.Inflate(bounds, -8, -8);
if (track.Width <= 0 || track.Height <= 0)
return;
var bar = new Rectangle(track.X, track.Y, (int)(track.Width * share), track.Height);
if (bar.Width > 0)
context.FillRectangle(BarColor, bar);
var name = Context.Instrument?.Instrument ?? "No instrument";
var center = new RenderStringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
context.DrawString($"{name}: {_tradesPerSecond} trades/s", Context.Font, Context.TextColor, track, center);
}
}
Example: header buttons and several instruments
A small quote board: a row with the last price and the number of trades for the selected instrument and for every
instrument listed in its setting, a toggle in the header that pauses the counting and a button that resets it. The
list of instruments is an ordinary setting; OnSettingsChanged runs at start and after every change and replaces
the additional instruments (Context.AdditionalInstruments is a copy of the list, so the loop may remove from it).
An instrument without data is drawn with its state instead of a price. Replace the default codes with the codes of
your data source as the instrument selector shows them.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Drawing;
using ATAS.Indicators;
using ATAS.Indicators.Modules;
using OFT.Rendering.Context;
using OFT.Rendering.Tools;
namespace ATAS.Docs.Samples.Modules;
/// <summary>The last price and the number of trades of the selected instrument and of the instruments listed in the settings.</summary>
[PlatformModule("mycompany.quote-board", DisplayName = "Quote board")]
public sealed class QuoteBoardModule : PlatformModule
{
private const int RowHeight = 22;
private readonly Dictionary<IPlatformModuleInstrument, int> _counts = new();
private readonly PlatformModuleButton _pause = new("pause")
{
ToolTip = "Pause counting",
Icon = PlatformModuleIcons.Pause,
IsCheckable = true
};
private string? _subscribed;
private int _selectedCount;
[Display(Name = "Other instruments (comma separated)", Order = 10)]
public string Instruments { get; set; } = "NQ, YM";
/// <summary>Trades of the selected instrument counted since the last reset.</summary>
public int SelectedCount => _selectedCount;
protected override void OnInitialize()
{
_pause.Click = () =>
{
// The platform has already flipped the toggle; the icon and the hint follow.
_pause.Icon = _pause.IsChecked ? PlatformModuleIcons.Play : PlatformModuleIcons.Pause;
_pause.ToolTip = _pause.IsChecked ? "Continue counting" : "Pause counting";
};
Context.Buttons.Add(_pause);
Context.Buttons.Add(new PlatformModuleButton("reset", "Reset", Reset) { ToolTip = "Reset the counters" });
}
protected override void OnSettingsChanged()
{
// The list is a setting: saved with the workspace, applied at start and after every change by the user.
if (Instruments == _subscribed)
return;
_subscribed = Instruments;
foreach (var instrument in Context.AdditionalInstruments)
Context.RemoveInstrument(instrument);
_counts.Clear();
foreach (var code in Instruments.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
// null: the platform does not know the instrument, or the module already has too many.
if (Context.AddInstrument(code) is { } instrument)
_counts[instrument] = 0;
}
Redraw();
}
protected override void OnInstrumentChanged()
{
_selectedCount = 0;
Redraw();
}
protected override void OnNewTrades(IReadOnlyList<MarketDataArg> trades)
{
if (!_pause.IsChecked)
_selectedCount += trades.Count;
Redraw();
}
protected override void OnNewTrades(IPlatformModuleInstrument instrument, IReadOnlyList<MarketDataArg> trades)
{
if (!_pause.IsChecked && _counts.ContainsKey(instrument))
_counts[instrument] += trades.Count;
Redraw();
}
protected override void OnInstrumentStateChanged(IPlatformModuleInstrument instrument) => Redraw();
protected override void OnRender(RenderContext context, Rectangle bounds)
{
var row = new Rectangle(bounds.X + 8, bounds.Y + 4, bounds.Width - 16, RowHeight);
if (Context.Instrument is { } selected)
{
var price = Context.LastTrade is { } last ? Context.FormatPrice(last.Price) : "-";
DrawRow(context, row, selected.Instrument, price, _selectedCount);
row.Y += RowHeight;
}
foreach (var instrument in Context.AdditionalInstruments)
{
// An instrument without data says why: Connecting, NoData or NotAllowed.
var price = instrument.State != PlatformModuleInstrumentState.Active
? instrument.State.ToString()
: instrument.LastTrade is { } last ? instrument.FormatPrice(last.Price) : "-";
DrawRow(context, row, instrument.Instrument.Instrument, price, _counts.GetValueOrDefault(instrument));
row.Y += RowHeight;
}
}
private void Reset()
{
_selectedCount = 0;
foreach (var instrument in Context.AdditionalInstruments)
_counts[instrument] = 0;
Redraw();
}
private void DrawRow(RenderContext context, Rectangle row, string name, string price, int count)
{
var left = new RenderStringFormat { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Center };
var right = new RenderStringFormat { Alignment = StringAlignment.Far, LineAlignment = StringAlignment.Center };
context.DrawString(name, Context.Font, Context.TextColor, row, left);
context.DrawString($"{price} {count}", Context.Font, Context.TextColor, row, right);
}
}
The module id
The argument of [PlatformModule] is the module id: the platform saves the panel in workspaces under this id,
not under the class name. The rules are the same as for the columns: the form <vendor>.<module-name> in lowercase
letters, digits and dashes (mycompany.tape-speed); the vendor atas is reserved; never change the id of a released
module, renaming the class or the namespace is safe; two modules cannot share an id, a module and a column may.
Problems found while loading (an invalid, reserved or duplicate id, a missing public parameterless constructor) are
written to the platform log, and the module is not listed.
Installing
The DLL goes into the Extensions or Indicators folder, like a chart indicator, and is reloaded the same way
(Project setup); one DLL may contain
chart indicators, heatmap indicators, columns and modules together. After a reload, a new DLL appears in the
Custom modules menu without a restart, and the open panels of a module switch to the new build and keep their
settings (the old instance gets OnDispose). If the DLL is removed, saved workspaces keep the panel: it shows the
module name with «Not available» and comes back with its settings when the DLL is installed again.
To sell a module, mark its class with [FeatureId] (Distribution): the module is
offered in the Custom modules menu only to accounts that have the feature, and a panel of it saved in a
workspace of an account without the feature shows «Not available» (the module is not created).
Tariff restriction
Custom modules can be restricted by the tariff (Tariffs): your modules are still listed
in the Custom modules menu, but opening one shows the tariff banner and the panel shows «Not available»; panels
already in a workspace stay as «Not available» panels with their settings kept, and the platform does not create or
call the module. The tariff is checked continuously, so the modules start working as soon as the restriction is
lifted. A module running when a restriction appears is stopped (it gets OnDispose).
Limitations of this version
- No market-by-order (MBO) data and no options data in modules.
- No trading: a module cannot send orders or read accounts and positions.
- No history: a module sees the data of an instrument, the selected one or an additional one, from the moment it is delivered; it cannot ask for past trades or bars.
- At most 16 additional instruments and 16 header buttons per module; the additional instruments get no data during a market replay.
- The module draws only inside its panel. Besides its header buttons it cannot add menu items, dialogs or controls of its own, and its buttons have no drop-down menus.
- In ATAS X the standard buttons
CloneandLayoutsdo not exist, so hiding them has no effect there.