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

//@version=5

// @description Kage's Miscelaneous library
library("libKageMisc", overlay = true)


/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//#region Debug

// @function     Print a numerical value in a label at last historical bar.
// @param _value (float) The value to be printed.
// @returns      Nothing. 
export print(float _value)  => 
    if barstate.islastconfirmedhistory
        label.new(x     = bar_index + 2,
                  y     = hl2, 
                  style = label.style_label_upper_left,
                  color = color.silver,
                  size  = size.large,
                  text  = str.tostring(_value)
                 )

//#endregion Debug


/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//#region Bars

// @function     Get the number of bars we have to go back to get data from a specific date.
// @param _year  (int) Year of the specific date.
// @param _month (int) Month of the specific date. Optional. Default = 1.
// @param _day   (int) Day of the specific date. Optional. Default = 1.
// @returns      (int) Number of bars to go back until reach the specific date.
export barsBackToDate(int _year, int _month = 1, int _day = 1) =>
    dayStamp = timestamp(_year, _month, _day)
    int barsBack = (time - dayStamp) / (1000 * timeframe.in_seconds(timeframe.period)) + 1

// @function     Calculates the size of the bar's body.
// @param _index (simple int) The historical index of the bar. Optional. Default = 0.
// @returns      (float) The size of the bar's body in price units.
export bodySize(simple int _index = 0) =>
    float bodySize = math.abs(close[_index] - open[_index])

// @function     Calculates the size of the bar with both shadows.
// @param _index (simple int) The historical index of the bar. Optional. Default = 0.
// @returns      (float) The size of the bar in price units.
export barSize(simple int _index = 0) =>
    float barSize = high[_index] - low[_index]

// @function     Proportion of chosen bar size to the body size
// @param _index (simple int) The historical index of the bar. Optional. Default = 0.
// @returns      (float) Percent ratio of the body size per close price.
export bodyBarRatio(simple int _index = 0) =>
    float bodyBarRatio = bodySize(_index) * 100.0 / barSize(_index)

// @function     Proportion of chosen bar body size to the close price
// @param _index (simple int) The historical index of the bar. Optional. Default = 0.
// @returns      (float) Percent ratio of the body size per close price.
export bodyCloseRatio(simple int _index = 0) =>
    float bodyCloseRatio = bodySize(_index) * 100.0 / close[_index]

// @function      A stochastic meassure for the the body size
// @param _index  (simple int) The historical index of the bar. Optional. Default = 0.
// @param _length (simple int) How many bars look back to stochastic calculus. Default = 144.
// @returns       (float) Percent ratio of the body stochastic measure.
export bodyStoch(simple int _index = 0, simple int _length = 144) =>
    float bodySize = math.abs(close - open)
    float bodyMin  = ta.lowest(bodySize,  _length + _index)
    float bodyMax  = ta.highest(bodySize, _length + _index)
    float bodyStoch = 100.0 * (bodySize(_index) - bodyMin) / (bodyMax - bodyMin)

// @function         Size of the current bar shadow. Either "top" or "bottom".
// @param _direction (string) Direction of the desired shadow. 
// @returns          (float) The size of the chosen bar's shadow in price units.
export shadowSize(string _direction) =>
    float thisShadowSize = 0.00
    if _direction == "top"
        //if green bar
        if close > open
            thisShadowSize := high - close
        //if red...
        else
            thisShadowSize := high - open
    else if _direction == "bottom"
        //if green bar
        if close > open
            thisShadowSize := open - low
        //if red...
        else
            thisShadowSize := close - low
    else
        thisShadowSize := float(na)
    thisShadowSize

// @function         Proportion of current bar shadow to the bar size
// @param _direction (string) Direction of the desired shadow. 
// @returns          (float) Ratio of the shadow size per body size.
export shadowBodyRatio(string _direction) =>
    float thisShadowBodyRatio = shadowSize(_direction) / bodySize()

//#endregion Bars


/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//#region Dates

// @function     Returns the last day of a month.
// @param _month (int) Month number.
// @returns      (int) The number (28, 30 or 31) of the last day of a given month.
export lastDayOfMonth(int _month) =>
    int last = switch _month 
        2  => 28
        4  => 30
        6  => 30
        9  => 30
        11 => 30
        => 31

