The Divergent Library
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
//
// © WhiteboxSoftware
//
// @version=5
//
// @description The Divergent Library contains functions that helps the development of indicators / strategies which use The Divergent indicator's Divergence Signal as an external source.

library("TheDivergentLibrary", overlay = true)

// Constants

// 04 09 22 05 18 07 05 14 03 05
// D  I  V  E  R  G  E  N  C  E
var int DIVERGENCE_MAGIC_NUMBER = 4922518751435

var int EXPECTED_SIGNAL_LENGTH = 13

var int CONFIG_REGULAR_BULLISH_ENABLED = 0
var int CONFIG_HIDDEN_BULLISH_ENABLED = 1
var int CONFIG_REGULAR_BEARISH_ENABLED = 2
var int CONFIG_HIDDEN_BEARISH_ENABLED = 3
var int CONFIG_PIVOT_DETECTION_SOURCE = 4
var int CONFIG_PIVOT_DETECTION_MODE = 5

// Functions

decimalToBitArray(int num) =>
    int n = num
    bool[] bits = array.new_bool()
    while (n > 0)
        array.push(bits, n % 2 == 1)
        n := int(n / 2)

    array.reverse(bits)

    bits

parseConfigFromSignal(float signal) =>
    string[] signalParts = str.split(str.tostring(signal), ".")
    int encodedConfig = int(str.tonumber(array.get(signalParts, 1)))

    decimalToBitArray(encodedConfig)

getConfig(bool[] context, int id) =>
    int index = array.size(context) - id - 1
    if index > 0
        array.get(context, index)
    else
        false

// @function Returns a boolean value indicating whether Regular Bullish divergence detection is enabled in The Divergent.
//
// @param context The context of The Divergent Library.
//
// @returns A boolean value indicating whether Regular Bullish divergence detection is enabled in The Divergent.
export isRegularBullishEnabled(bool[] context) =>
    getConfig(context, CONFIG_REGULAR_BULLISH_ENABLED)

// @function Returns a boolean value indicating whether Hidden Bullish divergence detection is enabled in The Divergent.
//
// @param context The context of The Divergent Library.
//
// @returns A boolean value indicating whether Hidden Bullish divergence detection is enabled in The Divergent.
export isHiddenBullishEnabled(bool[] context) =>
    getConfig(context, CONFIG_HIDDEN_BULLISH_ENABLED)

// @function Returns a boolean value indicating whether Regular Bearish divergence detection is enabled in The Divergent.
//
// @param context The context of The Divergent Library.
//
// @returns A boolean value indicating whether Regular Bearish divergence detection is enabled in The Divergent.
export isRegularBearishEnabled(bool[] context) =>
    getConfig(context, CONFIG_REGULAR_BEARISH_ENABLED)

// @function Returns a boolean value indicating whether Hidden Bearish divergence detection is enabled in The Divergent.
//
// @param context The context of The Divergent Library.
//
// @returns A boolean value indicating whether Hidden Bearish divergence detection is enabled in The Divergent.
export isHiddenBearishEnabled(bool[] context) =>
    getConfig(context, CONFIG_HIDDEN_BEARISH_ENABLED)

// @function Returns the 'Pivot Detection Source' setting of The Divergent. The returned value can be either "Oscillator" or "Price".
//
// @param context The context of The Divergent Library.
//
// @returns One of the following string values: "Oscillator" or "Price".
export getPivotDetectionSource(bool[] context) =>
    getConfig(context, CONFIG_PIVOT_DETECTION_SOURCE) ? "Price" : "Oscillator"

// @function Returns the 'Pivot Detection Mode' setting of The Divergent. The returned value can be either "Bodies" or "Wicks".
//
// @param context The context of The Divergent Library.
//
// @returns One of the following string values: "Bodies" or "Wicks".
export getPivotDetectionMode(bool[] context) =>
    getConfig(context, CONFIG_PIVOT_DETECTION_MODE) ? "Wicks" : "Bodies"

debugConfig(bool[] context) =>
    str.format(
      "Regular Bullish enabled = {0}\nHidden Bullish Enabled = {1}\nRegular Bearish Enabled = {2}\nHidden Bearish Enabled = {3}\nPivot Detection Source = {4}\nPivot Detection Mode = {5}",
      str.format("{0}", isRegularBullishEnabled(context)),
      str.format("{0}", isHiddenBullishEnabled(context)),
      str.format("{0}", isRegularBearishEnabled(context)),
      str.format("{0}", isHiddenBearishEnabled(context)),
      getPivotDetectionSource(context),
      getPivotDetectionMode(context)
      )

// @function Returns a boolean value indicating the link status to The Divergent indicator.
//
// @param context The context of The Divergent Library.
//
// @returns A boolean value indicating the link status to The Divergent indicator.
export isLinked(bool[] context) =>
    array.size(context) > 0 and array.get(context, 0) == true

createContext(bool linked, bool[] config) =>
    array.concat(array.from(linked), config)

printLinkStatus(bool[] context, bool debug) =>
    var table divLinkedTable = table.new(position.bottom_left, 1, 1, border_width = 5)
    var bool linked = isLinked(context)
    table.cell(divLinkedTable, 0, 0, " The Divergent: " + (linked ? "Linked" + (debug ? "\n\n" + debugConfig(context) : "") : "Not Linked"), text_color = color.white, bgcolor = linked ? color.new(color.green, 60) : color.red, text_halign = text.align_left)

