Skip to content
Level 5 · Real-Time AmiBroker UserProjectPart 23 · page 4 of 455 min Professional edition Live feed
55Minutes
12AFL functions
10Sources
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 here12

Project: Real-Time Quote Dashboard

A quote panel is the first thing most people build once they have a real-time feed, and it is also the first thing most people build badly. The bad version works beautifully during market hours on the machine it was written on, and lies confidently at every other time — displaying a bid of zero on a Sunday, a spread of minus one hundred per cent when the plugin drops, and a price from Thursday presented as though it were current.

This project builds the other kind. It shows the same information, and it is honest about where every number came from and how old it is. It also runs, deliberately and by design, on a machine with no data subscription at all.

Seven display requirements and two hard rules.

The display: last traded price; change against the previous close in price and percentage; the session open, high, low and range; volume; best bid and ask with their sizes; the spread in price and as a percentage of the mid; and the age of the information.

The two rules are what make it a project rather than an exercise.

It must never present a number it did not receive. Not a zero standing in for an absent bid, not yesterday’s close standing in for a live price, not a spread computed from one live side and one invented one. Where a value is unavailable, the panel says so.

It must always say which mode it is in. Not in small print at the bottom. In the largest type on the panel, at the top, before anything else can be read.

Mode When What the quote rows show
LIVE A real-time plugin answered GetRTData() Live values from the feed, each tagged feed
REPLAY Bar Replay is active and no feed answered Historical bars at the playback position, tagged bars, banner reads NOT LIVE
OFFLINE Nothing answered Values derived from stored bars, tagged bars, banner reads NOT LIVE

In REPLAY and OFFLINE the bid, ask and size rows read n/a. A bar database contains no quotes, and no arithmetic on Close will produce one.

Design decisions worth making before you type

Section titled “Design decisions worth making before you type”

The previous lessons established that GetRTData() is documented to work only with a real-time plugin and to yield Null for every field in the Standard edition, and that no numeric substitute is promised for the missing case. The design consequence is that every number in this panel has three states, not two: present, absent, and derived-from-bars.

A derived value inherits the worst state of its inputs. If either the last price or the previous close is absent, the change is absent — not zero. If either side of the quote is absent, the spread is absent. The formula does this explicitly with IsNull() tests rather than letting Null propagate through arithmetic, because propagation is silent and a test is not.

Every displayed value carries a one-word tag: feed if a real-time field supplied it, bars if it came from stored quotations. The tag is not decoration. In LIVE mode a vendor may supply "Last" and "Bid" but not "Prev", in which case the panel legitimately mixes a live price with a previous close read from the database — and you want to be able to see that at a glance rather than deduce it.

The tag is derived from exactly the same test that chose the value, in a separate function that takes the same arguments. That is more code than tracking a flag by hand, and it is worth it: a provenance label that can drift away from the value it labels is worse than none.

The banner is the largest thing on the panel

Section titled “The banner is the largest thing on the panel”

GfxSelectFont() is called twice: once at one and a half times the body size in bold for the mode banner, once at body size for everything else. In LIVE the banner is green; in the other two it is amber and contains the words NOT LIVE.

This is not politeness. A panel that has silently fallen back to stored data looks exactly like a working panel if the only difference is a small grey word somewhere. Make the failure mode loud enough that you cannot fail to notice it while looking at something else.

Session figures come from bars in every mode

Section titled “Session figures come from bars in every mode”

The panel computes the session open, high, low, range and volume from the chart’s own bars whether or not a feed is present. In OFFLINE and REPLAY those computed values are what gets displayed. In LIVE they are still computed — and if the vendor does not supply the corresponding field, they are what gets displayed there too.

They also serve as a free cross-check. When both are available and they disagree by more than rounding, something is wrong: a session definition mismatch, a timeshift problem, or a vendor whose “today” starts at a different hour than your database’s does.

The grouping is by calendar day, using Day() against the previous bar. On a daily chart that makes the session one bar, which is correct. On a 5-minute chart it is every bar so far that day, which is also correct. On a weekly or monthly chart it is neither, so the panel changes the row labels from “session” to “last bar” rather than presenting a meaningless figure under a meaningful name.

There are two clocks and they are not the same clock

Section titled “There are two clocks and they are not the same clock”

Now() reads the computer’s clock. Bar timestamps come from the database, offset by whatever the database timeshift is set to. The feed’s own "UpdateDate" and "UpdateTime" fields are a third reading, in DATENUM and TIMENUM encodings that need DateTimeConvert() before they can be compared with anything.

