Skip to content
Level 5 · Real-Time AmiBroker UserLessonPart 20 · page 1 of 430 min Professional edition Live feed
30Minutes
14AFL functions
11Sources
Professional + live feedRequires

This page needs the Professional edition and a real-time data feed. Every gated part of this course ships a Level A path that uses Bar Replay and historical data instead — look for it below.

AFL functions taught here14

Exchange Time versus Your Clock

Somebody decided what time to write on the bar in front of you. It was not the exchange, not exactly, and it was not AmiBroker either. By the time the bar reaches your chart at least four parties have had an opportunity to change that number, and none of them records what it means anywhere your formula can read.

By the end of this lesson you will be able to say, with evidence taken from your own database, which time zone your bar stamps are expressed in and whether a bar is stamped at the beginning or the end of the interval it covers. That sounds like a small thing. It is the assumption underneath every intraday session filter, every opening range, every VWAP anchor and every alert you will write for the rest of this track.

The number on a bar is the result of a negotiation between three clocks that have no obligation to agree.

The exchange clock runs in the exchange’s own local time. It defines when the session opens and closes, when the auction happens, when a short session ends. It changes for daylight saving on dates chosen by the country the exchange sits in, which is the subject of the third lesson in this part.

Your operating-system clock runs in whatever zone your computer is configured for. It is what Now() reports, in eleven documented formats, and it is what governs when AmiBroker actually executes a repeated scan or dispatches an alert. It is also completely unrelated to the numbers in the bar array — a point worth repeating because it is the single most common wrong assumption in intraday AFL.

The provider’s stamp is the one written into the data. The provider chose a time zone for it and chose whether a five-minute bar covering 09:30:00 to 09:34:59 is called “09:30” or “09:35”. Both choices are defensible, both are in use, and a provider is not obliged to tell you which it made in a place you will find before you need it.

Four chances to change one number

  1. Trades occurTimestamped by the exchange, in the exchange's local time
  2. Provider aggregatesGroups trades into bars, picks a time zone, picks start-of-interval or end-of-interval stamping
  3. Plugin deliversHands bars to AmiBroker; AmiBroker applies the database Time shift, in whole or fractional hours
  4. Database storesA date and a time, with nothing attached to say which zone they belong to
  5. Your formula readsTimeNum() returns that stored number, and compares it with one you typed from memory

An AFL bar carries a date and a time. DateNum() returns the date packed as 10000 * (year - 1900) + 100 * month + day, so 31 December 2001 becomes 1011231. TimeNum() returns the time packed as 10000 * hour + 100 * minute + second, so 12:37:15 becomes 123715. DateTime() returns both together in a single encoded value.

What none of them carries is a time zone. That absence is not stated as a headline anywhere in the documentation, but it is implied consistently by everything built around it. The Intraday Settings dialog describes its Time shift field as the number of hours between your local time zone and the exchange’s. Its trading-hours fields are documented as being expressed in your local time zone. Status("timeshift") returns that shift as a plain number of seconds. And the note attached to Status("lastbartimeleft") says the countdown works only when the database time shift is set so that the dates displayed on the chart match your local computer clock. Every one of those statements is about reconciling a bare number with a zone that lives outside it.

The practical consequence: a bar stamp is a claim about wall-clock time in an unspecified place. Two databases can hold the same trading day with stamps an hour apart, and both are internally consistent. Nothing will tell you which is which except comparing the stamps against something you know independently.

Four places, in the order they act.

The provider’s convention comes first and you cannot change it, only compensate for it. Part 18’s appendices record what each documented source says about its own timestamps and — just as usefully — where the official page is silent.

The database time shift in File → Database Settings → Intraday Settings moves every stamp in the database by a fixed number of hours. Part 19 covers configuring it; what matters here is its shape. It is a constant. A constant can express “this provider is five hours behind me”. It cannot express “this provider is five hours behind me for eight months of the year and six for the other four”, which is exactly what a pair of countries with different daylight saving rules produces.

