-- Available @ https://fxcodebase.com/code/viewtopic.php?f=17&t=74315
-- ── Author ─────────────────────────────────────────────────────────────────────
-- Developed by: Mario Jemic
-- Email:        mario.jemic@gmail.com
-- Website:      https://mario-jemic.com
--
-- ── Support & Donations ────────────────────────────────────────────────────────
-- PayPal:        https://goo.gl/9Rj74e
-- Patreon:       https://tiny.cc/1ybwxz
-- BuyMeACoffee:  https://tiny.cc/bj7vxz
--
-- Crypto:
--  BTC : 16F5k43RXibTmna4np8bPVgmXM1CzjXFJJ
--  SOL : 3nh5rpUKopcYLNU4zGCdUFAkM3iRQq8VVUmuzVG6VDf2
--  ETH/BNB/USDT/XRP (ERC20 & BEP20): 0xe53aab6bc468a963a02d1319660ee60cf80fc8e7
--
-- ── Copyright ──────────────────────────────────────────────────────────────────
-- © 2025 Gehtsoft USA LLC — https://fxcodebase.com

-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- <https://www.gnu.org/licenses/>.
local vars = {};
function Init()
    indicator:name("Heikin Ashi RSI Oscillator");
    indicator:description("Heikin Ashi RSI Oscillator");
    indicator:requiredSource(core.Bar);
    indicator:type(core.Oscillator);
    vars["TT_HARSI"] = "Period for the RSI calculations used to generate the" .. "candles. This seperate from the RSI plot/histogram length.";
    indicator.parameters:addInteger("param1", "Length", vars["TT_HARSI"], 14, 1);
    vars["TT_PBIAS"] = "Smoothing feature for the OPEN of the HARSI candles." .. "\n\nIncreases bias toward the prior open value which can" .. " help provide better visualisation of trend strength." .. "\n\n** By changing the Open values, High and Low can also" .. " be distorted - however Close will remain unchanged.";
    indicator.parameters:addInteger("param2", "Open Smoothing", vars["TT_PBIAS"], 1, 1, 100);
    indicator.parameters:addColor("param3", "Colour Pallette  ", "", core.colors().Teal);
    indicator.parameters:addColor("param4", " ", "", core.colors().Red);
    indicator.parameters:addColor("param5", " ", "", core.colors().Gray);
    indicator.parameters:addString("param6", "Source", "", "ohlc4");
    indicator.parameters:addStringAlternative("param6", "Open", "", "open");
    indicator.parameters:addStringAlternative("param6", "High", "", "high");
    indicator.parameters:addStringAlternative("param6", "Low", "", "low");
    indicator.parameters:addStringAlternative("param6", "Close", "", "close");
    indicator.parameters:addStringAlternative("param6", "Median", "", "median");
    indicator.parameters:addStringAlternative("param6", "Typical", "", "typical");
    indicator.parameters:addStringAlternative("param6", "Weighted", "", "weighted");
    indicator.parameters:addStringAlternative("param6", "OHLC4", "", "ohlc4");
    indicator.parameters:addStringAlternative("param6", "HLCC4", "", "hlcc4");
    indicator.parameters:addInteger("param7", "Length", "", 7, 1);
    vars["TT_SMRSI"] = "This option smoothes the RSI in a manner similar to HA" .. " open, but uses the realtime rsi rather than the prior" .. " close value.";
    indicator.parameters:addBoolean("param8", "Smoothed Mode RSI?", vars["TT_SMRSI"], true);
    indicator.parameters:addBoolean("param9", "Show RSI Plot?", "", true);
    indicator.parameters:addBoolean("param10", "Show RSI Histogram?", "", true);
    vars["TT_STOCH"] = "Uses the RSI generated by the above settings, and as such" .. " will be affected by the smoothing option.";
    indicator.parameters:addBoolean("param11", "Show Stochastic? ", vars["TT_STOCH"], false);
    indicator.parameters:addBoolean("param12", "Ribbon?", "", true);
    indicator.parameters:addInteger("param13", "Smoothing K", "", 3, 1);
    indicator.parameters:addInteger("param14", "Smoothing D", "", 3, 1);
    indicator.parameters:addInteger("param15", "Stochastic Length", "", 14, 1);
    vars["TT_STFIT"] = "Adjusts the vertical scaling of the stochastic, can help" .. " to prevent distortion of other data in the channel." .. "\n\nHas no impact cross conditions.";
    indicator.parameters:addInteger("param16", "Stoch Scaling %", vars["TT_STFIT"], 80, 1, 100);
    indicator.parameters:addInteger("param17", "OB", "", 20, 1, 50);
    indicator.parameters:addInteger("param18", "OB Extreme", "", 30, 1, 50);
    indicator.parameters:addInteger("param19", "OS", "", (-20), (-50), (-1));
    indicator.parameters:addInteger("param20", "OS Extreme", "", (-30), (-50), (-1));
