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.
What the panel has to do
Section titled “What the panel has to do”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”Absence is a value, and it has to survive
Section titled “Absence is a value, and it has to survive”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.
Provenance travels with the number
Section titled “Provenance travels with the number”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
- Was a feed field requested?The Data source parameter can force offline mode
- Did the feed answer?IsNull() on the returned value, not a comparison against zero
- Use the feed value, tag it "feed"Only when a real number came back
- Otherwise use the bar-derived value, tag it "bars"Which may itself be absent
- If neither exists, display "n/a"No substitution, no zero, no carried-over value
The complete formula
Section titled “The complete formula”Complete runnable 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 itGfxSetCoordsMode( 0 ); // pixel coordinatesGfxSetBkMode( 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();How it works
Section titled “How it works”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.
Key functions
Section titled “Key functions”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.
Expected result
Section titled “Expected result”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.
Validating it
Section titled “Validating it”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.
Common errors
Section titled “Common errors”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.
Extensions
Section titled “Extensions”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.
Where this project stops
Section titled “Where this project stops”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
- ConditionEvaluated in AFL
- AlertPart 25
- Human reviewYou look at it
- DecisionYours
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
Sources for this lesson
10 verified · checked 2026-08-31
- 01AmiBroker AFL Function Reference — GetRTDataamibroker.com/guide/afl/getrtdata.html2026-08-31
- 02AmiBroker AFL Function Reference — RequestTimedRefreshamibroker.com/guide/afl/requesttimedrefresh.html2026-08-31
- 03AmiBroker AFL Function Reference — GetPlaybackDateTimeamibroker.com/guide/afl/getplaybackdatetime.html2026-08-31
- 04AmiBroker AFL Function Reference — Statusamibroker.com/guide/afl/status.html2026-08-31
- 05AmiBroker AFL Function Reference — DateTimeConvertamibroker.com/guide/afl/datetimeconvert.html2026-08-31
- 06AmiBroker AFL Function Reference — DateTimeDiffamibroker.com/guide/afl/datetimediff.html2026-08-31
- 07AmiBroker AFL Function Reference — GfxTextOutamibroker.com/guide/afl/gfxtextout.html2026-08-31
- 08AmiBroker AFL Function Reference — GfxSetOverlayModeamibroker.com/guide/afl/gfxsetoverlaymode.html2026-08-31
- 09AmiBroker User's Guide — Bar Replayamibroker.com/guide/w_barreplay.html2026-08-31
- 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.