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
falsefor 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 setExoticTimeFrameinstead.Instrument:nullfor the chart's instrument, otherwise a CandleSeriesInstrument. A string converts to it as the instrument code ("NQZ6","ETHUSDT"); addExchangeto narrow the search, or give theSecurityIdor theContractIdwhen 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;nullis the default session of the instrument.
The subscription
SubscribeCandles returns an ICandleSeriesSubscription at once and loads the history in the background:
Candlesis the list of candles; it is an ICandleCreator, so it can also serve as the source of a CandlePartSeries.Reloadedis raised after the first load and after every automatic reload: a reconnect, a contract rollover, the end of a replay, a deeper history. Re-readCandlesthere. A handler attached after the load is called at once, so this event alone is enough. When a load fails, the series is empty andLoadedis faulted with the reason.OnNewCandle,OnCandleChangedandOnRemoveCandlereport the live changes only; the history is applied silently and announced byReloaded.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:
ArgumentExceptionwhen the instrument cannot be found;NotSupportedExceptionwhen the timeframe is not supported;InvalidOperationExceptionwhen 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,BestBidAskChangedandMarketDepthsChanged(a level with volume 0 is removed) are raised on the data thread of that instrument;- a handler attached to
BestBidAskChangedis called at once with the last known best bid and ask, which are also inBestBidandBestAsk; GetMarketDepthSnapshot()returns the current order book of a subscription that includesMarketDepth;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.
Related articles
- Lifecycle and threads: which threads run at the same time as
OnCalculate. - Tariffs: what a restricted account sees.
- Market data: the trades and the order book of the chart instrument.