end

local source;
local plot1;
local plot2;
local plot3_open;
local plot3_high;
local plot3_low;
local plot3_close;
local plot4;
local plot5;
local plot6;
local plot7;
local plot8;
local plot9;
function Create_f_zrsi_i(_length)
    local local_vars = {};
    local_vars["RSI1_x"] = instance:addInternalStream(0, 0);
    local_vars["RSI1"] = core.indicators:create("RSI", local_vars["RSI1_x"], _length);
    return {
        Clear = function()
        end,
        GetValue = function(_source, period, mode)
            SafeSetFloat(local_vars["RSI1_x"], period, _source);
            local_vars["RSI1"]:update(mode);
            return SafeMinus(local_vars["RSI1"].DATA:tick(period), 50);
        end
    };
end
function Create_f_rsi_i_b(_length, _mode)
    local local_vars = {};
    local_vars["f_zrsiFunc2"] = Create_f_zrsi_i(_length);
    local_vars["_smoothed"] = instance:addInternalStream(0, 0);
    local firstCall = true;
    return {
        Clear = function()
            firstCall = true;
        end,
        GetValue = function(_source, period, mode)
            if firstCall then
                firstCall = false;
                local_vars["f_zrsiFunc2"].Clear();
                SafeSetFloat(local_vars["_smoothed"], period, nil);
            else
                SafeSetFloat(local_vars["_smoothed"], period, SafeGetFloat(local_vars["_smoothed"], period - 1));
            end
            local_vars["_zrsi"] = local_vars["f_zrsiFunc2"].GetValue(_source, period, mode);
            SafeSetFloat(local_vars["_smoothed"], period, Triary(SafeGetFloat(local_vars["_smoothed"], period - 1) == nil, local_vars["_zrsi"], SafeDivide((SafePlus(SafeGetFloat(local_vars["_smoothed"], period - 1), local_vars["_zrsi"])), 2)));
            return Triary(_mode, SafeGetFloat(local_vars["_smoothed"], period), local_vars["_zrsi"]);
        end
    };
end
function Create_f_zstoch_i_i_i(_length, _smooth, _scale)
    local local_vars = {};
    local_vars["__stochastic1_source"] = instance:addInternalStream(0, 0);
    local_vars["__stochastic1_high"] = instance:addInternalStream(0, 0);
    local_vars["__stochastic1_low"] = instance:addInternalStream(0, 0);
            local_vars["__stochastic1"] = CreateStochastic(local_vars["__stochastic1_source"], local_vars["__stochastic1_high"], local_vars["__stochastic1_low"], _length);
    local_vars["MVA1_source"] = instance:addInternalStream(0, 0);
    local_vars["MVA1"] = core.indicators:create("MVA", local_vars["MVA1_source"], _smooth);
    return {
        Clear = function()
        end,
        GetValue = function(_source, period, mode)
    SafeSetFloat(local_vars["__stochastic1_source"], period, _source);
    SafeSetFloat(local_vars["__stochastic1_high"], period, _source);
    SafeSetFloat(local_vars["__stochastic1_low"], period, _source);
            local_vars["_zstoch"] = SafeMinus(local_vars["__stochastic1"]:get(period), 50);
            SafeSetFloat(local_vars["MVA1_source"], period, local_vars["_zstoch"]);
            local_vars["MVA1"]:update(mode);
            local_vars["_smoothed"] = local_vars["MVA1"].DATA:tick(period);
            local_vars["_scaled"] = SafeMultiply((SafeDivide(local_vars["_smoothed"], 100)), _scale);
            return local_vars["_scaled"];
        end
    };
