// intraday-breakout-scanner.afl
// Part 24 - Project: Intraday Breakout Scanner
//
// WHAT THIS IS
//   A repeatable intraday exploration that lists symbols whose price has crossed
//   above the high of their session opening range, while a daily trend gate and
//   a session volume-pace gate were both satisfied.
//
// WHAT THIS IS NOT
//   Evidence of anything. Every threshold below was chosen because it is round,
//   not because it was measured. There is no exit rule, no position sizing, no
//   cost model and no test. A list of symbols that satisfied a definition is a
//   list of symbols that satisfied a definition. Parts 27 to 31 are where a
//   rule set is turned into something that can be tested and can fail.
//
// HOW TO RUN IT
//   Formula Editor -> paste -> name it -> Send to Analysis
//   Apply to:  Filter, and choose a watch list you can actually stream
//   Range:     1 recent day(s)          (so today's whole candidate list rebuilds)
//   Settings:  Periodicity = the intraday interval you intend to scan
//   Press Explore. To repeat it, open the Settings drop-down, tick
//   "Auto repeat Scan/Explore" and set an interval (a plain number is minutes;
//   type 5s or 5sec for seconds).
//
//   With no live feed: run exactly the same thing over a historical intraday
//   database with Range = From-To covering one past session, and then again
//   under Tools -> Bar Replay. The two must agree. See the lesson.
//
// ASSUMPTIONS DECLARED UP FRONT
//   - Intraday database; the scan periodicity divides ORMinutes exactly.
//   - Bars stamped with the START of the interval (AmiBroker's default).
//   - Session times are in the database's own clock, not necessarily exchange
//     time. A configured time shift moves every threshold below.
//   - Daily values are the daily compression of this database and inherit its
//     Intraday Settings.
//   - Volume is present and non-zero. On a symbol with no volume every pace
//     test below is meaningless rather than merely wrong.

SetBarsRequired( sbrAll, sbrAll );

// ---------------------------------------------------------------------------
// Definition. Every number here is an assumption you are asked to change.
// ---------------------------------------------------------------------------
StartHour      = 9;      // session start, hour
StartMinute    = 30;     // session start, minute
EndHour        = 16;     // session end, hour
EndMinute      = 0;      // session end, minute
ORMinutes      = 30;     // length of the opening range
LastEntryHour  = 15;     // no candidate reported after this time
LastEntryMin   = 0;

BufferFrac     = 0.10;   // breakout must clear the OR high by this fraction of
                         // the OR range, so that one tick through does not count
MinVolumePace  = 1.20;   // session volume pace required (see the lesson: this is
                         // NOT "20% above average volume")
TrendLen       = 50;     // daily moving average used as the trend gate
VolDays        = 20;     // completed days in the volume baseline
MinTurnover    = 5000000;    // previous day close x volume, in the quote currency
MinORRangePct  = 0.30;   // reject a range too tight to mean anything
MaxORRangePct  = 6.00;   // reject a range so wide the symbol is in disarray

// ---------------------------------------------------------------------------
// Session geometry, in minutes since midnight. TimeNum() is decimal-packed and
// cannot be used for arithmetic; Hour() and Minute() can.
// ---------------------------------------------------------------------------
BarLengthMin    = Interval() / 60;
SessionStartMin = 60 * StartHour     + StartMinute;
SessionEndMin   = 60 * EndHour       + EndMinute;
CutoffMin       = 60 * LastEntryHour + LastEntryMin;
SessionMinutes  = SessionEndMin - SessionStartMin;

BarMin = 60 * Hour() + Minute();

InSession   = BarMin >= SessionStartMin AND BarMin < SessionEndMin;
PrevInSess  = Nz( Ref( InSession, -1 ) );
SessionOpen = InSession AND NOT PrevInSess;

InOR    = InSession AND BarMin <  SessionStartMin + ORMinutes;
AfterOR = InSession AND BarMin >= SessionStartMin + ORMinutes;

// ---------------------------------------------------------------------------
// Opening range, frozen with past bars only.
// ---------------------------------------------------------------------------
ORHigh = ValueWhen( InOR, HighestSince( SessionOpen, High ) );
ORLow  = ValueWhen( InOR, LowestSince( SessionOpen, Low ) );

ORRange    = ORHigh - ORLow;
ORRangePct = IIf( Close > 0, 100 * ORRange / Close, Null );

