Table of Contents

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 in static readonly fields (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 Version property (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 inherited Settings property;
  • 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.
  • Append expects non-decreasing timestamps; samples out of order are dropped.
  • Do not keep a HeatmapTickBatch or its spans after the call returns: the buffer is reused.
  • Keep each call cheap (proportional to the new ticks, not to the history). The cancellationToken of a call is cancelled after 2 seconds (30 seconds for WarmUpAsync); a call that then ends with OperationCanceledException counts 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.
  • OnStateResetCoreAsync is 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 a volatile field, 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 example mycompany.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 heatmap and atas are reserved for the built-in indicators: a TypeId starting with heatmap. or atas. (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 Version of 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.