Heatmap indicators
Note
Requires ATAS 8.0.15 or later: ATAS 7.0.9 has no heatmap indicator API.
A heatmap indicator draws on top of the Heatmap module and in its sub-panels. It is a different kind of indicator from a chart indicator:
- it does not work with bars: it reacts to individual trades, market-profile snapshots, timer ticks and, optionally, market-by-order (MBO) data;
- every instance runs on its own background task supervised by the platform; the platform never calls the data and
lifecycle methods of one instance concurrently, so they need no locks for the indicator's own fields, with one
exception,
IHeatmapProfileConsumer.GetRequiredProfilePeriods(see Threading); - the indicator owns a long-lived visual state and writes samples into it under a short exclusive lease; the renderer reads the state at frame rate without waiting for the indicator.
Heatmap indicators work the same way in the classic ATAS and in ATAS X.
What you build
A class library as for any extension (Project setup); the types are in ATAS.Indicators.Heatmap and OFT.Rendering.Heatmap. A heatmap indicator consists of:
- a class derived from
HeatmapIndicator<TSettings>marked with[HeatmapIndicator(typeId)], with a public parameterless constructor; - a descriptor built once in the static constructor with
Describe<TSelf>(): the visuals (price line, value area, level lines, sub-panel scalar or pair) and their series; the returned handles are kept instatic readonlyfields (the histogram kind has no built-in drawing and is not shown without a custom renderer); - a settings class with a public parameterless constructor and a
Versionproperty (the platform only checks that the property exists and saves its value with the other settings; compare it in your code to recognize settings saved by an older build); its public properties are shown in the indicator settings and are available through the inheritedSettingsproperty; - one or more capability interfaces; the platform sends only the data the indicator asks for:
| Interface | Implement when |
|---|---|
IHeatmapTradeTickConsumer |
you react to trades (ProcessTicksAsync); most indicators do |
IHeatmapProfileConsumer |
you react to market-profile snapshots (POC, value area, OHLC) |
IHeatmapMboConsumer |
you aggregate MBO data: icebergs, annotated trades (ProcessMboAsync) |
IHeatmapWarmupIndicator |
you want history before the live data starts |
IHeatmapTimerConsumer |
you need periodic calls independent of the data |
IHeatmapHistoricalDataLoadedConsumer |
you rebuild when the user scrolls into older history |
IHeatmapDisposableIndicator |
you hold resources that must be released |
Rules:
- Write to the state only inside
using var lease = State.BeginUpdate();in a data callback. Disposing the lease publishes the changes; a second lease while one is open throws. Appendexpects non-decreasing timestamps; samples out of order are dropped.- Do not keep a
HeatmapTickBatchor its spans after the call returns: the buffer is reused. - Keep each call cheap (proportional to the new ticks, not to the history). The
cancellationTokenof a call is cancelled after 2 seconds (30 seconds forWarmUpAsync); a call that then ends withOperationCanceledExceptioncounts as a timeout, and 3 timeouts within 30 seconds stop the indicator. A call that is still running when the same time has passed once more after the cancellation, or that throws any other exception, stops the indicator at once. OnStateResetCoreAsyncis called when the platform cleared the state (for example, the instrument changed): reset your accumulators there and do not write to the state.
Example
The indicator draws a VWAP line from trades and, when MBO data is available, the number of tracked icebergs in a sub-panel:
using System.Threading;
using System.Threading.Tasks;
using ATAS.Indicators.Heatmap;
using OFT.Rendering.Heatmap;
using OFT.Rendering.Heatmap.Mbo;
namespace ATAS.Docs.Samples.Heatmap;
public sealed class VwapIcebergsSettings
{
public int Version { get; set; } = 1;
public bool ShowIcebergCount { get; set; } = true;
}
[HeatmapIndicator("mycompany.vwap-icebergs", DisplayName = "VWAP + Icebergs")]
public sealed class VwapIcebergsIndicator
: HeatmapIndicator<VwapIcebergsSettings>
, IHeatmapTradeTickConsumer
, IHeatmapMboConsumer
{
private static readonly HeatmapIndicatorDescriptor _descriptor;
private static readonly HeatmapIndicatorSeriesHandle<decimal> _vwap;
private static readonly HeatmapIndicatorSeriesHandle<decimal> _icebergs;
private decimal _priceVolume;
private decimal _volume;
static VwapIcebergsIndicator()
{
var build = Describe<VwapIcebergsIndicator>();
_vwap = build.PriceLine("vwap.line", "VWAP")
.Series("vwap.price", HeatmapIndicatorSeriesRole.Price, HeatmapIndicatorValueKind.Price);
_icebergs = build.SubPanelScalar("icebergs.panel", "Icebergs")
.Series("icebergs.count", HeatmapIndicatorSeriesRole.Scalar, HeatmapIndicatorValueKind.Integer);
_descriptor = build.Done();
}
public override HeatmapIndicatorDescriptor Descriptor => _descriptor;
public ValueTask ProcessTicksAsync(HeatmapTickBatch ticks, CancellationToken cancellationToken)
{
using var lease = State.BeginUpdate();
var vwap = lease.Series(_vwap);
foreach (var tick in ticks)
{
_priceVolume += tick.Price * tick.Volume;
_volume += tick.Volume;
if (_volume > 0)
vwap.Append(tick.TimestampNanos, _priceVolume / _volume);
}
return ValueTask.CompletedTask;
}
// Not called at all while MBO data is unavailable to the indicator (no MBO feed for the
// instrument, or the tariff does not include MBO data for third-party indicators):
// the VWAP line keeps working, the iceberg panel just stays empty.
public ValueTask ProcessMboAsync(HeatmapMboDataSnapshot snapshot, CancellationToken cancellationToken)
{
if (!Settings.ShowIcebergCount || snapshot.Status.Kind != HeatmapMboStatusKind.Ok)
return ValueTask.CompletedTask;
using var lease = State.BeginUpdate();
lease.Series(_icebergs).Append(snapshot.LiveEdgeNanos, snapshot.Icebergs.Count);
return ValueTask.CompletedTask;
}
protected override ValueTask OnStateResetCoreAsync(
IHeatmapIndicatorContext context,
IHeatmapIndicatorRuntime runtime,
CancellationToken cancellationToken)
{
// The platform cleared the visual state (e.g. the instrument changed): drop the accumulators.
_priceVolume = 0;
_volume = 0;
return ValueTask.CompletedTask;
}
}
Threading
The calls of all the extension kinds side by side: Call model.
The platform calls ConfigureAsync, the data callbacks (ProcessTicksAsync, ProcessProfileAsync,
ProcessMboAsync, ...), the timer, warm-up and reset methods one at a time, on the instance's own task: fields used
only by them need no locks. ConfigureAsync comes first, with the saved settings, and again after every change of
the settings; the first call precedes the first OnStateResetCoreAsync, so Context and Runtime are still null
in it.
The exception is IHeatmapProfileConsumer.GetRequiredProfilePeriods. The profile pump of the platform calls it
from its own thread at any time, concurrently with the instance's task. It must be thread-safe, cheap and
non-blocking:
- return an immutable snapshot that is already published: compute the set in
ConfigureAsync(or another serialized method) and store it in avolatilefield, replacing the whole array, never modifying it in place; - do not compute anything from the working fields of the indicator there, do not take locks, do not change the state.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ATAS.Indicators.Heatmap;
using OFT.Rendering.Heatmap;
namespace ATAS.Docs.Samples.Heatmap;
public sealed class PocLevelsSettings
{
public int Version { get; set; } = 1;
public bool ShowWeeklyPoc { get; set; }
}
[HeatmapIndicator("mycompany.poc-levels", DisplayName = "POC levels")]
public sealed class PocLevelsIndicator
: HeatmapIndicator<PocLevelsSettings>
, IHeatmapProfileConsumer
{
private static readonly HeatmapIndicatorDescriptor _descriptor;
private static readonly HeatmapIndicatorSeriesHandle<decimal> _dailyPoc;
private static readonly HeatmapIndicatorSeriesHandle<decimal> _weeklyPoc;
// Read by the profile pump from another thread: replaced as a whole, never changed in place.
private volatile HeatmapProfilePeriod[] _requiredPeriods = { HeatmapProfilePeriod.CurrentDay };
static PocLevelsIndicator()
{
var build = Describe<PocLevelsIndicator>();
_dailyPoc = build.PriceLine("poc.daily", "Daily POC")
.Series("poc.daily.price", HeatmapIndicatorSeriesRole.Price, HeatmapIndicatorValueKind.Price);
_weeklyPoc = build.PriceLine("poc.weekly", "Weekly POC")
.Series("poc.weekly.price", HeatmapIndicatorSeriesRole.Price, HeatmapIndicatorValueKind.Price);
_descriptor = build.Done();
}
public override HeatmapIndicatorDescriptor Descriptor => _descriptor;
public override ValueTask ConfigureAsync(PocLevelsSettings settings, CancellationToken cancellationToken)
{
// Serialized with the data callbacks; publishes a new immutable set for the pump.
_requiredPeriods = settings.ShowWeeklyPoc
? new[] { HeatmapProfilePeriod.CurrentDay, HeatmapProfilePeriod.CurrentWeek }
: new[] { HeatmapProfilePeriod.CurrentDay };
return ValueTask.CompletedTask;
}
// May run concurrently with every other method: only returns the published snapshot.
public IReadOnlyCollection<HeatmapProfilePeriod> GetRequiredProfilePeriods() => _requiredPeriods;
public ValueTask ProcessProfileAsync(HeatmapMarketProfileSnapshot profile, CancellationToken cancellationToken)
{
// Serialized: the indicator's own fields and the state lease are safe to use here.
var series = profile.Period switch
{
HeatmapProfilePeriod.CurrentDay => _dailyPoc,
HeatmapProfilePeriod.CurrentWeek => _weeklyPoc,
_ => null
};
if (series == null)
return ValueTask.CompletedTask;
using var lease = State.BeginUpdate();
lease.Series(series).Append(profile.TimestampNanos, profile.Poc);
return ValueTask.CompletedTask;
}
}
An empty set means the instance needs no profile data now; the platform then requests no profiles for it.
The TypeId
The first argument of [HeatmapIndicator] is the TypeId: the stable identifier under which the platform saves the
indicator in heatmap templates and workspaces.
- It must have the form
<vendor>.<indicator-name>: lowercase Latin letters, digits and dashes, each part starting with a letter, exactly one dot, for examplemycompany.vwap-icebergs; the vendor part should identify you. An indicator with another TypeId (upper case, no dot, underscores, ...) is rejected when the assembly is loaded, with the reason in the platform log. SmartDOM columns, Watchlist columns and custom modules use the same rule for their ids. - The vendors
heatmapandatasare reserved for the built-in indicators: a TypeId starting withheatmap.oratas.(in any letter case) is rejected. - Never change the TypeId of a released indicator: saved templates would lose it. To change the settings format, keep
the TypeId, increase the
Versionof the settings class and convert older settings in your own code. - Two indicators cannot share a TypeId; if they do, the one loaded later is rejected.
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 and heatmap indicators together. After a reload, open the heatmap indicators window again: the new
build is listed there, and the instances already on the heatmap switch to it the next time the indicators are
applied (or when the heatmap is reopened). Problems found while loading (an invalid descriptor, a missing
parameterless constructor, an invalid, reserved or duplicate TypeId) are written to the platform log, and the
indicator is not listed.
MBO data
Implement IHeatmapMboConsumer to receive the MBO analytics: the platform keeps one MBO subscription per instrument
and sends every consumer the same immutable snapshot (HeatmapMboDataSnapshot): the annotated trades, the tracked
icebergs, the data status and the live edge. Each instance applies its own filters; do not modify the snapshot.
Check snapshot.Status.Kind before using the data: MBO is available only for instruments and connections that
provide it.
MBO data for third-party indicators can be restricted by the tariff (Tariffs): then
ProcessMboAsync is not called and Context.MboDataSource is null. Design the indicator so that its other parts
keep working without MBO data, as in the example above. Heatmap indicators from your own assemblies can be
restricted as a whole too; and when the platform loads a DLL, it runs the static constructor of every heatmap
indicator class and creates one instance to read its descriptor, whatever the account, so keep the constructors free
of side effects.