The panel prints the feed stamp, the age of that stamp measured against the PC clock, the timestamp of the last bar, and the database timeshift in seconds — four rows instead of one tidy “data age” figure. That is deliberate. A single staleness number computed across clocks that disagree is a number that looks authoritative and is not, and this is a common way for a dashboard to mislead its author.

How one row decides what to display

  1. Was a feed field requested?The Data source parameter can force offline mode
  2. Did the feed answer?IsNull() on the returned value, not a comparison against zero
  3. Use the feed value, tag it "feed"Only when a real number came back
  4. Otherwise use the bar-derived value, tag it "bars"Which may itself be absent
  5. If neither exists, display "n/a"No substitution, no zero, no carried-over value
Bid, ask and the two size rows have no bar-derived fallback, so with no feed they always reach the last step.

Complete runnable AFL

quote-dashboard.afl
// quote-dashboard.afl
// Part 23 - Project: Real-Time Quote Dashboard
//
// One chart pane reporting the state of one symbol: last traded price, change
// against the previous close, the session open, high, low and range, volume,
// best bid and ask with their sizes, the spread in both price and percentage
// terms, and how old the information is.
//
// It runs in three modes and always names the one it is in, at the top of the
// panel, in a larger font than anything else on it:
//
// LIVE a real-time plugin answered GetRTData(), so the quote rows are
// live values from your feed.
// REPLAY Bar Replay is active. Everything shown is historical data being
// played back at the playback position. It is NOT live.
// OFFLINE no real-time field answered. The price rows are derived from the
// bars in your database, each one tagged "bars". Bid, ask and
// their sizes read "n/a", because a bar database does not contain
// them and this formula will not invent them.
//
// Nothing here manufactures a quote. Every displayed number carries a tag
// saying whether it came from the feed or from stored bars, and any value
// that cannot be obtained is displayed as "n/a" rather than as zero.
//
// Assumptions and honest limits:
// - Apply to an empty chart pane. It draws its own panel and suppresses the
// underlying chart with GfxSetOverlayMode(2). Give the pane roughly 500
// pixels of height at the default font size, or reduce the font size.
// - LIVE mode needs the Professional edition and a database fed by a
// real-time data plugin. Both requirements are stated on the GetRTData()
// reference page, and neither is something AFL can work around.
// - Which fields a feed supplies is decided by the vendor. A field the
// vendor does not send reads "n/a" even in LIVE mode. rt-field-probe.afl
// tells you which ones yours sends.
// - Session figures in REPLAY and OFFLINE mode are derived from the bars of
// the chart's own interval, grouped by calendar day. On a daily chart the
// "session" is one bar. On a 5-minute chart it is every bar so far that
// day. On a weekly or monthly chart the grouping is not a trading session
// at all, and the panel labels those rows "last bar" instead.
// - Two clocks are involved. Bar timestamps are database time; Now() is your
// PC clock. Status("timeshift") reports the database timeshift in seconds
// and the panel prints it, because a staleness figure computed across two
// clocks that disagree is worse than no staleness figure.
_SECTION_BEGIN("Real-time quote dashboard");
//----------------------------------------------------------------------------
// 1. Settings.
//----------------------------------------------------------------------------
RefreshSeconds = Param( "Refresh interval (seconds)", 3, 1, 60, 1 );
PriceDecimals = Param( "Price decimals", 2, 0, 6, 1 );
FontPoints = Param( "Font size (points)", 10, 6, 20, 0.5 );
SourceChoice = ParamList( "Data source", "Automatic|Offline (historical bars only)", 0 );
PanelFace = ParamStr( "Panel font", "Tahoma" );
BackTone = ParamColor( "Panel background", colorBlack );
LabelTone = ParamColor( "Labels", colorLightGrey );
ValueTone = ParamColor( "Values", colorWhite );
LiveTone = ParamColor( "LIVE banner", colorBrightGreen );
NotLiveTone = ParamColor( "NOT LIVE banner", colorOrange );
RiseTone = ParamColor( "Change up", colorBrightGreen );
FallTone = ParamColor( "Change down", colorRed );
// NumToStr takes a format number where the fractional part is the number of
// decimals: 1.2 means two decimals. Building it from a parameter keeps one
// setting in charge of every price on the panel.
PriceFormat = 1 + PriceDecimals / 10;
RequestTimedRefresh( RefreshSeconds );
//----------------------------------------------------------------------------
// 2. Ask the feed. Do not assume it is there, and do not assume it is not.
//----------------------------------------------------------------------------
WantFeed = SourceChoice != "Offline (historical bars only)";
if( WantFeed )
{
RtLast = GetRTData( "Last" );
RtPrev = GetRTData( "Prev" );
RtOpen = GetRTData( "Open" );
RtHigh = GetRTData( "High" );
RtLow = GetRTData( "Low" );
RtVolume = GetRTData( "TotalVolume" );
RtBid = GetRTData( "Bid" );
RtAsk = GetRTData( "Ask" );
RtBidSize = GetRTData( "BidSize" );
RtAskSize = GetRTData( "AskSize" );
RtUpdDate = GetRTData( "UpdateDate" );
RtUpdTime = GetRTData( "UpdateTime" );
}
else
{
// Forcing offline mode is not a debugging convenience; it is how the panel
// is meant to be used by anyone without a subscription, and it must produce
// exactly the same display as a genuinely absent feed.
RtLast = Null;
RtPrev = Null;
RtOpen = Null;
RtHigh = Null;
RtLow = Null;
RtVolume = Null;
RtBid = Null;
RtAsk = Null;
RtBidSize = Null;
RtAskSize = Null;
RtUpdDate = Null;
RtUpdTime = Null;
}
// A feed is answering when at least one core quote field came back as a real
// number. Note what this test does not do: it does not treat zero as absence,
// and it never treats absence as zero.
FeedAnswering = ( NOT IsNull( RtLast ) ) OR ( NOT IsNull( RtBid ) ) OR ( NOT IsNull( RtAsk ) );
// GetPlaybackDateTime() returns the Bar Replay position, or zero when replay
// is not active. Zero is a legal-looking number, so it must be tested before
// the value is used for anything.
PlaybackStamp = GetPlaybackDateTime();
ReplayActive = PlaybackStamp != 0;
//----------------------------------------------------------------------------
// 3. Session figures derived from stored bars.
// These are the fallback, and they are also a sanity check on the feed.
//----------------------------------------------------------------------------
BarSeconds = Interval();
SessionLabel = "session";
if( BarSeconds >= 86400 ) SessionLabel = "last bar";
NewSession = Day() != Ref( Day(), -1 );
NewSession[ 0 ] = 1; // the first loaded bar starts one
BarsThisSession = Nz( BarsSince( NewSession ) ) + 1;
BarLast = LastValue( Close );
BarOpen = LastValue( ValueWhen( NewSession, Open ) );
BarHigh = LastValue( HHV( High, BarsThisSession ) );
BarLow = LastValue( LLV( Low, BarsThisSession ) );
BarVolume = LastValue( Sum( Volume, BarsThisSession ) );
BarPrev = LastValue( ValueWhen( NewSession, Ref( Close, -1 ) ) );
BarStamp = LastValue( DateTime() );
//----------------------------------------------------------------------------
// 4. Choose a value for each row, and remember where it came from.
//----------------------------------------------------------------------------
// Returns the live value when the feed supplied one, otherwise the value
// derived from bars - which may itself be Null, and is allowed to be.
function PickValue( LiveValue, BarValue, HaveFeed )
{
if( HaveFeed AND NOT IsNull( LiveValue ) ) Chosen = LiveValue;
else Chosen = BarValue;
return Chosen;
}
// The provenance of whatever PickValue chose, as text. Never guessed: it is
// derived from the same test PickValue used.
function PickTag( LiveValue, BarValue, HaveFeed )
{
if( HaveFeed AND NOT IsNull( LiveValue ) ) Tag = "feed";
else if( IsNull( BarValue ) ) Tag = "";
else Tag = "bars";
return Tag;
}
// Formats one cell. A missing value becomes the word "n/a", never a zero and
// never a value carried over from somewhere else.
function CellText( Value, Tag, Decimals )
{
if( IsNull( Value ) ) Text = "n/a";
else Text = NumToStr( Value, 1 + Decimals / 10 ) + " " + Tag;
return Text;
}
NumLast = PickValue( RtLast, BarLast, FeedAnswering );
NumPrev = PickValue( RtPrev, BarPrev, FeedAnswering );
NumOpen = PickValue( RtOpen, BarOpen, FeedAnswering );
NumHigh = PickValue( RtHigh, BarHigh, FeedAnswering );
NumLow = PickValue( RtLow, BarLow, FeedAnswering );
NumVolume = PickValue( RtVolume, BarVolume, FeedAnswering );
// Bid and ask have no equivalent anywhere in a bar database. Passing Null as
// the fallback is the whole point: with no feed these rows must read "n/a".
NumBid = PickValue( RtBid, Null, FeedAnswering );
NumAsk = PickValue( RtAsk, Null, FeedAnswering );
NumBidSize = PickValue( RtBidSize, Null, FeedAnswering );
NumAskSize = PickValue( RtAskSize, Null, FeedAnswering );
// Derived figures inherit the missing state of their inputs rather than
// quietly becoming zero.
if( IsNull( NumLast ) OR IsNull( NumPrev ) )
{
NumChange = Null;
NumChangePct = Null;
}
else
{
NumChange = NumLast - NumPrev;
NumChangePct = 100 * NumChange / Max( Abs( NumPrev ), 0.000001 );
}
if( IsNull( NumHigh ) OR IsNull( NumLow ) ) NumRange = Null;
else NumRange = NumHigh - NumLow;
if( IsNull( NumBid ) OR IsNull( NumAsk ) )
{
NumSpread = Null;
NumSpreadPct = Null;
}
else
{
NumSpread = NumAsk - NumBid;
MidPrice = ( NumAsk + NumBid ) / 2;
NumSpreadPct = 100 * NumSpread / Max( Abs( MidPrice ), 0.000001 );
}
//----------------------------------------------------------------------------
// 5. Freshness. Two clocks, reported separately.
//----------------------------------------------------------------------------
FeedStamp = Null;
if( ( NOT IsNull( RtUpdDate ) ) AND ( NOT IsNull( RtUpdTime ) ) )
{
// UpdateDate is a DATENUM and UpdateTime is a TIMENUM. Neither is a
// DateTime, so they must be converted before they mean anything.
FeedStamp = DateTimeConvert( 2, RtUpdDate, RtUpdTime );
}
AgeSeconds = Null;
if( NOT IsNull( FeedStamp ) )
{
// DateTime values are a bitset. Only == and != are reliable on them, so
// elapsed time comes from DateTimeDiff(), not from subtraction.
AgeSeconds = DateTimeDiff( Now( 5 ), FeedStamp );
}
TimeShiftSeconds = Status( "timeshift" );
//----------------------------------------------------------------------------
// 6. Mode banner.
//----------------------------------------------------------------------------
if( FeedAnswering )
{
ModeText = "LIVE";
ModeTone = LiveTone;
ModeDetail = "real-time fields answered by the data plugin";
}
else if( ReplayActive )
{
ModeText = "REPLAY - NOT LIVE";
ModeTone = NotLiveTone;
ModeDetail = "Bar Replay position " + DateTimeToStr( PlaybackStamp );
}
else
{
ModeText = "OFFLINE - NOT LIVE";
ModeTone = NotLiveTone;
ModeDetail = "no real-time field answered; every price below comes from stored bars";
}
//----------------------------------------------------------------------------
// 7. Draw.
//----------------------------------------------------------------------------
GfxSetOverlayMode( 2 ); // panel only: no price chart, no grid behind it
GfxSetCoordsMode( 0 ); // pixel coordinates
GfxSetBkMode( 1 ); // transparent text background
PanelWidth = Status( "pxwidth" );
PanelHeight = Status( "pxheight" );
GfxSelectSolidBrush( BackTone );
GfxSelectPen( BackTone );
GfxRectangle( 0, 0, PanelWidth, PanelHeight );
LeftMargin = 12;
ValueColumn = 200;
RowHeight = Round( 1.75 * FontPoints );
TopMargin = Round( 3.4 * FontPoints );
procedure PanelRow( RowIndex, LabelText, ValueText, Tone )
{
RowY = TopMargin + RowIndex * RowHeight;
GfxSetTextColor( LabelTone );
GfxTextOut( LabelText, LeftMargin, RowY );
GfxSetTextColor( Tone );
GfxTextOut( ValueText, ValueColumn, RowY );
}
// Banner first, in its own larger, heavier font, so that "NOT LIVE" is the
// first thing anyone reads.
GfxSelectFont( PanelFace, FontPoints * 1.5, 700 );
GfxSetTextColor( ModeTone );
GfxTextOut( Name() + " " + ModeText, LeftMargin, 6 );
GfxSelectFont( PanelFace, FontPoints, 400 );
PanelRow( 0, Interval( 2 ) + " chart", ModeDetail, LabelTone );
if( IsNull( NumChange ) ) ChangeTone = ValueTone;
else if( NumChange > 0 ) ChangeTone = RiseTone;
else if( NumChange < 0 ) ChangeTone = FallTone;
else ChangeTone = ValueTone;
PanelRow( 2, "Last", CellText( NumLast, PickTag( RtLast, BarLast, FeedAnswering ), PriceDecimals ), ValueTone );
PanelRow( 3, "Change", CellText( NumChange, "", PriceDecimals ), ChangeTone );
PanelRow( 4, "Change %", CellText( NumChangePct, "", 2 ), ChangeTone );
PanelRow( 5, "Previous close", CellText( NumPrev, PickTag( RtPrev, BarPrev, FeedAnswering ), PriceDecimals ), ValueTone );
PanelRow( 7, SessionLabel + " open", CellText( NumOpen, PickTag( RtOpen, BarOpen, FeedAnswering ), PriceDecimals ), ValueTone );
PanelRow( 8, SessionLabel + " high", CellText( NumHigh, PickTag( RtHigh, BarHigh, FeedAnswering ), PriceDecimals ), ValueTone );
PanelRow( 9, SessionLabel + " low", CellText( NumLow, PickTag( RtLow, BarLow, FeedAnswering ), PriceDecimals ), ValueTone );
PanelRow( 10, SessionLabel + " range", CellText( NumRange, "", PriceDecimals ), ValueTone );
PanelRow( 11, SessionLabel + " volume", CellText( NumVolume, PickTag( RtVolume, BarVolume, FeedAnswering ), 0 ), ValueTone );
PanelRow( 13, "Bid", CellText( NumBid, PickTag( RtBid, Null, FeedAnswering ), PriceDecimals ), ValueTone );
PanelRow( 14, "Bid size", CellText( NumBidSize, PickTag( RtBidSize, Null, FeedAnswering ), 0 ), ValueTone );
PanelRow( 15, "Ask", CellText( NumAsk, PickTag( RtAsk, Null, FeedAnswering ), PriceDecimals ), ValueTone );
PanelRow( 16, "Ask size", CellText( NumAskSize, PickTag( RtAskSize, Null, FeedAnswering ), 0 ), ValueTone );
PanelRow( 17, "Spread", CellText( NumSpread, "", PriceDecimals ), ValueTone );
PanelRow( 18, "Spread %", CellText( NumSpreadPct, "", 3 ), ValueTone );
if( IsNull( FeedStamp ) ) FeedStampText = "n/a - no feed update stamp";
else FeedStampText = DateTimeToStr( FeedStamp );
if( IsNull( AgeSeconds ) ) AgeText = "n/a";
else AgeText = NumToStr( AgeSeconds, 1.0 ) + " s (PC clock minus feed stamp)";
PanelRow( 20, "Feed update stamp", FeedStampText, ValueTone );
PanelRow( 21, "Feed stamp age", AgeText, ValueTone );
PanelRow( 22, "Last bar stamp", DateTimeToStr( BarStamp ), ValueTone );
PanelRow( 23, "Database timeshift", NumToStr( TimeShiftSeconds, 1.0 ) + " s", ValueTone );
PanelRow( 24, "Panel refresh", NumToStr( RefreshSeconds, 1.0 ) + " s requested", ValueTone );
_SECTION_END();

