Table of Contents

Other timeframes and instruments

An indicator is calculated on the bars of its chart, but it can also use the candles of a higher timeframe, the candles of another instrument and the live trades and quotes of another instrument. There are three ways, from the simplest:

You want Use
The whole indicator calculated on a higher timeframe, chosen by the user SupportsCalculationTimeFrame, no other code
Candles of another timeframe or instrument inside your calculation CandlesDataProvider, the candle series API
Live trades, best bid and ask or the order book of another instrument InstrumentMarketDataProvider

The calculation timeframe

Set SupportsCalculationTimeFrame = true in the constructor, and the settings of the indicator get a Timeframe parameter in the Calculation group. With a timeframe other than Chart, the platform calculates the indicator on the candles of that timeframe and shows the values on the chart bars: "SMA (14) @ M30" on an M5 chart.

public MtfReadyAverageIndicator()
    : base(true)
{
    // Shows the Timeframe setting (group Calculation): the platform calculates the indicator on the chosen
    // higher timeframe and every chart bar shows the value of the higher-timeframe candle it belongs to
    SupportsCalculationTimeFrame = true;
}
  • Every chart bar shows the value of the higher-timeframe candle it belongs to. The bars of the candle that is still forming change together on every tick until it closes; a reloaded chart shows exactly what the live chart ended with.
  • Steps or a smooth line. By default the value repeats on all the bars of a candle; the Smooth line parameter ramps it across them. Set SupportsCalculationTimeFrameSmoothing to false for an indicator whose output is a set of levels (trailing stops, support and resistance): a ramp between two levels draws prices the indicator never had.
  • Only higher timeframes. A timeframe lower than the chart's is refused and the setting returns to Chart. On charts not built by time (ticks, range, volume, ...) any timeframe is allowed.
  • Only candles. The higher-timeframe calculation runs in a separate hidden instance of the indicator with the instrument, the sessions and the settings of the chart, but without trades, order book, options, candle series of its own and trading data. Opt in only an indicator that needs nothing but the OHLCV of its candles; indicators that read the footprint, the order flow or the order book must not.
  • Your code can read the user's choice in CalculationTimeFrame. The parameter is shown only where the platform enables it for the account.

Candle series

CandlesDataProvider gives candles of any supported timeframe of the chart instrument or of another instrument, with history and live updates. It is null before the indicator is attached and in hosts without the API, and its IsAvailable is false when the tariff does not include it.

The request

A CandleSeriesRequest names:

  • TimeFrame: created with the factory methods of CandleSeriesTimeFrame: Minutes(n), Hours(n), Seconds(n), Ticks(n), Volume(n), Delta(n), Range(ticks), Renko(ticks), Daily(), Weekly(). For RangeX, RangeXV, RangeZ, RangeUS and ReversalX set ExoticTimeFrame instead.
  • Instrument: null for the chart's instrument, otherwise a CandleSeriesInstrument. A string converts to it as the instrument code ("NQZ6", "ETHUSDT"); add Exchange to narrow the search, or give the SecurityId or the ContractId when you know them.
  • DaysToLoad: the depth of the history in trading days, 5 by default. The platform keeps it between 1 and 30 and may limit it further by the tariff.
  • TradingSessionId: the trading session of the series; null is the default session of the instrument.

The subscription

SubscribeCandles returns an ICandleSeriesSubscription at once and loads the history in the background:

  • Candles is the list of candles; it is an ICandleCreator, so it can also serve as the source of a CandlePartSeries.
  • Reloaded is raised after the first load and after every automatic reload: a reconnect, a contract rollover, the end of a replay, a deeper history. Re-read Candles there. A handler attached after the load is called at once, so this event alone is enough. When a load fails, the series is empty and Loaded is faulted with the reason.
  • OnNewCandle, OnCandleChanged and OnRemoveCandle report the live changes only; the history is applied silently and announced by Reloaded.
  • LoadMoreHistoryAsync(days) deepens the history without a new subscription.
  • Dispose() releases the series. Whatever the indicator still holds when it is disposed is released by the platform.
Warning

The events come on the data thread of the source instrument, which for another instrument is not the thread of the chart's own calls, and the list of candles is appended on that thread. Copy the list in the event handlers and give the copy to OnCalculate, as the example does. Keep the handlers short: a slow handler holds up the data of that instrument.

Matching the chart bars

