// course-library.afl
// ---------------------------------------------------------------------------
// The course utility library.
//
//   Version : 1.00
//   Written : Part 11 - Reusable AFL Programming
//   Requires: AmiBroker 6.20 or later (StDev's Population argument)
//
// PURPOSE
//   A small, deliberately narrow set of helpers that later parts of the course
//   reuse: safe division, a warm-up guard, a liquidity measure, a normalised
//   volatility measure, a z-score, a three-state trend regime, and two display
//   formatters. Nothing here is a trading rule. Every function is a
//   transformation of past data with its assumptions written down.
//
// INSTALLATION
//   Save this file as course-library.afl in your own formula folder, for
//   example  C:\AmiBroker\Formulas\Custom\course-library.afl , and either
//
//     #include "C:\AmiBroker\Formulas\Custom\course-library.afl"   // single \
//
//   or set Tools -> Preferences -> AFL -> the standard include path to that
//   folder and then simply write
//
//     #include <course-library.afl>
//
//   Do not save it into the Formulas folder that ships with AmiBroker: the
//   official documentation warns that formulas supplied with AmiBroker are
//   overwritten by the next upgrade.
//
// CONVENTIONS
//   - Every public name starts with Lib, so this file cannot collide with your
//     own variables or with another library.
//   - Every variable a function assigns is declared local. Nothing in this file
//     reads or writes a global.
//   - No function here calls Param(). Parameters belong to the formula that
//     uses the library, not to the library.
//   - A function that cannot answer returns Null rather than a plausible
//     number, so the gap is visible on a chart instead of silently wrong.
//   - Periods are passed as numbers. Passing an array where a number is
//     expected raises Error 6 rather than producing a quiet wrong answer.
//
// CHANGELOG
//   1.00  First release. LibVersion, LibWarmupOK, LibSafeDivide,
//         LibMedianTurnover, LibIsLiquid, LibNormATR, LibZScore, LibRegime,
//         LibRegimeName, LibPercentText, LibMoneyText.
// ---------------------------------------------------------------------------

// The single executable statement in this file. It raises an AmiBroker error
// on any version older than 6.20 instead of letting StDev's third argument
// fail somewhere deep inside a formula that includes this library.
Version( 6.20 );

// ---------------------------------------------------------------------------
// LibVersion()
//   Purpose : Report which release of this library is loaded.
//   Inputs  : none
//   Returns : NUMBER
//   Notes   : Compare numerically, e.g. if( LibVersion() < 1.00 ) ...
// ---------------------------------------------------------------------------
function LibVersion()
{
    return 1.00;
}

// ---------------------------------------------------------------------------
// LibWarmupOK( RequiredBars )
//   Purpose : True from the bar on which an indicator needing RequiredBars of
//             history can first have a real value.
//   Inputs  : RequiredBars - NUMBER of bars of history required
//   Returns : ARRAY (Boolean, one value per bar)
//   Notes   : BarIndex() is zero-based, so bar RequiredBars is the first bar
//             with RequiredBars earlier bars behind it. A negative request is
//             clamped to zero rather than silently inverting the test.
// ---------------------------------------------------------------------------
function LibWarmupOK( RequiredBars )
{
    local Needed;
    local Result;

    Needed = RequiredBars;
    if( Needed < 0 ) Needed = 0;

    Result = BarIndex() >= Needed;

    return Result;
}

// ---------------------------------------------------------------------------
// LibSafeDivide( Numerator, Denominator, Fallback )
//   Purpose : Division that yields a value you chose wherever the result is
//             not a finite number.
//   Inputs  : Numerator   - ARRAY or NUMBER
//             Denominator - ARRAY or NUMBER
//             Fallback    - ARRAY or NUMBER used where the quotient is Null,
//                           NaN or infinite. Pass Null to leave a visible gap.
//   Returns : ARRAY
//   Notes   : IIf evaluates both branches, so the division still happens on
//             every bar; only the result is discarded. This is the pattern the
//             official Nz() page gives as the long-hand form of Nz().
// ---------------------------------------------------------------------------
function LibSafeDivide( Numerator, Denominator, Fallback )
{
    local Quotient;
    local Result;

    Quotient = Numerator / Denominator;
    Result   = IIf( IsFinite( Quotient ), Quotient, Fallback );

    return Result;
}

// ---------------------------------------------------------------------------
// LibMedianTurnover( LookbackPeriod )
//   Purpose : Typical money changing hands per bar, as a median rather than a
//             mean, so one enormous day does not carry the estimate.
//   Inputs  : LookbackPeriod - NUMBER of bars in the median window
//   Returns : ARRAY, in the currency the symbol is quoted in
//   Assumes : Volume is share volume, not contracts or notional. For futures
//             and for symbols quoted in cents this number is not money.
//   Warm-up : Null until LookbackPeriod bars are available.
// ---------------------------------------------------------------------------
function LibMedianTurnover( LookbackPeriod )
{
    local BarTurnover;
    local Result;

    BarTurnover = Close * Volume;
    Result      = Median( BarTurnover, LookbackPeriod );

    return Result;
}

// ---------------------------------------------------------------------------
// LibIsLiquid( LookbackPeriod, MinimumTurnover )
//   Purpose : A first-stage screening filter: is there normally enough money
//             traded in this symbol for a position to be enterable and
//             exitable at something close to the printed price?
//   Inputs  : LookbackPeriod  - NUMBER of bars for the median
//             MinimumTurnover - NUMBER, the threshold in quote currency
//   Returns : ARRAY (Boolean)
//   Notes   : Warm-up bars return False, not Null. That is a decision, not an
//             accident: for a screen, "unknown" and "excluded" should behave
//             the same way, and a Null here would propagate into every
//             condition it is combined with.
// ---------------------------------------------------------------------------
function LibIsLiquid( LookbackPeriod, MinimumTurnover )
{
    local Typical;
    local Result;

    Typical = LibMedianTurnover( LookbackPeriod );
    Result  = IIf( LibWarmupOK( LookbackPeriod ), Typical >= MinimumTurnover, False );

    return Result;
}