Download quote-dashboard.afl337 lines

The file is in seven numbered sections, and they run in dependency order.

Settings collects everything adjustable into one block: the refresh interval, the number of price decimals, the font, and the colour palette through ParamColor() so the panel can be matched to a dark or a light theme. The price decimals parameter is turned into a NumToStr() format number by the expression 1 + PriceDecimals / 10, which exploits the documented convention that the fractional part of the format is the digit count. One setting then governs every price on the panel.

Asking the feed is a single block of GetRTData() calls, wrapped in a test of the data source parameter. Choosing “Offline (historical bars only)” sets every one of those variables to Null instead of calling the function. That matters more than it looks: it means the offline path is exercised by exactly the same downstream code as a genuinely absent feed, so testing the offline display does not require disconnecting anything.

The feed-detection test then asks whether any of the last price, the bid or the ask came back as a real number. It uses IsNull(), never a comparison against zero, because zero is a legitimate value for several fields and absence is not a number at all.

GetPlaybackDateTime() supplies the replay test. It returns the Bar Replay position, or zero when replay is not active — and zero is a perfectly plausible-looking DateTime, so the result is compared against zero before it is used anywhere.

Session figures from bars builds the fallback set. Day() compared against the previous bar marks each new session, with the first loaded bar forced to be a session start so that the count is never Null. BarsSince() gives the bar count within the session, and that count is then used as a variable period for HHV(), LLV() and Sum() — all three are documented to accept an array period. ValueWhen() picks up the session’s opening price and the close of the bar before it started.