To use a candle of the series for a chart bar, look it up by the time of the bar. GetClosedCandleByTime(time) returns the last candle that had closed by that time: its values were known then, so the history and the live bars agree. GetCandleByTime(time) returns the candle that had started by then; on a historical bar its close is the final close, a value from the future. Use it only for the live, forming value; for history it makes the indicator repaint.

Limits and errors

SubscribeCandles throws:

  • ArgumentException when the instrument cannot be found;
  • NotSupportedException when the timeframe is not supported;
  • InvalidOperationException when the indicator already holds 16 series, when the timeframe or the number of open instruments is not allowed by the tariff, or when the indicator is detached.

For a one-off fetch without live updates (a scan, a correlation snapshot) use GetCandlesAsync: there is nothing to dispose, and concurrent requests of one indicator wait in a queue.

Example

The closes of a higher timeframe on the chart bars; the instrument is the chart's or the one the user types in the settings. The subscription starts in OnInitialize and again when a setting changes, the series is copied on the data thread, and the chart bars are recalculated after every reload.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;

using ATAS.Indicators;
using Utils.Common.Logging;

namespace ATAS.Docs.Samples.ChartIndicators.MarketData;

[DisplayName("Higher timeframe close")]
public class HigherTimeFrameCloseIndicator : Indicator
{
    private CandleSeriesTimeFrame _timeFrame = CandleSeriesTimeFrame.Hours(1);
    private CandleSeriesInstrument _instrument;
    private ICandleSeriesSubscription? _subscription;

    // A snapshot of the series, replaced on the source thread and read by OnCalculate
    private volatile IReadOnlyList<IndicatorCandle> _candles = Array.Empty<IndicatorCandle>();

    [Display(Name = "Timeframe", GroupName = "Series", Order = 10)]
    public CandleSeriesTimeFrame SeriesTimeFrame
    {
        get => _timeFrame;
        set
        {
            if (value.IsChart || value == _timeFrame)
                return;

            _timeFrame = value;
            Resubscribe();
        }
    }

    // Empty: the instrument of the chart; otherwise a code such as "NQZ6" or "ETHUSDT"
    [Display(Name = "Instrument", GroupName = "Series", Order = 20)]
    public CandleSeriesInstrument SeriesInstrument
    {
        get => _instrument;
        set
        {
            _instrument = value;
            Resubscribe();
        }
    }

    public HigherTimeFrameCloseIndicator()
        : base(true)
    {
    }

    protected override void OnInitialize()
    {
        Resubscribe();
    }

    protected override void OnCalculate(int bar, decimal value)
    {
        this[bar] = GetClosedClose(GetCandle(bar).Time);
    }

    protected override void OnDispose()
    {
        Release();
    }

    private void Resubscribe()
    {
        Release();

        // Null before the indicator is attached (settings are applied before that) and in hosts without the API
        if (CandlesDataProvider is not { IsAvailable: true } provider)
            return;

        try
        {
            var subscription = provider.SubscribeCandles(new CandleSeriesRequest
            {
                TimeFrame = _timeFrame,
                Instrument = _instrument.IsEmpty ? null : _instrument,
                DaysToLoad = 10,
            });

            // After the first load and after every automatic reload (reconnect, rollover, deeper history)
            subscription.Reloaded += OnReloaded;
            subscription.OnNewCandle += OnCandleListChanged;
            subscription.OnRemoveCandle += OnCandleListChanged;

            _subscription = subscription;
            IsLoading = true;
        }
        catch (Exception e) when (e is ArgumentException or InvalidOperationException or NotSupportedException)
        {
            // An unknown instrument, a tariff restriction, the limit of series per indicator
            this.LogWarn("Cannot subscribe to {0}: {1}", _timeFrame, e.Message);
        }
    }

    private void Release()
    {
        if (_subscription is not { } subscription)
            return;

        _subscription = null;
        _candles = Array.Empty<IndicatorCandle>();

        subscription.Reloaded -= OnReloaded;
        subscription.OnNewCandle -= OnCandleListChanged;
        subscription.OnRemoveCandle -= OnCandleListChanged;
        subscription.Dispose();
    }

    private void OnReloaded(ICandleSeriesSubscription subscription)
    {
        // A background thread: copy the list here, where it does not change, and recalculate the chart bars
        _candles = subscription.Candles.ToArray();
        IsLoading = false;

        if (subscription.Loaded.IsFaulted)
            this.LogWarn("The series did not load: {0}", subscription.Loaded.Exception?.GetBaseException().Message);

        DoActionInGuiThread(RecalculateValues);
    }

