// opening-range-panel.afl
// Part 24 - Defining Intraday Setup Conditions
//
// Draws, on an intraday chart, the three quantities the scanner in this part
// depends on, so that you can look at them before you trust them:
//
//   1. the session opening range, frozen at the moment the range window closes;
//   2. the session volume pace ("relative volume"), measured against a baseline
//      built only from COMPLETED previous days;
//   3. a daily trend gate, read from yesterday's completed daily bar.
//
// Nothing here is a trading rule. It is a set of definitions made visible.
//
// Assumptions declared up front:
//   - An intraday database, and a chart interval that divides the opening-range
//     length exactly. A 30-minute opening range on 7-minute bars has no bar
//     boundary to end on, and the level you get will not be the level you meant.
//   - Bars are stamped with the START time of the interval, which is AmiBroker's
//     documented default (Tools -> Preferences -> Intraday). If your bars carry
//     the END time instead, every time comparison below is one bar out.
//   - The session times below are entered in the SAME clock the database uses.
//     If a time shift is configured, these are shifted times, not exchange
//     times. This formula cannot detect that; Part 20 shows how to check it.
//   - The daily values come from daily compression OF THIS DATABASE, so they
//     inherit its Intraday Settings - session filtering and the daily-compression
//     basis in particular. They are not guaranteed to equal an end-of-day
//     vendor's daily bars for the same symbol.

_SECTION_BEGIN( "Opening range panel" );

StartHour   = Param( "Session start - hour",             9,  0, 23,  1 );
StartMinute = Param( "Session start - minute",          30,  0, 59,  1 );
EndHour     = Param( "Session end - hour",              16,  0, 23,  1 );
EndMinute   = Param( "Session end - minute",             0,  0, 59,  1 );
ORMinutes   = Param( "Opening range length (minutes)",  30,  5, 240, 5 );
VolDays     = Param( "Volume baseline (completed days)", 20, 5, 100, 1 );
TrendLen    = Param( "Daily trend average (days)",      50, 10, 200, 5 );

SetBarsRequired( sbrAll, sbrAll );   // daily compression from intraday bars is one
                                     // of the documented QuickAFL edge cases

// ---------------------------------------------------------------------------
// Session geometry. Everything is expressed in minutes since midnight, because
// TimeNum() is a decimal-packed number: 100000 minus 100 is not "one minute
// before 10:00". Hour() and Minute() give values you can safely do arithmetic on.
// ---------------------------------------------------------------------------
BarLengthMin    = Interval() / 60;
SessionStartMin = 60 * StartHour + StartMinute;
SessionEndMin   = 60 * EndHour   + EndMinute;
SessionMinutes  = SessionEndMin - SessionStartMin;

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

InSession   = BarMin >= SessionStartMin AND BarMin < SessionEndMin;
PrevInSess  = Nz( Ref( InSession, -1 ) );   // Nz, so the first bar of the array
                                            // does not compare against Null
SessionOpen = InSession AND NOT PrevInSess;

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

// ---------------------------------------------------------------------------
// The opening range. HighestSince/LowestSince give the running extremes of the
// session so far; ValueWhen freezes them.
//
// ValueWhen( InOR, x ) returns x as it stood on the most recent bar where InOR
// was true. Once the range window has closed, that bar is the LAST bar of the
// range - so the level is frozen using past bars only, with no forward
// reference anywhere in the expression.
// ---------------------------------------------------------------------------
ORHighRunning = HighestSince( SessionOpen, High );
ORLowRunning  = LowestSince( SessionOpen, Low );

ORHigh = ValueWhen( InOR, ORHighRunning );
ORLow  = ValueWhen( InOR, ORLowRunning );

// Before today's range has closed, ValueWhen is still holding YESTERDAY's
// levels. Blank them rather than drawing a line that does not exist yet.
ORHighPlot = IIf( AfterOR, ORHigh, Null );
ORLowPlot  = IIf( AfterOR, ORLow,  Null );
ORRange    = ORHighPlot - ORLowPlot;

// ---------------------------------------------------------------------------
// Volume pace and the daily trend gate, both built from completed daily bars.
// ---------------------------------------------------------------------------
SessionVolume = SumSince( SessionOpen, Volume, True );   // includes the open bar
ElapsedMin    = BarMin - SessionStartMin + BarLengthMin; // minutes of session done

TimeFrameSet( inDaily );
    // Ref( ..., -1 ) steps back one DAILY bar, so today's still-forming daily
    // bar is excluded from both of these.
    BaselineVolume = Ref( MA( Volume, VolDays ), -1 );
    DailyTrendUp   = Ref( Close > MA( Close, TrendLen ), -1 );
TimeFrameRestore();

// expandFirst is safe here, and it is safe only because of the Ref( ..., -1 )
// above. What lands on today's first intraday bar is yesterday's completed
// value. Without the shift, this same call would hand today's unfinished daily
// bar to the first bar of the morning - the documented look-ahead leak.
BaselineVolume = TimeFrameExpand( BaselineVolume, inDaily, expandFirst );
DailyTrendUp   = TimeFrameExpand( DailyTrendUp,   inDaily, expandFirst );

// The flat-rate assumption: if today traded like an average recent day, and
// volume arrived evenly through the session, this is what would have traded by
// now. Volume does NOT arrive evenly - see the lesson - so a pace above 1.0
// early in the session is normal rather than notable.
ExpectedVolume = BaselineVolume * ElapsedMin / SessionMinutes;
VolumePace     = IIf( InSession AND ExpectedVolume > 0,
                      SessionVolume / ExpectedVolume, Null );

// ---------------------------------------------------------------------------
// Output.
// ---------------------------------------------------------------------------
Plot( Close, "Close", colorDefault, styleCandle );
Plot( ORHighPlot, "OR high", colorGreen, styleStaircase | styleThick );
Plot( ORLowPlot,  "OR low",  colorRed,   styleStaircase | styleThick );

// Own scale: the pace ratio sits around 1 and would otherwise be an invisible
// flat line at the bottom of a price axis.
Plot( VolumePace, "Volume pace", colorBlue, styleLine | styleOwnScale );

PlotShapes( IIf( SessionOpen, shapeUpArrow, shapeNone ), colorLightGrey, 0, Low, -18 );

ReplayActive = GetPlaybackDateTime() > 0;   // zero when Bar Replay is not running

Title = Name() + "  " + Interval( 2 ) + "    " +
        WriteIf( InSession, "in session", "outside session" ) + "    " +
        WriteIf( AfterOR, "OR complete",
                 WriteIf( InOR, "OR forming", "no opening range yet" ) ) + "    " +
        "OR " + WriteVal( ORLowPlot, 1.2 ) + " - " + WriteVal( ORHighPlot, 1.2 ) +
        "  (range " + WriteVal( ORRange, 1.2 ) + ")    " +
        "pace " + WriteVal( VolumePace, 1.2 ) + "x    " +
        WriteIf( DailyTrendUp, "daily trend up", "daily trend not up" ) + "    " +
        WriteIf( ReplayActive, "[Bar Replay active]", "" );

// A modest, deliberately unambitious refresh. Part 23 covers what this costs and
// why a shorter interval on a heavier formula is how people peg a CPU.
RequestTimedRefresh( 5 );

_SECTION_END();
