-- More information about this indicator can be found at:
-- http://fxcodebase.com/code/viewtopic.php?f=31&t=64552
-- Id: 18048
--+------------------------------------------------------------------+
--|                               Copyright © 2018, Gehtsoft USA LLC |
--|                                            http://fxcodebase.com |
--+------------------------------------------------------------------+
--|                                      Developed by : Mario Jemic  |
--|                                          mario.jemic@gmail.com   |
--+------------------------------------------------------------------+
--|                                 Support our efforts by donating  |
--|                                 Patreon : https://goo.gl/GdXWeN  |
--|                                  Paypal : https://goo.gl/9Rj74e  |
--|                    BitCoin : 15VCJTLaz12Amr7adHSBtL9v8XomURo9RF  |
--|               BitCoin Cash : 1BEtS465S3Su438Kc58h2sqvVvHK9Mijtg  |
--|           Ethereum : 0x8C110cD61538fb6d7A2B47858F0c0AaBd663068D  |
--|                   LiteCoin : LLU8PSY2vsq7B9kRELLZQcKf5nJQrdeqwD  |
--+------------------------------------------------------------------+

function Init() --The strategy profile initialization
    strategy:name("ChandelierExit Indicator position Close Strategy ")
    strategy:description("")
    strategy:setTag("strategy_type", "Money management");

    strategy:setTag("NonOptimizableParameters", "Email,SendEmail,SoundFile,RecurrentSound,PlaySound, ShowAlert")

    strategy.parameters:addGroup("Price")
    strategy.parameters:addString("Type", "Price Type", "", "Bid")
    strategy.parameters:addStringAlternative("Type", "Bid", "", "Bid")
    strategy.parameters:addStringAlternative("Type", "Ask", "", "Ask")

    strategy.parameters:addString("TF", "Time frame", "", "m1")
    strategy.parameters:setFlag("TF", core.FLAG_PERIODS)

    strategy.parameters:addGroup("Calculation")
    strategy.parameters:addInteger("Range", "Range", "", 7)
    strategy.parameters:addInteger("Shift", "Shift", "", 0)
    strategy.parameters:addInteger("ATRPeriod", "ATR Period", "", 9)
    strategy.parameters:addDouble("ATRMultipl", "ATRMultipl", "", 2.5)

    strategy.parameters:addGroup("Exit")
    strategy.parameters:addString("Trade", "(non-FIFO) Choose Trade", "", "")
    strategy.parameters:setFlag("Trade", core.FLAG_TRADE)

    CreateTradingParameters()
end

function CreateTradingParameters()
    strategy.parameters:addGroup("Execution Parameters")

    strategy.parameters:addBoolean("AllowTrade", "Allow strategy to trade", "", true)
    strategy.parameters:setFlag("AllowTrade", core.FLAG_ALLOW_TRADE)

    strategy.parameters:addString("AccountType", "Account Type", "", "Automatic")
    strategy.parameters:addStringAlternative("AccountType", "FIFO", "", "FIFO")
    strategy.parameters:addStringAlternative("AccountType", "non FIFO", "", "NON")
    strategy.parameters:addStringAlternative("AccountType", "Automatic", "", "Automatic")

    strategy.parameters:addString("ExitExecutionType", "Exit Execution Type", "", "Live")
    strategy.parameters:addStringAlternative("ExitExecutionType", "End of Turn", "", "EndOfTurn")
    strategy.parameters:addStringAlternative("ExitExecutionType", "Live", "", "Live")

    strategy.parameters:addGroup("Trade Parameters")

    -- strategy.parameters:addString("CustomID", "Custom Identifier", "The identifier that can be used to distinguish strategy instances", "TEST");

    strategy.parameters:addString("Direction", "Type of Signal / Trade", "", "direct")
    strategy.parameters:addStringAlternative("Direction", "Direct", "", "direct")
    strategy.parameters:addStringAlternative("Direction", "Reverse", "", "reverse")

    strategy.parameters:addString("Account", "Account to trade on", "", "")
    strategy.parameters:setFlag("Account", core.FLAG_ACCOUNT)

    strategy.parameters:addGroup("Alerts")
    strategy.parameters:addBoolean("ShowAlert", "ShowAlert", "", true)
    strategy.parameters:addBoolean("PlaySound", "Play Sound", "", false)
    strategy.parameters:addFile("SoundFile", "Sound File", "", "")
    strategy.parameters:setFlag("SoundFile", core.FLAG_SOUND)
    strategy.parameters:addBoolean("RecurrentSound", "Recurrent Sound", "", true)
    strategy.parameters:addBoolean("SendEmail", "Send Email", "", false)
    strategy.parameters:addString("Email", "Email", "", "")
    strategy.parameters:setFlag("Email", core.FLAG_EMAIL)

    strategy.parameters:addGroup("Time Parameters")

    strategy.parameters:addInteger("ToTime", "Convert the date to", "", 6)
    strategy.parameters:addIntegerAlternative("ToTime", "EST", "", 1)
    strategy.parameters:addIntegerAlternative("ToTime", "UTC", "", 2)
    strategy.parameters:addIntegerAlternative("ToTime", "Local", "", 3)
    strategy.parameters:addIntegerAlternative("ToTime", "Server", "", 4)
    strategy.parameters:addIntegerAlternative("ToTime", "Financial", "", 5)
    strategy.parameters:addIntegerAlternative("ToTime", "Display", "", 6)

    strategy.parameters:addString("StartTime", "Start Time for Trading", "", "00:00:00")
    strategy.parameters:addString("StopTime", "Stop Time for Trading", "", "24:00:00")
    --*********************************************************************************************************
    strategy.parameters:addBoolean("ManageExit", "Use Exit  after Stop Time", "", true)
    --*********************************************************************************************************
    strategy.parameters:addBoolean("UseMandatoryClosing", "Use Mandatory Closing", "", false)
    strategy.parameters:addString("ExitTime", "Mandatory Closing  Time", "", "23:59:00")
    strategy.parameters:addInteger("ValidInterval", "Valid interval for operation in second", "", 60)
