HighTimeframeSampling
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © SimpleCryptoLife

//@version=5

// @description Library for sampling high timeframe (HTF) data. Returns a matrix of historical values, an array of historical values, an arbitrary historical value, or the highest/lowest value in a range, spending a single security() call.
// An optional pass-through for the chart timeframe is included. Other than that case, the data is fixed and does not alter over the course of the HTF bar. It behaves consistently on historical and elapsed realtime bars.
// For now, it returns floating-point numbers only, and bools for the first two functions.

library("HighTimeframeSampling", overlay = true)

// 🙏 Credits: This library is (yet another) attempt at a solution of the problems in using HTF data that were laid out by Pinecoders - to whom, especially to Luc F, many thanks are due - in "security() revisited" - https://www.tradingview.com/script/00jFIl5w-security-revisited-PineCoders/, which I recommend you consult first. Go ahead, I'll wait.
//      All code is my own.

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//         WHAT'S THE PROBLEM? OR, WHY NOT JUST USE SECURITY()  
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// There are many difficulties with using HTF data, and many potential solutions. It's not really possible to convey it only in words: you need to see it on a chart.
// Before using this library, please refer to my other HTF library, HighTimeframeTiming: https://www.tradingview.com/script/F9vFt8aX-HighTimeframeTiming/, which explains it extensively, compares many different solutions, and demonstrates (what I think are) the advantages of using this very library, namely, that it's stable, accurate, versatile and inexpensive. Then if you agree, come back here and choose your function.

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//                          MOAR EXPLANATION  
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 🧹 Housekeeping: To see which plot is which, turn line labels on: Settings > Scales > Indicator Name Label. Vertical lines at the top of the chart show the open of a HTF bar: grey for historical and white for real-time bars.

// ‼ LIMITATIONS: To avoid strange behaviour, use this library on liquid assets and at chart timeframes high enough to reliably produce updates at least once per bar period.
//      A more conventional and universal limitation is that the library does not offer an unlimited view of historical bars. You need to define upfront how many HTF bars you want to store. Very large numbers might conceivably run into data or performance issues.
//      This should be obvious, but to be clear, you need to call the function on every bar update to avoid missing data.
//      Another thing that should probably go without saying: you must supply the same timeframe to the functions here as you used in your security() call for the raw data.

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//                       BRING ON THE FUNCTIONS  
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// @function f_HTF_Float(string _HTF, float _source, int _arrayHistory, int _HTF_Offset, bool _useLiveDataOnChartTF=false)
//      Returns a floating-point number from a higher timeframe, with a historical operator within an abitrary (but limited) number of bars.
// @param string _HTF is the string that represents the higher timeframe. It must be in a format that the request.security() function recognises. The input timeframe cannot be lower than the chart timeframe or an error is thrown.
// @param float _source is the source value that you want to sample, e.g. close, open, etc., or you can use any floating-point number.
// @param int _arrayHistory is the number of HTF bars you want to store and must be greater than zero. You can't go back further in history than this number of bars (minus one, because the current/most recent available bar is also stored).
// @param int _HTF_Offset is the historical operator for the value you want to return. E.g., if you want the most recent fixed close, _source=close and _HTF_Offset = 0. If you want the one before that, _HTF_Offset=1, etc.
//      The number of HTF bars to look back must be zero or more, and must be one less than the number of bars stored.
// @param bool _useLiveDataOnChartTF uses live data on the chart timeframe.
//      If the higher timeframe is the same as the chart timeframe, store the live value (i.e., from this very bar). For all truly higher timeframes, store the fixed value (i.e., from the previous bar).
//      The default is to use live data for the chart timeframe, so that this function works intuitively, that is, it does not fix data unless it has to (i.e., because the data is from a higher timeframe).
//      This means that on default settings, on the chart timeframe, it matches the raw source values from security()[0].
//      You can override this behaviour by passing _useLiveDataOnChartTF as false. Then it will fix all data for all timeframes.
// @returns a floating-point value that you requested from the higher timeframe.