Choosing values is three small user functions. PickValue() returns the live value when the feed supplied one and the bar-derived value otherwise. PickTag() answers the same question and returns the word feed, bars or an empty string. CellText() formats a number or returns the string n/a. Bid, ask and the sizes are passed Null as their bar-derived fallback, which is how those rows reach n/a without any special case.

The derived figures — change, change percentage, range, spread, spread percentage — are then computed inside explicit IsNull() guards, each one producing Null when any input is missing. Divisions are floored with Max( Abs( x ), 0.000001 ) so that a genuine zero previous close cannot produce an infinity.

Freshness converts the feed’s update stamp with DateTimeConvert( 2, ... ) and measures its age with DateTimeDiff( Now( 5 ), FeedStamp ). DateTimeDiff() is used rather than subtraction because DateTime values are a bitset, and the documentation is explicit that only equality and inequality are reliable on them.

The mode banner is a three-way decision, in priority order: a feed that answered beats everything; replay beats plain offline; offline is the default. Each branch sets the banner text, its colour, and a one-line explanation.

Drawing switches the pane to overlay mode 2, which displays low-level graphics only, with no chart and no grid behind them, then fills the pane with the background colour and writes the rows. PanelRow() is a procedure taking a row index, a label, a value string and a colour; the row spacing and the two column positions are globals assigned above the procedure definition, which is what makes them visible inside it. Row indices are written out explicitly with gaps, so the visual grouping of the panel is readable in the source.