For imported text data the equivalent lever is the ASCII importer’s $TIMESHIFT command, documented as a number of hours to shift date and time stamps during import, accepting fractional and negative values — $TIMESHIFT -11.5 shifts eleven and a half hours backwards. Because it acts at import time it is baked into the stored bars, which makes it harder to undo than the database setting and easier to forget.

Finally, Tools → Preferences → Intraday carries a setting called “Time compressed bars show”, whose documented options are first tick, last tick, START time of interval and END time of interval. This one does not touch the stored data at all. It changes the timestamp AmiBroker displays and returns for every time-compressed bar — which is to say every bar on every chart interval above the database’s base interval.

Take one five-minute block of trading: every trade between 09:30:00 and 09:34:59 inclusive. There is one bar. There are two reasonable names for it.

The same four bars under two stamping conventions

Illustrative bar stamps for a 09:30 open on five-minute bars. Under end-of-interval stamping, no bar carries 09:30:00 at all — and a formula that tests for it finds nothing, silently.
BarBar 1Bar 2Bar 3Bar 4
Trading covered09:30–09:3409:35–09:3909:40–09:4409:45–09:49
Stamped at start093000093500094000094500
Stamped at end093500094000094500095000
TimeNum() == 93000start convention1000
TimeNum() == 93000end convention0000
Illustrative bar stamps for a 09:30 open on five-minute bars. Under end-of-interval stamping, no bar carries 09:30:00 at all — and a formula that tests for it finds nothing, silently.

Read the last two rows together. A formula written as TimeNum() == 93000 is not asking “the first bar of the session”. It is asking “the bar whose stamp is exactly 09:30:00”, and under the second convention that bar does not exist. The formula does not fail. It produces Null and carries on, and every level derived from it is empty.

The reverse case is worse, because it does not look empty. Under end-of-interval stamping the bar labelled 093500 is the first bar of the session, not the second, so a formula that skips it to “avoid the opening bar” is actually skipping the first two blocks of trading. The chart still draws. The numbers are still plausible. Nothing anywhere says the definition moved.

Two useful facts sit alongside this. Status("lastbarend") returns the DateTime of the end of the last bar, and the documentation gives the example that a five-minute bar at 09:00 has an end time of 09:04:59 — which tells you that AmiBroker’s internal model of that bar is start-stamped and five minutes long. And Tools → Preferences → Intraday offers “Align custom minute bars to regular market hours”, documented with the example that without it a 45-minute bar series starts at 9:00, 9:45 and 10:30, while with it and a 9:30 open it aligns to 9:30, 10:15 and 11:00. Custom intervals are built on grid boundaries, not on the session, unless you ask for the session.

Finding out which convention your data uses

Section titled “Finding out which convention your data uses”

Stop reasoning about it. Ask the database.

Produce one row per trading day showing the stamp on the first bar of that day, the stamp on the last bar, how many bars the day contains, and the difference in minutes between the first stamp and the session open you believe the instrument has. Three columns and a subtraction settle a question that is otherwise a guess.

Complete runnable AFL