// @function f_HTF_ArrayFloat(string _HTF, float _source, int _arrayHistory, bool _useLiveDataOnChartTF=false, int _startIn, int _endIn)
//      Returns an array of historical values *of a single variable* from a higher timeframe, starting with the current bar. Optionally, returns a slice of the array. The array is in reverse chronological order, i.e., index 0 contains the most recent value.
// @param string _HTF is the string that represents the higher timeframe. It must be in a format that the request.security() function recognises. The input timeframe cannot be lower than the chart timeframe or an error is thrown.
// @param float _source is the source value that you want to sample, e.g. close, open, etc., or you can use any floating-point number.
// @param int _arrayHistory is the number of HTF bars you want to keep in the array.
// @param bool _useLiveDataOnChartTF uses live data on the chart timeframe.
//      If the higher timeframe is the same as the chart timeframe, store the live value (i.e., from this very bar). For all truly higher timeframes, store the fixed value (i.e., from the previous bar).
//      The default is to use live data for the chart timeframe, so that this function works intuitively, that is, it does not fix data unless it has to (i.e., because the data is from a higher timeframe).
//      This means that on default settings, on the chart timeframe, it matches raw source values from security().
//      You can override this behaviour by passing _useLiveDataOnChartTF as false. Then it will fix all data for all timeframes.
// @param int _startIn is the array index to begin taking a slice. Must be at least one less than the length of the array; if out of bounds it is corrected to 0.  
// @param int _endIn is the array index BEFORE WHICH to end the slice. If the ending index of the array slice would take the slice past the end of the array, it is corrected to the end of the array. The ending index of the array slice must be greater than or equal to the starting index. If the end is less than the start, the whole array is returned. If the starting index is the same as the ending index, an empty array is returned. If either the starting or ending index is negative, the entire array is returned (which is the default behaviour; this is effectively a switch to bypass the slicing without taking up an extra parameter).
// @returns an array of HTF values.


// @function f_HTF_HighestFloat(string _HTF="", float _source, int _arrayHistory, bool _useLiveDataOnChartTF=true, int _rangeIn)
//      Returns the highest value within a range consisting of a given number of bars back from the most recent bar.
// @param string _HTF is the string that represents the higher timeframe. It must be in a format that the request.security() function recognises. The input timeframe cannot be lower than the chart timeframe or an error is thrown.
// @param float _source is the source value that you want to sample, e.g. close, open, etc., or you can use any floating-point number.
// @param int _arrayHistory is the number of HTF bars you want to store and must be greater than zero. You can't have a range greater than this number.
// @param bool _useLiveDataOnChartTF uses live data on the chart timeframe.
//      If the higher timeframe is the same as the chart timeframe, store the live value (i.e., from this very bar). For all truly higher timeframes, store the fixed value (i.e., from the previous bar).
//      The default is to use live data for the chart timeframe, so that this function works intuitively, that is, it does not fix data unless it has to (i.e., because the data is from a higher timeframe).
//      This means that on default settings, on the chart timeframe, it matches raw source values from security().
//      You can override this behaviour by passing _useLiveDataOnChartTF as false. Then it will fix all data for all timeframes.
// @param _rangeIn is the number of bars to include in the range of bars from which we want to find the highest value. It is NOT the historical operator of the last bar in the range. The range always starts at the current bar. A value of 1 doesn't make much sense but the function will generously return the only value it can anyway. A value less than 1 doesn't make sense and will return an error. A value that is higher than the number of stored values will be corrected to equal the number of stored values.
// @returns a floating-point number representing the highest value in the range.


// @function f_HTF_LowestFloat(string _HTF="", float _source, int _arrayHistory, bool _useLiveDataOnChartTF=true, int _rangeIn)
//      Returns the lowest value within a range consisting of a given number of bars back from the most recent bar. 
// @param string _HTF is the string that represents the higher timeframe. It must be in a format that the request.security() function recognises. The input timeframe cannot be lower than the chart timeframe or an error is thrown.
// @param float _source is the source value that you want to sample, e.g. close, open, etc., or you can use any floating-point number.
// @param int _arrayHistory is the number of HTF bars you want to store and must be greater than zero. You can't go back further in history than this number of bars (minus one, because the current/most recent available bar is also stored).
// @param bool _useLiveDataOnChartTF uses live data on the chart timeframe.
//      If the higher timeframe is the same as the chart timeframe, store the live value (i.e., from this very bar). For all truly higher timeframes, store the fixed value (i.e., from the previous bar).
//      The default is to use live data for the chart timeframe, so that this function works intuitively, that is, it does not fix data unless it has to (i.e., because the data is from a higher timeframe).
//      This means that on default settings, on the chart timeframe, it matches raw source values from security().
//      You can override this behaviour by passing _useLiveDataOnChartTF as false. Then it will fix all data for all timeframes.
// @param _rangeIn is the number of bars to include in the range of bars from which we want to find the highest value. It is NOT the historical operator of the last bar in the range. The range always starts at the current bar. A value of 1 doesn't make much sense but the function will generously return the only value it can anyway. A value less than 1 doesn't make sense and will return an error. A value that is higher than the number of stored values will be corrected to equal the number of stored values.
// @returns a floating-point number representing the lowest value in the range.