end
function Create_f_rsiHeikinAshi_i(_length)
    local local_vars = {};
    local_vars["f_zrsiFunc5"] = Create_f_zrsi_i(_length);
    local_vars["!_closeRSI_stream"] = instance:addInternalStream(0, 0);
    local_vars["f_zrsiFunc6"] = Create_f_zrsi_i(_length);
    local_vars["f_zrsiFunc7"] = Create_f_zrsi_i(_length);
    local_vars["_close"] = instance:addInternalStream(0, 0);
    local_vars["_open"] = instance:addInternalStream(0, 0);
    local firstCall = true;
    return {
        Clear = function()
            firstCall = true;
        end,
        GetValue = function(period, mode)
            if firstCall then
                firstCall = false;
                local_vars["f_zrsiFunc5"].Clear();
                local_vars["f_zrsiFunc6"].Clear();
                local_vars["f_zrsiFunc7"].Clear();
                SafeSetFloat(local_vars["_open"], period, nil);
            else
                SafeSetFloat(local_vars["_open"], period, SafeGetFloat(local_vars["_open"], period - 1));
            end
            local_vars["_closeRSI"] = local_vars["f_zrsiFunc5"].GetValue(source.close:tick(period), period, mode);
            SafeSetFloat(local_vars["!_closeRSI_stream"], period, local_vars["_closeRSI"]);
            local_vars["!_closeRSI_stream_index"] = 1;
            local_vars["_openRSI"] = Nz(SafeGetFloat(local_vars["!_closeRSI_stream"], local_vars["!_closeRSI_stream_index"]), local_vars["_closeRSI"]);
            local_vars["_highRSI_raw"] = local_vars["f_zrsiFunc6"].GetValue(source.high:tick(period), period, mode);
            local_vars["_lowRSI_raw"] = local_vars["f_zrsiFunc7"].GetValue(source.low:tick(period), period, mode);
            local_vars["_highRSI"] = SafeMax(local_vars["_highRSI_raw"], local_vars["_lowRSI_raw"]);
            local_vars["_lowRSI"] = SafeMin(local_vars["_highRSI_raw"], local_vars["_lowRSI_raw"]);
            SafeSetFloat(local_vars["_close"], period, SafeDivide((SafePlus(SafePlus(SafePlus(local_vars["_openRSI"], local_vars["_highRSI"]), local_vars["_lowRSI"]), local_vars["_closeRSI"])), 4));
            SafeSetFloat(local_vars["_open"], period, Triary(SafeGetFloat(local_vars["_open"], period - vars["i_smoothing"]) == nil, SafeDivide((SafePlus(local_vars["_openRSI"], local_vars["_closeRSI"])), 2), SafeDivide((SafePlus((SafeMultiply(SafeGetFloat(local_vars["_open"], period - 1), vars["i_smoothing"])), SafeGetFloat(local_vars["_close"], period - 1))), (vars["i_smoothing"] + 1))));
            local_vars["_high"] = SafeMax(local_vars["_highRSI"], SafeMax(SafeGetFloat(local_vars["_open"], period), SafeGetFloat(local_vars["_close"], period)));
            local_vars["_low"] = SafeMin(local_vars["_lowRSI"], SafeMin(SafeGetFloat(local_vars["_open"], period), SafeGetFloat(local_vars["_close"], period)));
            return SafeGetFloat(local_vars["_open"], period), local_vars["_high"], local_vars["_low"], SafeGetFloat(local_vars["_close"], period);
        end
    };