GetRTData() and its field names came from the first lesson of this part; RequestTimedRefresh() and Status() from the second and third. New here:

GetPlaybackDateTime() returns the Bar Replay position as a DateTime, or zero when replay is not running. GfxSetOverlayMode( 2 ) suppresses the chart and grid so the pane is a panel rather than an annotated chart. GfxSetCoordsMode( 0 ) selects pixel coordinates. GfxSetBkMode( 1 ) makes text backgrounds transparent. GfxSelectFont() takes a face name, a point size that may be fractional, and a weight in inked pixels per thousand, where 700 is bold. GfxTextOut() writes a string at a pixel position using the current font and text colour. Status( "pxwidth" ) and Status( "pxheight" ) give the pane’s pixel size, which is how the background rectangle knows how large to be. ParamColor() and ParamList() build the palette and the mode selector.

Apply the formula to an empty chart pane and give it roughly 500 pixels of height at the default 10-point font, or reduce the font size. The panel occupies the whole pane.

The banner reads the symbol name followed by LIVE, REPLAY - NOT LIVE or OFFLINE - NOT LIVE, in bold at one and a half times the body size, green in the first case and amber in the other two. Below it, the chart interval and a one-line explanation of the mode.

Then the rows, in three groups separated by blank lines:

Row LIVE REPLAY or OFFLINE
Last feed value, tagged feed last close, tagged bars
Change, Change % computed, coloured green up or red down computed from bars, same colouring
Previous close "Prev" if the vendor sends it, otherwise from bars from bars
Session open, high, low feed day values if sent, otherwise from bars from bars
Session range high minus low high minus low
Session volume "TotalVolume" if sent, otherwise summed from bars summed from bars
Bid, Bid size, Ask, Ask size feed values, tagged feed n/a
Spread, Spread % computed from bid and ask n/a
Feed update stamp converted from the two feed fields n/a - no feed update stamp
Feed stamp age seconds, against the PC clock n/a
Last bar stamp database timestamp of the last bar same
Database timeshift seconds, from Status( "timeshift" ) same
Panel refresh the interval you requested same

