// ===========================================================================
//  Data path report
//
//  WHAT IT IS FOR
//    A chart pane that puts the three clocks of a real-time setup side by side
//    and measures the distance between them:
//      1. this computer's clock,
//      2. the timestamp of the newest bar AmiBroker holds for this symbol,
//      3. the time of the last update a real-time plugin reported.
//    When a chart appears to have stopped, one of those three has stopped
//    moving, and which one tells you which link in the chain to go and inspect.
//
//  HOW TO RUN IT
//    Formula Editor -> Apply Indicator, into its own chart pane.
//    It runs on any database, with or without a real-time feed. On an
//    end-of-day database it reports that no plugin update was seen, which is
//    the correct answer for that database, not a failure of the formula.
//
//  ASSUMPTIONS, STATED SO THEY CAN BE CHECKED
//    - Now(5) is the PC clock. Bar timestamps come out of the database and may
//      be shifted towards exchange time, so the two are not necessarily
//      measured on the same clock. The database timeshift is printed as well
//      so that the size of that discrepancy is visible rather than hidden.
//    - The User's Guide documents Status("lastrtupdate") as the date/time of
//      the last update sent by a real-time plugin. It does not document what
//      the call returns on a database that has no plugin attached, so this
//      formula accepts the value only when it is positive and yields an age
//      inside a plausible band, and otherwise reports "not seen".
//    - Status("lastbartimeleft") is documented for time-based bars only and
//      relies on the database timeshift being correct, so it is shown only
//      when it comes back positive.
//    - The staleness threshold below is a choice you make about your own
//      tolerance. It is not a statement about any vendor's service level.
// ===========================================================================

_SECTION_BEGIN( "Data path report" );

// Both of these are judgements rather than facts, so they live at the top
// where they can be seen and changed.
RefreshSeconds = Param( "Refresh every (seconds)", 2, 1, 60, 1 );
StaleAfterSecs = Param( "Call the feed stale after (seconds)", 30, 5, 600, 5 );

// Re-execute this pane on a timer. Every refresh runs the entire formula
// again, so this is the one setting in the file with a CPU cost attached.
RequestTimedRefresh( RefreshSeconds );

// --- Clock 1: this machine -------------------------------------------------
PcClock = Now( 5 );

// --- Clock 2: the newest bar the database holds for the selected symbol ----
NewestBar     = LastValue( DateTime() );
BarAgeSeconds = DateTimeDiff( PcClock, NewestBar );

// --- Clock 3: the last thing a real-time plugin said -----------------------
// Anything that is not a positive timestamp yielding an age between "a minute
// in the future" and "thirty days ago" is treated as no report at all, rather
// than being printed as though it meant something.
LastRtUpdate = Status( "lastrtupdate" );
RtAgeSeconds = DateTimeDiff( PcClock, LastRtUpdate );
SawRtUpdate  = LastRtUpdate > 0 AND RtAgeSeconds > -60 AND RtAgeSeconds < 30 * 86400;

// --- Bar Replay, which quietly overrides everything above ------------------
// GetPlaybackDateTime() returns zero when replay is not running, so the test
// has to be explicit or the zero prints as a date in 1899.
PlaybackPosition = GetPlaybackDateTime();
ReplayRunning    = PlaybackPosition > 0;

TimeShiftHours   = Status( "timeshift" ) / 3600;
SecondsLeftInBar = Status( "lastbartimeleft" );

// Interval( 2 ) is the readable name of the CHART interval, not of the
// database base interval - AFL has no documented way to report the latter.
// Display it, but never compare it as text: use Interval() in seconds.
ReportText =
      "DATA PATH REPORT for " + Name() + "\n"
    + "Chart interval: " + Interval( 2 )
    + "  (" + NumToStr( Interval(), 1.0 ) + " seconds per bar)\n"
    + "Database timeshift: " + NumToStr( TimeShiftHours, 1.2 ) + " hours\n"
    + "\n"
    + "PC clock now:      " + DateTimeToStr( PcClock ) + "\n"
    + "Newest bar held:   " + DateTimeToStr( NewestBar )
    + "   (" + NumToStr( BarAgeSeconds / 60, 1.1 ) + " minutes behind the PC clock)\n";

if( SawRtUpdate )
    ReportText = ReportText
        + "Last plugin update: " + DateTimeToStr( LastRtUpdate )
        + "   (" + NumToStr( RtAgeSeconds, 1.0 ) + " seconds ago)\n";
else
    ReportText = ReportText + "Last plugin update: not seen\n";

if( SecondsLeftInBar > 0 )
    ReportText = ReportText
        + "Current bar completes in " + NumToStr( SecondsLeftInBar, 1.0 ) + " seconds\n";

// The verdict is deliberately about which link to inspect, not about whether
// anything is wrong. A quiet feed and a broken feed look identical from here.
if( ReplayRunning )
{
    Verdict = "BAR REPLAY IS ACTIVE (position "
            + DateTimeToStr( PlaybackPosition )
            + "). Every bar above is a replayed bar. Press STOP in the Bar "
            + "Replay window before reading any of this as live.";
    VerdictColour = colorOrange;
}
else
{
    if( NOT SawRtUpdate )
    {
        Verdict = "No real-time plugin update has been reported. Either this "
                + "database is not fed by a streaming plugin, or the plugin "
                + "has delivered nothing since AmiBroker started.";
        VerdictColour = colorLightGrey;
    }
    else
    {
        if( RtAgeSeconds > StaleAfterSecs )
        {
            Verdict = "The plugin last reported "
                    + NumToStr( RtAgeSeconds, 1.0 )
                    + " seconds ago, which is past your threshold of "
                    + NumToStr( StaleAfterSecs, 1.0 )
                    + " seconds. Check the plugin status light before "
                    + "concluding the market is quiet.";
            VerdictColour = colorRed;
        }
        else
        {
            Verdict = "The plugin reported within your threshold. That says "
                    + "the feed is arriving; it says nothing about whether "
                    + "the prices in it are correct.";
            VerdictColour = colorGreen;
        }
    }
}

Title = ReportText + "\n" + Verdict;

// The pane is about the text above; the price line is here only so that the
// pane has something to scale against and can sit under a price chart.
Plot( Close, "Close", VerdictColour, styleLine | styleNoTitle );

_SECTION_END();