end
local orderId
local tradeId
local requestId

local AccountType
local Source, TickSource
local MaxNumberOfPositionInAnyDirection, MaxNumberOfPosition
local SoundFile = nil
local RecurrentSound = false
local ALLOWEDSIDE
local AllowTrade
local Offer
local CanClose
local Account
local Amount
local SetLimit
local Limit
local SetStop
local Stop
local TrailingStop
local ShowAlert
local Email
local SendEmail
local BaseSize
local EntyExecutionType, ExitExecutionType
local CloseOnOpposite
local first
local Direction
--local CustomID;
local PositionCap
local TF
local OpenTime, CloseTime, ExitTime
local LastEntry, LastExit
local ToTime
local ValidInterval, UseMandatoryClosing
--*********************************************************************************************************
local ManageExit, Exit
--*********************************************************************************************************

--Indicator parameters
local Range, Shift, ATRPeriod, ATRMultipl

function Prepare(nameOnly)
    --CustomID = instance.parameters.CustomID;
    AccountType = instance.parameters.AccountType
    ExitExecutionType = instance.parameters.ExitExecutionType
    ManageExit = instance.parameters.ManageExit

    Direction = instance.parameters.Direction == "direct"
    TF = instance.parameters.TF
    ToTime = instance.parameters.ToTime

    if ToTime == 1 then
        ToTime = core.TZ_EST
    elseif ToTime == 2 then
        ToTime = core.TZ_UTC
    elseif ToTime == 3 then
        ToTime = core.TZ_LOCAL
    elseif ToTime == 4 then
        ToTime = core.TZ_SERVER
    elseif ToTime == 5 then
        ToTime = core.TZ_FINANCIAL
    elseif ToTime == 6 then
        ToTime = core.TZ_TS
    end

    tradeId = instance.parameters.Trade

    local trade = core.host:findTable("trades"):find("TradeID", tradeId)
    assert(trade ~= nil, "Trade can not be found")

    ValidInterval = instance.parameters.ValidInterval
    UseMandatoryClosing = instance.parameters.UseMandatoryClosing

    LastEntry = nil
    LastExit = nil
    --*********************************************************************************************************
    ManageExit = instance.parameters.ManageExit

    --*********************************************************************************************************

    --Indicator parameters
    Range = instance.parameters.Range
    Shift = instance.parameters.Shift
    ATRPeriod = instance.parameters.ATRPeriod
    ATRMultipl = instance.parameters.ATRMultipl

    assert(TF ~= "t1", "The time frame must not be tick")

    name = profile:id() .. ", " .. instance.bid:name()
    instance:name(name)

    PrepareTrading()

    if nameOnly then
        return
    end

    assert(
        core.indicators:findIndicator("CHANDELIEREXIT_SS") ~= nil,
        "Please, download and install CHANDELIEREXIT_SS.LUA indicator"
    )

    if ExitExecutionType == "Live" then
        TickSource = ExtSubscribe(1, nil, "t1", instance.parameters.Type == "Bid", "close")
    end

    Source = ExtSubscribe(2, nil, TF, instance.parameters.Type == "Bid", "bar")
    Indicator =
        core.indicators:create(
        "CHANDELIEREXIT_SS",
        Source,
        Range,
        Shift,
        ATRPeriod,
        ATRMultipl,
        core.rgb(0, 255, 0),
        core.rgb(255, 0, 0)
    )
    first = Source.close:first()

    ValidInterval = instance.parameters.ValidInterval
    UseMandatoryClosing = instance.parameters.UseMandatoryClosing

    local valid
    OpenTime, valid = ParseTime(instance.parameters.StartTime)
    assert(valid, "Time " .. instance.parameters.StartTime .. " is invalid")
    CloseTime, valid = ParseTime(instance.parameters.StopTime)
    assert(valid, "Time " .. instance.parameters.StopTime .. " is invalid")
    ExitTime, valid = ParseTime(instance.parameters.ExitTime)
    assert(valid, "Time " .. instance.parameters.ExitTime .. " is invalid")

    if UseMandatoryClosing then
        core.host:execute("setTimer", 100, math.max(ValidInterval / 2, 1))
    end

    --local trade_in_progress=false;