bar-timestamp-audit.afl
// ===========================================================================
// Bar timestamp audit
// Answers three questions about an intraday database that no dialog in
// AmiBroker answers directly:
// 1. What time is the FIRST bar of each trading day stamped with?
// 2. What time is the LAST bar of each trading day stamped with?
// 3. Do those stamps imply start-of-interval or end-of-interval stamping,
// and are they in exchange time or in some other time zone?
//
// HOW TO RUN
// Analysis window -> Apply to: Current symbol (or a small watch list),
// Range: the last one to three months, Periodicity: the database's base
// interval, then Explore. The result is one row per trading day.
//
// READING THE RESULT
// Compare "First bar of day" with the session open the exchange publishes
// for THIS instrument. Three outcomes are common:
// - first stamp == official open -> start-of-interval stamps
// - first stamp == official open + 1 interval -> end-of-interval stamps
// - both stamps displaced by whole hours -> a time-zone or database
// time-shift difference
// "Offset vs open" turns the third case into arithmetic instead of a guess:
// it is the difference in minutes between the first stamp of the day and
// the open time entered in the parameters.
//
// ASSUMPTIONS, STATED SO THEY CAN BE CHECKED
// - The database holds intraday bars. On end-of-day data every day has one
// bar, the audit still runs, and it tells you nothing useful.
// - The open time entered is the official open for this instrument, not for
// whichever market you happen to think of as the main one.
// - A day with a small bar count is not necessarily broken. Half days and
// holidays are real; distinguishing them from a failed download is a
// separate exercise and this audit does not attempt it.
// - Bar counts count bars that are present. They cannot detect a bar that
// was never delivered in the middle of a session.
// ===========================================================================
ExpectedOpenHour = Param( "Exchange open - hour (0-23)", 9, 0, 23, 1 );
ExpectedOpenMinute = Param( "Exchange open - minute (0-59)", 30, 0, 59, 1 );
// A new trading day begins on the first bar whose date differs from the
// previous bar's date. Nz() supplies a value for bar zero, where Ref() has
// nothing to look back at and would otherwise return Null.
PreviousDate = Nz( Ref( DateNum(), -1 ), 0 );
NewDay = DateNum() != PreviousDate;
// The last bar of a day is the bar immediately before a new day starts, plus
// the final bar in the array, which has no successor to compare against.
LastBarOfDay = Nz( Ref( NewDay, 1 ), 1 );
BarsInDay = BarsSince( NewDay ) + 1;
// TimeNum() packs time as 10000*hour + 100*minute + second. That encoding is
// safe to compare and unsafe to subtract, so anything arithmetic is done in
// minutes-since-midnight, built from Hour() and Minute().
MinutesOfDay = Hour() * 60 + Minute();
FirstMinuteOfDay = ValueWhen( NewDay, MinutesOfDay );
ExpectedOpenMin = ExpectedOpenHour * 60 + ExpectedOpenMinute;
OffsetMinutes = FirstMinuteOfDay - ExpectedOpenMin;
// DateTimeConvert( 2, date, time ) builds a DateTime from a DateNum and a
// TimeNum, so the first stamp of the day can be printed with formatDateTime
// instead of as a bare five- or six-digit number.
FirstStampOfDay = DateTimeConvert( 2, DateNum(), ValueWhen( NewDay, TimeNum() ) );
// Status( "timeshift" ) reports the database time shift in seconds (5.60+).
// It is the offset AmiBroker itself applies; it is not the exchange's offset.
TimeShiftHours = Status( "timeshift" ) / 3600;
Filter = LastBarOfDay;
SetOption( "NoDefaultColumns", True );
AddTextColumn( Name(), "Symbol", 1.0, colorDefault, colorDefault, 70 );
AddColumn( DateTime(), "Last bar of day", formatDateTime );
AddColumn( FirstStampOfDay, "First bar of day", formatDateTime );
AddColumn( BarsInDay, "Bars", 1.0 );
// The offset is the finding. The background tint is a convenience only: the
// number itself carries the whole message and is readable without colour.
AddColumn( OffsetMinutes, "Offset vs open (min)", 1.0, colorDefault,
IIf( OffsetMinutes != 0, colorRose, colorDefault ) );
AddTextColumn( NumToStr( Interval(), 1.0, False ), "Bar length (s)" );
AddTextColumn( NumToStr( TimeShiftHours, 1.2, False ), "DB time shift (h)" );
SetSortColumns( 2 );

Download bar-timestamp-audit.afl86 lines

The audit is built from one idea repeated: a new trading day begins on the first bar whose date differs from the previous bar’s date. Nz( Ref( DateNum(), -1 ), 0 ) looks back one bar and supplies a value for bar zero, where there is nothing to look back at; comparing that with DateNum() marks every day boundary in the array.

From that single marker everything else follows. The last bar of a day is the bar immediately before the next day’s first bar, plus the final bar of the array, which has no successor. BarsSince( NewDay ) + 1 gives the bar’s position within its day, so on the last bar of the day it is the day’s bar count. ValueWhen( NewDay, ... ) reaches back to the value that any array held on the day’s first bar and holds it for the rest of the day.

