Loading...
Searching...
No Matches
Custom modules

Overview

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 (RenderContext, see «Graphics»), 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 RenderContext and asks for a new frame when its picture changes;
  • receives the trades, best bid/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 (see «Additional instruments»);
  • can put buttons of its own into the header of the panel and hide some of the standard ones (see «Header buttons»);
  • gets the size changes of the panel, mouse input (move, press, release, click, wheel) and keyboard input, and a timer;
  • has user settings — public properties edited in the settings window of the panel and saved with the workspace, exactly like indicator settings.

A module reads market data and draws. It cannot place, modify or cancel orders and has no access to accounts or positions.

Building a module in your own assembly

Create a class library exactly as for a chart indicator (see «Development of a user indicator»). Besides ATAS.Indicators.dll, reference OFT.Rendering.dll (drawing, mouse input) and ATAS.DataFeedsCore.dll from the platform folder, with <Private>false</Private>.

A module is:

  • a public class derived from PlatformModule (namespace ATAS.Indicators.Modules) with a public parameterless constructor;
  • marked with the [PlatformModule(id)] attribute — the stable module id (see below) and, optionally, DisplayName: the name in the menu and in the panel title;
  • an override of OnRender; every other method is optional.

Where the user opens it

  • ATAS: the Widgets tab of the main window ribbon, button Custom modules — one item per installed module.
  • ATAS X: the + button of the workspace tabs, item Custom modules — one item per installed module.

The item is 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 (changing the instrument of one panel changes it in the others), 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 (see «Additional instruments»)
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 (see «Header buttons»)
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 in OnRender.

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 (see below) 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, and TimeZone / TimeZoneOffset — the time zone set for the instrument in the platform, as chart indicators get it), 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 (a fixed font, Segoe UI 10)
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 (see «Header buttons»)
AdditionalInstruments, MaxAdditionalInstruments, AddInstrument(codeOrId, exchange, withDepth), RemoveInstrument(instrument), FindInstruments(text, maxCount) the instruments the module added to the selected one (see «Additional instruments»)

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",
Icon = PlatformModuleIcons.Pause,
IsCheckable = true
};
protected override void OnInitialize()
{
Context.Buttons.Add(_pause);
Context.Buttons.Add(new PlatformModuleButton("reset", "Reset", () => _count = 0));
}
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, so it follows the theme. 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 (see «Errors and OnDispose»).

Changing the buttons. Add, remove and change the buttons at any time from the callbacks of the module (a caption that counts, an icon that follows a toggle, a button that is enabled only while there is data): 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 (the widget header has no such button)

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 user always keeps the ways to close the panel, to link it and to reach the settings of a module that has settings.

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:

protected override void OnInitialize()
{
_nq = Context.AddInstrument("NQ"); // trades, best bid/ask, statistics
_btc = Context.AddInstrument("BTCUSDT", "BinanceFutures", withDepth: true); // ...and the order book
}
protected override void OnNewTrades(IPlatformModuleInstrument instrument, IReadOnlyList<MarketDataArg> trades)
{
if (instrument == _nq)
_nqVolume += trades.Sum(static trade => trade.Volume);
}