On a weekly or monthly chart the three “session” labels read “last bar” instead.

Six checks. The first four need nothing but the database you already have.

Force offline mode with a feed connected. Set the Data source parameter to “Offline (historical bars only)”. The banner must change to OFFLINE - NOT LIVE, the price rows must switch their tags from feed to bars, and the four quote rows must switch to n/a. Set it back and watch them return. This exercises the entire offline path in about four seconds and is the check to repeat after every edit.

Cross-check the derived session figures. With the panel in offline mode on a daily chart, compare the session open, high and low against the last bar in the Quote Editor. They should match exactly. Then switch the chart to a 5-minute interval on a symbol with intraday history and confirm the session high equals the highest high of that day’s bars rather than the last bar’s high.

Check the weekly relabelling. Switch the chart to weekly. The three session labels must become “last bar”. If they still read “session”, the interval test is wrong.

Run it under Bar Replay. Open Tools then Bar Replay, set the start date, and press Play or Pause. The banner must change to REPLAY - NOT LIVE and name the playback position, and the values must advance as the replay steps. Press Stop and the banner must return to OFFLINE. This is the check that the replay test is testing the right thing — a formula that treats zero as a valid playback position will claim to be in replay mode permanently.

With a feed: compare against the Real-Time Quote window. Put the same symbol in the Real-Time Quote window and watch both. The two bid values will differ at any given instant, and that is expected rather than a fault: the quote window is documented to refresh at least ten times a second, while the panel only sees a new value when the formula re-executes. What must not happen is a persistent disagreement that never resolves, or a panel value that stops changing while the quote window keeps moving.