// @function f_HTF_MatrixFloat(string _HTF="", float[] _sourceArray, int _matrixHistory, bool _useLiveDataOnChartTF=true)
//      Returns a matrix of historical values *of an array* from a higher timeframe, starting with the current bar. Unlike f_HTF_ArrayFloat, there is no option to return a slice of the matrix. The matrix is in reverse chronological order, i.e., row 0 contains the most recent values.
// @param string _HTF is the string that represents the higher timeframe. It must be in a format that the request.security() function recognises. The input timeframe cannot be lower than the chart timeframe or an error is thrown.
// @param float[] _sourceArray is the source array that you want to sample, e.g. close, open, etc., or you can use any floating-point number.
// @param int _matrixHistory is the number of HTF bars for which you want to keep arrays in the matrix.
// @param bool _useLiveDataOnChartTF uses live data on the chart timeframe.
//      If the higher timeframe is the same as the chart timeframe, store the live value (i.e., from this very bar). For all truly higher timeframes, store the fixed value (i.e., from the previous bar).
//      The default is to use live data for the chart timeframe, so that this function works intuitively, that is, it does not fix data unless it has to (i.e., because the data is from a higher timeframe).
//      This means that on default settings, on the chart timeframe, it matches raw source values from security().
//      You can override this behaviour by passing _useLiveDataOnChartTF as false. Then it will fix all data for all timeframes.
// @returns a matrix of historical HTF array values.


// @function f_HTF_Bool(string _HTF, bool _source, int _arrayHistory, int _HTF_Offset, bool _useLiveDataOnChartTF=false)
//      Returns a boolean value from a higher timeframe, with a historical operator within an abitrary (but limited) number of bars.
// @param string _HTF is the string that represents the higher timeframe. It must be in a format that the request.security() function recognises. The input timeframe cannot be lower than the chart timeframe or an error is thrown.
// @param bool _source is the source value that you want to sample.
// @param int _arrayHistory is the number of HTF bars you want to store and must be greater than zero. You can't go back further in history than this number of bars (minus one, because the current/most recent available bar is also stored).
// @param int _HTF_Offset is the historical operator for the value you want to return. E.g., if you want the most recent fixed close, _source=close and _HTF_Offset = 0. If you want the one before that, _HTF_Offset=1, etc.
//      The number of HTF bars to look back must be zero or more, and must be one less than the number of bars stored.
// @param bool _useLiveDataOnChartTF uses live data on the chart timeframe.
//      If the higher timeframe is the same as the chart timeframe, store the live value (i.e., from this very bar). For all truly higher timeframes, store the fixed value (i.e., from the previous bar).
//      The default is to use live data for the chart timeframe, so that this function works intuitively, that is, it does not fix data unless it has to (i.e., because the data is from a higher timeframe).
//      This means that on default settings, on the chart timeframe, it matches the raw source values from security()[0].
//      You can override this behaviour by passing _useLiveDataOnChartTF as false. Then it will fix all data for all timeframes.
// @returns a boolean value that you requested from the higher timeframe.


// @function f_HTF_ArrayBool(string _HTF, bool _source, int _arrayHistory, bool _useLiveDataOnChartTF=false, int _startIn, int _endIn)
//      Returns an array of historical values *of a single variable* from a higher timeframe, starting with the current bar. Optionally, returns a slice of the array. The array is in reverse chronological order, i.e., index 0 contains the most recent value.
// @param string _HTF is the string that represents the higher timeframe. It must be in a format that the request.security() function recognises. The input timeframe cannot be lower than the chart timeframe or an error is thrown.
// @param bool _source is the source value that you want to sample.
// @param int _arrayHistory is the number of HTF bars you want to keep in the array.
// @param bool _useLiveDataOnChartTF uses live data on the chart timeframe.
//      If the higher timeframe is the same as the chart timeframe, store the live value (i.e., from this very bar). For all truly higher timeframes, store the fixed value (i.e., from the previous bar).
//      The default is to use live data for the chart timeframe, so that this function works intuitively, that is, it does not fix data unless it has to (i.e., because the data is from a higher timeframe).
//      This means that on default settings, on the chart timeframe, it matches raw source values from security().
//      You can override this behaviour by passing _useLiveDataOnChartTF as false. Then it will fix all data for all timeframes.
// @param int _startIn is the array index to begin taking a slice. Must be at least one less than the length of the array; if out of bounds it is corrected to 0.  
// @param int _endIn is the array index BEFORE WHICH to end the slice. If the ending index of the array slice would take the slice past the end of the array, it is corrected to the end of the array. The ending index of the array slice must be greater than or equal to the starting index. If the end is less than the start, the whole array is returned. If the starting index is the same as the ending index, an empty array is returned. If either the starting or ending index is negative, the entire array is returned (which is the default behaviour; this is effectively a switch to bypass the slicing without taking up an extra parameter).
// @returns an array of HTF values.


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//                                            INPUTS                                               |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|