Naming an instrument. There are two ways, and both 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.Instrument is such a code) and, optionally, the exchange — its code or its name as IInstrumentInfo.Exchange shows 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 — a module on a delayed instrument gets delayed neighbours — 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 returns PlatformModuleInstrumentInfo items — Id, Code, Exchange, Name, TickSize, IsDelayed — to show to the user; an added instrument has the same Id. Do not parse an identifier and do not show it.
var found = Context.FindInstruments("NQ"); // NQ@CME, NQ@CME (IsDelayed), NQZ6@CME, ...
var instrument = Context.AddInstrument(found[0].Id); // exactly that one
SavedInstruments = instrument?.Id ?? ""; // a setting of the module: see «Saving the list»

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 — OnNewTrades(instrument, trades), OnBestBidAskChanged(instrument, bestPrice), OnSummaryChanged(instrument, summary), and OnMarketDepthChanged(instrument, levels) for an instrument added with withDepth: true — 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 callbacks without the instrument argument stay those of the selected instrument, so a module written for one instrument works unchanged. The order book is delivered only on request 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 connection is down, or only the delayed source is connected and the instrument is the real-time one), 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 (see «Developer API and 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. The state changes are delivered like the data, within the time of a pass (see «Threading»): with many instruments, or slow callbacks, they are spread over several passes, always in the same order for one instrument — the state, its statistics, its order book, then its data.

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 — a module at the limit can replace one instrument by another — and no callback of a removed instrument follows, also when the module removes it inside one of its own callbacks. Every way a module instance ends gives the subscriptions back the same way: the panel is closed, the module is stopped by an error or by the tariff, its settings are replaced as a whole, the DLL is reloaded. 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 — they are the business of the module. 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 (the settings are already restored there) or in OnSettingsChanged (which also follows the user's edits), as the second example 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 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 Context from 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 OnInitialize are called on the same background thread as the data, not on the thread of the panel (the one exception: a module stopped by an error that starts again because its settings are opened or changed), 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, and the rest of the platform keeps working. Until OnInitialize returns 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 in OnTimer.
  • Trades and order book changes are delivered in arrival order. 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 OnInstrumentChanged comes 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. The panel does not wait for a busy module: 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. They are shown in the settings window of the panel (use [Display] for names, groups and order, [Browsable(false)] to hide one), 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 module below shows the tape speed — the number of trades of the selected instrument during the last second — as a horizontal bar.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Drawing;
namespace MyCompany.Modules;
[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;
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);
}
}
Represents an order for trading on a financial exchange.
Definition Order.cs:13
Base class of a custom module written in a custom assembly: a panel of the classic ATAS and a widget ...
Definition PlatformModule.cs:40
Represents an abstract rendering context for drawing on a chart.
Definition RenderContext.cs:17
int FillRectangle(Color brush1, Color brush2, Rectangle rect)
Fills a rectangle with a gradient using two specified colors.
Definition RenderContext.cs:85
abstract int DrawString(string s, RenderFont font, Color brush, int x, int y)
Draws a string at the specified position using the specified font and brush.
Encapsulates text layout information (such as alignment, orientation and tab stops) display manipulat...
Definition RenderStringFormat.cs:12
Definition IPlatformModuleContext.cs:1
Definition IIndicatorAnalyticsService.cs:1
@ Rectangle
Rectangle graphic object.
Definition IRenderContextSettings.cs:1
Definition RenderAdjustableArrowCap.cs:2

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.

Example: header buttons and several instruments

The module below is 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.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Drawing;
namespace MyCompany.Modules;
[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";
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);
}
}
A button a PlatformModule shows in the header of its panel, next to the standard buttons of the platf...
Definition PlatformModuleButton.cs:18
An instrument a PlatformModule asked for in addition to the one selected in its panel (IPlatformModul...
Definition IPlatformModuleInstrument.cs:39
@ Reset
The row was removed because the book was reset (reconnect, clear, snapshot restart); not a market act...

The list of instruments is an ordinary setting, so it is saved with the workspace and edited in the settings window; 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. The toggle changes its own icon and hint in its click handler — the header follows. An instrument without data is drawn with its state (Connecting, NoData, NotAllowed) instead of a price. Replace the default codes with the codes of your data source as the instrument selector shows them.

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 SmartDOM and Watchlist columns:

  • use the form <vendor>.<module-name> in lowercase letters, digits and dashes, for example mycompany.tape-speed; the vendor part should identify you (company or author name);
  • the vendor atas is reserved: a module from your assembly whose id starts with atas. is rejected when the assembly is loaded, and the reason is written to the platform log;
  • never change the id of a released module — saved workspaces would lose it; renaming the class or the namespace, on the contrary, is safe;
  • two modules cannot share an id; if they do, only one of them is listed. A module and a column may use the same id.

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 module

Put the DLL into the Extensions folder or into the Indicators folder next to it, where chart indicators are installed (AppData%\ATAS\Extensions or AppData%\ATAS\Indicators for the classic ATAS, AppData%\ATAS X\Extensions or AppData%\ATAS X\Indicators for ATAS X; the platform reads both), or distribute it through the marketplace — one DLL may contain chart indicators, heatmap indicators, SmartDOM and Watchlist columns and modules together.

The platform watches both folders. ATAS X loads a new or replaced DLL by itself about a second after the files stop changing; the classic ATAS shows a reload button in the status bar and loads the libraries when the user presses it (an AI agent uses the ATAS Connect tool extensions_reload). A file that could not be loaded is named with the reason and does not stop the other files — see Extending %ATAS, «Reloading a library». After the libraries are reloaded, a new DLL appears in the Custom modules menu without a restart, and open panels of a module switch to the new build and keep their settings (the old instance gets OnDispose).

To sell a module, mark its class with [FeatureId("…")] (namespace OFT.Attributes), as for chart and heatmap indicators: 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).

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.

Tariff restriction

Custom modules can be restricted by the tariff (see «Developer API and tariffs»). In that case 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 in it 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 (for example, after the user logs on with another account) — no need to open them again. 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 Clone and Layouts do not exist, so hiding them has no effect there.

Related articles