// replay-alert-harness.afl
// Part 26 - Replay Exercise: Intraday Setups and Alerts
//
// A breakout alert built so that it can be rehearsed under Bar Replay instead
// of being tested for the first time in a live session. It writes to the Alert
// Output window and stamps every line with the replay position, so the log you
// end up with says WHEN in the replayed session each alert was raised.
//
// It ends where this course ends: an alert, a human reading it, a decision.
// There is no order routing here and none is coming.
//
// How to run it:
//   Tools -> Preferences -> Alerts: tick "Enable alerts from" custom indicators,
//   or nothing you do below will reach the Alert Output window.
//   Formula Editor -> paste -> name it "Replay alert harness" -> Apply Indicator.
//   Window -> Alert Output to see the lines arrive.
//   Then Tools -> Bar Replay, Step interval = your base interval, Speed 1.
//
// Assumptions declared up front:
//   - Chart formula on an intraday database. It runs on daily bars too, in
//     which case leave the session filter off.
//   - The breakout level uses the previous BreakoutLook bars only. The current
//     bar's own high is excluded, so the level cannot move to meet the price.
//   - Session times are read with TimeNum(), which reports the timestamp your
//     database holds. If your bars are stamped in a different time zone from
//     the exchange, this filter is wrong in exactly that many hours - Part 20
//     covers how to check which convention your data uses.
//   - Alerts here are informational. No position size, no risk, no order.

_SECTION_BEGIN( "Replay alert harness" );

BreakoutLook  = Param( "Breakout lookback (bars)", 20, 2, 200, 1 );
MinRelVolume  = Param( "Minimum relative volume", 1.5, 0, 10, 0.1 );
VolumeLook    = Param( "Relative-volume lookback", 20, 5, 200, 1 );
UseSession    = ParamToggle( "Apply session filter", "No|Yes", 1 );
SessionStart  = Param( "Session start (HHMMSS)", 100000, 0, 235959, 100 );
SessionEnd    = Param( "Session end (HHMMSS)", 154500, 0, 235959, 100 );
CompletedOnly = ParamToggle( "Alert on completed bars only", "No|Yes", 1 );
LookbackBars  = Param( "AlertIf lookback argument", 2, 1, 10, 1 );
AlertFlags    = Param( "AlertIf flags", 1 + 2, 0, 15, 1 );

// --- The setup -------------------------------------------------------------
PriorHigh = Ref( HHV( High, BreakoutLook ), -1 );
Breakout  = Cross( Close, PriorHigh );

AverageVolume = MA( Volume, VolumeLook );
RelVolume     = IIf( AverageVolume > 0, Volume / AverageVolume, 0 );
VolumeOk      = RelVolume >= MinRelVolume;

InSession = IIf( UseSession,
                 TimeNum() >= SessionStart AND TimeNum() <= SessionEnd,
                 True );

// The last bar on an intraday chart is still forming: a condition can go true
// and false again inside it. This is AmiBroker's own documented remedy - alert
// only from bars that have closed.
//
// The interaction between this guard and the lookback argument is NOT written
// down anywhere, and it is the first thing this lab asks you to establish.
// AlertIf examines only the `lookback` most recent bars, `lookback` defaults to
// 1, and BarComplete is false on that bar by construction. Two readings follow.
// Either the alert for a signal on bar T appears when you step onto T+1, or a
// lookback of 1 leaves nothing for it to fire from at all. LookbackBars is a
// parameter so that you can run both and record which one your build does.
// The default of 2 is the setting that produces output under either reading.
BarComplete = IIf( CompletedOnly, BarIndex() < LastValue( BarIndex() ), True );

Condition = Breakout AND VolumeOk AND InSession AND BarComplete;

// --- Where the clock is ----------------------------------------------------
PlaybackPos = GetPlaybackDateTime();   // zero when Bar Replay is not active

if ( PlaybackPos )
{
    ClockText = "REPLAY " + DateTimeToStr( PlaybackPos );
}
else
{
    ClockText = "live/EOD " + Now( 0 );
}

AlertText = "Breakout candidate: " + Name()
          + " closed above the prior " + NumToStr( BreakoutLook, 1.0 )
          + "-bar high on " + NumToStr( LastValue( RelVolume ), 1.2 )
          + "x volume   [" + ClockText + "]"
          + "   -- review before acting; this is not an order";

// Type 1 is AmiBroker's "buy" alert type. The type is the key the built-in
// de-duplication state machine remembers per symbol, so give different kinds of
// alert different types or they will suppress each other.
// Flags default to 1+2+4+8, which also suppresses repeats of the same type and
// the same timestamp. The parameter above starts at 1+2 so that you can see
// every raised alert while you are rehearsing, and can put the suppression
// bits back once you know what the formula does.
AlertIf( Condition, "", AlertText, 1, AlertFlags, LookbackBars );

// --- What you see on the chart ---------------------------------------------
Plot( Close, "Close", colorDefault, styleCandle );
Plot( PriorHigh, "Prior " + NumToStr( BreakoutLook, 1.0 ) + "-bar high",
      colorBlue, styleLine | styleThick );

PlotShapes( IIf( Condition, shapeUpArrow, shapeNone ), colorBrightGreen, 0, Low );

// Read the state as text, not as a colour: the title says in words whether the
// three parts of the condition are true on the last visible bar.
Title = Name() + "   " + ClockText + "\n"
      + "Breakout: " + WriteIf( LastValue( Breakout ), "yes", "no" )
      + "    Volume filter: " + WriteIf( LastValue( VolumeOk ), "yes", "no" )
      + " (" + NumToStr( LastValue( RelVolume ), 1.2 ) + "x)"
      + "    In session: " + WriteIf( LastValue( InSession ), "yes", "no" ) + "\n"
      + "Alert raised on this bar: "
      + WriteIf( LastValue( Condition ), "YES", "no" );

_SECTION_END();
