Lifecycle and threads
A chart indicator is a passive object: the platform creates it, gives it the settings, attaches it to a chart, calls it for the history and for every tick, draws it and finally disposes it. This article lists those steps in order, the thread each one runs on, and what the platform does when your code throws. The same rules apply to chart strategies, which are indicators too; the calls of the other extension kinds are summarized in Call model.
The steps
| Step | What the platform calls | Thread |
|---|---|---|
| 1. Create | The constructor | Any thread; when a chart loads its template, a background thread |
| 2. Apply the settings | The setters of the saved public properties | The same thread, right after the constructor |
| 3. Attach | OnDataProviderChanged; the indicator subscribes to the market data and the trading events of the chart | The same thread |
| 4. Calculate the history | OnRecalculate, then for every bar from 0: OnInitialize (once per instance, before the first bar) and OnCalculate, then OnFinishRecalculate | The same thread |
| 5. Live data | OnCalculate for the last bar on every change of it; OnNewTrade, MarketDepthChanged, OnBestBidAskChanged, OnCumulativeTrade |
Data threads of the platform |
| 6. Trading events | OnPositionChanged, OnOrderChanged, OnNewMyTrade and the others | Threads of the trading connection |
| 7. Draw | OnRender for the layouts subscribed with SubscribeToDrawingEvents | The drawing thread of the chart |
| 8. Dispose | OnDispose | The thread that removes the indicator or reloads the chart |
The steps 5 to 7 run at the same time, each on its own thread; step 4 is repeated whenever the indicator is recalculated (see below).
Create and apply the settings
When a chart is opened, or reloads its data, every indicator is created again from the saved template: the
constructor runs, then the saved values are written through the property setters. At this moment the indicator is
not attached yet: DataProvider, InstrumentInfo, ChartInfo and the data providers are null, and there are no
bars. A setter that calls RecalculateValues then only calls
OnRecalculate; nothing is calculated until the indicator is attached. So:
- create the data series and set the defaults in the constructor, and do nothing that needs the chart there;
- in a setter, check for
nullbefore using the chart, the instrument or a data provider; - start subscriptions (timers, other timeframes, options, files) in
OnInitialize, when the indicator is attached.
Calculate the history
When the indicator gets its source data series, the platform calculates it completely: OnRecalculate, then
OnCalculate for every bar from 0 to CurrentBar - 1, then OnFinishRecalculate. Before the first bar of the first
full calculation the platform calls OnInitialize, once per instance.
Note
OnInitialize is called inside a full calculation that has at least one bar. An indicator attached to a chart
without bars (a new instrument that waits for its first trade) gets its bars live, through OnCalculate alone, and
OnInitialize is not called until the next full calculation of the same instance.
A full calculation clears the data series and the drawing objects of the indicator (TrendLines, Rectangles,
Labels, the horizontal lines till touch) and the indicators added with Add, but not your fields: reset them
in OnRecalculate.
protected override void OnRecalculate()
{
// Before every full calculation: the platform clears the data series, your fields are yours to reset
_sessionHigh = 0;
_sessionLow = 0;
_label = "";
}
Live data
On the last bar OnCalculate is called for every change of the bar: a trade that updates it, or a new bar. The same
bar is therefore calculated many times, so the update must give the same result when it is repeated:
protected override void OnCalculate(int bar, decimal value)
{
var candle = GetCandle(bar);
// The current bar is calculated again on every tick: the update must give the same result
// when it is repeated for the same bar, which a running maximum and minimum do
if (IsNewSession(bar))
{
_sessionHigh = candle.High;
_sessionLow = candle.Low;
}
else
{
_sessionHigh = Math.Max(_sessionHigh, candle.High);
_sessionLow = Math.Min(_sessionLow, candle.Low);
}
_high[bar] = _sessionHigh;
_low[bar] = _sessionLow;
if (bar == CurrentBar - 1)
_label = $"Session range: {_sessionHigh - _sessionLow}";
}
When the calls for the same bar come faster than ten times the duration of the previous calculation, the platform skips some of them; before it calculates the next bar it calculates the skipped bar once more, so the last values of every bar are taken into account.
When the indicator is calculated again
The platform does not call RecalculateValues on an attached indicator by itself. Instead:
- A setting changes: nothing happens unless the setter calls
RecalculateValues(see Settings). The calculation then runs on the thread of the setter, usually the UI thread. - The user changes the source of the indicator: a full calculation.
- The chart reloads its data, changes the instrument, the time frame or the trading session, or applies a template: the indicator is disposed and created again from its saved settings, and the new instance goes through all the steps from the beginning. Nothing survives in your fields; keep what must survive in the settings or in the storage.
Call RecalculateValues yourself when data your calculation depends on arrives from elsewhere, for example after an
asynchronous load. Call it through DoActionInGuiThread, as the setters do, rather than from a data thread.
Dispose
OnDispose is called once, when the indicator is removed from the chart, when its chart is closed and when the chart
reloads (see above). After it the platform clears the data series, detaches the source, removes the timer
subscriptions of the indicator and detaches it from the data: the market data, the candle series of other timeframes
and instruments, the options and the live data of other instruments that the indicator subscribed to are released.
Release there what the platform does not know about: your own tasks, cancellation tokens, event handlers.
protected override void OnDispose()
{
// The indicator was removed, or the chart reloaded its data: in that case a new instance takes over
this.LogInfo("Session extremes: disposed");
}
Threads
OnCalculatenever runs concurrently with itself: the full calculation and the live calls for one instance take the same lock. Fields used only byOnCalculateneed no locks.- Everything else may run at the same time as
OnCalculate: the market data and trading callbacks, the timers,OnRender, the setters (the settings window, on the UI thread) and the continuations of your own tasks. Share data between them with immutable snapshots,volatilereferences,Interlockedor a lock of your own, and never write to the data series from them. OnRenderruns on the drawing thread while the calculation goes on. Compute inOnCalculate, publish the result, and only draw inOnRender:
protected override void OnRender(RenderContext context, DrawingLayouts layout)
{
// Read what the calculation published; do not calculate here
var label = _label;
if (label.Length == 0)
return;
context.DrawString(label, _font, Color.Gray, ChartArea.X + 8, ChartArea.Y + 8);
}
- UI work goes through DoActionInGuiThread. The action is queued to the UI thread of the chart window and the method returns at once, without waiting for it. Before the indicator is attached the call does nothing.
- Background tasks are yours to stop. A task started from the indicator keeps running after
OnDisposeunless you cancel it; checkIsDisposedor a cancellation token before you touch the indicator from it.
Errors
The platform catches what your code throws, writes it to the log (see Diagnostics) and keeps the indicator on the chart:
| Where | What happens |
|---|---|
OnCalculate |
Logged as OnCalculate error, at most once per 10 seconds per instance. The bar keeps the values written before the exception; the next call is made as usual |
OnRender |
Logged at most once per 10 seconds per instance; the other indicators of the panel are drawn |
| Market data callbacks | Logged (Process ticks error., Process best prices error.), the next data is delivered as usual |
| Trading callbacks | Logged (Process order changed error. and similar) |
| Timer callbacks | Logged (Failed to call timer callback.) |
| Constructor, setters | The indicator is not created: it is missing from the chart after the load, and the log has Loading indicator error |
So an exception never stops an indicator, but an indicator that throws on every tick still loses that data: treat the
log message as a bug to fix, not as a recovery mechanism. A slow OnCalculate is not stopped either; it makes the
platform skip live calls for the same bar (see above).
Example
The complete indicator used above: the high and the low of the current session, a field state reset in
OnRecalculate, and a label that the calculation publishes and OnRender draws.
using System;
using System.ComponentModel;
using System.Drawing;
using ATAS.Indicators;
using OFT.Rendering.Context;
using OFT.Rendering.Tools;
using Utils.Common.Logging;
namespace ATAS.Docs.Samples.ChartIndicators;
[DisplayName("Session extremes")]
public class SessionExtremesIndicator : Indicator
{
private readonly ValueDataSeries _high = new("SessionHigh", "Session high");
private readonly ValueDataSeries _low = new("SessionLow", "Session low");
private readonly RenderFont _font = new("Arial", 10);
// The calculation state: used only by OnCalculate, which never runs concurrently with itself
private decimal _sessionHigh;
private decimal _sessionLow;
// Published for OnRender, which runs on the drawing thread while the calculation goes on
private volatile string _label = "";
public SessionExtremesIndicator()
{
_high.Color = Color.SeaGreen.Convert();
_low.Color = Color.IndianRed.Convert();
DataSeries[0] = _high;
DataSeries.Add(_low);
EnableCustomDrawing = true;
SubscribeToDrawingEvents(DrawingLayouts.Final);
}
protected override void OnInitialize()
{
// Once per instance, right before the first bar of the first full calculation
this.LogInfo("Session extremes: calculating {0} bars", CurrentBar);
}
protected override void OnRecalculate()
{
// Before every full calculation: the platform clears the data series, your fields are yours to reset
_sessionHigh = 0;
_sessionLow = 0;
_label = "";
}
protected override void OnCalculate(int bar, decimal value)
{
var candle = GetCandle(bar);
// The current bar is calculated again on every tick: the update must give the same result
// when it is repeated for the same bar, which a running maximum and minimum do
if (IsNewSession(bar))
{
_sessionHigh = candle.High;
_sessionLow = candle.Low;
}
else
{
_sessionHigh = Math.Max(_sessionHigh, candle.High);
_sessionLow = Math.Min(_sessionLow, candle.Low);
}
_high[bar] = _sessionHigh;
_low[bar] = _sessionLow;
if (bar == CurrentBar - 1)
_label = $"Session range: {_sessionHigh - _sessionLow}";
}
protected override void OnRender(RenderContext context, DrawingLayouts layout)
{
// Read what the calculation published; do not calculate here
var label = _label;
if (label.Length == 0)
return;
context.DrawString(label, _font, Color.Gray, ChartArea.X + 8, ChartArea.Y + 8);
}
protected override void OnDispose()
{
// The indicator was removed, or the chart reloaded its data: in that case a new instance takes over
this.LogInfo("Session extremes: disposed");
}
}
Related articles
- Performance: what to keep out of
OnCalculateandOnRender. - Time, sessions and timers: the timer and the market time.
- Chart strategies: overview and lifecycle: what a strategy adds to these steps.
- Call model: the same table for every extension kind.