// Inputs needed for the library
bool in_show_HTF_Close = input.bool(defval=true, title="Show raw security()[0] data", group="Display (floats)", tooltip="The source float data that is requested using request.security().")
bool in_show_f_HTF_Float = input.bool(defval=true, title="Show function 1, f_HTF_Float()", tooltip="A single value [H] bars back.")
bool in_show_f_HTF_ArrayFloatLabel = input.bool(defval=true, title="Show function 2, f_HTF_ArrayFloat()", tooltip="The entire array of HTF values")
bool in_show_f_HTF_HighestFloat = input.bool(defval=true, title="Show function 3, f_HTF_HighestFloat()", tooltip="The highest HTF value in a range.")
bool in_show_f_HTF_LowestFloat = input.bool(defval=true, title="Show function 4, f_HTF_LowestFloat()", tooltip="The lowest HTF value in a range.")
bool in_show_f_HTF_MatrixFloat = input.bool(defval=true, title="Show function 5, f_HTF_MatrixFloat()", tooltip="A matrix of historical values of an array.")

bool in_show_HTF_BoolRaw = input.bool(defval=true, title="Show raw security()[0] data", group="Display (bools)", tooltip="The source bool data that is requested using request.security().")
bool in_show_f_HTF_Bool = input.bool(defval=true, title="Show function 1, f_HTF_Bool()", tooltip="A single boolean value [H] bars back.")
bool in_show_f_HTF_ArrayBoolLabel = input.bool(defval=true, title="Show function 2, f_HTF_ArrayBool()", tooltip="The entire array of HTF boolean values")

string in_HTF = input.timeframe(defval="", title="Higher Timeframe", group="General settings")
int in_historyLength = input.int(minval=1, maxval=100, defval=10, title="Number of HTF source values to store", tooltip="Must be one greater than the number of values to look back.")
bool in_useLiveDataOnChartTF = input.bool(defval=true, title="Use live data on chart timeframe (HTF data is always fixed)")
int in_HTF_Offset = input.int(defval=0, minval=0, maxval=4, title="No. of HTF bars to look back [H]", group="Single values")
int in_start = input.int(minval=-1, maxval=999, defval=0, title="Starting index of array subset", group="Arrays", tooltip="The array index to begin taking a slice. This number comes out the same whether you think of it as the array index or as the historical operator [ ]. A value of 0 starts at the current HTF bar. Wrong values are fixed silently; if irreparable, we return the entire array. If you don't supply any start and end values, the entire array is returned.")
int in_end = input.int(minval=-1, maxval=999, defval=-1, title="Ending index of array subset", tooltip="The array index BEFORE WHICH to end the slice. If you supply an end value of 2, the value at index 2 is NOT returned. This is the behaviour of the built-in array slice function and I kept it for consistency. This number comes out the same whether you think of it as the array index or as the historical operator [ ]. A value of 1 ends at the current HTF bar. Wrong values are fixed silently; if irrepairable, we return the entire array. If you don't supply any start and end values, the entire array is returned.")
int in_range = input.int(defval=5, minval=0, maxval=999, group="Highest/Lowest Float in a Range", title="Range", tooltip="Range (number of total bars starting from the current bar) within which to look for the highest/lowest value")

// === Import libraries
import SimpleCryptoLife/Labels/2 as SCL_Label


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//                                            RAW HTF DATA                                         |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|

// These raw HTF values are requested directly from the security() call on the current bar. 
// The close is used as the source for all the float library functions (except f_HTF_MatrixFloat, which uses OHLC), and can also be plotted.
// The boolean value _isUpCandle is used as the source for boolean functions.
f_OHLC() =>
    _open=open, _high=high, _low=low, _close=close
    bool _isUpCandle=_close > _open ? true : false
    [_open, _high, _low, _close, _isUpCandle]

[HTF_Open, HTF_High, HTF_Low, HTF_Close, HTF_IsUpCandle] = request.security(syminfo.tickerid, in_HTF, f_OHLC(), barmerge.gaps_off, barmerge.lookahead_off)


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//                                 FLOAT FUNCTION 1: GET A VALUE                                   |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|