// ORHigh carries yesterday's value until today's range has closed, so nothing
// below may look at it unless AfterOR is true.
ORUsable = AfterOR AND NOT IsNull( ORHigh ) AND ORRange > 0 AND
           ORRangePct >= MinORRangePct AND ORRangePct <= MaxORRangePct;

// ---------------------------------------------------------------------------
// Context gates, all built from COMPLETED daily bars.
// ---------------------------------------------------------------------------
SessionVolume = SumSince( SessionOpen, Volume, True );
ElapsedMin    = BarMin - SessionStartMin + BarLengthMin;

TimeFrameSet( inDaily );
    BaselineVolume = Ref( MA( Volume, VolDays ), -1 );
    DailyTrendUp   = Ref( Close > MA( Close, TrendLen ), -1 );
TimeFrameRestore();

BaselineVolume = TimeFrameExpand( BaselineVolume, inDaily, expandFirst );
DailyTrendUp   = TimeFrameExpand( DailyTrendUp,   inDaily, expandFirst );

ExpectedVolume = BaselineVolume * ElapsedMin / SessionMinutes;
VolumePace     = IIf( ExpectedVolume > 0, SessionVolume / ExpectedVolume, Null );

// Negative shift, so both of these are the previous COMPLETED daily bar.
PrevDayClose  = TimeFrameGetPrice( "C", inDaily, -1 );
PrevDayVolume = TimeFrameGetPrice( "V", inDaily, -1 );
PrevTurnover  = PrevDayClose * PrevDayVolume;

// ---------------------------------------------------------------------------
// Setup, trigger, and one report per session.
// ---------------------------------------------------------------------------
Setup = ORUsable AND
        Nz( DailyTrendUp ) AND
        Nz( VolumePace ) >= MinVolumePace AND
        Nz( PrevTurnover ) >= MinTurnover AND
        BarMin <= CutoffMin;

BreakLevel = ORHigh + BufferFrac * ORRange;
Trigger    = Setup AND Cross( Close, BreakLevel );

// One report per symbol per session: ExRem suppresses further triggers until the
// next session opens.
Trigger = ExRem( Trigger, SessionOpen );

// Setting Buy lets the same file run under the Scan action, which is what Part 25
// attaches an alert to. There is deliberately no Sell, no stop and no exit: this
// file is not a strategy and must not be sent to the back-tester as one.
Buy = Trigger;

// ---------------------------------------------------------------------------
// Exploration output.
// ---------------------------------------------------------------------------
AboveLevelPct = IIf( ORHigh > 0, 100 * ( Close - ORHigh ) / ORHigh, Null );
MinutesIn     = BarMin - SessionStartMin;
SignalAgeSec  = DateTimeDiff( Now( 5 ), DateTime() );

DefinitionTag = "OR" + NumToStr( ORMinutes, 1.0, False ) + "m from " +
                NumToStr( StartHour, 1.0, False ) + ":" +
                NumToStr( StartMinute, 1.0, False ) + "  buf " +
                NumToStr( 100 * BufferFrac, 1.0, False ) + "%  pace>=" +
                NumToStr( MinVolumePace, 1.2, False ) + "  MA" +
                NumToStr( TrendLen, 1.0, False ) + "d";

Filter = Trigger;

AddColumn( Close,         "Close",             1.2 );
AddColumn( ORHigh,        "OR high",           1.2 );
AddColumn( ORLow,         "OR low",            1.2 );
AddColumn( ORRangePct,    "OR range %",        1.2 );
AddColumn( AboveLevelPct, "Above OR high %",   1.2 );
AddColumn( VolumePace,    "Volume pace",       1.2 );
AddColumn( PrevTurnover / 1000000, "Prev turnover (m)", 1.1 );
AddColumn( MinutesIn,     "Minutes into session", 1.0 );
AddColumn( SignalAgeSec,  "Signal age (s)",    1.0 );
AddTextColumn( DefinitionTag, "Definition" );

// Column 11 is "Signal age (s)". Ascending puts the freshest row first - which
// is the only ordering that does not quietly encourage you to act on the oldest
// signal in the list.
SetSortColumns( 11 );

// COUNT only, on the first numeric column: how many candidates this run produced.
AddSummaryRows( 16, 1.0, 3 );