The arithmetic deliberately avoids TimeNum(). That encoding packs hours, minutes and seconds into decimal digits, so TimeNum() - 100 is “one minute earlier” only when the minute field is non-zero; comparisons are safe and subtraction is not. Minutes since midnight, built as Hour() * 60 + Minute(), is the form you can do arithmetic on. The offset column is that subtraction against the open time you typed into the parameters.

DateTimeConvert( 2, DateNum(), ... ) builds a proper DateTime from a date and a time so the first stamp can be printed with formatDateTime rather than as a bare five-digit number, and Status("timeshift") / 3600 reports the shift AmiBroker itself is applying, in hours, alongside the evidence rather than buried in a dialog.

  • DateNum() — the bar’s date as 10000 * (year - 1900) + 100 * month + day.
  • TimeNum() — the bar’s time as 10000 * hour + 100 * minute + second. Compare it; do not do arithmetic on it.
  • DateTimeConvert( format, date, time ) — format 2 converts a DateNum plus an optional TimeNum into a DateTime. The time argument is meaningful only for format 2.
  • Status("timeshift") — the database time shift in seconds, available since 5.60.
  • Interval() — the current bar interval in seconds; Interval(2) returns its name as a string, which is fine for display and, as the documentation warns, unsafe to compare against because the names are translated in localised builds.

Read the offset column first, then the bar count. A constant offset of 60, 120 or 300 minutes across the whole history is a time-zone difference. A constant offset equal to one interval is end-of-interval stamping. An offset that changes on two specific dates a year is daylight saving, which the third lesson takes apart. An offset that appears only on some days, with an unusually low bar count, is a short session or missing data, which is the second lesson’s territory.

Run the audit twice on the same symbol at two different chart intervals — the base interval, and something four or five times larger. Under start-of-interval stamping the first stamp of the day does not change. Under end-of-interval stamping it moves by the difference in interval length, because the end of the first five-minute block and the end of the first thirty-minute block are not the same instant. That single comparison distinguishes the two conventions without needing to know anything about your provider.

Then change Tools → Preferences → Intraday → "Time compressed bars show" to the other setting and re-run. The stored data has not moved; the audit’s output has. Change it back to whichever value you intend to standardise on, and write that value down somewhere, because a colleague’s machine will not have it.

  • Running it on an end-of-day database. Every day has one bar, the offset column is meaningless, and the audit tells you nothing. It will not error, which is the problem.
  • Entering the open time of the wrong market. The parameters take the open of this instrument. A futures contract, a cross-listed share and an index quoted on the same screen do not share a session.
  • Reading a zero offset as proof that nothing is shifted. Zero means the first stamp matches the open you typed. If you typed the open in the wrong zone, zero is confirming your error back to you. Check the time-shift column beside it.
  • Assuming a low bar count means broken data. Short sessions exist and are scheduled. Distinguishing them is the next lesson.

Add a column counting the bars between the first stamp of the day and the first stamp of the previous day, in minutes, using the same ValueWhen( ..., 2 ) trick the shift detector uses in the third lesson. A day where that difference is not a multiple of twenty-four hours is a day where something moved, and you have found it without knowing in advance what to look for.

The audit answers a question about history. During a live session you also want the answer to a different one: are these three clocks agreeing right now?

Complete runnable AFL