end
function Prepare(nameOnly)
    source = instance.source;
    local name = string.format("%s(%s)", profile:id(), source:name());
    instance:name(name);
    if nameOnly then
        return;
    end
    vars["i_lenHARSI"] = instance.parameters.param1;
    vars["i_smoothing"] = instance.parameters.param2;
    vars["i_colUp"] = instance.parameters.param3;
    vars["i_colDown"] = instance.parameters.param4;
    vars["i_colWick"] = instance.parameters.param5;
    vars["i_source"] = PineScriptUtils:CreateSource(source, instance.parameters.param6);
    vars["i_lenRSI"] = instance.parameters.param7;
    vars["i_mode"] = instance.parameters.param8;
    vars["i_showPlot"] = instance.parameters.param9;
    vars["i_showHist"] = instance.parameters.param10;
    vars["i_showStoch"] = instance.parameters.param11;
    vars["i_ribbon"] = instance.parameters.param12;
    vars["i_smoothK"] = instance.parameters.param13;
    vars["i_smoothD"] = instance.parameters.param14;
    vars["i_stochLen"] = instance.parameters.param15;
    vars["i_stochFit"] = instance.parameters.param16;
    vars["i_upper"] = instance.parameters.param17;
    vars["i_upperx"] = instance.parameters.param18;
    vars["i_lower"] = instance.parameters.param19;
    vars["i_lowerx"] = instance.parameters.param20;
    vars["f_rsiFunc1"] = Create_f_rsi_i_b(vars["i_lenRSI"], vars["i_mode"]);
    vars["f_zstochFunc3"] = Create_f_zstoch_i_i_i(vars["i_stochLen"], vars["i_smoothK"], vars["i_stochFit"]);
    vars["MVA2_source"] = instance:addInternalStream(0, 0);
    vars["MVA2"] = core.indicators:create("MVA", vars["MVA2_source"], vars["i_smoothD"]);
    vars["f_rsiHeikinAshiFunc4"] = Create_f_rsiHeikinAshi_i(vars["i_lenHARSI"]);
    plot1 = instance:addStream("plot1", core.Line, "", "", core.colors().Blue, 0, 0);
    plot1:setStyle(core.LINE_NONE);
    plot1:addLevel(vars["i_upperx"], core.LINE_SOLID, 1, core.colors().Silver + math.floor(60 / 100 * 255) * 16777216);
    vars["upperx"] = vars["i_upperx"];
    plot1:addLevel(vars["i_upper"], core.LINE_SOLID, 1, core.colors().Silver + math.floor(80 / 100 * 255) * 16777216);
    vars["upper"] = vars["i_upper"];
    plot1:addLevel(0, core.LINE_DOT, 1, core.colors().Orange);
    vars["median"] = 0;
    plot1:addLevel(vars["i_lower"], core.LINE_SOLID, 1, core.colors().Silver + math.floor(80 / 100 * 255) * 16777216);
    vars["lower"] = vars["i_lower"];
    plot1:addLevel(vars["i_lowerx"], core.LINE_SOLID, 1, core.colors().Silver + math.floor(60 / 100 * 255) * 16777216);
    vars["lowerx"] = vars["i_lowerx"];
    vars["channel1_from"] = instance:addInternalStream(0, 0);
    vars["channel1_to"] = instance:addInternalStream(0, 0);
    channel1_color = core.colors().Red + math.floor(90 / 100 * 255) * 16777216;
    instance:createChannelGroup("channel1", "channel1", vars["channel1_from"], vars["channel1_to"], Graphics:GetColor(channel1_color), channel1_color and 100 - Graphics:GetTransparencyPercent(channel1_color) or 100, true);
    vars["channel2_from"] = instance:addInternalStream(0, 0);
    vars["channel2_to"] = instance:addInternalStream(0, 0);
    channel2_color = core.colors().Blue + math.floor(90 / 100 * 255) * 16777216;
    instance:createChannelGroup("channel2", "channel2", vars["channel2_from"], vars["channel2_to"], Graphics:GetColor(channel2_color), channel2_color and 100 - Graphics:GetTransparencyPercent(channel2_color) or 100, true);
    vars["channel3_from"] = instance:addInternalStream(0, 0);
    vars["channel3_to"] = instance:addInternalStream(0, 0);
    channel3_color = core.colors().Green + math.floor(90 / 100 * 255) * 16777216;
    instance:createChannelGroup("channel3", "channel3", vars["channel3_from"], vars["channel3_to"], Graphics:GetColor(channel3_color), channel3_color and 100 - Graphics:GetTransparencyPercent(channel3_color) or 100, true);
    plot2 = instance:addStream("plot2", core.Bar, "RSI Histogram", "RSI Histogram", core.colors().Silver + math.floor(80 / 100 * 255) * 16777216, 0, 0);
    plot2:setWidth(1);
    plot3_open = instance:addStream("plot3_open", core.Line, "Open", "Open", core.colors().Black, 0, 0);
    plot3_high = instance:addStream("plot3_high", core.Line, "High", "High", core.colors().Black, 0, 0);
    plot3_low = instance:addStream("plot3_low", core.Line, "Low", "Low", core.colors().Black, 0, 0);
    plot3_close = instance:addStream("plot3_close", core.Line, "Close", "Close", core.colors().Black, 0, 0);
    instance:createCandleGroup("plot3", "plot3", plot3_open, plot3_high, plot3_low, plot3_close);
    vars["colShadow"] = core.rgb(0, 0, 0) + math.floor(20 / 100 * 255) * 16777216;
    plot4 = instance:addStream("plot4", core.Line, "RSI Shadow", "RSI Shadow", vars["colShadow"], 0, 0);
    plot4:setWidth(3);
    plot4:setStyle(core.LINE_SOLID);
    vars["colRSI"] = core.rgb(250, 200, 50) + math.floor(0 / 100 * 255) * 16777216;
    plot5 = instance:addStream("plot5", core.Line, "RSI Overlay", "RSI Overlay", vars["colRSI"], 0, 0);
    plot5:setWidth(1);
    plot5:setStyle(core.LINE_SOLID);
    vars["plot_rsi"] = plot5;
    plot6 = instance:addStream("plot6", core.Line, "Stoch K Shadow", "Stoch K Shadow", core.colors().Blue, 0, 0);
    plot6:setWidth(3);
    plot6:setStyle(core.LINE_SOLID);
    plot7 = instance:addStream("plot7", core.Line, "Stoch D Shadow", "Stoch D Shadow", core.colors().Blue, 0, 0);
    plot7:setWidth(3);
    plot7:setStyle(core.LINE_SOLID);
    plot8 = instance:addStream("plot8", core.Line, "Stoch K", "Stoch K", core.colors().Blue, 0, 0);
    plot8:setWidth(1);
    plot8:setStyle(core.LINE_SOLID);
    vars["plot_stochK"] = plot8;
    plot9 = instance:addStream("plot9", core.Line, "Stoch D", "Stoch D", core.colors().Blue, 0, 0);
    plot9:setWidth(1);
    plot9:setStyle(core.LINE_SOLID);
    vars["plot_stochD"] = plot9;
    channel4_color = core.colors().Blue;
    instance:createChannelGroup("channel4", "channel4", vars["plot_stochK"], vars["plot_stochD"], Graphics:GetColor(channel4_color), channel4_color and 100 - Graphics:GetTransparencyPercent(channel4_color) or 100, true);