export f_HTF_Float(string _HTF="", float _source, int _arrayHistory, int _HTF_offset=0, bool _useLiveDataOnChartTF=true) =>
    var string _errorTextFunctionName = "f_HTF_Float", var string errorTextLibraryName = "SimpleCryptoLife/HighTimeframeSampling"  // Error-checking.
    if timeframe.in_seconds(_HTF) < timeframe.in_seconds(timeframe.period)
        runtime.error("Input timeframe cannot be lower than the chart timeframe [Error 001:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")
    if _arrayHistory < 1
        runtime.error("The number of stored values configured must be more than zero." + " [Error 002:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")  
    if _HTF_offset < 0
        runtime.error("The number of HTF bars to look back must be zero or more." + " [Error 003:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")   
    if _HTF_offset + 1 > _arrayHistory
        runtime.error("The number of stored values configured is " + str.tostring(_arrayHistory) + ", and must be at least " + str.tostring(_HTF_offset+1) + " in order to look " + str.tostring(_HTF_offset) + " HTF bars back." + " [Error 004:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")     
    
    var float[] _arraySource = array.new_float(_arrayHistory,na)  // Create the array to hold the floats.
    bool _isChartTimeframe = _HTF=="" or _HTF == timeframe.period  // Check if we are using a "higher" timeframe that's actually the chart timeframe.
    var int _offset = _isChartTimeframe and _useLiveDataOnChartTF==true ? 0 : 1  // Use live data on chart TF if that option is selected; otherwise use fixed (the previous bar's) data. 
    // By using timeframe.change(), we store the source value only when the HTF bar *opens*. We do updates throughout every bar on the chart timeframe; otherwise all throughout the chart bar that corresponds to the first HTF bar.
    // It might be more efficient to update only at the beginning of the chart bar, but a) it didn't seem to work, and b) even if I could get it working, I suspect it might be less robust.
    if _isChartTimeframe or timeframe.change(_HTF)
        array.unshift(_arraySource,_source[_offset])  // Add the source value to index 0 and shift other indexes along. It's important to keep the order so that it's simple to look up values.
        if array.size(_arraySource) > _arrayHistory
            array.pop(_arraySource)  // Remove the oldest value, keeping the array the same length.

    float _myFloat = array.get(_arraySource,_HTF_offset)  // Return the one value requested.
    _myFloat

float HTF_FloatValue = f_HTF_Float(in_HTF, HTF_Close, in_historyLength, in_HTF_Offset, in_useLiveDataOnChartTF)  // Call the function and use the offset to get a certain value


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//                             FLOAT FUNCTION 2: GET AN ARRAY OF VALUES                            |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|

export f_HTF_ArrayFloat(string _HTF="", float _source, int _arrayHistory, bool _useLiveDataOnChartTF=true, int _startIn=0, int _endIn=-1) =>

    var string _errorTextFunctionName = "f_HTF_ArrayFloat", var string errorTextLibraryName = "SimpleCryptoLife/HighTimeframeSampling"
    // For timeframe problems we throw an error because we can't fix them from here.
    if timeframe.in_seconds(_HTF) < timeframe.in_seconds(timeframe.period)
        runtime.error("Input timeframe cannot be lower than the chart timeframe [Error 005:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")
    // For a negative or zero array length we throw errors because we can't guess what the user wants this particular value to be.
    if _arrayHistory < 1
        runtime.error("The number of stored values configured must be more than zero." + " [Error 006:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")        
    var float[] _arraySource = array.new_float(_arrayHistory,na)
    bool _isChartTimeframe = _HTF=="" or _HTF == timeframe.period
    var int _offset = _isChartTimeframe and _useLiveDataOnChartTF==true ? 0 : 1
    if _isChartTimeframe or timeframe.change(_HTF)
        array.unshift(_arraySource,_source[_offset])
        if _arrayHistory > array.size(_arraySource)
            array.pop(_arraySource)
    // Fix all slice start and end problems silently, erring towards returning more of the array.
    int _start = (_startIn < 0) or (_endIn < 0) ? 0 : _startIn >= _arrayHistory ? 0 : _endIn < _startIn ? 0 : _startIn
    int _end = (_startIn < 0) or (_endIn < 0) ? _arrayHistory : _endIn < _startIn ? _arrayHistory : _endIn < _startIn ? _arrayHistory : _endIn > _arrayHistory ? _arrayHistory : _endIn
    float[] _arraySubset = array.slice(_arraySource,_start,_end)
    _arraySubset

float[] HTF_ArrayFloat = f_HTF_ArrayFloat(in_HTF, HTF_Close, in_historyLength, in_useLiveDataOnChartTF, in_start, in_end)


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//                      FLOAT FUNCTION 3: RETURN THE HIGHEST VALUE IN A RANGE                      |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|

