// realtime-breakout-alert.afl
// Part 25 - Project: Real-Time Breakout Alert
//
// Watches one symbol on one chart pane and raises at most one alert per
// session, on the first completed bar whose high exceeds the highest high of
// the previous N bars plus a volatility buffer, inside a session window you
// define.
//
// It raises an alert. It does not place an order, and nothing in this course
// does. The chain ends at: alert -> you read it -> you decide.
//
// Assumptions declared up front:
//   - Any interval. On an intraday interval the session filter and the
//     once-per-session latch do real work; on daily bars they are harmless.
//   - Bar time-stamping: TimeNum() returns the START or the END time of the
//     interval depending on Tools -> Preferences -> Intraday. The session
//     window below is compared against whatever that setting produces, so
//     confirm which convention your database uses before trusting it.
//   - Alerts appear only if "custom indicators" is ticked under
//     Tools -> Preferences -> Alerts, "Enable alerts from".
//   - Neither a real-time feed nor the Professional edition is required to RUN
//     this. A live feed only changes how often the pane re-executes. Bar
//     Replay (Tools -> Bar Replay) drives it just as well, and that is the
//     tested, no-subscription path.

_SECTION_BEGIN( "Real-Time Breakout Alert" );

LookbackBars = Param( "Breakout lookback (bars)", 20,     2,   400,    1 );
BufferAtr    = Param( "Buffer (x ATR)",            0.10,  0,     2,    0.05 );
AtrPeriod    = Param( "ATR period",               14,     1,   100,    1 );
SessionStart = Param( "Session start (HHMMSS)",   93000,  0, 235959, 100 );
SessionEnd   = Param( "Session end (HHMMSS)",     155900, 0, 235959, 100 );
UseSession   = ParamToggle( "Session filter", "Off|On", 1 );
AlertsOn     = ParamToggle( "Alerts",         "Off|On", 1 );
RefreshSecs  = Param( "Self-refresh (seconds)", 5, 0, 300, 1 );
AlertLookback = Param( "Alert lookback (bars)", 2, 1, 20, 1 );

// Ask this pane to re-execute on a timer. RequestTimedRefresh() works with or
// without a data plugin, which is exactly what makes the formula testable with
// no subscription. Set the parameter to 0 to switch the timer off.
if( RefreshSecs > 0 )
{
    RequestTimedRefresh( RefreshSecs );
}

// --- The level -----------------------------------------------------------
// Ref( ..., -1 ) shifts the running high back one bar, so the current bar is
// never part of the level it is being measured against. Without the shift the
// high of the breakout bar raises the level it is trying to break.
PriorHigh = Ref( HHV( High, LookbackBars ), -1 );
Buffer    = BufferAtr * ATR( AtrPeriod );
Level     = PriorHigh + Buffer;

// Warm-up: HHV and ATR are Null until they have enough bars, and a Null
// quietly poisons every comparison it touches. Say so explicitly.
HaveLevel = NOT IsNull( Level );

// --- The event -----------------------------------------------------------
// Cross() is true on the one bar where High first exceeded the level. The
// state "High > Level" stays true for as long as the market stays up there,
// which is the difference between one alert and forty.
RawBreak = HaveLevel AND Cross( High, Level );

// --- The session gate ----------------------------------------------------
InSession = NOT UseSession
            OR ( TimeNum() >= SessionStart AND TimeNum() <= SessionEnd );

// --- Once per session ----------------------------------------------------
// A change of calendar day resets the latch, so the first qualifying break of
// each session passes and every later one is blocked until the next session.
NewSession = Day() != Ref( Day(), -1 );
FirstBreak = ExRem( RawBreak AND InSession, NewSession );

// --- Completed bars only -------------------------------------------------
// The newest bar is still being built by the feed or by Bar Replay: its high
// can still grow. Alerting on it is how one breakout becomes six alerts.
BarComplete = BarIndex() < LastValue( BarIndex() );
Trigger     = FirstBreak AND BarComplete;

// --- The alert -----------------------------------------------------------
// GetPlaybackDateTime() returns zero when Bar Replay is not running, so this
// labels replayed alerts and keeps a practice run out of your real log.
Playback = GetPlaybackDateTime();

if( Playback > 0 )
{
    ModeText = "[REPLAY] ";
}
else
{
    ModeText = "";
}

AlertText = ModeText + "BREAKOUT  " + Name()
          + "  " + Interval( 2 )
          + "  bar "   + DateTimeToStr( LastValue( DateTime() ) )
          + "  high "  + NumToStr( LastValue( High ),  1.4 )
          + "  level " + NumToStr( LastValue( Level ), 1.4 );

// Type 1 (buy), default flags, and an explicit lookback of 2.
//
// The lookback is the argument people leave alone and should not. AlertIf reads
// only the lookback most recent bars, and its default of 1 is exactly the bar
// BarComplete excludes - so with the default nothing would ever fire. Two bars
// puts the most recent completed bar inside the window.
//
// Bit 8 of the flags, "do not display repeated alerts having the same
// date/time", is then what keeps that one completed-bar signal from being
// re-reported on every timed refresh for the whole life of the next bar. The
// AFL above decides WHICH bar may alert; bit 8 decides that it is said once.
if( AlertsOn )
{
    AlertIf( Trigger, "", AlertText, 1, 1+2+4+8, AlertLookback );
}

// --- Drawing --------------------------------------------------------------
Plot( Close, "Close",          colorDefault, styleCandle );
Plot( Level, "Breakout level", colorOrange,  styleLine | styleStaircase );

PlotShapes( Trigger * shapeUpArrow, colorGreen, 0, Low, -20 );

Title = Name() + "  " + Interval( 2 ) + "  " + ModeText
      + "\nLevel "          + NumToStr( LastValue( Level ), 1.4 )
      + "    Last high "    + NumToStr( LastValue( High ),  1.4 )
      + "\nRaw breaks: "    + NumToStr( LastValue( Cum( RawBreak ) ),   1.0 )
      + "    After latch: " + NumToStr( LastValue( Cum( FirstBreak ) ), 1.0 )
      + "    Alertable: "   + NumToStr( LastValue( Cum( Trigger ) ),    1.0 );

_SECTION_END();