// @function     Return the short name of a month.
// @param _month (int)    Month number.
// @returns      (string) The short name ("Jan", "Feb"...) of a given month.
export nameOfMonth(int _month) =>
    string name = switch _month
        1  => "Jan"
        2  => "Feb"
        3  => "Mar"
        4  => "Apr"
        5  => "May"
        6  => "Jun"
        7  => "Jul"
        8  => "Aug"
        9  => "Sep"
        10 => "Oct"
        11 => "Nov"
        12 => "Dec"
        => "ERROR"

//#endregion Dates


/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//#region Math

// @function            Calculate Profit/Loss between two values.
// @param _initialValue (float) Initial value.
// @param _finalValue   (float) Final value = Initial value + delta.
// @returns             (float) Profit/Loss as a percentual change.
export pl(float _initialValue, float _finalValue) =>
    float percentPL = 100.00 * (_finalValue - _initialValue) / _initialValue

// @function Generalist Moving Average (GMA).
// @param _Type   (string) Type of average to be used. Either "EMA", "HMA", "RMA", "SMA", "SWMA", "WMA" or "VWMA".
// @param _Source (series float) Series of values to process.
// @param _Length (simple int) Number of bars (length).
// @returns       (float) The value of the chosen moving average.
export gma(string _Type, series float _Source, simple int _Length) =>
    ema  = ta.ema(_Source, _Length)
    hma  = ta.hma(_Source, _Length)
    rma  = ta.rma(_Source, _Length)
    sma  = ta.sma(_Source, _Length)
    swma = ta.swma(_Source)
    wma  = ta.wma(_Source, _Length)
    vwma = ta.vwma(_Source, _Length)

    float returnMA = switch _Type 
        "EMA" =>
            ema
        "HMA" =>
            hma
        "RMA" =>
            rma
        "SMA" =>
            sma
        "SWMA" =>
            swma
        "WMA" =>
            wma
        "VWMA"=>
            vwma
    returnMA

//#endregion Math


/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//#region Text

// @function            Transform a percentual value in a X Factor value.
// @param _percentValue (float)  Percentual value to be transformed.
// @param _minXFactor   (float)  Minimum X Factor to that the conversion occurs. Optional. Default = 10.
// @returns             (string) A formated string.
export xFormat(float _percentValue, float _minXFactor = 10.00) =>
    float  xFactor      = 1.00 + ( _percentValue / 100.00 )
    string returnValue  = ""
    if xFactor >= _minXFactor
        returnValue := str.tostring(xFactor, "###,##0.0X")
    else if xFactor < _minXFactor
        returnValue := str.tostring(_percentValue, "###,##0.0")  + "%"

//#endregion Text


/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//#region Strategy

// @function Check if the open trade direction is long.
// @returns  (bool) True if the open position is long.
export isLong() =>
    strategy.position_size > 0

// @function Check if the open trade direction is short.
// @returns  (bool) True if the open position is short.
export isShort() =>
    strategy.position_size < 0

// @function Returns the entry price of the last openned trade.
// @returns  (float) The last entry price.
export lastPrice() =>
    strategy.opentrades.entry_price(strategy.opentrades - 1)

// @function Returns the number of bars since last trade was oppened.
// @returns  (series int) 
export barsSinceLastEntry() =>
    strategy.opentrades > 0 ? bar_index - strategy.opentrades.entry_bar_index(strategy.opentrades - 1) : na

// @function          Returns the number of bars since the chosen trade's entry
// @param _tradeIndex (int) Trade index from wich the information is wanted
// @param _type       (string) Type of trade. "O" for open trade or "C" for closed trade. Optional. Default is "O"
// @returns           (series int) Number of bars
export barsSinceTradeEntry(int _tradeIndex = 0, string _type = "O") =>
    int barsSinceTradeEntry = switch _type
        "O" => strategy.opentrades   > 0 ? bar_index - strategy.opentrades.entry_bar_index(_tradeIndex)   : int(na)
        "C" => strategy.closedtrades > 0 ? bar_index - strategy.closedtrades.entry_bar_index(_tradeIndex) : int(na)
        => int(na)

//#endregion Strategy


/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//#region Bot

//Bot strings
BOT_NAME_FROSTY = "FrostyBot"
BOT_NAME_ZIG    = "Zignaly"

// @function Return the name of the FrostyBot Bot.
// @returns  (string) A string containing the name.
export getBotNameFrosty() =>
    BOT_NAME_FROSTY