end

function Update(period, mode)
    if period == 0 or mode == core.UpdateAll then
        vars["f_rsiFunc1"].Clear();
        vars["f_zstochFunc3"].Clear();
        vars["f_rsiHeikinAshiFunc4"].Clear();
    else
    end
    vars["TT_HARSI"] = "Period for the RSI calculations used to generate the" .. "candles. This seperate from the RSI plot/histogram length.";
    vars["TT_PBIAS"] = "Smoothing feature for the OPEN of the HARSI candles." .. "\n\nIncreases bias toward the prior open value which can" .. " help provide better visualisation of trend strength." .. "\n\n** By changing the Open values, High and Low can also" .. " be distorted - however Close will remain unchanged.";
    vars["TT_SMRSI"] = "This option smoothes the RSI in a manner similar to HA" .. " open, but uses the realtime rsi rather than the prior" .. " close value.";
    vars["TT_STOCH"] = "Uses the RSI generated by the above settings, and as such" .. " will be affected by the smoothing option.";
    vars["TT_STFIT"] = "Adjusts the vertical scaling of the stochastic, can help" .. " to prevent distortion of other data in the channel." .. "\n\nHas no impact cross conditions.";
    vars["GROUP_CAND"] = "Config » HARSI Candles";
    vars["INLINE_COL"] = "Colour Pallette";
    vars["GROUP_PLOT"] = "Config » RSI Plot";
    PineScriptUtils:UpdateSources(period, mode);
    vars["GROUP_STOCH"] = "Config » Stochastic RSI Plot";
    vars["INLINE_STDS"] = "Stoch Draw States";
    vars["GROUP_CHAN"] = "Config » OB/OS Boundaries";
    vars["RSI"] = vars["f_rsiFunc1"].GetValue(vars["i_source"]:tick(period), period, mode);
    vars["StochK"] = vars["f_zstochFunc3"].GetValue(vars["RSI"], period, mode);
    SafeSetFloat(vars["MVA2_source"], period, vars["StochK"]);
    vars["MVA2"]:update(mode);
    vars["StochD"] = vars["MVA2"].DATA:tick(period);
    O_ret_val, H_ret_val, L_ret_val, C_ret_val = vars["f_rsiHeikinAshiFunc4"].GetValue(period, mode);
    vars["O"] = O_ret_val;
    vars["H"] = H_ret_val;
    vars["L"] = L_ret_val;
    vars["C"] = C_ret_val;
    vars["bodyColour"] = Triary(SafeGreater(vars["C"], vars["O"]), vars["i_colUp"], vars["i_colDown"]);
    vars["wickColour"] = vars["i_colWick"];
    vars["colShadow"] = core.rgb(0, 0, 0) + math.floor(20 / 100 * 255) * 16777216;
    vars["colNone"] = core.rgb(0, 0, 0) + math.floor(100 / 100 * 255) * 16777216;
    vars["colRSI"] = core.rgb(250, 200, 50) + math.floor(0 / 100 * 255) * 16777216;
    vars["colStochK"] = core.rgb(0, 148, 255) + math.floor(0 / 100 * 255) * 16777216;
    vars["colStochD"] = core.rgb(255, 106, 0) + math.floor(0 / 100 * 255) * 16777216;
    vars["colStochFill"] = Triary(SafeGE(vars["StochK"], vars["StochD"]), vars["colStochK"] + math.floor(50 / 100 * 255) * 16777216, vars["colStochD"] + math.floor(50 / 100 * 255) * 16777216);
    SafeSetFloat(vars["channel1_from"], period, vars["upper"]);
    SafeSetFloat(vars["channel1_to"], period, vars["upperx"]);
    SafeSetFloat(vars["channel2_from"], period, vars["upper"]);
    SafeSetFloat(vars["channel2_to"], period, vars["lower"]);
    SafeSetFloat(vars["channel3_from"], period, vars["lower"]);
    SafeSetFloat(vars["channel3_to"], period, vars["lowerx"]);
    plot2[period] = Triary(vars["i_showHist"], vars["RSI"], nil);
    plot3_open_value = vars["O"];
    plot3_close_value = vars["C"];
    plot3_color = vars["bodyColour"];
    if plot3_color then
        plot3_open[period] = plot3_open_value;
        plot3_high[period] = vars["H"];
        plot3_low[period] = vars["L"];
        plot3_close[period] = plot3_close_value;
        plot3_open:setColor(period, plot3_color);
    else
        plot3_open:setNoData(period);
        plot3_high:setNoData(period);
        plot3_low:setNoData(period);
        plot3_close:setNoData(period);
    end
    plot4[period] = Triary(vars["i_showPlot"], vars["RSI"], nil);
    plot5[period] = Triary(vars["i_showPlot"], vars["RSI"], nil);
    plot6[period] = Triary(vars["i_showStoch"], vars["StochK"], nil);
    plot6:setColor(period, Triary(not (vars["i_ribbon"]), vars["colShadow"], vars["colNone"]));
    plot7[period] = Triary(vars["i_showStoch"], vars["StochD"], nil);
    plot7:setColor(period, Triary(not (vars["i_ribbon"]), vars["colShadow"], vars["colNone"]));
    plot8[period] = Triary(vars["i_showStoch"], vars["StochK"], nil);
    plot8:setColor(period, Triary(not (vars["i_ribbon"]), vars["colStochK"], vars["colNone"]));
    plot9[period] = Triary(vars["i_showStoch"], vars["StochD"], nil);
    plot9:setColor(period, Triary(not (vars["i_ribbon"]), vars["colStochD"], vars["colNone"]));
    vars["plot_stochK"]:setColor(period, Triary(vars["i_ribbon"], vars["colStochFill"], nil));