With a feed: disconnect the plugin. Use the plugin’s own context menu on the connection status area to shut down the connection. The panel must fall back to OFFLINE - NOT LIVE rather than freezing on the last live values it happened to have. Reconnect and confirm it comes back. This is the failure mode that matters most, because it is the one that happens without you noticing.

Nz() on a quote field. The most common one, and the reason the panel exists in this form. It converts “no feed” into a price of zero, after which the change, the spread and the percentage rows all produce confident nonsense. IsNull() is the test; Nz() belongs on counters and accumulators, not on prices.

Testing == 0 instead of IsNull(). Zero is a real value for open interest, for trade volume before the first print of the day, and for the dividend on a company that pays none. Treating it as absence hides genuine data.

Using an unguarded GetPlaybackDateTime(). It returns zero when replay is inactive, and zero formats as a date perfectly happily, so an unguarded panel announces that replay is running at some point in 1899.

Comparing DateTime values with > or <. The documentation states that DateTime is a bitset and that only == and != are reliable on it, with DateTimeDiff() provided for ordering. Code that compares timestamps with the ordinary operators works most of the time, which is precisely what makes it dangerous.

Assuming the vendor supplies every field. A missing field in LIVE mode is not a bug in your formula. Run the field probe from the first lesson of this part, find out what your feed actually sends, and design around that rather than around the documented list.

Refreshing too often to notice. A one-second refresh on a panel with a heavy formula behind it makes the whole layout sluggish and tells you nothing extra: the feed itself determines how often the numbers change. Measure the panel with the cost meter from the previous lesson, then choose an interval.

Reading n/a as a fault. It is the panel working. A dashboard that never says n/a is a dashboard that has been taught to guess.

Add a second symbol column using GetRTDataForeign(), which the documentation describes as much faster than switching context with SetForeign(). The provenance and absence logic transfers unchanged; the layout does not, so you will need to pass a column offset into PanelRow().

Record a spread history. Sample the spread into a static variable array on each timer refresh, guarded by Status( "redrawaction" ) == 1 so that scrolling does not add samples, and plot the result. This is the only way to get a history of a quote field, because the feed gives you the present and nothing else. Watch the memory note in the static-variable documentation: an array static consumes eight bytes per bar and is not released until you remove it.

Add a bar countdown from Status( "lastbartimeleft" ), remembering that it works for time-based bars only and needs the database timeshift set correctly. Compare it against Status( "lastbartimeleftrt" ), which measures from the last real-time update instead of from the PC clock, and note which one behaves sensibly outside trading hours.

Add a staleness alarm that turns the banner amber when the feed stamp age exceeds a threshold you set. Then think carefully about what threshold is right for an instrument that simply has not traded for ten minutes, which is not the same condition as a broken feed.

The panel reports. It does not act, and this course does not extend it to acting.

The chain this course teaches ends at a person: a condition becomes true, something tells you about it, you look at it, and you decide. Part 25 adds the alerting step properly, including how to stop the same alert arriving forty times. Automated order execution exists in AmiBroker, through separate interfaces, and it is a serious subject with consequences that belong to you rather than to a formula. It is not taught here, and a quote panel is emphatically not the place to start it.