// @function Return the name of the FrostyBot Bot.
// @returns  (string) A string containing the name.
export getBotNameZig() =>
    BOT_NAME_ZIG 

//#endregion Bot


/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//#region Symbol


// @function             Converts currency value to ticks
// @param _currencyValue (float) Value to be converted.
// @returns              (float) Value converted to minticks.
export getTicksValue(float _currencyValue) =>
    float ticksValue = _currencyValue / ( syminfo.mintick * syminfo.pointvalue ) // math.round_to_mintick() ??? 

// @function               Formats the symbol string to be used with a bot
// @param _botName         (string) Bot name constant. Either BOT_NAME_FROSTY or BOT_NAME_ZIG. Optional. Default is empty string.
// @param _botCustomSymbol (string) Custom string. Optional. Default is empy string.
// @returns                (string) A string containing the symbol for the bot. If all arguments are empty, the current symbol is returned in Binance format.
export getSymbol(string _botName = "", string _botCustomSymbol = "") =>
    string returnSymbol = _botCustomSymbol
    if returnSymbol == ""
        returnSymbol := switch _botName
            BOT_NAME_FROSTY => str.upper(syminfo.basecurrency) + "/" + str.upper(syminfo.currency)
            BOT_NAME_ZIG    => str.lower(syminfo.basecurrency) + str.lower(syminfo.currency)
            => str.upper(syminfo.basecurrency) + str.upper(syminfo.currency)
    returnSymbol

//#endregion Symbol


/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//#region Info