three-clocks-panel.afl
// ===========================================================================
// Three clocks panel
// Puts the clocks that disagree side by side in one chart title, so that a
// disagreement is visible rather than inferred:
// 1. the operating-system clock - Now( 5 )
// 2. the stamp on the newest bar - DateTime() at the last bar
// 3. the data source's own last update - Status( "lastrtupdate" )
// together with the database time shift AmiBroker applies between the source
// and those bar stamps, and the Bar Replay position when replay is running.
//
// HOW TO RUN
// Apply as an indicator to a chart pane. It draws no series, only a title,
// so give it a thin pane above the price chart.
//
// WHAT IT NEEDS
// Clocks 1 and 2 work in any edition, on any data, including an imported
// end-of-day database. Clock 3 is populated only while a real-time plugin
// is reporting DateUpdate / TimeUpdate. On an imported or end-of-day
// database it stays blank, which is correct behaviour and not a fault.
//
// ASSUMPTIONS, STATED SO THEY CAN BE CHECKED
// - Now() reads the computer's clock. It knows nothing about the exchange
// and nothing about the database time shift, so the "behind by" figures
// are only meaningful once bar stamps and the machine clock refer to the
// same time zone. When they do not, that is what this panel shows, and
// showing it is the point.
// - DateTime values are bitsets. They are compared here only with
// DateTimeDiff(), never with the > or < operators.
// - The panel reports staleness. It cannot tell you whether the data that
// did arrive is correct.
// ===========================================================================
_SECTION_BEGIN( "Three clocks" );
RefreshSeconds = Param( "Refresh interval (seconds)", 5, 1, 60, 1 );
RequestTimedRefresh( RefreshSeconds, True );
TimeShiftHours = Status( "timeshift" ) / 3600;
MachineClock = Now( 5 ); // DateTime of the operating-system clock
LastBarStamp = LastValue( DateTime() ); // stamp AmiBroker holds for the newest bar
FeedStamp = Status( "lastrtupdate" ); // DateTime the plugin last reported
// DateTimeDiff returns seconds, positive when the first argument is the later
// of the two. This is the sanctioned way to order two DateTime values.
BarLagSeconds = DateTimeDiff( MachineClock, LastBarStamp );
// GetPlaybackDateTime() returns zero when Bar Replay is not running, which is
// the documented way to test whether replay is active.
PlaybackStamp = GetPlaybackDateTime();
FeedLine = "not reported by this data source";
if( FeedStamp )
{
FeedLine = DateTimeToStr( FeedStamp )
+ " (" + NumToStr( DateTimeDiff( MachineClock, FeedStamp ), 1.0, False )
+ " s ago by the machine clock)";
}
ReplayLine = "Bar Replay: not active";
if( PlaybackStamp )
{
ReplayLine = "Bar Replay position: " + DateTimeToStr( PlaybackStamp );
}
Title = "THREE CLOCKS " + Name() + " " + Interval( 2 ) + " bars\n"
+ "1 Machine clock, Now() : " + DateTimeToStr( MachineClock ) + "\n"
+ "2 Newest bar stamp : " + DateTimeToStr( LastBarStamp )
+ " (" + NumToStr( BarLagSeconds, 1.0, False )
+ " s behind the machine clock)\n"
+ "3 Data source last update: " + FeedLine + "\n"
+ " Database time shift : " + NumToStr( TimeShiftHours, 1.2, False ) + " hours\n"
+ " " + ReplayLine;
_SECTION_END();

Download three-clocks-panel.afl75 lines

The panel prints the operating-system clock from Now(5), the stamp on the newest bar, and the data source’s own last-update time from Status("lastrtupdate"), together with the database time shift and — when Bar Replay is running — the replay position from GetPlaybackDateTime(), which the documentation defines as returning zero when replay is not active.

Two details in that formula are load-bearing. First, the gaps are measured with DateTimeDiff() and never with > or <. The DateTime documentation is explicit for version 5.27 and above: a DateTime is a bitset, two of them can be reliably compared only for equality or inequality, and any greater-than or less-than comparison using the normal operators can give wrong results. DateTimeDiff( a, b ) returns the gap in seconds, positive when a is the later value, and is the sanctioned way to order them.

Second, the “seconds behind” figures are only meaningful once the bar stamps and the machine clock refer to the same zone. If your database is deliberately shifted to show exchange time while your computer sits three zones away, the panel will report the newest bar as hours behind. That is not a stale feed; it is the panel telling you the truth about your configuration, which is precisely what you asked it for.

A bar’s timestamp is a bare date and time with no zone attached, assigned by a provider whose convention you have to establish rather than assume, then possibly moved by a database time shift, and displayed according to a preference that can differ between two machines looking at the same database.