end

function ReleaseInstance()
    core.host:execute("killTimer", 100)
end

function ParseTime(time)
    local pos = string.find(time, ":")
    if pos == nil then
        return nil, false
    end
    local h = tonumber(string.sub(time, 1, pos - 1))
    time = string.sub(time, pos + 1)
    pos = string.find(time, ":")
    if pos == nil then
        return nil, false
    end
    local m = tonumber(string.sub(time, 1, pos - 1))
    local s = tonumber(string.sub(time, pos + 1))
    return (h / 24.0 + m / 1440.0 + s / 86400.0), ((h >= 0 and h < 24 and m >= 0 and m < 60 and s >= 0 and s < 60) or -- time in ole format
        (h == 24 and m == 0 and s == 0)) -- validity flag
end

function InRange(now, openTime, closeTime)
    if openTime < closeTime then
        return now >= openTime and now <= closeTime;
    end
    if openTime > closeTime then
        return now > openTime or now < closeTime;
    end

    return now == openTime;
end

function PrepareTrading()
    ALLOWEDSIDE = instance.parameters.ALLOWEDSIDE

    local PlaySound = instance.parameters.PlaySound
    if PlaySound then
        SoundFile = instance.parameters.SoundFile
    else
        SoundFile = nil
    end
    assert(not (PlaySound) or (PlaySound and SoundFile ~= ""), "Sound file must be chosen")

    ShowAlert = instance.parameters.ShowAlert
    RecurrentSound = instance.parameters.RecurrentSound

    SendEmail = instance.parameters.SendEmail

    if SendEmail then
        Email = instance.parameters.Email
    else
        Email = nil
    end
    assert(not (SendEmail) or (SendEmail and Email ~= ""), "E-mail address must be specified")

    AllowTrade = instance.parameters.AllowTrade
    Account = instance.parameters.Account
    BaseSize = core.host:execute("getTradingProperty", "baseUnitSize", instance.bid:instrument(), Account)
    Offer = core.host:findTable("offers"):find("Instrument", instance.bid:instrument()).OfferID

    if AccountType == "FIFO" then
        CanClose = false
    elseif AccountType == "NON" then
        CanClose = true
    else
        CanClose = core.host:execute("getTradingProperty", "canCreateMarketClose", instance.bid:instrument(), Account)
    end
end

function ExtUpdate(id, source, period) -- The method called every time when a new bid or ask price appears.
    if AllowTrade then
        if not (checkReady("trades")) or not (checkReady("orders")) then
            return
        end
    end

    if period < 0 then
        return
    end

    if ExitExecutionType == "Live" then
        if id ~= 1 then
            return
        end
        period = core.findDate(Source, TickSource:date(period), false)
    else
        if id ~= 2 then
            return
        end
    end

    now = core.host:execute("getServerTime")
    now = core.host:execute("convertTime", core.TZ_EST, ToTime, now)
    -- get only time
    now = now - math.floor(now)

    -- update indicators.
    Indicator:update(core.UpdateLast)

    if ExitExecutionType == "Live" and id == 1 or ExitExecutionType ~= "Live" and id ~= 1 then
        ExitFunction(now, period)
    end
end

function ExitFunction(now, period)
    if not (now >= OpenTime and now <= CloseTime) and not ManageExit then
        return
    end

    if (LastExit == Source:serial(period)) then
        return
    end

    if Indicator.DATA:colorI(period) == core.rgb(0, 255, 0) then
        --and  Indicator.DATA:colorI(period-1) ~=  core.rgb(0, 255, 0)
        if Direction then
            if haveTrades("B") then
                exitSpecific("B")
                Signal("Close Long")
            end
        else
            if haveTrades("S") then
                exitSpecific("S")
                Signal("Close Short")
            end
        end
        LastExit = Source:serial(period)
    end
    if Indicator.DATA:colorI(period) == core.rgb(255, 0, 0) then
        --	and  Indicator.DATA:colorI(period-1) ~=  core.rgb(255, 0, 0)
        if Direction then
            if haveTrades("S") then
                exitSpecific("S")
                Signal("Close Short")
            end
        else
            if haveTrades("B") then
                exitSpecific("B")
                Signal("Close Long")
            end
        end

        LastExit = Source:serial(period)
    end