// @function Calculates and shows a board of Profit/Loss through the years.
// @returns  Nothing.
export showProfitLossBoard() =>

    ///////////////////////////////////////////////////////
    //#region Bar on bar

    int MATRIX_COLS = 14

    var int   currentYear = 0
    var float yEquity     = strategy.equity
    var       mPL         = matrix.new<float>(0, MATRIX_COLS)

    //Populate de matrix as long as the historical bars are processed
    if strategy.closedtrades + strategy.opentrades > 0
        if year > currentYear
            matrix.add_row(mPL)
            currentYear := year
            matrix.set(mPL, matrix.rows(mPL) - 1, 0, currentYear)
            yEquity     := strategy.equity

        lastMonthDay = lastDayOfMonth(month)
        if dayofmonth == lastMonthDay
            barsBack = barsBackToDate(year, month, 1)
            calcMPL  = pl(strategy.equity[barsBack], strategy.equity) 
            matrix.set(mPL, matrix.rows(mPL) - 1, month, calcMPL)
            calcYPL  = pl(yEquity, strategy.equity)
            matrix.set(mPL, matrix.rows(mPL) - 1, MATRIX_COLS -1, calcYPL)

    //#endregion Bar on bar

    ///////////////////////////////////////////////////////
    //#region Last Bar

    //Only populate the table on the last historiacal bar
    if barstate.islastconfirmedhistory

        ///////////////////////////////////////////////////////
        //#region Constants
            
        //Period of data
        int FIRST_YEAR = int(matrix.get(mPL,                    0, 0))
        int LAST_YEAR  = int(matrix.get(mPL, matrix.rows(mPL) - 1, 0))

        //Table dimensions are based on the matrix dimensions
        int TABLE_COLS_HEADER = 1
        int TABLE_COLS_MONTHS = MATRIX_COLS - 2
        int TABLE_COLS_TOTAL  = MATRIX_COLS

        int TABLE_ROWS_HEADER = 2
        int TABLE_ROWS_FOOTER = 1
        int TABLE_ROWS_DETAIL = (LAST_YEAR - FIRST_YEAR + 1)
        int TABLE_ROWS_TOTAL  = TABLE_ROWS_HEADER + TABLE_ROWS_DETAIL + TABLE_ROWS_FOOTER
        
        int TABLE_ROW_FIRST_YEAR = TABLE_ROWS_HEADER
        int TABLE_ROW_LAST_YEAR  = TABLE_ROWS_HEADER + TABLE_ROWS_DETAIL - 1
        int TABLE_ROW_FOOTER     = TABLE_ROWS_TOTAL  - TABLE_ROWS_FOOTER

        //#endregion

        ///////////////////////////////////////////////////////
        //#region Variables and Calculations

        //Self explained
        currentRow = 0
        currentCol = 0

        //Last bar data
        lastBarDay  = dayofmonth(time)
        barsBack    = barsBackToDate(year, month, 1)
        calcLastMPL = pl(strategy.equity[barsBack], strategy.equity)
        matrix.set(mPL, matrix.rows(mPL) - 1, month, calcLastMPL)
        calcLastYPL = pl(yEquity, strategy.equity)
        matrix.set(mPL, matrix.rows(mPL) - 1, MATRIX_COLS -1, calcLastYPL)

        //#endregion

        var plBoard = table.new(position.bottom_center,
                               columns = TABLE_COLS_TOTAL,
                               rows    = TABLE_ROWS_TOTAL,
                               bgcolor = color.new(color.white, 95))
                    
        ///////////////////////////////////////////////////////
        //#region Header

        //Board title
        currentRow := 0
        currentCol := 0
        table.cell(table_id   = plBoard,
                 column      = currentCol, 
                 row         = currentRow, 
                 text        = "Monthly Profit/Loss", 
                 text_color  = color.white)
        table.merge_cells(plBoard, 0, 0, TABLE_COLS_TOTAL - 1, 0)

        //Months header line
        currentRow += 1
        for mCol = 1 to TABLE_COLS_MONTHS
            table.cell(table_id   = plBoard,
                     column      = mCol, 
                     row         = currentRow, 
                     text        = nameOfMonth(mCol),
                     text_color  = color.white)

        //Annual column reader
        currentRow := 1
        currentCol := TABLE_COLS_TOTAL - 1
        table.cell(table_id   = plBoard,
                 column      = currentCol, 
                 row         = currentRow, 
                 text        = "In the Year", 
                 text_color  = color.white)
                                
        //Footer header
        currentRow := TABLE_ROW_FOOTER
        currentCol := TABLE_COLS_TOTAL - 2
        table.cell(table_id   = plBoard,
                 column      = currentCol, 
                 row         = currentRow,
                 text        = "TOTAL", 
                 text_color  = color.white)                    
    
        //#endregion Header

        ///////////////////////////////////////////////////////
        //#region Data

        //yRow stands for both year and row
        for yRow = TABLE_ROW_FIRST_YEAR to TABLE_ROW_LAST_YEAR
            //Column of years
            yCol   = 0
            yValue = matrix.get(mPL, yRow - TABLE_ROWS_HEADER, yCol)
            yText  = str.tostring(yValue, "#")
            table.cell(table_id    = plBoard,
                     column      = yCol, 
                     row         = yRow, 
                     text        = yText, 
                     text_color  = color.white,
                     text_halign = text.align_right)
            
            //Columns of months: mCol stands for both month and column
            for mCol = 1 to TABLE_COLS_MONTHS
                calcMPL = matrix.get(mPL, yRow - TABLE_ROWS_HEADER, mCol)
                textMPL = xFormat(calcMPL)
                table.cell(table_id    = plBoard,
                         column      = mCol, 
                         row         = yRow, 
                         text        = textMPL, 
                         text_color  = calcMPL >= 0.00 ? color.green : (calcMPL < 0.00 ? color.red : na),
                         text_halign = text.align_right)
            
            //Column of annual P/Ls
            yCol := TABLE_COLS_TOTAL - 1
            calcYPL = matrix.get(mPL, yRow - TABLE_ROWS_HEADER, yCol)
            textYPL = xFormat(calcYPL)
            table.cell(table_id    = plBoard,
                     column      = yCol, 
                     row         = yRow, 
                     text        = textYPL, 
                     text_color  = calcYPL >= 0.00 ? color.green : (calcYPL < 0.00 ? color.red : na),
                     text_halign = text.align_right)

        //TOTAL data
        currentRow := TABLE_ROWS_TOTAL - 1
        currentCol := TABLE_COLS_TOTAL - 1
        calcTotal = pl(strategy.initial_capital, strategy.equity)
        textTotal = xFormat(calcTotal)
        table.cell(table_id    = plBoard,
                 column      = currentCol, 
                 row         = currentRow, 
                 text        = textTotal, 
                 text_color  = calcTotal >= 0.00 ? color.green : (calcTotal < 0.00 ? color.red : na),
                 text_halign = text.align_right)

        //#endregion Data

    //#endregion Last Bar


//#endregion Info


