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:
RenderContext and asks for a new frame when its picture changes;A module reads market data and draws. It cannot place, modify or cancel orders and has no access to accounts or positions.
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:
PlatformModule (namespace ATAS.Indicators.Modules) with a public parameterless constructor;[PlatformModule(id)] attribute — the stable module id (see below) and, optionally, DisplayName: the name in the menu and in the panel title;OnRender; every other method is optional.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.
| 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») |
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.
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.
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.
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.
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.
The module gets the data of the instrument selected in the panel without asking. It can also ask for the data of other instruments:
Naming an instrument. There are two ways, and both go into the first argument of AddInstrument:
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.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.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.
Context from threads of your own.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.OnInstrumentChanged comes before the data of the new one. The additional instruments are not affected.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.
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.
The module below shows 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.
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.
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 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:
<vendor>.<module-name> in lowercase letters, digits and dashes, for example mycompany.tape-speed; the vendor part should identify you (company or author name);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;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.
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.
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).
Clone and Layouts do not exist, so hiding them has no effect there.RenderContext drawing API.[Display].