Start-of-interval and end-of-interval stamping both exist. Under the second, no bar carries the session open time at all, so an equality test against it finds nothing and says nothing. You now have an audit that settles the question from your own data in one Exploration, and a panel that keeps the three clocks visible while you work.

The next lesson takes the timestamps you have just learned to trust and uses them for what they are for: deciding which bars belong to which session.

Check your understanding

Question 1. A five-minute bar covers all trades from 09:30:00 to 09:34:59. Under end-of-interval stamping, what does `TimeNum() == 93000` select on the day's first bar?
FirstBar = TimeNum() == 93000;
Show the answer and why

Answer: Nothing — no bar carries that stamp

End-of-interval stamping names that bar 09:35:00. No bar in the day carries 09:30:00, so the comparison is false everywhere and any level derived from it stays empty. The formula does not error; it simply produces nothing.

Question 2. Which comparison of two DateTime values does the official documentation say can give wrong results?
Show the answer and why

Answer: a > b

From version 5.27 the DateTime page states that a DateTime is a bitset, that two values can be reliably compared only for equality or inequality, and that greater-than or less-than comparisons using the normal operators may be wrong. DateTimeDiff() returns the gap in seconds and is the sanctioned way to order them.

Question 3. Your audit shows a constant offset of exactly one bar length between the first stamp of every day and the exchange open. Which explanation fits best?
Show the answer and why

Answer: The data is stamped at the end of each interval

An offset equal to the interval length, constant across the whole history and moving when you change chart interval, is the signature of end-of-interval stamping. A time-zone difference would show as whole hours and would not change with the chart interval; a missing first bar would not be perfectly consistent across every day.

Question 4. Which of these can change what `TimeNum()` returns for a given block of trading? Select all that apply.
Show the answer and why

Answer: The provider's stamping convention, The database Time shift setting, Preferences → Intraday → "Time compressed bars show"

The first three all act on the stamps AmiBroker holds or displays. The operating-system time zone changes what Now() reports and when scheduled work runs, but the bar array carries no zone and is not re-interpreted when you change the machine's.

Question 5. True or false: a database time shift of zero proves the bar stamps are in exchange time.
Show the answer and why

Answer: False

False. Status("timeshift") reports only what AmiBroker is adding. Zero means AmiBroker is not moving the stamps — the provider may still have delivered them in its own zone. Only comparing the stamps against a session time you know independently settles it.

Sources for this lesson

11 verified · checked 2026-08-31

  1. 01AmiBroker AFL Function Reference — TimeNumamibroker.com/guide/afl/timenum.html2026-08-31
  2. 02AmiBroker AFL Function Reference — DateTimeamibroker.com/guide/afl/datetime.html2026-08-31
  3. 03AmiBroker AFL Function Reference — DateTimeDiffamibroker.com/guide/afl/datetimediff.html2026-08-31
  4. 04AmiBroker AFL Function Reference — DateTimeConvertamibroker.com/guide/afl/datetimeconvert.html2026-08-31
  5. 05AmiBroker AFL Function Reference — Nowamibroker.com/guide/afl/now.html2026-08-31
  6. 06AmiBroker AFL Function Reference — Status§ timeshift, lastbarend, lastbartimeleft, lastrtupdateamibroker.com/guide/afl/status.html2026-08-31
  7. 07AmiBroker AFL Function Reference — Intervalamibroker.com/guide/afl/interval.html2026-08-31
  8. 08AmiBroker AFL Function Reference — GetPlaybackDateTimeamibroker.com/guide/afl/getplaybackdatetime.html2026-08-31
  9. 09AmiBroker User's Guide — Database Settings window§ Intraday Settingsamibroker.com/guide/w_dbsettings.html2026-08-31
  10. 10AmiBroker User's Guide — Preferences window§ Intraday tabamibroker.com/guide/w_preferences.html2026-08-31
  11. 11AmiBroker User's Guide — Import from ASCII file§ $TIMESHIFTamibroker.com/guide/d_ascii.html2026-08-31

Every technical claim on this page was checked against the official AmiBroker documentation on the date shown. Where the course disagrees with folklore, the source is how you can tell which one to trust.