_SECTION_BEGIN( "Stale data monitor" );

/*  Stale data monitor - Part 21, Real-Time Charts.

    WHAT IT DOES
        Answers one question, continuously and visibly: how long has it been
        since data last moved? It reports FRESH, STALE or UNKNOWN, shows the
        age in seconds, names which clock and which data stamp it used, and
        can raise one alert on the transition into staleness.

    WHY "UNKNOWN" IS A STATE
        A monitor that says FRESH when it has nothing to measure is worse
        than no monitor. If neither a plugin update stamp nor a time-based
        bar end is available, this formula says so instead of guessing.

    WHICH CLOCK, WHICH STAMP
        Reference clock:
            Bar Replay position when replay is active, otherwise Now( 5 ).
        Data stamp, in order of preference:
            1. Status( "lastrtupdate" ) - the plugin's own last-update time.
               Preferred, because it moves on every update rather than once
               per bar. Not used during Bar Replay, where the playback clock
               and the live plugin clock are unrelated.
            2. Status( "lastbarend" ) - the end of the newest bar. Available
               on any time-based interval with or without a feed, which is
               what makes this formula work at Level A.

    ASSUMPTIONS AND LIMITS - read before relying on it
        - A bar-end age is naturally as old as one bar, so when the bar end
          is the stamp the tolerance has one whole interval added to it. A
          plugin stamp gets no such allowance.
        - Status( "lastrtupdate" ) depends on the plugin sending correct
          update timestamps. The documentation warns that most sources send
          non-current stamps at weekends and that the IQFeed plugin sends
          them only inside regular trading hours. Outside those hours this
          monitor will report STALE, correctly and uselessly.
        - Status( "lastbarend" ) and the countdown need the database time
          shift set correctly, and work on time-based bars only.
        - Now( 5 ) is the PC clock. A wrong PC clock produces a wrong age.
        - DateTime values are compared with == and != only; ordering goes
          through DateTimeDiff().
        - Alerts from a custom indicator reach the Alert Output window only
          if Tools -> Preferences -> Alerts has "custom indicators" ticked
          under "Enable alerts from".
        - This detects that data stopped. It cannot tell you why, and no AFL
          function reports the plugin connection status.
*/

ToleranceSeconds = Param( "Stale after (seconds)", 90, 5, 3600, 5 );
RefreshSeconds   = Param( "Check every (seconds)", 2, 1, 60, 1 );
RaiseAlert       = ParamToggle( "Alert on going stale", "No|Yes", 0 );

// False so the monitor keeps checking while the main window is minimised.
RequestTimedRefresh( RefreshSeconds, False );

Playback = GetPlaybackDateTime();          // zero when Bar Replay is inactive
RtUpdate = Nz( Status( "lastrtupdate" ) );
BarEnd   = Nz( Status( "lastbarend" ) );
BarSecs  = Interval( 0 );

if( Playback != 0 )
{
    RefNow    = Playback;
    ClockText = "Bar Replay position";
}
else
{
    RefNow    = Now( 5 );
    ClockText = "system clock";
}

UsingBarEnd = False;

if( RtUpdate != 0 AND Playback == 0 )
{
    LastData   = RtUpdate;
    StampText  = "plugin update stamp";
}
else
{
    if( BarEnd != 0 AND BarSecs > 0 )
    {
        LastData    = BarEnd;
        StampText   = "end of newest bar";
        UsingBarEnd = True;
    }
    else
    {
        LastData  = 0;
        StampText = "none available";
    }
}

// A bar-end stamp only moves once per bar, so allow one whole interval.
if( UsingBarEnd )
    Allowance = BarSecs + ToleranceSeconds;
else
    Allowance = ToleranceSeconds;

if( LastData == 0 )
{
    AgeSeconds = 0;
    StateText  = "UNKNOWN";
    StateColor = colorLightGrey;
    AgeText    = "nothing to measure";
}
else
{
    AgeSeconds = DateTimeDiff( RefNow, LastData );
    AgeText    = NumToStr( AgeSeconds, 1.0 ) + " s since last movement, tolerance "
                 + NumToStr( Allowance, 1.0 ) + " s";

    if( AgeSeconds > Allowance )
    {
        StateText  = "STALE";
        StateColor = colorRose;
    }
    else
    {
        StateText  = "FRESH";
        StateColor = colorPaleGreen;
    }
}

// One alert on the transition into staleness, not one per refresh.
// The state is kept per symbol and per pane, so two panes do not fight.
StateKey  = "p21_stalestate_" + Name() + "_" + NumToStr( GetChartID(), 1.0, False );
WasStale  = Nz( StaticVarGet( StateKey ) );
IsStale   = StateText == "STALE";

if( RaiseAlert AND IsStale AND WasStale == 0 )
{
    LastBarOnly = BarIndex() == LastValue( BarIndex() );

    // Flags 1 + 2 only: write to the Alert Output window and beep. The
    // built-in repeat suppression (flags 4 and 8) is deliberately left off,
    // because the static variable above already does that job and does it
    // per pane rather than per symbol.
    AlertIf( LastBarOnly, "", "Data for " + Name() + " has not moved for "
             + NumToStr( AgeSeconds, 1.0 ) + " seconds", 8, 1 + 2 );
}

StaticVarSet( StateKey, IsStale );

SetChartOptions( 2, chartWrapTitle );
SetChartBkColor( StateColor );

Title =
    EncodeColor( colorBlack ) + "DATA " + StateText + "   |   " + Name()
  + "   |   " + Interval( 2 ) + "\n"
  + AgeText + "\n"
  + "Clock: " + ClockText + "   |   Stamp: " + StampText;

_SECTION_END();