end

function AsyncOperationFinished(cookie, success, message, message1, message2)
end
PineScriptUtils = {};
PineScriptUtils.Sources = {};
function PineScriptUtils:CreateSource(source, sourceType)
    if sourceType ~= "ohlc4" then
        return source[sourceType];
    end
    local newSource = {};
    newSource.Stream = instance:addInternalStream(0, 0);
    function newSource:Update(period, mode)
        self.Stream[period] = (source.open[period] + source.high[period] + source.low[period] + source.close[period]) / 4;
    end
    self.Sources[#self.Sources + 1] = newSource;
    return newSource.Stream;
end
function PineScriptUtils:UpdateSources(period, mode)
    for i, src in ipairs(self.Sources) do
        src:Update(period, mode);
    end
end
function PineScriptUtils:TimeframeFromLength(length)
    if length == "t" then
        return "t1"
    elseif length == "D" then
        return "D1";
    elseif length == "W" then
        return "W1";
    elseif length == "M" then
        return "M1";
    end
    local length_number = tonumber(length);
    if length_number < 3600 then
        return "m" .. tostring(length_number / 60);
    end
    return "H" .. tostring(length_number / 3600)
end
function PineScriptUtils:ParseSession(session)
    local session_info = {};
    local _, _, from_hour, from_minute, to_hour, to_minute = string.find(session, "(%d%d)(%d%d)-(%d%d)(%d%d)");
    session_info.from_hour = from_hour and tonumber(from_hour) or 0;
    session_info.from_minute = from_minute and tonumber(from_minute) or 0;
    session_info.from = (session_info.from_hour * 60.0 + session_info.from_minute) * 60.0;
    session_info.to_hour = to_hour and tonumber(to_hour) or 23;
    session_info.to_minute = to_minute and tonumber(to_minute) or 59;
    session_info.to = (session_info.to_hour * 60.0 + session_info.to_minute) * 60.0;
    function session_info:IsInRange(time)
        time = math.floor(time * 86400 + 0.5);
        if self.from < self.to then
            return time >= self.from and time <= self.to;
        end
        if self.from > self.to then
            return time > self.from or time < self.to;
        end
    
        return time == self.from;
    end
    return session_info;
end
function PineScriptUtils:Time(period, timeframe_length, session, timezone)
    local timeframe = PineScriptUtils:TimeframeFromLength(timeframe_length);
    if PineScriptUtils.tradingWeekOffset == nil then
        PineScriptUtils.tradingWeekOffset = core.host:execute("getTradingWeekOffset");
        PineScriptUtils.tradingDayOffset = core.host:execute("getTradingDayOffset");
    end
    local s, e = core.getcandle(timeframe, instance.source:date(period), PineScriptUtils.tradingDayOffset, PineScriptUtils.tradingWeekOffset);
    local session_info = PineScriptUtils:ParseSession(session);
    if not session_info:IsInRange(s % 1) then
        return nil;
    end
    
    return s * 86400000;
end
function PineScriptUtils:WeekOfYear(time, timezone)
    local time_ole = time / 86400000;
    local date_table = core.dateToTable(time_ole)
    date_table.month = 1;
    date_table.day = 1;
    date_table.hour = 0;
    date_table.min = 0;
    date_table.sec = 0;
    local first_day_ole = core.tableToDate(date_table);
    date_table = core.dateToTable(first_day_ole);
    first_day_ole = first_day_ole - date_table.wday + 1;
    return math.floor(time_ole - first_day_ole / 7);
end

function Timestamp(year, month, day, hour, minute, second, tz)
    local date = {};
    date.month = month;
    date.day = day;
    date.year = year;
    date.hour = hour;
    date.min = minute;
    date.sec = second;
    return core.tableToDate(date);
end

function BarSizeInMS(barSize)
    local s, e = core.getcandle(barSize, core.now(), 0, 0)
    return (e - s) * 86400000;
end

function NumberToBool(n)
    return n ~= nil and n ~= 0;
end

function GetTrueRange(source, period)
    if period == 0 then
        return nil;
    end
    local num1 = math.abs(source.high[period] - source.low[period]);
    local num2 = math.abs(source.high[period] - source.close[period - 1]);
    local num3 = math.abs(source.close[period - 1] - source.low[period]);
    return math.max(num1, num2, num3);
end
function SafeMinus(left, right)
    if left == nil or right == nil then
        return nil;
    end
    return left - right;
end
function SafeMultiply(left, right)
    if left == nil or right == nil then
        return nil;
    end
    return left * right;
end
function SafePlus(left, right)
    if left == nil or right == nil then
        return nil;
    end
    return left + right;
end
function SafeConcat(left, right)
    if left == nil then
        return right;
    end
    if right == nil then
        return left;
    end
    return left .. right;
end
function SafeDivide(left, right)
    if left == nil or right == nil or right == 0 then
        return nil;
    end
    return left / right;
end
function SafeGreater(left, right)
    if left == nil or right == nil then
        return nil;
    end
    return left > right;
end
function SafeGE(left, right)
    if left == nil or right == nil then
        return nil;
    end
    return left >= right;
end
function SafeLess(left, right)
    if left == nil or right == nil then
        return nil;
    end
    return left < right;
end
function SafeLE(left, right)
    if left == nil or right == nil then
        return nil;
    end
    return left <= right;
end
function SafeMax(left, right)
    if left == nil or right == nil then
        return nil;
    end
    return math.max(left, right);
end
function SafeMin(left, right)
    if left == nil or right == nil then
        return nil;
    end
    return math.min(left, right);
end
function SafeAbs(value)
    if value == nil then
        return nil;
    end
    return math.abs(value);
end
function SafeNegative(left)
    if left == nil then
        return nil;
    end
    return -left;
end
function SafeSetBool(stream, period, value)
    if period == nil then
        return;
    end
    if value == nil then
        stream:setNoData(period);
        return;
    end
    stream[period] = value and 1 or 0;
end
function SafeGetBool(stream, period)
    if stream == nil or period == nil or not stream:hasData(period) then
        return nil;
    end
    return stream[period] == 1;
end
function SafeSetFloat(stream, period, value)
    if period == nil then
        return;
    end
    if value == nil then
        stream:setNoData(period);
        return;
    end
    stream[period] = value;
end
function SafeGetFloat(stream, period)
    if stream == nil or period == nil then
        return nil;
    end
    if not stream:hasData(period) then
        return nil;
    end
    return stream[period];
end
function Float(number)
    return number and number or nil;
end
function Int(number)
    return number and number or nil;
end
function Color(color)
    return color and color or nil;
end
function ToLine(line)
    return line;
end
function ToBox(box)
    return box;
end
function Round(num, idp)
    if num == nil then
        return nil;
    end
    if idp and idp > 0 then
        local mult = 10 ^ idp
        return math.floor(num * mult + 0.5) / mult
    end
    return math.floor(num + 0.5)
end
function Nz(value, defaultValue)
    if defaultValue == nil then
        defaultValue = 0;
    end
    return value and value or defaultValue;
end
function Triary(condition, trueValue, falseValue)
    if condition == nil or condition == false then
        return falseValue;
    end
    return trueValue;
end
function SafeCrossesUnder(val1, val2, period)
    if val1 == nil or val2 == nil or period < 2 then
        return false;
    end
    return core.crossesUnder(val1, val2, period);
end
function SafeCrossesOver(val1, val2, period)
    if val1 == nil or val2 == nil or period < 2 then
        return false;
    end
    return core.crossesOver(val1, val2, period);
end
function SafeCrosses(val1, val2, period)
    if val1 == nil or val2 == nil or period < 2 then
        return false;
    end
    return core.crosses(val1, val2, period);
end
function SafeCos(val)
    if val == nil then
        return nil;
    end
    return math.cos(val);
end
function SafeSin(val)
    if val == nil then
        return nil;
    end
    return math.sin(val);
end
function SafeMathExMax(source, period, length)
    if source:size() < length then
        return nil;
    end
    return mathex.max(source, core.rangeTo(period, length));
end
function SafeMathExMin(source, period, length)
    if source:size() < length then
        return nil;
    end
    return mathex.min(source, core.rangeTo(period, length));
end
function SafeMathExStdev(source, period, length)
    if source:size() < length then
        return nil;
    end
    return mathex.stdev(source, core.rangeTo(period, length));
end
function CreateStochastic(source, high, low, length)
    local stoch = {};
    stoch.Source = source;
    stoch.High = high;
    stoch.Low = low;
    stoch.Length = length;
    function stoch:get(period)
        if period + self.Length >= self.Source:first() then
            return nil;
        end
        local low = mathex.min(self.Low, core.rangeTo(period, self.Length));
        local high = mathex.max(self.High, core.rangeTo(period, self.Length));
        if low == high then
            return 100;
        end
        return 100 * (self.Source[period] - low) / (high - low);
    end
    return stoch;
end
Graphics = {};
Graphics.NextId = 1;
Graphics.Pens = {};
Graphics.Brushes = {};
Graphics.Fonts = {};
function Graphics:FindPen(width, color, style, context)
    if color == nil then
        return -1;
    end
    for i, pen in ipairs(Graphics.Pens) do
        if pen.Width == width and pen.Color == color then
            context:createPen(pen.Id, context:convertPenStyle(style), width, color);
            return pen.Id;
        end
    end
    local newPen = {};
    newPen.Id = Graphics.NextId;
    newPen.Width = width;
    newPen.Color = color;

    context:createPen(newPen.Id, context:convertPenStyle(style), width, color);
    Graphics.NextId = Graphics.NextId + 1;
    Graphics.Pens[#Graphics.Pens + 1] = newPen;
    return newPen.Id;
end
function Graphics:FindBrush(color, context)
    if color == nil then
        return -1;
    end
    for i, brush in ipairs(Graphics.Brushes) do
        if brush.Color == color then
            context:createSolidBrush(brush.Id, color)
            return brush.Id;
        end
    end
    local newBrush = {};
    newBrush.Id = Graphics.NextId;
    newBrush.Color = color;
    context:createSolidBrush(newBrush.Id, color)
    Graphics.NextId = Graphics.NextId + 1;
    Graphics.Brushes[#Graphics.Brushes + 1] = newBrush;
    return newBrush.Id;
end
function Graphics:FindFont(font, xSize, ySize, corner, context)
    for i, font in ipairs(self.Fonts) do
        if font.xSize == xSize and font.Name == font then
            return font.Id;
        end
    end
    local newFont = {};
    newFont.Id = Graphics.NextId;
    newFont.xSize = xSize;
    newFont.Name = font;
    context:createFont(newFont.Id, font, 0, xSize, context.LEFT);
    Graphics.NextId = Graphics.NextId + 1;
    Graphics.Fonts[#Graphics.Fonts + 1] = newFont;
    return newFont.Id;
end
function Graphics:SplitColorAndTransparency(clr)
    if clr == nil then
        return nil, nil;
    end
    local transparency = (math.floor(clr / 16777216) % 255);
    local color = clr - transparency * 16777216;
    return color, transparency;
end
function Graphics:GetColor(clr)
    local color, transparency = self:SplitColorAndTransparency(clr);
    return color;
end
function Graphics:GetTransparency(clr)
    local color, transparency = self:SplitColorAndTransparency(clr);
    return transparency;
end
function Graphics:GetTransparencyPercent(clr)
    local color, transparency = self:SplitColorAndTransparency(clr);
    if transparency == nil then
        return nil;
    end
    return math.floor(transparency * 100.0 / 255.0 + 0.5);
end
function Graphics:AddTransparency(clr, transp)
    if clr == nil or transp == nil then
        return nil;
    end
    color, _ = Graphics:SplitColorAndTransparency(clr);
    return color + math.floor(transp / 100 * 255) * 16777216;
end
-- Available @ https://fxcodebase.com/code/viewtopic.php?f=17&t=74315
-- ── Author ─────────────────────────────────────────────────────────────────────
-- Developed by: Mario Jemic
-- Email:        mario.jemic@gmail.com
-- Website:      https://mario-jemic.com
--
-- ── Support & Donations ────────────────────────────────────────────────────────
-- PayPal:        https://goo.gl/9Rj74e
-- Patreon:       https://tiny.cc/1ybwxz
-- BuyMeACoffee:  https://tiny.cc/bj7vxz
--
-- Crypto:
--  BTC : 16F5k43RXibTmna4np8bPVgmXM1CzjXFJJ
--  SOL : 3nh5rpUKopcYLNU4zGCdUFAkM3iRQq8VVUmuzVG6VDf2
--  ETH/BNB/USDT/XRP (ERC20 & BEP20): 0xe53aab6bc468a963a02d1319660ee60cf80fc8e7
--
-- ── Copyright ──────────────────────────────────────────────────────────────────
-- © 2025 Gehtsoft USA LLC — https://fxcodebase.com

-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- <https://www.gnu.org/licenses/>.