AUTOMATED TRADING

Unstoppable Grid Trading

Get the most out of your grid trading bot strategy using HaasScript
    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
								
+2.4%
+1.2%
-0.6%
Apr 02, 23, 00:55:49 Bot deactivated
Apr 02, 23, 00:55:49 Backtest start date: 04/02/23 00:55:49 GTM+0
Apr 02, 23, 00:55:49 Backtest end date: 04/02/23 00:55:49 GTM+0
Apr 02, 23, 00:55:49 Backtest took: 00:00:01.6642358ms
Apr 02, 23, 00:55:49 ----- Custom Report -----
Apr 02, 23, 00:55:49 Estimated Profit: 0.01 BTC
Apr 02, 23, 00:55:49 ----- Backtest report BINANCE_BNB_BTC_ -----
Apr 02, 23, 00:55:49 Gross profits: 100.00000000 BNB
Apr 02, 23, 00:55:49 Fee costs: 10.00000000 BNB
Apr 02, 23, 00:55:49 Realized profits: 90.00000000 BNB
Apr 02, 23, 00:55:49 Return on investment: 10.0000 %
Apr 02, 23, 00:55:49 Price change: 0.0306%
Apr 02, 23, 00:55:49 Closed positions: 100x
Apr 02, 23, 00:55:49 Profitable positions: 60x (90.00%)
Apr 02, 23, 00:55:49 Losing positions: 40x (10.00%)
Apr 02, 23, 00:55:49 Average margin: 5.00000000 BNB
Apr 02, 23, 00:55:49 Average realized profits: 10.00000000 BNB
Apr 02, 23, 00:55:49 Executed orders: 100x
Apr 02, 23, 00:55:49 Completed order: 100x
Apr 02, 23, 00:55:49 Average open time: 10 seconds
Apr 02, 23, 00:55:49 ----- Performance report BINANCE_BNB_BTC_ -----
Apr 02, 23, 00:55:49 Max. DrawDown: 1.00% / 40.00000000 BNB
Apr 02, 23, 00:55:49 Sharpe Ratio: 1.00
Apr 02, 23, 00:55:49 Sortino Ratio: 1.00
Apr 02, 23, 00:55:49 Win %: 3.00 %
Apr 02, 23, 00:55:49 Profit Ratio: 3.00
Apr 02, 23, 00:55:49 Profit Factor: 3.00
Apr 02, 23, 00:55:49 CPC Index: 1.00
Apr 02, 23, 00:55:49 Tail Ratio: 2.00
Apr 02, 23, 00:55:49 Common Sense Ratio: 2.00
Apr 02, 23, 00:55:49 Outlier Win Ratio: 3.00
Apr 02, 23, 00:55:49 Outlier Loss Ratio: 1.00
Apr 02, 23, 00:55:49 Profit Margin Ratio: 54.00
Apr 02, 23, 00:55:49 Biggest Win: 100.00000000
Apr 02, 23, 00:55:49 Biggest Loss: -100.00000000
Apr 02, 23, 00:55:49 Highest Point in PNL: 5.00000000
Apr 02, 23, 00:55:49 Lowest Point in PNL: -5.00000000

*These numbers are for illustration only and do not reflect past or future performance.

Our crypto grid trading bot is often referred to as the flash crash trading bot. The grid trading bot will set pre-orders above and below a specified base price. Buy orders are placed at predefined price points that fall below the set base price. Conversely, sell orders are placed above this base price. When a buy order has been completed, the base price will be moved down and a sell order will be placed on the old base price. When a sell order has been completed the base price moves up and a buy order is placed on the old base price.

  • Lower entry point

    Enter and exit more positions at lower price points which translates into a more accessible trading strategy.

  • Ease of use

    As one of the easier to understand trading strategies, the grid trading bot is straightforward to use and customizable.

  • Automation friendly

    Due to the necessity to enter and exit positions quickly and calculate new grids, the grid trading strategy perfectly compliments bots.

  • Sideways markets

    Grid trading bots have the unique advantage of turning a profit in sideways and stagnant markets.

  • Add liquidity

    You increase the market liquidity of the exchange's market by placing both buy and sell orders.

  • Risk management

    You can earn a small but steady profit with minimal risks when you bet on stablecoin pairs.

Frequently Asked Questions

  • What does the grid bot do?

    GRID bot is made to make a profit on a sideways (flat) market by trading between multiple order levels (’grid’).

    The bot will use two currencies of the pair (e.g. BTC / USDT) for placing limit orders for buying and selling and will make a profit in one currency.

    If you choose to make a profit in USDT, the bot will be buying and selling BTC for profit in USDT and vice versa — if you choose BTC, then your bot will be selling it for USDT to buy back cheaper with profit in BTC.

    The bot keeps trading as long as the price stays inside the grid.

    If the price breaks outside, the grid follows it automatically and the bot stays active.

    In other words, the bot makes a profit in USDT on a flat market and follows the price if it moves up, or accumulates Bitcoin on a flat market and follows the price if it goes down.

  • What is the grid bot strategy?

    The driving force of this strategy is a stochastic price fluctuation, also referred to as a ’flat’ or ’sideways’ market.

    When the price is fluctuating in a tight range, you could potentially make an infinite profit by just making trades repeatedly.

    In reality, the price does not stick anywhere for too long, so you need to cover as many levels as possible by splitting your investment into multiple Buy and Sell orders aka ’order grid’.

    You are making profits steadily as long as the price is bouncing inside the grid so that you are buying let’s say 0.1 BTC on each Buy level and selling it at a higher Sell level for profit in USDT.

    The success factors of your strategy are time and volatility — the longer the price stays inside the grid, the longer you keep trading, and the more volatile is the price, the more trades you make.

    Overall, the grid strategy may work well as it is utilizing the only guaranteed market direction — to the right.

  • Is this bot backtest ready?

    We offer a 7-day free trial so you can access all premium features of the GRID bot in live trading mode as well as in the risk-free virtual Demo mode. Moreover, we provide the Backtesting function which is available in the bot editor so you can run the bot in a quick simulation.

Discover alternatives to the Grid Trading Bot

Pro Upgrade1. Completely Free2. Unrestricted Trial3.

Try TradeServer Cloud for 7-days and discover opportunities you've been missing.
Start free 7-day trial ›
1 TradeServer Cloud Pro upgrade is a 7-day trial of a premium subscription. Automatically downgraded to Lite on expiration or non-payment.
2 During the 7-day TradeServer Cloud Pro trial you will not be required to pay to access Pro plan features.
3 Access to premium features within TradeServer Cloud does not include third-party restrictions or unforeseen issues.