Where the course's chain ends

  1. ConditionEvaluated in AFL
  2. AlertPart 25
  3. Human reviewYou look at it
  4. DecisionYours
Everything past the fourth box is outside the scope of this course.

You have built a panel that reports one symbol’s state and tells you the provenance and age of every number on it. It runs in three modes, names the mode in the largest type on the panel, and treats absence as information rather than as an inconvenience to be zeroed away. Its offline mode is not a degraded version of the real thing — it is the mode the panel was developed and tested in, and the only difference in live mode is that four rows stop saying n/a.

That ordering is the point of the whole project. A dashboard designed offline first is a dashboard that behaves when the feed goes away, and the feed will go away.

Check your understanding

Question 1. The panel is in OFFLINE mode. What should the Bid row display?
Show the answer and why

Answer: n/a, because a bar database contains no quotes and none may be invented

Bid and ask are passed Null as their bar-derived fallback precisely so that this row reaches "n/a". Substituting the close would produce a spread of zero and a mid equal to the close, both of which look like data and are not.

Question 2. Why does the formula test `PlaybackStamp != 0` before using GetPlaybackDateTime()?
PlaybackStamp = GetPlaybackDateTime();
ReplayActive  = PlaybackStamp != 0;
Show the answer and why

Answer: Because the function returns zero when Bar Replay is not active, and zero formats as a valid-looking date

The reference page documents a zero return when replay is inactive, and its own example guards on it. Zero is a legal number that will render as a date string, so an unguarded panel would claim to be replaying at all times.

Question 3. Which of these are reasons the panel computes session figures from bars even in LIVE mode? Select all that apply.
Show the answer and why

Answer: A vendor may not supply the corresponding quote field, It provides a cross-check that can reveal a session or timeshift mismatch, It means the offline path is exercised by the same code every time the panel runs

Field availability is vendor-dependent, the two sources disagreeing is diagnostic information, and computing the fallback unconditionally means it is never a rarely-tested branch. It has no effect on how fast the quote function runs.

Question 4. Your panel and the Real-Time Quote window show different bid values at the same moment. What is the correct conclusion?
Show the answer and why

Answer: Nothing is wrong: the quote window refreshes at least ten times a second while the panel only updates when the formula re-executes

This asymmetry is stated on the GetRTData() page. The function returns the value at the moment of formula execution, and execution is governed by the refresh interval, so momentary disagreement is expected. A panel value that stops changing entirely is a different symptom and does need investigating.

Question 5. What is the fastest way to test the panel’s offline behaviour on a machine that has a live feed connected?
Show the answer and why

Answer: Set the Data source parameter to "Offline (historical bars only)", which sets the same variables to Null that an absent feed would

The parameter sets every quote variable to Null instead of calling the function, so everything downstream takes exactly the path an absent feed produces. Disconnecting the plugin is also a valid test — and a necessary one before you rely on the panel — but it is slow and disruptive to repeat after every edit.

Sources for this lesson

10 verified · checked 2026-08-31

  1. 01AmiBroker AFL Function Reference — GetRTDataamibroker.com/guide/afl/getrtdata.html2026-08-31
  2. 02AmiBroker AFL Function Reference — RequestTimedRefreshamibroker.com/guide/afl/requesttimedrefresh.html2026-08-31
  3. 03AmiBroker AFL Function Reference — GetPlaybackDateTimeamibroker.com/guide/afl/getplaybackdatetime.html2026-08-31
  4. 04AmiBroker AFL Function Reference — Statusamibroker.com/guide/afl/status.html2026-08-31
  5. 05AmiBroker AFL Function Reference — DateTimeConvertamibroker.com/guide/afl/datetimeconvert.html2026-08-31
  6. 06AmiBroker AFL Function Reference — DateTimeDiffamibroker.com/guide/afl/datetimediff.html2026-08-31
  7. 07AmiBroker AFL Function Reference — GfxTextOutamibroker.com/guide/afl/gfxtextout.html2026-08-31
  8. 08AmiBroker AFL Function Reference — GfxSetOverlayModeamibroker.com/guide/afl/gfxsetoverlaymode.html2026-08-31
  9. 09AmiBroker User's Guide — Bar Replayamibroker.com/guide/w_barreplay.html2026-08-31
  10. 10AmiBroker User's Guide — Real-Time Quote windowamibroker.com/guide/w_rtquote.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.