export f_HTF_HighestFloat(string _HTF="", float _source, int _arrayHistory, bool _useLiveDataOnChartTF=true, int _rangeIn) =>

    var string _errorTextFunctionName = "f_HTF_HighestFloat", var string errorTextLibraryName = "SimpleCryptoLife/HighTimeframeSampling"
    if timeframe.in_seconds(_HTF) < timeframe.in_seconds(timeframe.period)
        runtime.error("Input timeframe cannot be lower than the chart timeframe [Error 007:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")
    if _arrayHistory < 1
        runtime.error("The number of stored values configured must be more than zero." + " [Error 008:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")        
    var float[] _arraySource = array.new_float(_arrayHistory,na)
    bool _isChartTimeframe = _HTF=="" or _HTF == timeframe.period
    var int _offset = _isChartTimeframe and _useLiveDataOnChartTF==true ? 0 : 1
    if _isChartTimeframe or timeframe.change(_HTF)
        array.unshift(_arraySource,_source[_offset])
        if _arrayHistory > array.size(_arraySource)
            array.pop(_arraySource)
    // Error-check the range. A value of 1 doesn't make much sense but the function will generously return the only value it can anyway. A value less than 1 doesn't make any sense and will return an error. 
    if _rangeIn < 1
        runtime.error("The range within which to look for the highest value must be greater than zero." + " [Error 009:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")  
    int _range = _rangeIn > _arrayHistory ? _arrayHistory : _rangeIn  // A range that is higher than the number of stored values will be corrected to equal the number of stored values.
    float[] _arraySubset = array.slice(_arraySource,0,_range)
    float _highest = array.max(_arraySubset)  // Get the largest value (array.max(_arraySubset,0) breaks it for some reason)
    _highest

float HTF_Highest = f_HTF_HighestFloat(in_HTF, HTF_Close, in_historyLength, in_useLiveDataOnChartTF, in_range)


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//                      FLOAT FUNCTION 4: RETURN THE LOWEST VALUE IN A RANGE                       |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|

export f_HTF_LowestFloat(string _HTF="", float _source, int _arrayHistory, bool _useLiveDataOnChartTF=true, int _rangeIn) =>

    var string _errorTextFunctionName = "f_HTF_LowestFloat", var string errorTextLibraryName = "SimpleCryptoLife/HighTimeframeSampling"
    if timeframe.in_seconds(_HTF) < timeframe.in_seconds(timeframe.period)
        runtime.error("Input timeframe cannot be lower than the chart timeframe [Error 010:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")
    if _arrayHistory < 1
        runtime.error("The number of stored values configured must be more than zero." + " [Error 011:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")        
    var float[] _arraySource = array.new_float(_arrayHistory,na)
    bool _isChartTimeframe = _HTF=="" or _HTF == timeframe.period
    var int _offset = _isChartTimeframe and _useLiveDataOnChartTF==true ? 0 : 1
    if _isChartTimeframe or timeframe.change(_HTF)
        array.unshift(_arraySource,_source[_offset])
        if _arrayHistory > array.size(_arraySource)
            array.pop(_arraySource)
    if _rangeIn < 1
        runtime.error("The range within which to look for the lowest value must be greater than zero." + " [Error 012:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")  
    int _range = _rangeIn > _arrayHistory ? _arrayHistory : _rangeIn
    float[] _arraySubset = array.slice(_arraySource,0,_range)
    float _lowest = array.min(_arraySubset)
    _lowest

float HTF_Lowest = f_HTF_LowestFloat(in_HTF, HTF_Close, in_historyLength, in_useLiveDataOnChartTF, in_range)


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//                             FLOAT FUNCTION 5: GET A MATRIX OF VALUES                            |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|

export f_HTF_MatrixFloat(string _HTF="", float[] _sourceArray, int _matrixHistory, bool _useLiveDataOnChartTF=true) =>

    var string _errorTextFunctionName = "f_HTF_MatrixFloat", var string errorTextLibraryName = "SimpleCryptoLife/HighTimeframeSampling"
    // For timeframe problems we throw an error because we can't fix them from here.
    if timeframe.in_seconds(_HTF) < timeframe.in_seconds(timeframe.period)
        runtime.error("Input timeframe cannot be lower than the chart timeframe [Error 013:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")
    // For a negative or zero array length we throw errors because we can't guess what the user wants this particular value to be.
    if _matrixHistory < 1
        runtime.error("The number of stored values configured must be more than zero." + " [Error 014:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")

    int _sourceArrayLength = array.size(_sourceArray)  // Get the length of the input array
    // Throw an error if the input array has changed size
    if _sourceArrayLength > _sourceArrayLength[1] or _sourceArrayLength < _sourceArrayLength[1]
        runtime.error("Length of input array _sourceArray must stay the same [Error 015:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")
    var _matrixSource = matrix.new<float>(_matrixHistory, _sourceArrayLength)
    bool _isChartTimeframe = _HTF=="" or _HTF == timeframe.period
    var int _offset = _isChartTimeframe and _useLiveDataOnChartTF==true ? 0 : 1
    int _matrixRows = matrix.rows(_matrixSource)
    if _isChartTimeframe or timeframe.change(_HTF)
        matrix.add_row(_matrixSource, 0, _sourceArray[_offset])  // Insert the source array at the beginning of the matrix. I don't know if you can just add [offset] to an array but let's try.
        if _matrixRows > _matrixHistory
            matrix.remove_row(_matrixSource,(_matrixRows))  // Remove the last row of the matrix if it has more rows than the input height
    _matrixSource


