Performance
The recommendations below apply to any chart indicator, in the classic ATAS and in ATAS X alike, and to chart strategies: a strategy is an indicator too and is calculated in the same way.
Calculation: OnCalculate
The platform calls OnCalculate for every historical bar and, on the current bar, on every tick, so the code inside
it must be as light as possible:
- Avoid allocations in
OnCalculate. Do not create collections, strings, LINQ queries, lambdas or temporary objects on every call, and avoid boxing. Keep reusable buffers and objects in fields and initialize them in the constructor or inOnInitialize. - Create data series once. Create
ValueDataSeriesand the other series in the constructor and add them toDataSeries; do not recreate them inOnCalculate. Write the result by bar index (series[bar] = value). - Cache the candle. Call
GetCandle(bar)once and keep the result in a local variable instead of requesting the same candle repeatedly for the same bar. Remember the valid index range0..CurrentBar-1. - Footprint data is heavier than bar data. Iterating all price levels of a candle on every tick is expensive: do it only when necessary and cache intermediate results where possible. GetAllPriceLevels() creates a new object for every price level; the overload that takes a PriceVolumeInfo writes every level into the passed object instead, so keep that object in a field, read its values inside the loop and do not store references to it:
using ATAS.Indicators;
namespace ATAS.Docs.Samples.ChartIndicators;
public class PriceLevelsCacheIndicator : Indicator
{
private readonly PriceVolumeInfo _level = new();
protected override void OnCalculate(int bar, decimal value)
{
var candle = GetCandle(bar);
var maxVolume = 0m;
// Every price level is written into the same _level object:
// read its values inside the loop and do not store references to it.
foreach (var level in candle.GetAllPriceLevels(_level))
{
if (level.Volume > maxVolume)
maxVolume = level.Volume;
}
this[bar] = maxVolume;
}
}
Rendering: OnRender
If the indicator draws in OnRender, that is a hot path too:
- The call frequency of
OnRenderdepends on the layouts you subscribe to with SubscribeToDrawingEvents. The Final layout is redrawn on every chart repaint, including mouse movement;LatestBaron every tick;Historicalon a new candle, scrolling and zooming. Subscribe only to the layouts you need: an unnecessary subscription toFinalforces drawing on every cursor move. - Draw only what is visible. Iterate the visible bars, from
FirstVisibleBarNumber to
LastVisibleBarNumber, not the whole history; get the
coordinates with
ChartInfo.GetXByBarandChartInfo.GetYByPrice. - Do not compute in
OnRender. Perform the heavy calculations inOnCalculateand store the result; leave only the drawing itself inOnRender. Reuse pens and fonts (RenderPen,RenderFont) instead of creating them on every frame. - Call RedrawChart sparingly, only when the picture actually needs updating; calling it on every tick produces redundant repaints.
Threads
The platform calls your code on its own threads, not on the UI thread. Follow this contract instead of relying on internal thread details (the order of the calls and their threads are in Lifecycle and threads):
- Do UI work only through DoActionInGuiThread. Anything that must run on the interface thread goes through this method, never directly from a calculation or data callback.
- Market-data callbacks arrive on the data-feed thread:
OnNewTrade,OnCumulativeTradeandMarketDepthChanged. - Do not assume different callbacks share a thread. If you keep mutable state in fields and access it from more
than one callback, guard it, or keep the writes in
OnCalculate. Never write toDataSeriesfrom your own background threads. OnRenderonly draws. Keep the computation inOnCalculateand store the result.
ATAS X
If the indicator must work in ATAS X, do not use custom WPF editors or other Windows-only UI components: use the
attributes ([Display] and the others) for the settings instead of hand-written WPF controls. See
Project setup for the types ATAS X converts.