end

-- NG: Introduce async function for timer/monitoring for the order results
function ExtAsyncOperationFinished(cookie, success, message)
    if cookie == 100 then
        -- timer
        if UseMandatoryClosing and AllowTrade then
            now = core.host:execute("getServerTime")
            now = core.host:execute("convertTime", core.TZ_EST, ToTime, now)
            -- get only time
            now = now - math.floor(now)

            -- check whether the time is in the exit time period
            if now >= ExitTime and now < ExitTime + (ValidInterval / 86400.0) then
                if not checkReady("trades") then
                    return
                end

                if haveTrades("B") then
                    exitSpecific("B")
                    Signal("Close Long")
                end

                if haveTrades("S") then
                    exitSpecific("S")
                    Signal("Close Short")
                end
            end
        end
    elseif cookie == 200 and not success then
        terminal:alertMessage(
            instance.bid:instrument(),
            instance.bid[instance.bid:size() - 1],
            "Open order failed" .. message,
            instance.bid:date(instance.bid:size() - 1)
        )
    elseif cookie == 201 and not success then
        terminal:alertMessage(
            instance.bid:instrument(),
            instance.bid[instance.bid:size() - 1],
            "Close order failed" .. message,
            instance.bid:date(instance.bid:size() - 1)
        )
    elseif cookie == 201 and success then
        core.host:execute("stop")
    end
end

--===========================================================================--
--                    TRADING UTILITY FUNCTIONS                              --
--============================================================================--

function Signal(Label)
    if ShowAlert then
        terminal:alertMessage(instance.bid:instrument(), instance.bid[NOW], Label, instance.bid:date(NOW))
    end

    if SoundFile ~= nil then
        terminal:alertSound(SoundFile, RecurrentSound)
    end

    if Email ~= nil then
        terminal:alertEmail(Email, profile:id() .. " : " .. Label, FormatEmail(Source, NOW, Label))
    end
end

function checkReady(table)
    local rc
    if Account == "TESTACC_ID" then
        -- run under debugger/simulator
        rc = true
    else
        rc = core.host:execute("isTableFilled", table)
    end

    return rc
end

function tradesCount(BuySell)
    local count = 0

    local trade = core.host:findTable("trades"):find("TradeID", tradeId)

    if trade ~= nil and (trade.BS == BuySell or BuySell == nil) then
        count = count + 1
    end

    return count
end

function haveTrades(BuySell)
    local trade = core.host:findTable("trades"):find("TradeID", tradeId)

    local found = false

    if trade ~= nil and (trade.BS == BuySell or BuySell == nil) then
        found = true
    end

    return found
end

function exitSpecific(BuySell)
    if not AllowTrade then
        return
    end

    --side
    -- closes all positions of the specified direction (B for buy, S for sell)

    local valuemap
    local trade = core.host:findTable("trades"):find("TradeID", tradeId)

    if trade == nil then
        core.host:execute("stop")
    end

    if CanClose then
        -- non-FIFO account, create a close market order
        valuemap = core.valuemap()
        valuemap.OrderType = "CM"
        valuemap.OfferID = Offer
        valuemap.AcctID = Account
        valuemap.Quantity = trade.Lot
        valuemap.TradeID = trade.TradeID
        --valuemap.CustomID = CustomID;
        if trade.BS == "B" then
            valuemap.BuySell = "S"
        else
            valuemap.BuySell = "B"
        end
        success, msg = terminal:execute(201, valuemap)
        if not (success) then
            terminal:alertMessage(
                instance.bid:instrument(),
                instance.bid[instance.bid:size() - 1],
                "Close order failed" .. msg,
                instance.bid:date(instance.bid:size() - 1)
            )
            return false
        end
    else
        -- FIFO account, create an opposite market order
        valuemap = core.valuemap()
        valuemap.OrderType = "OM"
        valuemap.OfferID = Offer
        valuemap.AcctID = Account
        valuemap.Quantity = trade.Lot
        --valuemap.CustomID = CustomID;
        if trade.BS == "B" then
            valuemap.BuySell = "S"
        else
            valuemap.BuySell = "B"
        end
        success, msg = terminal:execute(201, valuemap)
        if not (success) then
            terminal:alertMessage(
                instance.bid:instrument(),
                instance.bid[instance.bid:size() - 1],
                "Close order failed" .. msg,
                instance.bid:date(instance.bid:size() - 1)
            )
            return false
        end
    end
end

dofile(core.app_path() .. "\\strategies\\standard\\include\\helper.lua")