var float[] sourceArray = array.new_float(4,na)  // Create the input array for the function to sample
array.set(sourceArray,0,HTF_Open), array.set(sourceArray,1,HTF_High), array.set(sourceArray,2,HTF_Low), array.set(sourceArray,3,HTF_Close)  // Populate it with HTF OHLC values

HTF_Matrix = f_HTF_MatrixFloat(_HTF=in_HTF, _sourceArray=sourceArray, _matrixHistory=in_historyLength, _useLiveDataOnChartTF=in_useLiveDataOnChartTF)


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//                                 BOOL FUNCTION 1: GET A VALUE                                   |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|

export f_HTF_Bool(string _HTF="", bool _source, int _arrayHistory, int _HTF_offset=0, bool _useLiveDataOnChartTF=true) =>
    var string _errorTextFunctionName = "f_HTF_Bool", var string errorTextLibraryName = "SimpleCryptoLife/HighTimeframeSampling"  // Error-checking.
    if timeframe.in_seconds(_HTF) < timeframe.in_seconds(timeframe.period)
        runtime.error("Input timeframe cannot be lower than the chart timeframe [Error 016:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")
    if _arrayHistory < 1
        runtime.error("The number of stored values configured must be more than zero." + " [Error 017:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")  
    if _HTF_offset < 0
        runtime.error("The number of HTF bars to look back must be zero or more." + " [Error 018:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")   
    if _HTF_offset + 1 > _arrayHistory
        runtime.error("The number of stored values configured is " + str.tostring(_arrayHistory) + ", and must be at least " + str.tostring(_HTF_offset+1) + " in order to look " + str.tostring(_HTF_offset) + " HTF bars back." + " [Error 019:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")     
    
    var bool[] _arraySource = array.new_bool(_arrayHistory,na)  // Create the array to hold the bools.
    bool _isChartTimeframe = _HTF=="" or _HTF == timeframe.period  // Check if we are using a "higher" timeframe that's actually the chart timeframe.
    var int _offset = _isChartTimeframe and _useLiveDataOnChartTF==true ? 0 : 1  // Use live data on chart TF if that option is selected; otherwise use fixed (the previous bar's) data. 
    // By using timeframe.change(), we store the source value only when the HTF bar *opens*. We do updates throughout every bar on the chart timeframe; otherwise all throughout the chart bar that corresponds to the first HTF bar.
    // It might be more efficient to update only at the beginning of the chart bar, but a) it didn't seem to work, and b) even if I could get it working, I suspect it might be less robust.
    if _isChartTimeframe or timeframe.change(_HTF)
        array.unshift(_arraySource,_source[_offset])  // Add the source value to index 0 and shift other indexes along. It's important to keep the order so that it's simple to look up values.
        if array.size(_arraySource) > _arrayHistory
            array.pop(_arraySource)  // Remove the oldest value, keeping the array the same length.

    bool _myBool = array.get(_arraySource,_HTF_offset)  // Return the one value requested.
    _myBool

bool HTF_BoolValue = f_HTF_Bool(in_HTF, HTF_IsUpCandle, in_historyLength, in_HTF_Offset, in_useLiveDataOnChartTF)  // Call the function and use the offset to get a certain value


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//                             BOOL FUNCTION 2: GET AN ARRAY OF VALUES                            |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|

