Table of Contents

Time, sessions and timers

This article describes the times an indicator works with: the times of the bars, the current market time and the time zone the chart shows; the trading sessions; and the timer that calls the indicator periodically, also when no trades arrive.

Bar times

A candle has two times:

  • Time: the time the bar opened;
  • LastTime: the time of the last trade of the bar.

The times are in the market-data time of the instrument, the time base of its trades: UTC for most instruments, the local time of the exchange for some (for example the stocks of the dxFeed connection). The values have DateTimeKind.Unspecified; do not convert them with ToLocalTime or ToUniversalTime.

The time the chart shows is the bar time shifted by the time zone the user selected for the instrument: InstrumentInfo.TimeZoneOffset, with minute precision (zones such as UTC+5:30 included). TimeZone is the same offset rounded to whole hours, kept for older code. Add the offset when you show a time to the user or compare it with a time the user typed in the settings:

var shownTime = candle.Time + InstrumentInfo.TimeZoneOffset;

The kind of the chart and its period are in ChartType and TimeFrame, as text.

The current time

MarketTime is "now" in the time base of the bar times: compare bar and trade times with it. For a feed delayed by 15 minutes it is 15 minutes behind, as the data is. UtcTime is "now" in UTC. Both are corrected by the time synchronization of the platform, so a wrong clock on the computer does not shift them, and in Market Replay both are the replayed time: they stand still while the replay is paused and run faster with the replay speed.

Important

Do not use DateTime.Now or DateTime.UtcNow to measure how old a bar or a trade is: in Market Replay, with a delayed feed or with a wrong clock on the computer they give a wrong answer. Use MarketTime.

Sessions

To find out whether a bar opens a new trading session, call IsNewSession(bar) with the bar index; IsNewWeek(bar) and IsNewMonth(bar) work the same way for the week and the month. They compare the opening times of the bar and of the previous bar against the trading session selected for the chart (the session setting of the chart, the default session of the instrument unless the user chose another one). All three return true for bar 0.

The example raises an alert at the first bar of every new session (see Alerts):

using System.Windows.Media;

using ATAS.Indicators;

namespace ATAS.Docs.Samples.ChartIndicators;

public class NewSessionAlertIndicator : Indicator
{
    private int _lastAlertBar = -1;

    protected override void OnCalculate(int bar, decimal value)
    {
        // true for the first bar of a session (and for bar 0); IsNewWeek and IsNewMonth work the same way
        if (!IsNewSession(bar) || bar == _lastAlertBar)
            return;

        _lastAlertBar = bar;

        // Alerts only for the live bar: the history would produce one alert per past session
        if (bar != CurrentBar - 1)
            return;

        var instrument = InstrumentInfo?.Instrument ?? "";
        AddAlert("alert1", instrument, "New session", Colors.Black, Colors.White);
    }
}

The working window of a session, its start and end, comes from GetSessionWorkingTime(time) of DataProvider.OnlineDataProvider: the window that contains the time, or null when the time is outside a working window or the schedule of the session is unknown. The times of the window are in the time base of the bars.

if (DataProvider?.OnlineDataProvider.GetSessionWorkingTime(MarketTime) is { } session)
{
    var timeLeft = session.To - MarketTime;
}

Timers

SubscribeToTimer(period, callback) calls the callback periodically, whether trades arrive or not: for a clock, a countdown, or a check that does not depend on the data.

  • The period is market time: it must be at least 50 ms and a multiple of 50 ms, otherwise the method throws ArgumentException. The calls are aligned to multiples of the period (a 1-second timer is called at the start of every second), and in Market Replay they follow the replayed time.
  • The indicator must be attached: before that the method throws InvalidOperationException. Subscribe in OnInitialize.
  • The callback runs on a background thread, at the same time as OnCalculate and OnRender (see Lifecycle and threads). Do not write to the data series there: publish a value for OnRender and call RedrawChart, or call RecalculateValues through DoActionInGuiThread.
  • An exception in the callback is written to the log and the timer keeps running.
  • UnsubscribeFromTimer stops it; when the indicator is disposed, the platform removes its subscriptions by itself. Subscribing the same callback with the same period twice has no effect.

The example shows the opening time of the last bar as the chart shows it, how long the bar has been open, and how long the session has left, refreshed every second:

using System;
using System.ComponentModel;
using System.Drawing;
using System.Threading;

using ATAS.Indicators;
using OFT.Rendering.Context;
using OFT.Rendering.Tools;

namespace ATAS.Docs.Samples.ChartIndicators;

[DisplayName("Bar clock")]
public class BarClockIndicator : Indicator
{
    private static readonly TimeSpan _timerPeriod = TimeSpan.FromSeconds(1);

    private readonly RenderFont _font = new("Arial", 11);

    // Written by the calculation, read by the timer: a long is read and written atomically with Interlocked
    private long _lastBarTimeTicks;
    private volatile string _text = "";

    public BarClockIndicator()
        : base(true)
    {
        ((ValueDataSeries)DataSeries[0]).IsHidden = true;

        EnableCustomDrawing = true;
        SubscribeToDrawingEvents(DrawingLayouts.Final);
    }

    protected override void OnInitialize()
    {
        // A period of market time: at least 50 ms and a multiple of 50 ms.
        // The subscription is removed by the platform when the indicator is disposed.
        SubscribeToTimer(_timerPeriod, OnTimer);
    }

    protected override void OnCalculate(int bar, decimal value)
    {
        if (bar == CurrentBar - 1)
            Interlocked.Exchange(ref _lastBarTimeTicks, GetCandle(bar).Time.Ticks);
    }

    private void OnTimer()
    {
        // A background thread: no data series writes here, only a value for OnRender
        var barTicks = Interlocked.Read(ref _lastBarTimeTicks);

        if (barTicks == 0)
            return;

        // MarketTime is "now" in the time base of the bar times; DateTime.Now is not
        var now = MarketTime;
        var barTime = new DateTime(barTicks);

        // The chart shows times shifted by the time zone the user selected for the instrument
        var shownTime = barTime + (InstrumentInfo?.TimeZoneOffset ?? TimeSpan.Zero);
        var text = $"Bar {shownTime:HH:mm:ss}, open for {now - barTime:hh\\:mm\\:ss}";

        if (DataProvider?.OnlineDataProvider.GetSessionWorkingTime(now) is { } session)
            text += $", session ends in {session.To - now:hh\\:mm}";

        _text = text;
        RedrawChart();
    }

    protected override void OnRender(RenderContext context, DrawingLayouts layout)
    {
        var text = _text;

        if (text.Length != 0)
            context.DrawString(text, _font, Color.Gray, ChartArea.X + 8, ChartArea.Bottom - 24);
    }
}

The opening time of the bar is passed from OnCalculate to the timer through a long field with Interlocked: the timer must not read the candles itself, because the list of candles changes on another thread.