// @function Initialises The Divergent Library's context with the signal produced by The Divergent on the first bar. The value returned from this function is called the "context of The Divergent Library". Some of the other functions of this library requires you to pass in this context.
//
// @param firstBarSignal The signal from The Divergent indicator on the first bar.
// @param displayLinkStatus A boolean value indicating whether the Link Status window should be displayed in the bottom left corner of the chart. Defaults to true.
// @param debug A boolean value indicating whether the Link Status window should display debug information. Defaults to false.
//
// @returns A bool[] array containing the context of The Divergent Library.
export init(float firstBarSignal, bool displayLinkStatus = true, bool debug = false) =>
    bool[] context = if barstate.isfirst and int(firstBarSignal) == DIVERGENCE_MAGIC_NUMBER
        createContext(true, parseConfigFromSignal(firstBarSignal))
    else
        createContext(false, array.new_bool())

    if displayLinkStatus
        printLinkStatus(context, debug)

    context

// @function Processes a signal from The Divergent and returns a 5-tuple with the decoded signal: [int divergenceType, int priceBarIndexStart, int priceBarIndexEnd, int oscillatorBarIndexStart, int oscillatorBarIndexEnd]. `divergenceType` can be one of the following values: na → No divergence was detected, 1 → Regular Bullish, 2 → Regular Bullish early, 3 → Hidden Bullish, 4 → Hidden Bullish early, 5 → Regular Bearish, 6 → Regular Bearish early, 7 → Hidden Bearish, 8 → Hidden Bearish early.
//
// @param signal The signal from The Divergent indicator.
//
// @returns A 5-tuple with the following values: [int divergenceType, int priceBarIndexStart, int priceBarIndexEnd, int oscillatorBarIndexStart, int oscillatorBarIndexEnd].
export processSignal(float signal) =>
    int length = not na(signal) ? int(math.log10(signal)) + 1 : 0

    if not barstate.isfirst and length == EXPECTED_SIGNAL_LENGTH
        string[] chars = str.split(str.tostring(signal), "")

        int type = int(str.tonumber(array.get(chars, 0)))
        int divPriceBarIndexStartOffset = int(str.tonumber(array.get(chars, 1) + array.get(chars, 2) + array.get(chars, 3)))
        int divPriceBarIndexEndOffset = int(str.tonumber(array.get(chars, 4) + array.get(chars, 5) + array.get(chars, 6)))
        int divOscBarIndexStartOffset = int(str.tonumber(array.get(chars, 7) + array.get(chars, 8) + array.get(chars, 9)))
        int divOscBarIndexEndOffset = int(str.tonumber(array.get(chars, 10) + array.get(chars, 11) + array.get(chars, 12)))

        [
          type,
          bar_index - divPriceBarIndexStartOffset,
          bar_index - divPriceBarIndexEndOffset,
          bar_index - divOscBarIndexStartOffset,
          bar_index - divOscBarIndexEndOffset
          ]
    else
        [na, na, na, na, na]

// Usage instructions

var string usageInstructions = "Usage instructions\n"
  + "================\n\n"
  + "1. Add The Divergent indicator to your chart\n\n"
  + "2. Create a script in which you import the library:\n\n"
  + " import WhiteboxSoftware/TheDivergentLibrary/1 as tdl\n\n"
  + "3. Add a source input to your script:\n\n"
  + " float divSignal = input.source(title = \"The Divergent Link\", defval = close)\n\n"
  + " Unfortunately there is no way to default the source to 'The Divergent: Divergence Signal',\n"
  + " so you will have to select 'The Divergent: Divergence Signal' as the source every time you\n"
  + " add the script to your chart (and every time you make changes to it).\n\n"
  + "4. Initialise the library (it is important to use `var` to avoid initialising on every bar!):\n\n"
  + " var bool[] context = tdl.init(divSignal)\n\n"
  + "5. Call the processSignal function on every bar to check whether a divergence was detected:\n\n"
  + " [divergence, priceStart, priceEnd, oscStart, oscEnd] = tdl.processSignal(divSignal)\n\n"
  + " // The returned `divergence` variable will have one of the following (int) values:\n"
  + " //\n"
  + " // na → No divergence was detected\n"
  + " // 1 → Regular Bullish\n"
  + " // 2 → Regular Bullish early\n"
  + " // 3 → Hidden Bullish\n"
  + " // 4 → Hidden Bullish early\n"
  + " // 5 → Regular Bearish\n"
  + " // 6 → Regular Bearish early\n"
  + " // 7 → Hidden Bearish\n"
  + " // 8 → Hidden Bearish early\n\n"
  + "6. Use the divergence as part of your entry condition:\n\n"
  + " if barstate.isconfirmed and priceAboveEma200 and ta.barssince(divergence == 1) < 5\n"
  + "  strategy.entry(...)"

var table usageInstructionsTable = table.new(position.middle_center, 1, 1, border_width = 3, frame_width = 10, bgcolor = color.new(#1e222d, 0), frame_color = color.new(#1e222d, 0))
table.cell(usageInstructionsTable, 0, 0, usageInstructions, text_color = color.new(#b2b5be, 0), text_size = size.normal, text_halign = text.align_left)