// ---------------------------------------------------------------------------
// LibNormATR( ATRPeriod )
//   Purpose : Average True Range expressed as a percentage of price, so that a
//             300-currency-unit share and a 3-currency-unit share can be
//             compared on the same axis.
//   Inputs  : ATRPeriod - NUMBER of bars for Wilder's average
//   Returns : ARRAY, percent
//   Notes   : Null wherever Close is zero or missing, so a broken quote leaves
//             a gap in the plot instead of a spike.
// ---------------------------------------------------------------------------
function LibNormATR( ATRPeriod )
{
    local AverageRange;
    local Result;

    AverageRange = ATR( ATRPeriod );
    Result       = 100 * LibSafeDivide( AverageRange, Close, Null );

    return Result;
}

// ---------------------------------------------------------------------------
// LibZScore( InputArray, LookbackPeriod )
//   Purpose : How unusual the current value of an array is, measured in
//             standard deviations of its own recent history.
//   Inputs  : InputArray     - ARRAY to normalise
//             LookbackPeriod - NUMBER of bars in the window
//   Returns : ARRAY
//   Notes   : StDev's third argument is passed explicitly. The documented
//             default is the population standard deviation, which is what a
//             spreadsheet calls STDEV.P, not STDEV. Where the window is flat
//             the standard deviation is zero and the result is Null.
// ---------------------------------------------------------------------------
function LibZScore( InputArray, LookbackPeriod )
{
    local Mean;
    local Spread;
    local Result;

    Mean   = MA( InputArray, LookbackPeriod );
    Spread = StDev( InputArray, LookbackPeriod, True );
    Result = LibSafeDivide( InputArray - Mean, Spread, Null );

    return Result;
}

// ---------------------------------------------------------------------------
// LibRegime( FastPeriod, SlowPeriod )
//   Purpose : Classify each bar into one of three states, so that a strategy
//             can be gated on market conditions instead of being run blind.
//   Inputs  : FastPeriod - NUMBER, short moving-average length
//             SlowPeriod - NUMBER, long moving-average length
//   Returns : ARRAY of  +1 (uptrend), 0 (neither), -1 (downtrend),
//             Null during warm-up.
//   Definition: uptrend   = Close above the slow average AND fast above slow
//               downtrend = Close below the slow average AND fast below slow
//               neither   = anything else
//   Notes   : This is a definition, not a fact about markets. Change the
//             periods and the classification changes with them. Nothing here
//             claims that an uptrend continues.
// ---------------------------------------------------------------------------
function LibRegime( FastPeriod, SlowPeriod )
{
    local FastAverage;
    local SlowAverage;
    local UpCondition;
    local DownCondition;
    local Ready;
    local Result;

    FastAverage = MA( Close, FastPeriod );
    SlowAverage = MA( Close, SlowPeriod );

    UpCondition   = Close > SlowAverage AND FastAverage > SlowAverage;
    DownCondition = Close < SlowAverage AND FastAverage < SlowAverage;

    Ready = LibWarmupOK( SlowPeriod );

    Result = IIf( Ready,
                  IIf( UpCondition, 1, IIf( DownCondition, -1, 0 ) ),
                  Null );

    return Result;
}

// ---------------------------------------------------------------------------
// LibRegimeName( RegimeArray )
//   Purpose : Turn one bar of LibRegime output into text for a chart title or
//             a commentary.
//   Inputs  : RegimeArray - ARRAY produced by LibRegime()
//   Returns : STRING describing the SELECTED bar only
//   Notes   : Display only. It reads a single bar, so it must never be used to
//             build a signal. IsNull is tested before the value is read,
//             because SelectedValue of a Null bar cannot be told apart from a
//             genuine zero.
// ---------------------------------------------------------------------------
function LibRegimeName( RegimeArray )
{
    local Unknown;
    local Code;
    local Result;

    Unknown = SelectedValue( IsNull( RegimeArray ) );
    Code    = SelectedValue( RegimeArray );

    Result = "neutral";

    if( Unknown )
    {
        Result = "not enough history";
    }
    else
    {
        if( Code > 0 ) Result = "uptrend";
        if( Code < 0 ) Result = "downtrend";
    }

    return Result;
}

// ---------------------------------------------------------------------------
// LibPercentText( ValueArray )
//   Purpose : Format a percentage for display with two decimal places.
//   Inputs  : ValueArray - ARRAY or NUMBER already expressed in percent
//   Returns : STRING, e.g. "2.41%"
//   Notes   : Given an array, NumToStr formats the selected value. Display
//             only.
// ---------------------------------------------------------------------------
function LibPercentText( ValueArray )
{
    local Result;

    Result = NumToStr( ValueArray, 1.2 ) + "%";

    return Result;
}

// ---------------------------------------------------------------------------
// LibMoneyText( ValueArray )
//   Purpose : Format a currency amount for display, with thousands separators
//             and no decimals.
//   Inputs  : ValueArray - ARRAY or NUMBER
//   Returns : STRING, e.g. "12,480,000"
//   Notes   : The thousands separator itself is a Windows regional setting,
//             configured in AmiBroker under Tools -> Preferences -> Misc.
// ---------------------------------------------------------------------------
function LibMoneyText( ValueArray )
{
    local Result;

    Result = NumToStr( ValueArray, 1.0, True );

    return Result;
}

// End of course-library.afl
