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
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.
Enter and exit more positions at lower price points which translates into a more accessible trading strategy.
As one of the easier to understand trading strategies, the grid trading bot is straightforward to use and customizable.
Due to the necessity to enter and exit positions quickly and calculate new grids, the grid trading strategy perfectly compliments bots.
Grid trading bots have the unique advantage of turning a profit in sideways and stagnant markets.
You increase the market liquidity of the exchange's market by placing both buy and sell orders.
You can earn a small but steady profit with minimal risks when you bet on stablecoin pairs.
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.
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.
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.