export f_HTF_ArrayBool(string _HTF="", bool _source, int _arrayHistory, bool _useLiveDataOnChartTF=true, int _startIn=0, int _endIn=-1) =>

    var string _errorTextFunctionName = "f_HTF_ArrayBool", var string errorTextLibraryName = "SimpleCryptoLife/HighTimeframeSampling"
    // For timeframe problems we throw an error because we can't fix them from here.
    if timeframe.in_seconds(_HTF) < timeframe.in_seconds(timeframe.period)
        runtime.error("Input timeframe cannot be lower than the chart timeframe [Error 020:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")
    // For a negative or zero array length we throw errors because we can't guess what the user wants this particular value to be.
    if _arrayHistory < 1
        runtime.error("The number of stored values configured must be more than zero." + " [Error 021:" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")        
    var bool[] _arraySource = array.new_bool(_arrayHistory,na)
    bool _isChartTimeframe = _HTF=="" or _HTF == timeframe.period
    var int _offset = _isChartTimeframe and _useLiveDataOnChartTF==true ? 0 : 1
    if _isChartTimeframe or timeframe.change(_HTF)
        array.unshift(_arraySource,_source[_offset])
        if _arrayHistory > array.size(_arraySource)
            array.pop(_arraySource)
    // Fix all slice start and end problems silently, erring towards returning more of the array.
    int _start = (_startIn < 0) or (_endIn < 0) ? 0 : _startIn >= _arrayHistory ? 0 : _endIn < _startIn ? 0 : _startIn
    int _end = (_startIn < 0) or (_endIn < 0) ? _arrayHistory : _endIn < _startIn ? _arrayHistory : _endIn < _startIn ? _arrayHistory : _endIn > _arrayHistory ? _arrayHistory : _endIn
    bool[] _arraySubset = array.slice(_arraySource,_start,_end)
    _arraySubset

bool[] HTF_ArrayBool = f_HTF_ArrayBool(in_HTF, HTF_IsUpCandle, in_historyLength, in_useLiveDataOnChartTF, in_start, in_end)


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//                                            PLOTS                                                |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|

plot(in_show_HTF_Close ? HTF_Close : na, color=color.new(color.yellow,40), linewidth=14, title="  Raw request.security()[0] float data")
plotchar(in_show_HTF_BoolRaw, char="B", color=(HTF_IsUpCandle ? color.green : not HTF_IsUpCandle ? color.red : color.yellow), location=location.top, size=size.small, title="  Raw request.security()[0] bool data")

plot(in_show_f_HTF_Float ? HTF_FloatValue : na, style=plot.style_circles, title="  😍 f_HTF_Float()", color=color.new(color.orange,30), linewidth=12)

plotchar(timeframe.change(in_HTF), "timeframe.change(in_HTF)", "|", location.top, color=barstate.ishistory ? color.gray : color.white, size = size.normal)  // Show when a new HTF bar opens

// Show the bool value plus array in a label
string labelTextBool = in_show_f_HTF_Bool ? "Bool value [" + str.tostring(in_HTF_Offset) + "]:  ": na
labelTextBool += in_show_f_HTF_Bool and HTF_BoolValue ? "True" : in_show_f_HTF_Bool and HTF_BoolValue == false ? "False" : in_show_f_HTF_Bool ? "na" : na
labelTextBool += in_show_f_HTF_ArrayBoolLabel ? "\nBool array:  " + str.tostring(HTF_ArrayBool) : na
SCL_Label.labelLast(_condition=in_show_f_HTF_Bool or in_show_f_HTF_ArrayBoolLabel, _style=label.style_label_down, _text=labelTextBool, _offset=0, _y=high, _textAlign=text.align_left)

// Show the float array, plus highest/lowest values, in a label
string labelTextHighestLowestFloat = in_show_f_HTF_ArrayFloatLabel ? "Float Array:  " + str.tostring(HTF_ArrayFloat) : na
string labelTextHighest = in_show_f_HTF_HighestFloat ? "\nHighest Value in last " + str.tostring(in_range) + " bars: " + str.tostring(HTF_Highest) : na
labelTextHighestLowestFloat += labelTextHighest
string labelTextLowest = in_show_f_HTF_LowestFloat ? "\nLowest Value in last " + str.tostring(in_range) + " bars: " + str.tostring(HTF_Lowest) : na
labelTextHighestLowestFloat += labelTextLowest
SCL_Label.labelLast(_condition=in_show_f_HTF_HighestFloat or in_show_f_HTF_LowestFloat or in_show_f_HTF_ArrayFloatLabel, _style=label.style_label_up, _text=labelTextHighestLowestFloat, _offset=0, _y=low, _textAlign=text.align_left)

// Show the matrix contents in a table
if in_show_f_HTF_MatrixFloat
    var t = table.new(position.top_right, 2, 2, color.green)
    table.cell(t, 0, 0, "f_HTF_MatrixFloat\nOHLC:")
    table.cell(t, 0, 1, str.tostring(HTF_Matrix))


// ======================================================= //
//                                                         //
//   (╯°□°）╯︵ ǝlʇoʇsᴉɹ∀ - sɹɐǝʎ ʇou `spǝǝp uᴉ ǝʌᴉl ǝM    //
//                                                         //
// ======================================================= //