[HaasOnline] Flash Crash Bot (v4 only)

stable
By pshai in Trading Bots Published October 2021 👁 6,019 views 💬 8 comments ★ Staff Pick

Description

The FlashCrashBot recreated in HaasScript is here! Ugh.. Almost 1900 lines of code! It was a challenge to create this bot, I can tell you that... This bot differs from the original Custom Bot version, as it contains a bit more safeties, a StartControl and "Keep Following" setting for Follow-The-Trend feature! ;) But, it is missing buttons and features to add and remove buy and sell orders. :( Spread Type, Spread, Total Buy Amount and Total Sell Amount values are not allowed to be changed once the bot has started running! Please setup your bot properly before starting it. The "Keep Following" feature in FTT will continue following the trend when the counter-buy or -sell orders are all filled and bot is back to its original grid. This is especially useful in prolonged trends then you just want to stay with the price! The "Start Control" feature allows you to setup and start your bot without laying down any orders until the price has breached above or below the set Trigger Price. This helps you plan ahead! When trading both sides simultaneously, you cannot use ANY safeties for "When Trigger Above Buys" (trigger above buy-grid) and "When Trigger Below Sells" (trigger below sell-grid). Also the "Buy/Sell and Move" safety types are not allowed for outside the grids when trading both sides simultaneously. IN CASE you spot ANY issues with the bot, please contact me in Discord or drop a comment below. ALSO NOTE that the script does not work in v3; it was designed for and created with v4.
HaasScript
-- [HaasOnline] Flash Crash Bot
-- Author: pshai

EnableHighSpeedUpdates(true)
HideOrderSettings()
HideTradeAmountSettings()

--===================================================================
-- == Miscellaneous Usefulness
        
    -- deep clone an object
    function clone(original)
        local copy = {}
        for k, v in pairs(original) do
            if GetType(v) == ArrayDataType then
                v = clone(v)
            end
            copy[k] = v
        end
        return copy
    end

--===================================================================
-- == Logger
    local LoggerLevels = {
        ErrorsOnly = 'Errors Only',
        ErrorsAndWarnings = 'Erorrs & Warnings',
        All = 'All'
    }

    local Logger = {
        _level = InputOptions('DEBUG Level',
                LoggerLevels.All,
                LoggerLevels,
                {group = '     DEBUG'}),
        _in = 'Main',
        _prevIn = {},
        _log = Log,
        _warn = LogWarning,
        _error = LogError
    }

    function Logger:level()
        if Logger._level == LoggerLevels.ErrorsOnly then
            return 0
        elseif Logger._level == LoggerLevels.ErrorsAndWarnings then
            return 1
        elseif Logger._level == LoggerLevels.All then
            return 2
        end

        LogError('Logger level undefined: "' .. Logger._level .. '"')
    end

    function Logger:where()
        local ret = ''
        local prevs = self._prevIn
        if #prevs > 0 then
            for i = 1, #prevs do
                if prevs[i] != '' and #prevs[i] > 0 then
                    ret = ret .. prevs[i] .. '::'
                end
            end
        end
        return ret .. self._in
    end

    function Logger:enter(to)
        self._prevIn = ArrayAdd(self._prevIn, self._in)
        self._in = to
    end

    function Logger:exit()
        self._in = ArrayLast(self._prevIn)
        self._prevIn = ArrayPop(self._prevIn)
    end

    function Logger:log(msg, color)
        if self:level() >= 2 then
            if not color then
                color = ''
            end

            local _in = Logger:where()
            self._log('['.._in..'] '..msg, color)
        end
    end

    function Logger:warn(msg)
        if self:level() >= 1 then
            local _in = Logger:where()
            self._warn('['.._in..'] '..msg)
        end
    end

    function Logger:error(msg)
        local _in = Logger:where()
        self._error('['.._in..'] '..msg)
        DeactivateBot('FCB Deactivation on Error')
    end

--===================================================================
--===================================================================


--===================================================================
-- == Options for inputs
    local options = {
        SpreadTypes = {
            Fixed = 'Fixed Amount',
            Percentage = 'Percentage',
            PercentageBoost = 'Percentage With Boost',
            Exponential = 'Exponential'
        },

        StartControlTypes = {
            AbovePrice = 'Above Price',
            BelowPrice = 'Below Price'
        },

        Currencies = {
            Base = 'Base',
            Quote = 'Quote'
        },

        -- above sell grid
        MoveInAboveSellActions = {
            None = 'None',
            Stop = 'Stop',
            BuyAndStop = 'Buy Sold & Stop',
            BuyAndMove = 'Buy Sold & Move Grid'
        },

        -- below sell grid
        MoveOutBelowSellActions = {
            None = 'None',
            Stop = 'Stop',
            SellAndStop = 'Sell & Stop',
            SellAndFlip = 'Sell & Flip to Buy'
        },

        -- above buy grid
        MoveInAboveBuyActions = {
            None = 'None',
            Stop = 'Stop',
            BuyAndStop = 'Buy & Stop',
            BuyAndFlip = 'Buy & Flip to Sell'
        },

        -- below buy grid
        MoveOutBelowBuyActions = {
            None = 'None',
            Stop = 'Stop',
            SellAndStop = 'Sell Bought & Stop',
            SellAndMove = 'Sell Bought & Move Grid'
        },
    }


--===================================================================
-- == Input settings
    InputGroupHeader('Price Settings')
    local basePrice = Input('1. Base Price', 50000)
    local spreadType = InputOptions('2.1. Spread Type', options.SpreadTypes.Percentage, options.SpreadTypes)
    local spread = Input('2.2. Spread', 1, 'Used with [Fixed Amount], [Percentage] and [Percentage With Boost] spread types.')
    local spreadBoost = Input('2.3. Boost %', 0, 'Used with [Percentage With Boost] spread type.')
    local spreadMult = Input('2.4. Multiplier', 0, 'Used with [Exponential] spread type.')
    local spreadMin = Input('2.5. Min. Spread %', 0, 'Used with [Exponential] spread type.')
    local spreadMax = Input('2.6. Max. Spread %', 0, 'Used with [Exponential] spread type.')
​
    InputGroupHeader('Amount Settings')
    local usedCurrency = InputOptions('1. Used Currency', options.Currencies.Base, options.Currencies)
    local totalBuyAmt = Input('2.1. Total Buy Amount', 0)
    local totalSellAmt = Input('2.2. Total Sell Amount', 0)
    local orderSize = Input('3. Order Size', 0)
    local refillDelay = Input('4. Refill Delay', 0, 'Optional refill delay, set in minutes. Set to zero to disable.')
​
    InputGroupHeader('Start Control Settings')
    --[[
        Start Control:
        - ability to start after price breach (above/below X price)
    ]]
    local sc = {
        enabled = Input('1. Enabled', false),
        triggerPrice = Input('2. Trigger Price', 0),
        type = InputOptions('3. Type', options.StartControlTypes.AbovePrice, options.StartControlTypes)
    }
​
    InputGroupHeader('Follow The Trend')
    local ftt = {
        enabled = Input('1.1. Enabled', false, 'Follow The Trend feature allows the FCB to follow the price moves with a pre-defined channel and an interval.'
                ..'If the current price is outside the channel at checkup, the grid will be adjusted to new prices. For the ['.. options.SpreadTypes.Fixed ..']'
                ..'and ['.. options.SpreadTypes.Percentage ..'] spread types, use settings 3.1. and 3.2. and for ['.. options.SpreadTypes.PercentageBoost ..']'
                ..'and ['.. options.SpreadTypes.Exponential ..'] spread type use settings 4.1. and 4.2.'),
        keepFollowing = Input('1.2. Keep Following', false, 'If set to true, FTT will continue updating when slots return to their original state. If false, FTT will only be updated until the very first order has been filled.'),
        interval = Input('2. Check Interval', 240),
        channelSize = Input('3.1. Channel Size', 0, 'Channel size is measured in slots. Used with [Fixed] and [Percentage] spread type.'),
        channelOffset = Input('3.2. Channel Offset', 0, 'Channel offset is measured in slots. Used with [Fixed] and [Percentage] spread type.'),
        channelSize2 = Input('4.1. Channel Size %', 0, 'Channel size in percentages. Used with [Percentage With Boost] and [Exponential] spread type.'),
        channelOffset2 = Input('4.2. Channel Offset %', 0, 'Channel offset in percentages. Used with [Percentage With Boost] and [Exponential] spread type.')
    }
​
    InputGroupHeader('Safeties')
    local safeties = {
        enabled = Input('1. Enabled', false, 'The safeties allows FCB to move assets in or out from the market. In case of the sell-side grid, the settings 3.1.'
                ..'and 3.2. control what happens outside the grid and for buy-side grids, the settings 4.1. and 4.2. control what happens on that side. The stops will also cancel all outstanding orders.'),
        trigger = Input('2. Trigger Level %', 0),
​
        --[[
            when selling;
            * above grid can buy-back and stop or re-adjust grid, so it will continue selling after that buy-back, or NONE
            * below grid can sell total amount and stop or flip to buying the sold amount, or NONE
        ]]
        sellSide ={
            above = InputOptions('3.1. When Trigger Above Sells', options.MoveInAboveSellActions.None, options.MoveInAboveSellActions),
            below = InputOptions('3.2. When Trigger Below Sells', options.MoveOutBelowSellActions.None, options.MoveOutBelowSellActions),
        },
​
        --[[
            when buying;
            * above grid can buy total amount and stop or flip to selling the bought amount, or NONE
            * below grid can sell-out and stop or re-adjust grid, so it will continue buying after the sell-out, or NONE
        ]]
        buySide ={
            above = InputOptions('4.1. When Trigger Above Buys', options.MoveInAboveBuyActions.None, options.MoveInAboveBuyActions),
            below = InputOptions('4.2. When Trigger Below Buys', options.MoveOutBelowBuyActions.None, options.MoveOutBelowBuyActions),
        }
    }


-- =============================================================
-- == Handy helper(s)

    local function InputChanged(id, value)
        if not value then
            return false
        end

        local oldValue = Load(id, value)
        Save(id, value)
        return oldValue != value
    end


-- ===================================================================
-- == Check correct input settings
    local function CheckInputs()
​
        Logger:enter('Settings_Check')
        local isOK = true
​
        if InputChanged('spreadType', spreadType) or InputChanged('spread', spread) then
            Logger:enter('Price_Settings')
            Logger:error('Cannot change spread settings while bot is running!')
            Logger:exit()
​
            isOK = false
        end

        if InputChanged('totalBuyAmt', totalBuyAmt) or InputChanged('totalSellAmount', totalSellAmt) then
            Logger:enter('Price_Settings')
            Logger:error('Cannot change amount settings while bot is running!')
            Logger:exit()
​
            isOK = false
        end
​
        if ftt.enabled then
            Logger:enter('Ftt_Settings')
​
            if totalBuyAmt > 0 and totalSellAmt > 0 then
​
                Logger:error('Cannot use FTT when bot is setup to trade both sides.')
                isOK = false
            
            elseif spreadType == options.SpreadTypes.PercentageBoost
            or spreadType == options.SpreadTypes.Exponential
            then
​                -- YES, there is an error for FTT + these grid types, even though
                -- FTT has implementation for these. These just doesn't work together,
                -- no matter how hard you try, unless you ready for that insane spam
                -- of cancel and replace of orders...
                Logger:error('Cannot use FTT with spread type ['.. spreadType ..'].')
                isOK = false
            end
​
            Logger:exit()
        end
​
        if safeties.enabled then
​
​
            Logger:enter('Safety_Settings')
​
            if totalBuyAmt > 0 and totalSellAmt > 0 then
​
​
                if safeties.sellSide.below != options.MoveOutBelowSellActions.None then
                    Logger:error('Cannot use "'.. safeties.sellSide.below ..'" (or any other) when trading both sides.')
                    isOK = false
                end
​
                if safeties.buySide.above != options.MoveInAboveBuyActions.None then
                    Logger:error('Cannot use "'.. safeties.buySide.above ..'" (or any other) when trading both sides.')
                    isOK = false
                end
​
                if safeties.sellSide.above == options.MoveInAboveSellActions.BuyAndMove then
                    Logger:error('Cannot use "'.. safeties.sellSide.above ..'" when trading both sides.')
                    isOK = false
                end
​
                if safeties.buySide.below == options.MoveOutBelowBuyActions.SellAndMove then
                    Logger:error('Cannot use "'.. safeties.buySide.above ..'" when trading both sides.')
                    isOK = false
                end
​
                Logger:exit()
            end
        end
​
        if isOK then
            if Load('ci:init', true) then
                LogWarning('-----------------------------------------------------------')
                LogWarning('You have been warned!')
                LogWarning('Make sure you have all set before starting the bot!')
                LogWarning('The bot will not like that and will break if you do so.')
                LogWarning('Do NOT change settings while the bot is running!')
                LogWarning('!! WARNING !!')
                LogWarning('-----------------------------------------------------------')
                Save('ci:init', false)
            end
        end
        
        Logger:exit()
        return isOK
    end
​

--===================================================================
--===================================================================



-- =============================================================
-- == Enumerations
    local enums = {
        SlotTypes = {
            Empty = 0,
            Buy = 1,
            Sell = 2
        }
    }


-- =============================================================
-- == Start Control
    local StartControl = {
        IsRunning = false
    }

    function StartControl:update()
        self:load()

        local isRunning = self.IsRunning

        if isRunning and InputChanged('sc.enabled', sc.enabled) and sc.enabled then


            isRunning = false
            Logger:warn('Start Control enabled: halting bot and cancelling orders...')
        elseif not sc.enabled and not isRunning then


            isRunning = true
            Logger:warn('Bot starting...')

        elseif not isRunning then


            local cp = CurrentPrice()
            local tp = sc.triggerPrice
            local type = sc.type

            if (cp.close > tp and type == options.StartControlTypes.AbovePrice)
            or (cp.close < tp and type == options.StartControlTypes.BelowPrice)
            then


                isRunning = true

                Logger:warn('Trigger price breached, starting...')
            end
        end

        self.IsRunning = isRunning

        self:save()
    end

    function StartControl:isBotRunning()
        return self.IsRunning
    end

    function StartControl:save()
        Save('sc:ir', self.IsRunning)
    end

    function StartControl:load()
        self.IsRunning = Load('sc:ir', false)
    end


-- =============================================================
-- == Follow The Trend (FTT)

    local FTT = {
        BasePrice = basePrice,
        High = -1,
        Low = -1,
        LastUpdate = -1,
        ShouldRebuildGrid = false,
        BaseKeyMove = 0,
        KeepFollowing = ftt.keepFollowing,
        Grid = nil
    }

    function FTT:load()
        self.BasePrice = Load('ftt:bp', basePrice)
        self.High = Load('ftt:h', 0)
        self.Low = Load('ftt:l', 0)
        self.LastUpdate = Load('ftt:t', 0)
    end

    function FTT:save()
        Save('ftt:bp', self.BasePrice)
        Save('ftt:h', self.High)
        Save('ftt:l', self.Low)
        Save('ftt:t', self.LastUpdate)
    end

    function FTT:getBasePrice()
        return self.BasePrice
    end

    function FTT:setBasePrice(newPrice)
        self.BasePrice = newPrice
    end

    function FTT:getChannel()
        local st = spreadType
        local offset = 0
        local size = 0
        local spread = 0

        if st == options.SpreadTypes.Fixed
        or st == options.SpreadTypes.Percentage
        then
            -- get channel offset and size based on fixed grid
            spread = self.Grid:getSpread(1)
            offset = spread * ftt.channelOffset
            size = spread * ftt.channelSize

        elseif st == options.SpreadTypes.PercentageBoost
        or st == options.SpreadTypes.Exponential
        then
            -- get channel offset and size based on percentages
            local cp = CurrentPrice()
            offset = SubPerc(cp.close, 100 - ftt.channelOffset2)
            size = SubPerc(cp.close, 100 - ftt.channelSize2)

        end

        return spread, offset, size
    end

    function FTT:calculateChannel(offset, size)
        local bp = self.BasePrice

        if totalBuyAmt > 0 then
            self.High = bp + offset + size
            self.Low = bp + offset

        elseif totalSellAmt > 0 then
            self.High = bp - offset
            self.Low = bp - offset - size

        end
    end

    function FTT:plot()
        local h = self.High
        local l = self.Low
        local bp = self.BasePrice
        
        if ftt.enabled and h > 0 and l > 0 then
            PlotBands(
                Plot(0, 'Ftt:High', h, Cyan),
                Plot(0, 'Ftt:Low', l, Cyan),
                SkyBlue(10)
            )
        end

        if bp > 0 then
            Plot(0, 'Ftt:BasePrice', bp, Orange) 
        end
    end

    function FTT:update(isNoPos, force)
        Logger:enter('FTT')

        self:load()
        self:plot()

        if not ftt.enabled and not force then
            self:save()
            Logger:exit()
            return
        end

        self.ShouldRebuildGrid = false

        if not isNoPos then
            self:save()
            Logger:exit()
            return
        end

        if not force and Time() < self.LastUpdate + ftt.interval * 60 then
            self:save()
            Logger:exit()
            return
        end

        local spread, offset, size = self:getChannel()
        self:calculateChannel(offset, size)

        -- dont mess with timing if we forced update
        if not force then
            self.LastUpdate = Time()
        end

        local cp = CurrentPrice()
        self.BaseKeyMove = 0


        -- update?
        if cp.high > self.High then

            -- move grid up
            if spread > 0 then

                -- if spread is set, we are using fixed/percentage grid
                while self.High < cp.high do
                    self.BasePrice = self.BasePrice + spread -- move BP by 1 step
                    self:calculateChannel(offset, size) -- recalc channel
                    self.BaseKeyMove = self.BaseKeyMove - 1
                end
            else

                -- spread was not set, assuming boosted/exponential grid
                self.BasePrice = cp.close -- we have no fixed steps so... just update BP
                self:calculateChannel(offset, size) -- recalc channel
            end

            self.ShouldRebuildGrid = true

        elseif cp.low < self.Low then

            -- move grid down
            if spread > 0 then

                -- if spread is set, we are using fixed/percentage grid
                local bp = self.BasePrice

                while self.Low > cp.low do
                    self.BasePrice = self.BasePrice - spread -- move BP by 1 step
                    self:calculateChannel(offset, size) -- recalc channel
                    self.BaseKeyMove = self.BaseKeyMove + 1
                end
            else

                -- spread was not set, assuming boosted/exponential grid
                self.BasePrice = cp.close -- we have no fixed steps so... just update BP
                self:calculateChannel(offset, size) -- recalc channel
            end

            self.ShouldRebuildGrid = true
        end

        self:save()
        Logger:exit()
    end




-- =============================================================
-- == Orderbook builder
    local Grid = {
        Loaded = false,
        IfFlipped = false,
        TotalBuyAmount = 0,
        TotalSellAmount = 0,
        BuySlotCount = 0, 
        SellSlotCount = 0,
        PriceArray = Load('gps', {}),
    }

    -- since we now know what "Grid" is (as a variable),
    -- we need to tell that to the FTT module.
    -- And believe me; I know what you are thinking!
    FTT.Grid = Grid
    ---------------------------


    function Grid:load()
        if self.Loaded then
            return
        end

        self.IsFlipped = Load('grid:if', false)
        self.TotalBuyAmount = Load('grid:tba', totalBuyAmt)
        self.TotalSellAmount = Load('grid:tsa', totalSellAmt)
        self.BuySlotCount = (self.TotalBuyAmount > 0 and orderSize > 0) and ArrayGet(Floor(self.TotalBuyAmount / orderSize), 1) or 0
        self.SellSlotCount = (self.TotalSellAmount > 0 and orderSize > 0) and ArrayGet(Floor(self.TotalSellAmount / orderSize), 1) or 0

        self.Loaded = true
    end

    function Grid:save()
        Save('grid:if', self.IsFlipped)
        Save('grid:tba', self.TotalBuyAmount)
        Save('grid:tsa', self.TotalSellAmount)
    end

    function Grid:getTotalBuy()
        if not self.Loaded then
            Logger:error('Grid not initialized')
        end
        return self.IsFlipped and self.TotalSellAmount or self.TotalBuyAmount
    end

    function Grid:getTotalSell()
        if not self.Loaded then
            Logger:error('Grid not initialized')
        end
        return self.IsFlipped and self.TotalBuyAmount or self.TotalSellAmount
    end

    function Grid:getBuyCount()
        if not self.Loaded then
            Logger:error('Grid not initialized')
        end
        return self.IsFlipped and self.SellSlotCount or self.BuySlotCount
    end

    function Grid:getSellCount()
        if not self.Loaded then
            Logger:error('Grid not initialized')
        end
        return self.IsFlipped and self.BuySlotCount or self.SellSlotCount
    end

    

    -- Used to add input buttons with callbacks
    function Grid:init()
        -- There was some magic here, aaaaand it's gone.

        self:load()
    end

    function Grid:flip()
        self.IsFlipped = not self.IsFlipped
    end

    function Grid:getSpread(index, totalOrders)
        local st = spreadType
        local bp = basePrice
        local spr = spread
        local boost = spreadBoost
        local mult = spreadMult
        local min = spreadMin
        local max = spreadMax
        local ret = 0

        -- Fixed spread
        if st == options.SpreadTypes.Fixed then

            ret = spr * index

        -- Percentage-based spread
        elseif st == options.SpreadTypes.Percentage then

            ret = bp / 100 * spr * index
        
        -- Percentage with boost addition
        elseif st == options.SpreadTypes.PercentageBoost then

            ret = bp / 100 * (spr + boost * index) * index
        
        -- Exponential spread
        elseif st == options.SpreadTypes.Exponential then

            -- helper function to calculate linear interpolation
            local function lerp(x, y, a)
                return x + (y - x) * a
            end

            -- do linear interpolation between min and max
            -- but using an exponential alpha value
            ret = bp / 100 * lerp(min, max, Pow(index / totalOrders, mult))
        end
        
        return ret
    end

    -- function to build grid prices into an array
    function Grid:buildGrid(bp)
        --local bp = basePrice
        local spr, count
        local newArr = {}

        -- init array
        for i=1, 200 do
            newArr[i] = -1
        end

        -- set base price in the middle
        newArr[100] = bp
        
        -- buy prices
        count = self:getBuyCount()
        if count > 0 then
            for i = 1, count do
                spr = Grid:getSpread(i, count)
                newArr[100 + i] = bp - spr
            end
        end

        -- sell prices
        count = self:getSellCount()
        if count > 0 then
            for i = 1, count do
                spr = Grid:getSpread(i, count)
                newArr[100 - i] = bp + spr
            end
        end

        Save('gps', newArr)
        self.PriceArray = newArr
    end





-- =============================================================
-- == Slot Object
    local SlotObject = {
        Index = 0,
        OrderId = '',
        InUse = false,
        IsActive = false,
        CurrentType = enums.SlotTypes.Empty,
        DefaultType = enums.SlotTypes.Empty,
        RefillTime = 0,
        FilledAmount = 0

        --[[

            TODO:
                * slots have to have their own corresponding position!
                * create new position for "entries" and close them with "exits"
                * this way dont have to fuck about with fees...
        ]]
    }

    local SlotObjectLists = {
        Init = false,
        Buy = {},
        Sell = {}
    }

    function SlotObject:load(index)
        Logger:enter('load')

        local obj = clone(self)
        local type = enums.SlotTypes.Empty

        if index > 0 then
            type = enums.SlotTypes.Buy
            
            Logger:log('Load buy slot at index ' .. index)
        elseif index < 0 then
            type = enums.SlotTypes.Sell
            
            Logger:log('Load sell slot at index ' .. index)
        end

        obj.Index           = Load(index .. 'i', index)
        obj.OrderId         = Load(index .. 'oid', '')
        obj.InUse           = Load(index .. 'iu', false)
        obj.CurrentType     = Load(index .. 'ct', type)
        obj.DefaultType     = Load(index .. 'dt', type)
        obj.RefillTime      = Load(index .. 'rt', 0)
        obj.FilledAmount    = Load(index .. 'fa', 0)

        Logger:exit()

        return obj
    end

    function SlotObject:save(index)
        Logger:enter('save')

        if not self then
            Logger:error('Cannot use save() as function; use as method.')
        else
            if not index then
                Logger:error('SlotObject index is not set.')
                Logger:exit()
                return
            end
        end

        -- DO NOT save the slot object itself!
        -- ONLY save its values!
        Save(index .. 'i',      self.Index)
        Save(index .. 'oid',    self.OrderId)
        Save(index .. 'iu',     self.InUse)
        Save(index .. 'ct',     self.CurrentType)
        Save(index .. 'dt',     self.DefaultType)
        Save(index .. 'rt',     self.RefillTime)
        Save(index .. 'fa',     self.FilledAmount)

        Logger:log('SlotObject on index ' .. index .. ' was saved.')
        Logger:exit()
    end

    function SlotObject:loadAll(buys, sells)
        if SlotObjectLists.Init then
            return
        end

        Logger:enter('loadAll')

        local buySlots = {}
        local sellSlots = {}

        for i = 1, buys do
            buySlots[#buySlots + 1] = SlotObject:load(i)
        end

        for i = 1, sells do
            sellSlots[#sellSlots + 1] = SlotObject:load(-i)
        end

        SlotObjectLists.Buy = buySlots
        SlotObjectLists.Sell = sellSlots
        SlotObjectLists.Init = true

        Logger:exit()
    end

    function SlotObject:reset()
        self.CurrentType = self.DefaultType

        self:check(true)
    end

    function SlotObject:resetAll()
        Logger:enter('resetAll')

        local buySlots = SlotObjectLists.Buy
        local sellSlots = SlotObjectLists.Sell
        local buys = #buySlots
        local sells = #sellSlots

        for i=1, buys do
            SlotObject:get(i):reset()
        end

        for i=1, sells do
            SlotObject:get(-i):reset()
        end

        Logger:exit()
    end

    function SlotObject:saveAll()
        Logger:enter('saveAll')

        if not SlotObjectLists.Init then
            Logger:error('SlotObjectLists not initialized.')
            Logger:exit()
            return
        end

        local buySlots = SlotObjectLists.Buy
        local sellSlots = SlotObjectLists.Sell
        local buys = #buySlots
        local sells = #sellSlots

        for i=1, buys do
            SlotObject:get(i):save(i)
        end

        for i=1, sells do
            SlotObject:get(-i):save(-i)
        end

        Logger:exit()
    end

    function SlotObject:get(index)
        Logger:enter('get')

        if not SlotObjectLists.Init then
            Logger:error('SlotObjectLists not initialized.')
            Logger:exit()
            return nil
        end

        if index > 0 then
            local listLen = #SlotObjectLists.Buy

            if Grid:getBuyCount() > listLen then
                local slot = SlotObject:load(index)
                SlotObjectLists.Buy[listLen + 1] = slot
                
                Logger:exit()
                return slot
            end

            Logger:exit()
            return SlotObjectLists.Buy[index]

        elseif index < 0 then
            local listLen = #SlotObjectLists.Sell

            if Grid:getSellCount() > listLen then
                local slot = SlotObject:load(index)
                SlotObjectLists.Sell[listLen + 1] = slot

                Logger:exit()    
                return slot
            end

            Logger:exit()
            return SlotObjectLists.Sell[-index]

        end

        Logger:error('Cannot get() an index of '..index)
        Logger:exit()
        return nil
    end

    function SlotObject:processFill(amt)
        Logger:enter('processFill')

        local type = self.CurrentType
        local dtype = self.DefaultType

        if type == dtype then
            self.FilledAmount = amt
        else
            self.FilledAmount = 0
        end

        if type == enums.SlotTypes.Buy then


            self.Index = self.Index - 1
            self.CurrentType = enums.SlotTypes.Sell

            Logger:log('Buy order filled, preparing to place counter-sell... (filled order: "'..self.OrderId..'")')

        elseif type == enums.SlotTypes.Sell then


            self.Index = self.Index + 1
            self.CurrentType = enums.SlotTypes.Buy

            Logger:log('Sell order filled, preparing to place counter-buy... (filled order: "'..self.OrderId..'")')
        end

        if refillDelay > 0 then
            self.RefillTime = Time() + refillDelay * 60
        end

        Logger:exit()
    end

    function SlotObject:plotOrder(order)
        local ctype = self.CurrentType
        local dtype = self.DefaultType
        local oid = order.orderId
        local price = order.price
        local color = ''
        local name = ''

        if ctype == enums.SlotTypes.Buy then

            if ctype == dtype then
                color = Green
            else
                color = Yellow
            end
            
            name = 'Buy-' .. self.Index
                
        elseif ctype == enums.SlotTypes.Sell then

            if ctype == dtype then
                color = Red
            else
                color = Yellow
            end

            name = 'Sell-' .. self.Index
        end

        Plot(0, name, price, {c = color, id = oid})

        Logger:log(name .. ' is open order at ' .. price)
    end

    function SlotObject:check(shouldCancel)
        Logger:enter('check')

        local oid = self.OrderId
        
        if oid != '' then

            local order = OrderContainer(oid)

            if order.isOpen then

                if shouldCancel then

                    CancelOrder(oid)
                end

                self:plotOrder(order)
            else

                if order.isFilled then

                    self:processFill(order.filledAmount)
                elseif order.isCancelled then

                    -- nothing to do.
                    Logger:log('Order was cancelled, preparing to replace...')
                end

                oid = ''
            end
        end

        self.OrderId = oid
        self.InUse = oid != ''

        Logger:exit()
        
        return oid == ''
    end

    function SlotObject:getPrice()
        Logger:enter('getPrice')

        local index = self.Index
        local bp = FTT:getBasePrice()
        local price = -1

        if index == 0 then
            price = bp
        else
            local buys = Grid:getBuyCount()
            local sells = Grid:getSellCount()

            if buys > 0 and index > buys then
                Logger:error('Trying to place buy order out of grid.')
                Logger:exit()
                return nil
            end

            if sells > 0 and index < -sells then
                Logger:error('Trying to place sell order out of grid.')
                Logger:exit()
                return nil
            end

            price = Grid.PriceArray[100 + index]
        end

        Logger:log('Price for index ' .. index .. ': ' .. price)
        Logger:exit()

        return price
    end

    function SlotObject:getAmount(price)
        Logger:enter('getAmount')

        if not price then
            Logger:error('Developer error: cannot calculate order size.')
            Logger:exit()
            return
        end

        local amount = orderSize

        if self.CurrentType != self.DefaultType and self.FilledAmount > 0 then
            return self.FilledAmount
        end

        if usedCurrency == options.Currencies.Quote then
            amount = amount / price
        end

        Logger:exit()
 
        return ParseTradeAmount('', price, amount)
    end

    function SlotObject:execute()
        Logger:enter('execute')

        -- wait for refill delay
        if refillDelay > 0 and self.RefillTime >= Time() then
            Logger:exit()
            return
        end

        --local price = ParseTradePrice('', self:getPrice())
        local price = self:getPrice()
        local amount = self:getAmount(price)
        local ctype = self.CurrentType

        if ctype == enums.SlotTypes.Buy then
            -- place buy order
            Logger:log('Placing BUY order at ' .. price)

            self.OrderId = PlaceBuyOrder(price, amount, {type = NoTimeOutOrderType, note = 'Buy-'..self.Index})

        elseif ctype == enums.SlotTypes.Sell then
            -- place sell order
            Logger:log('Placing SELL order at ' .. price)

            self.OrderId = PlaceSellOrder(price, amount, {type = NoTimeOutOrderType, note = 'Sell-'..self.Index})

        end

        Logger:exit()
    end


    function SlotObject:shiftAll(moveKey)
        Logger:enter('shiftAll')

        local up = moveKey < 0
        local down = moveKey > 0
        local steps = Abs(moveKey)
        local buySlots = SlotObjectLists.Buy
        local sellSlots = SlotObjectLists.Sell
        local buys = #buySlots
        local sells = #sellSlots

        Logger:log('attempting to shift grid (buys: '..buys..' | sells: '..sells..')')

        if buys > 0 then

            while steps > 0 do
                Logger:log('steps: '..steps)

                if up then

                    for i = 1, buys do
                        local slot = buySlots[i]

                        slot.Index = slot.Index + 1

                        if slot.Index > buys then

                            if slot.OrderId != '' then

                                CancelOrder(slot.OrderId)
                                slot.OrderId = ''
                            end
                        end
                    end

                    -- discard the last cell
                    buySlots = ArrayPop(buySlots)

                    local newSlot = SlotObject:load(9999)
                    newSlot.Index = 1

                    buySlots = ArrayUnshift(buySlots, newSlot)

                elseif down then


                    for i = buys, 1, -1 do
                        local slot = buySlots[i]

                        slot.Index = slot.Index - 1

                        if slot.Index < 1 then

                            if slot.OrderId != '' then

                                CancelOrder(slot.OrderId)
                                slot.OrderId = ''
                            end
                        end
                    end

                    -- discard the first cell
                    buySlots = ArrayShift(buySlots)

                    local newSlot = SlotObject:load(9999)
                    newSlot.Index = buys

                    buySlots = ArrayAdd(buySlots, newSlot)
                end

                steps = steps - 1
            end
        end

        if sells > 0 then

            while steps > 0 do
                Logger:log('steps: '..steps)

                if up then

                    for i = 1, sells do
                        local slot = sellSlots[i]

                        slot.Index = slot.Index + 1

                        if slot.Index > -1 then

                            if slot.OrderId != '' then

                                CancelOrder(slot.OrderId)
                                slot.OrderId = ''
                            end
                        end
                    end

                    -- discard the first cell
                    sellSlots = ArrayShift(sellSlots)

                    local newSlot = SlotObject:load(-9999)
                    newSlot.Index = -sells

                    sellSlots = ArrayAdd(sellSlots, newSlot)

                elseif down then


                    for i = sells, 1, -1 do
                        local slot = sellSlots[i]

                        slot.Index = slot.Index - 1

                        if slot.Index < -sells then

                            if slot.OrderId != '' then

                                CancelOrder(slot.OrderId)
                                slot.OrderId = ''
                            end
                        end
                    end

                    
                    -- discard the last cell
                    sellSlots = ArrayPop(sellSlots)

                    local newSlot = SlotObject:load(-9999)
                    newSlot.Index = -1

                    sellSlots = ArrayUnshift(sellSlots, newSlot)
                end

                steps = steps - 1
            end
        end


        SlotObjectLists.Buy = buySlots
        SlotObjectLists.Sell = sellSlots

        Logger:exit()
    end

    function SlotObject:cancelAll()
        Logger:enter('SlotObject:cancelAll')

        local buys = Grid:getBuyCount()
        local sells = Grid:getSellCount()

        -- buy orders on the positive indices
        if buys > 0 then

            for i=1, buys do
                -- get loaded slot
                local slot = SlotObject:get(i)

                -- cancel order
                slot:check(true)
            end
        end

        -- sell orders on the negative indices
        if sells > 0 then

            for i=1, sells do
                -- get slot
                local slot = SlotObject:get(-i)

                -- cancel order
                slot:check(true)
            end
        end

        Logger:exit()
    end

    local function UpdateSlots(cancelOrders, baseKeyMove)
        Logger:enter('UpdateSlots')

        -- slot counts
        local buys = Grid:getBuyCount()
        local sells = Grid:getSellCount()

        -- shift grid is necessary
        if baseKeyMove and baseKeyMove != 0 then

            if IsAnyOrderOpen() then

                Logger:log('Move grid by: ' .. baseKeyMove)
                SlotObject:shiftAll(baseKeyMove)
            else
                Logger:log('No need to shift grid; no orders open')
            end
        end
        
        -- buy orders on the positive indices
        if buys > 0 then

            for i=1, buys do
                -- get loaded slot
                local slot = SlotObject:get(i)

                -- check order state and update info
                if slot:check(cancelOrders) then

                    -- create order if none open
                    slot:execute()
                end
            end

            -- get rid of excessive orders
            local list = SlotObjectLists.Buy
            local listLen = #list
            if buys < listLen then
                while buys < listLen do
                    local slot = SlotObject:get(listLen)

                    slot:check(true)
                    slot:reset()

                    list[listLen] = nil
                    listLen = #list
                end
            end
        end

        -- sell orders on the negative indices
        if sells > 0 then

            for i=1, sells do
                -- get slot
                local slot = SlotObject:get(-i)

                -- check
                if slot:check(cancelOrders) then

                    -- execute order
                    slot:execute()
                end
            end

            -- get rid of excessive orders
            local list = SlotObjectLists.Sell
            local listLen = #list
            if sells < listLen then
                while sells < listLen do
                    local slot = SlotObject:get(-listLen)

                    slot:check(true)
                    slot:reset()

                    list[listLen] = nil
                    listLen = #list
                end
            end
        end

        Logger:exit()
    end





-- =============================================================
-- == Position Manager
    local PosMan = {
        Soid = '', -- sell order id
        Boid = '', -- buy order id
        Note = ''
    }

    function PosMan:load()
        self.Soid = Load('posman:soid', '')
        self.Boid = Load('posman:boid', '')
        self.Note = Load('posman:n', '')
    end

    function PosMan:save()
        Save('posman:soid', self.Soid)
        Save('posman:boid', self.Boid)
        Save('posman:n', self.Note)
    end

    function PosMan:hasPosition(pid)
        return GetPositionDirection(pid or '') != NoPosition
    end

    function PosMan:getAmount(isAbove, isBuyGrid)
        if isAbove and isBuyGrid then
            return Grid:getTotalBuy()
        
        elseif not isAbove and not isBuyGrid then
            return Grid:getTotalSell()
        
        else
            return GetPositionAmount()
        end

        return nil -- should not happen
    end

    -- dump
    function PosMan:sell(amount, note)
        local cp = CurrentPrice()
        
        if self.Soid == '' then
            self.Note = note
            self.Soid = PlaceSellOrder(cp.bid, amount, {type = MarketOrderType, note = note})
        else
            Logger:log('PosMan already has open sell order.')
        end
    end

    -- buy back
    function PosMan:buy(amount, note)
        local cp = CurrentPrice()
        
        if self.Boid == '' then
            self.Note = note
            self.Boid = PlaceBuyOrder(cp.ask, amount, {type = MarketOrderType, note = note})
        else
            Logger:log('PosMan already has open buy order.')
        end
    end

    function PosMan:update()
        if self.Soid != '' and not IsOrderOpen(self.Soid) then
            self.Soid = ''
        end

        if self.Boid != '' and not IsOrderOpen(self.Boid) then
            self.Boid = ''
        end
    end




-- =============================================================
-- == Safeties
    local SafetyControl = {}

    function SafetyControl:load()
        
    end

    function SafetyControl:save()
        
    end

    function SafetyControl:stop(isAbove, isBuyGrid)
        Logger:enter('stop')
        -- build msg
        local part1 = isAbove and 'above ' or 'below '
        local part2 = isBuyGrid and 'Buy ' or 'Sell '
        local msg = 'SafetyControl: Stop triggered '
                    .. part1
                    .. part2
                    .. 'grid.'
        
        -- cancel all slots
        SlotObject:cancelAll()

        -- deactivate
        DeactivateBot(msg, false)
        Logger:exit()
    end

    function SafetyControl:tradeAndStop(isAbove, isBuyGrid)
        Logger:enter('tradeAndStop')

        local amount = PosMan:getAmount(isAbove, isBuyGrid)
        
        if isBuyGrid then
            if isAbove then
                PosMan:buy(amount, 'Buy & Stop')
                SlotObject:resetAll()
                self:stop(isAbove, isBuyGrid)
            else
                PosMan:sell(amount, 'Sell Bought & Stop')
                SlotObject:resetAll()
                self:stop(isAbove, isBuyGrid)
            end
        else
            if isAbove then
                PosMan:buy(amount, 'Buy Sold & Stop')
                SlotObject:resetAll()
                self:stop(isAbove, isBuyGrid)
            else
                PosMan:sell(amount, 'Sell & Stop')
                SlotObject:resetAll()
                self:stop(isAbove, isBuyGrid)
            end
        end
        
        Logger:exit()
    end

    function SafetyControl:tradeAndMove(isAbove, isBuyGrid)
        Logger:enter('tradeAndMove')

        local amount = PosMan:getAmount(isAbove, isBuyGrid)

        if isBuyGrid then
            if isAbove then
                Logger:error('Cannot buy and move a buy grid.')
                return
            end

            PosMan:sell(amount, 'Sell Bought & Move')
            SlotObject:resetAll()
            FTT:update(true, true)
            
        else
            if not isAbove then
                Logger:error('Cannot sell and move a sell grid.')
                return
            end
            
            PosMan:buy(amount, 'Buy Sold & Move')
            SlotObject:resetAll()
            FTT:update(true, true)
        end

        Logger:exit()
    end

    function SafetyControl:tradeAndFlip(isAbove, isBuyGrid, triggerPrice)
        Logger:enter('tradeAndFlip')

        local amount = PosMan:getAmount(isAbove, isBuyGrid)

        if isBuyGrid then
            if not isAbove then
                Logger:error('Cannot sell a buy grid and flip it into a buy grid.')
                return
            end
            
            -- we are above a buy grid
            -- we wanted to buy total buy amount using buy grid
            -- we need to buy total buy amount
            -- and flip grid into a sell grid

            -- TODO:
            -- 1. buy total buy amount
            -- 2. flip buy-grid to sell-grid
            -- 3. continue normal FCB
            PosMan:buy(amount, 'Buy & Flip')
            Grid:flip()
            SlotObject:resetAll()
            FTT:setBasePrice(triggerPrice)
            FTT:update(true, true)
            Grid:buildGrid(FTT:getBasePrice())

        else
            if isAbove then
                Logger:error('Cannot buy a sell grid and flip into a sell grid.')
                return
            end
            
            -- we are below a sell grid
            -- we wanted to sell total sell amount using sell grid
            -- we need to sell total sell amount
            -- and flip grid into a buy grid

            -- TODO:
            -- 1. sell total sell amount
            -- 2. flip sell-grid to buy-grid
            -- 3. continue normal FCB
            PosMan:sell(amount, 'Sell & Flip')
            Grid:flip()
            SlotObject:resetAll()
            FTT:setBasePrice(triggerPrice)
            FTT:update(true, true)
            Grid:buildGrid(FTT:getBasePrice())
        end

        Logger:exit()
    end

    function SafetyControl:update()
        Logger:enter('SafetyControl::update')

        if not safeties.enabled then
            return
        end

        -- load SafetyControl settings
        self:load()

        -- Initiate Position Manager
        PosMan:load()
        PosMan:update()
        

        -- get settings
        local buySlots = Grid:getBuyCount()
        local sellSlots = Grid:getSellCount()
        local bp = FTT:getBasePrice()
        local buySpreadMax = Grid:getSpread(buySlots, buySlots)
        local sellSpreadMax = Grid:getSpread(sellSlots, sellSlots)
        local trigger = safeties.trigger
        local cp = CurrentPrice()
        local pos_id = PositionContainer().positionId

        -- set stuff
        local above = {
            sells = {
                mode = safeties.sellSide.above,
                level = AddPerc(bp + sellSpreadMax, trigger)
            },
            buys = {
                mode = safeties.buySide.above,
                level = AddPerc(bp, trigger)
            }                
        }
        local below = {
            sells = {
                mode = safeties.sellSide.below,
                level = SubPerc(bp, trigger)
            },
            buys = {
                mode = safeties.buySide.below,
                level = SubPerc(bp - buySpreadMax, trigger)
            }
        }

        if above.sells.mode != options.MoveInAboveSellActions.None and sellSlots > 0 then
            -- plot
            Plot(0, 'Safety-3.1', above.sells.level, {c = Red, w = 2, id = pos_id})
        end

        if below.sells.mode != options.MoveOutBelowSellActions.None and sellSlots > 0 then
            -- plot
            Plot(0, 'Safety-3.2', below.sells.level, {c = Red, w = 2, id = pos_id})
        end

        if above.buys.mode != options.MoveInAboveBuyActions.None and buySlots > 0 then
            -- plot
            Plot(0, 'Safety-4.1', above.buys.level, {c = Red, w = 2, id = pos_id})
        end

        if below.buys.mode != options.MoveOutBelowBuyActions.None and buySlots > 0 then
            -- plot
            Plot(0, 'Safety-4.2', below.buys.level, {c = Red, w = 2, id = pos_id})
        end

        
        if sellSlots > 0 then

            -- above sell grid
            if cp.high >= above.sells.level then

                -- stop
                if above.sells.mode == options.MoveInAboveSellActions.Stop then
                    SafetyControl:stop(true, false)

                -- buy and stop
                elseif above.sells.mode == options.MoveInAboveSellActions.BuyAndStop then
                    SafetyControl:tradeAndStop(true, false)
                    
                -- buy and move
                elseif above.sells.mode == options.MoveInAboveSellActions.BuyAndMove then
                    SafetyControl:tradeAndMove(true, false)

                end

            -- below sell grid
            elseif cp.low <= below.sells.level then

                -- stop
                if below.sells.mode == options.MoveOutBelowSellActions.Stop then
                    SafetyControl:stop(false, false)

                -- sell and stop
                elseif below.sells.mode == options.MoveOutBelowSellActions.SellAndStop then
                    SafetyControl:tradeAndStop(false, false)

                -- sell and flip
                elseif below.sells.mode == options.MoveOutBelowSellActions.SellAndFlip then
                    SafetyControl:tradeAndFlip(false, false, below.sells.level)

                end
            end
        end

        
        if buySlots > 0 then
            
            -- above buy grid
            if cp.high >= above.buys.level then

                -- stop
                if above.buys.mode == options.MoveInAboveBuyActions.Stop then
                    SafetyControl:stop(true, true)

                -- buy and stop
                elseif above.buys.mode == options.MoveInAboveBuyActions.BuyAndStop then
                    SafetyControl:tradeAndStop(true, true)

                -- buy and flip
                elseif above.buys.mode == options.MoveInAboveBuyActions.BuyAndFlip then
                    SafetyControl:tradeAndFlip(true, true, above.buys.level)

                end

            -- below buy grid
            elseif cp.low <= below.buys.level then

                -- stop
                if below.buys.mode == options.MoveOutBelowBuyActions.Stop then
                    SafetyControl:stop(false, true)

                -- sell and stop
                elseif below.buys.mode == options.MoveOutBelowBuyActions.SellAndStop then
                    SafetyControl:tradeAndStop(false, true)

                -- sell and move
                elseif below.buys.mode == options.MoveOutBelowBuyActions.SellAndMove then
                    SafetyControl:tradeAndMove(false, true)

                end
            end
        end

        -- save SafetyControl settings
        self:save()

        -- save Position Manager
        PosMan:save()

        Logger:exit()
    end


-- =============================================================
-- == Custom Reports

    -- TODO:
    -- * report start settings ???
    -- * report last safety triggered and action used for that
    -- * report current bought/sold (slots filled) and selling/buying (slots waiting for counter-trades) amounts

-- =============================================================
-- == Run function

    local function Run()
        Logger:enter('Run')

        local shouldInit = Load('init', true)

        -- load all slots
        local buys, sells = Grid:getBuyCount(), Grid:getSellCount()
        SlotObject:loadAll(buys, sells)

        -- update Follow The Trend
        local updateFtt = false
        if FTT.KeepFollowing then
            local pos_amt = GetPositionAmount()
            updateFtt = pos_amt <= 0 --MinimumTradeAmount()
        else
            local pos_dir = GetPositionDirection()
            updateFtt = pos_dir == NoPosition
        end
        
        FTT:update(updateFtt)

        -- check if should init
        if shouldInit or FTT.ShouldRebuildGrid then

            Logger:warn('Rebuilding GRID...')
            Grid:buildGrid(FTT:getBasePrice())
            Save('init', false)
        end

        -- update slots
        UpdateSlots(false, FTT.BaseKeyMove)

        -- update safeties
        SafetyControl:update()

        -- save all slots
        SlotObject:saveAll()
    end


-- =============================================================
-- == Main Logic


    -- initialize Grid
    Grid:init()


    -- only if inputs are fine, proceed with update
    if CheckInputs() then

        -- update Start Control system
        StartControl:update()

        -- Run() if we are... running
        if StartControl:isBotRunning() then
            Run()

            Grid:save()

        else
            -- if not running, spam notification msg every 5 mins
            local wtimer = Load('wtimer', Time())

            if wtimer < Time() then
                Logger:warn('Bot is running, but actions halted...')

                Save('wtimer', Time() + 5 * 60)
            end
            

            -- we dont want active orders here
            if IsAnyOrderOpen() then
                SlotObject:cancelAll()
            end

        end
    else
        if IsAnyOrderOpen() then
            SlotObject:cancelAll()
            SlotObject:resetAll()
        end

        -- ¯\_(ツ)_/¯
        DeactivateBot('Cannot run FCB because of bad settings. Please clear the bot, reconfigure and restart.')

    end

8 Comments

Sign in to leave a comment.

U
uwdbDs over 4 years ago

ERROR: Backtest has failed. There is a execution error.
ERROR: Circular reference detected.

Any idea what could cause that? Probably settings, but which one?

P
Paskal over 4 years ago

Please write the approximate settings for this bot, deposit $ 2000

P
pshai over 4 years ago

No idea.. That's the error I hate the most, that's all I know.

P
pshai over 4 years ago

Set "1. Used Currency" to Quote
Set "2.1. Total Buy Amount" to for example 2000
Set "3. Order Size" to for example 20

Now, your setup will create 100 buy orders, all worth 20 bucks. All you need to do next is to setup the grid the way you want it.

B
Burcb over 4 years ago

Hello pshai, great work thank you.
My version is v3.3.39.0 RELEASE, so does that mean this script won't run on my system? I also couldn't find a version 4 of the HTS, even beta version is v3.3.42.0?
Thank you.

P
pshai over 4 years ago

For v4 (beta testing) you will need to join our Discord server. I have not done any tests in v3 for this, so I do not know if it works or doesn't work. That is why I labelled it "v4 only".

A
AbdeezysCEXBots about 4 years ago

It's a nice bot. I like how it works.
The only things I would like on it would be a Stop Loss parameter, and perhaps a take profit value, where the bot would sell any remaining tokens.

The stop loss would allow me to step away from the chart and not worry about a flash crash that drops below my risk level.

Other than that, I really enjoy the simplicity and functionality.

J
JeffVernon almost 4 years ago

:bow: :bow: great script!