    private void OnCandleListChanged(IndicatorCandle candle)
    {
        // A new higher-timeframe candle: the previous one has closed and the chart bars may use it
        if (_subscription is { } subscription)
            _candles = subscription.Candles.ToArray();
    }

    private decimal GetClosedClose(DateTime time)
    {
        // The last candle that had CLOSED by the time of the bar: its close was known then, so the
        // history and the live bars agree. The candle that merely started by then would leak its final close
        // into the past.
        var candles = _candles;
        var index = -1;

        for (var i = candles.Count - 1; i >= 1; i--)
        {
            if (candles[i].Time <= time)
            {
                index = i - 1;
                break;
            }
        }

        return index >= 0 ? candles[index].Close : 0;
    }
}

IsLoading = true shows a loading mark next to the name of the indicator on the chart until the series has loaded.

Live data of other instruments

InstrumentMarketDataProvider subscribes to the trades, the best bid and ask and the order book of any instrument the platform can open: SubscribeMarketData with a MarketDataSubscriptionRequest (the instrument and the kinds of data, trades and best bid and ask by default) returns an IMarketDataSubscription:

  • NewTrades, BestBidAskChanged and MarketDepthsChanged (a level with volume 0 is removed) are raised on the data thread of that instrument;
  • a handler attached to BestBidAskChanged is called at once with the last known best bid and ask, which are also in BestBid and BestAsk;
  • GetMarketDepthSnapshot() returns the current order book of a subscription that includes MarketDepth;
  • Dispose() releases it; the platform releases what is left when the indicator is disposed.

SubscribeMarketData throws ArgumentException when the instrument cannot be found and InvalidOperationException when the indicator already holds 16 subscriptions, when the tariff does not allow the request, or when the indicator is detached. There is no history: the data starts with the subscription.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

using ATAS.Indicators;
using Utils.Common.Logging;

namespace ATAS.Docs.Samples.ChartIndicators.MarketData;

[DisplayName("Spread to another instrument")]
public class InstrumentSpreadIndicator : Indicator
{
    private string _otherCode = "";
    private IMarketDataSubscription? _subscription;

    // The last trade price of the other instrument; decimal is not atomic, so it is guarded
    private readonly object _sync = new();
    private decimal _otherPrice;

    [Display(Name = "Other instrument", GroupName = "Spread")]
    public string OtherCode
    {
        get => _otherCode;
        set
        {
            _otherCode = value?.Trim() ?? "";
            Resubscribe();
        }
    }

    public InstrumentSpreadIndicator()
    {
        Panel = IndicatorDataProvider.NewPanel;
    }

    protected override void OnInitialize()
    {
        Resubscribe();
    }

    protected override void OnCalculate(int bar, decimal value)
    {
        decimal other;

        lock (_sync)
            other = _otherPrice;

        // Live quotes only: the history bars get no value
        if (bar == CurrentBar - 1 && other != 0)
            this[bar] = value - other;
    }

    protected override void OnDispose()
    {
        Release();
    }

    private void Resubscribe()
    {
        Release();

        if (_otherCode.Length == 0 || InstrumentMarketDataProvider is not { IsAvailable: true } provider)
            return;

        try
        {
            _subscription = provider.SubscribeMarketData(new MarketDataSubscriptionRequest
            {
                Instrument = _otherCode,
                Types = MarketDataSubscriptionTypes.Trades,
            });

            // The thread of the other instrument, not the chart's: keep the handler short
            _subscription.NewTrades += OnOtherTrades;
        }
        catch (Exception e) when (e is ArgumentException or InvalidOperationException)
        {
            this.LogWarn("Cannot subscribe to {0}: {1}", _otherCode, e.Message);
        }
    }

    private void Release()
    {
        if (_subscription is not { } subscription)
            return;

        _subscription = null;
        subscription.NewTrades -= OnOtherTrades;
        subscription.Dispose();
    }

    private void OnOtherTrades(IEnumerable<MarketDataArg> trades)
    {
        MarketDataArg? last = null;

        foreach (var trade in trades)
            last = trade;

        if (last is null)
            return;

        lock (_sync)
            _otherPrice = last.Price;
    }
}

The spread is written on the chart's own calls of OnCalculate, that is when the chart instrument trades. A value that must follow the trades of the other instrument as well is better published for OnRender and drawn there.