[HaasOnline] Flash Crash Bot (v4 only)
stableDescription
The FlashCrashBot recreated in HaasScript is here!
Ugh.. Almost 1900 lines of code! It was a challenge to create this bot, I can tell you that... This bot differs from the original Custom Bot version, as it contains a bit more safeties, a StartControl and "Keep Following" setting for Follow-The-Trend feature! ;) But, it is missing buttons and features to add and remove buy and sell orders. :(
Spread Type, Spread, Total Buy Amount and Total Sell Amount values are not allowed to be changed once the bot has started running! Please setup your bot properly before starting it.
The "Keep Following" feature in FTT will continue following the trend when the counter-buy or -sell orders are all filled and bot is back to its original grid. This is especially useful in prolonged trends then you just want to stay with the price!
The "Start Control" feature allows you to setup and start your bot without laying down any orders until the price has breached above or below the set Trigger Price. This helps you plan ahead!
When trading both sides simultaneously, you cannot use ANY safeties for "When Trigger Above Buys" (trigger above buy-grid) and "When Trigger Below Sells" (trigger below sell-grid). Also the "Buy/Sell and Move" safety types are not allowed for outside the grids when trading both sides simultaneously.
IN CASE you spot ANY issues with the bot, please contact me in Discord or drop a comment below. ALSO NOTE that the script does not work in v3; it was designed for and created with v4.
HaasScript
-- [HaasOnline] Flash Crash Bot
-- Author: pshai
EnableHighSpeedUpdates(true)
HideOrderSettings()
HideTradeAmountSettings()
--===================================================================
-- == Miscellaneous Usefulness
-- deep clone an object
function clone(original)
local copy = {}
for k, v in pairs(original) do
if GetType(v) == ArrayDataType then
v = clone(v)
end
copy[k] = v
end
return copy
end
--===================================================================
-- == Logger
local LoggerLevels = {
ErrorsOnly = 'Errors Only',
ErrorsAndWarnings = 'Erorrs & Warnings',
All = 'All'
}
local Logger = {
_level = InputOptions('DEBUG Level',
LoggerLevels.All,
LoggerLevels,
{group = ' DEBUG'}),
_in = 'Main',
_prevIn = {},
_log = Log,
_warn = LogWarning,
_error = LogError
}
function Logger:level()
if Logger._level == LoggerLevels.ErrorsOnly then
return 0
elseif Logger._level == LoggerLevels.ErrorsAndWarnings then
return 1
elseif Logger._level == LoggerLevels.All then
return 2
end
LogError('Logger level undefined: "' .. Logger._level .. '"')
end
function Logger:where()
local ret = ''
local prevs = self._prevIn
if #prevs > 0 then
for i = 1, #prevs do
if prevs[i] != '' and #prevs[i] > 0 then
ret = ret .. prevs[i] .. '::'
end
end
end
return ret .. self._in
end
function Logger:enter(to)
self._prevIn = ArrayAdd(self._prevIn, self._in)
self._in = to
end
function Logger:exit()
self._in = ArrayLast(self._prevIn)
self._prevIn = ArrayPop(self._prevIn)
end
function Logger:log(msg, color)
if self:level() >= 2 then
if not color then
color = ''
end
local _in = Logger:where()
self._log('['.._in..'] '..msg, color)
end
end
function Logger:warn(msg)
if self:level() >= 1 then
local _in = Logger:where()
self._warn('['.._in..'] '..msg)
end
end
function Logger:error(msg)
local _in = Logger:where()
self._error('['.._in..'] '..msg)
DeactivateBot('FCB Deactivation on Error')
end
--===================================================================
--===================================================================
--===================================================================
-- == Options for inputs
local options = {
SpreadTypes = {
Fixed = 'Fixed Amount',
Percentage = 'Percentage',
PercentageBoost = 'Percentage With Boost',
Exponential = 'Exponential'
},
StartControlTypes = {
AbovePrice = 'Above Price',
BelowPrice = 'Below Price'
},
Currencies = {
Base = 'Base',
Quote = 'Quote'
},
-- above sell grid
MoveInAboveSellActions = {
None = 'None',
Stop = 'Stop',
BuyAndStop = 'Buy Sold & Stop',
BuyAndMove = 'Buy Sold & Move Grid'
},
-- below sell grid
MoveOutBelowSellActions = {
None = 'None',
Stop = 'Stop',
SellAndStop = 'Sell & Stop',
SellAndFlip = 'Sell & Flip to Buy'
},
-- above buy grid
MoveInAboveBuyActions = {
None = 'None',
Stop = 'Stop',
BuyAndStop = 'Buy & Stop',
BuyAndFlip = 'Buy & Flip to Sell'
},
-- below buy grid
MoveOutBelowBuyActions = {
None = 'None',
Stop = 'Stop',
SellAndStop = 'Sell Bought & Stop',
SellAndMove = 'Sell Bought & Move Grid'
},
}
--===================================================================
-- == Input settings
InputGroupHeader('Price Settings')
local basePrice = Input('1. Base Price', 50000)
local spreadType = InputOptions('2.1. Spread Type', options.SpreadTypes.Percentage, options.SpreadTypes)
local spread = Input('2.2. Spread', 1, 'Used with [Fixed Amount], [Percentage] and [Percentage With Boost] spread types.')
local spreadBoost = Input('2.3. Boost %', 0, 'Used with [Percentage With Boost] spread type.')
local spreadMult = Input('2.4. Multiplier', 0, 'Used with [Exponential] spread type.')
local spreadMin = Input('2.5. Min. Spread %', 0, 'Used with [Exponential] spread type.')
local spreadMax = Input('2.6. Max. Spread %', 0, 'Used with [Exponential] spread type.')
InputGroupHeader('Amount Settings')
local usedCurrency = InputOptions('1. Used Currency', options.Currencies.Base, options.Currencies)
local totalBuyAmt = Input('2.1. Total Buy Amount', 0)
local totalSellAmt = Input('2.2. Total Sell Amount', 0)
local orderSize = Input('3. Order Size', 0)
local refillDelay = Input('4. Refill Delay', 0, 'Optional refill delay, set in minutes. Set to zero to disable.')
InputGroupHeader('Start Control Settings')
--[[
Start Control:
- ability to start after price breach (above/below X price)
]]
local sc = {
enabled = Input('1. Enabled', false),
triggerPrice = Input('2. Trigger Price', 0),
type = InputOptions('3. Type', options.StartControlTypes.AbovePrice, options.StartControlTypes)
}
InputGroupHeader('Follow The Trend')
local ftt = {
enabled = Input('1.1. Enabled', false, 'Follow The Trend feature allows the FCB to follow the price moves with a pre-defined channel and an interval.'
..'If the current price is outside the channel at checkup, the grid will be adjusted to new prices. For the ['.. options.SpreadTypes.Fixed ..']'
..'and ['.. options.SpreadTypes.Percentage ..'] spread types, use settings 3.1. and 3.2. and for ['.. options.SpreadTypes.PercentageBoost ..']'
..'and ['.. options.SpreadTypes.Exponential ..'] spread type use settings 4.1. and 4.2.'),
keepFollowing = Input('1.2. Keep Following', false, 'If set to true, FTT will continue updating when slots return to their original state. If false, FTT will only be updated until the very first order has been filled.'),
interval = Input('2. Check Interval', 240),
channelSize = Input('3.1. Channel Size', 0, 'Channel size is measured in slots. Used with [Fixed] and [Percentage] spread type.'),
channelOffset = Input('3.2. Channel Offset', 0, 'Channel offset is measured in slots. Used with [Fixed] and [Percentage] spread type.'),
channelSize2 = Input('4.1. Channel Size %', 0, 'Channel size in percentages. Used with [Percentage With Boost] and [Exponential] spread type.'),
channelOffset2 = Input('4.2. Channel Offset %', 0, 'Channel offset in percentages. Used with [Percentage With Boost] and [Exponential] spread type.')
}
InputGroupHeader('Safeties')
local safeties = {
enabled = Input('1. Enabled', false, 'The safeties allows FCB to move assets in or out from the market. In case of the sell-side grid, the settings 3.1.'
..'and 3.2. control what happens outside the grid and for buy-side grids, the settings 4.1. and 4.2. control what happens on that side. The stops will also cancel all outstanding orders.'),
trigger = Input('2. Trigger Level %', 0),
--[[
when selling;
* above grid can buy-back and stop or re-adjust grid, so it will continue selling after that buy-back, or NONE
* below grid can sell total amount and stop or flip to buying the sold amount, or NONE
]]
sellSide ={
above = InputOptions('3.1. When Trigger Above Sells', options.MoveInAboveSellActions.None, options.MoveInAboveSellActions),
below = InputOptions('3.2. When Trigger Below Sells', options.MoveOutBelowSellActions.None, options.MoveOutBelowSellActions),
},
--[[
when buying;
* above grid can buy total amount and stop or flip to selling the bought amount, or NONE
* below grid can sell-out and stop or re-adjust grid, so it will continue buying after the sell-out, or NONE
]]
buySide ={
above = InputOptions('4.1. When Trigger Above Buys', options.MoveInAboveBuyActions.None, options.MoveInAboveBuyActions),
below = InputOptions('4.2. When Trigger Below Buys', options.MoveOutBelowBuyActions.None, options.MoveOutBelowBuyActions),
}
}
-- =============================================================
-- == Handy helper(s)
local function InputChanged(id, value)
if not value then
return false
end
local oldValue = Load(id, value)
Save(id, value)
return oldValue != value
end
-- ===================================================================
-- == Check correct input settings
local function CheckInputs()
Logger:enter('Settings_Check')
local isOK = true
if InputChanged('spreadType', spreadType) or InputChanged('spread', spread) then
Logger:enter('Price_Settings')
Logger:error('Cannot change spread settings while bot is running!')
Logger:exit()
isOK = false
end
if InputChanged('totalBuyAmt', totalBuyAmt) or InputChanged('totalSellAmount', totalSellAmt) then
Logger:enter('Price_Settings')
Logger:error('Cannot change amount settings while bot is running!')
Logger:exit()
isOK = false
end
if ftt.enabled then
Logger:enter('Ftt_Settings')
if totalBuyAmt > 0 and totalSellAmt > 0 then
Logger:error('Cannot use FTT when bot is setup to trade both sides.')
isOK = false
elseif spreadType == options.SpreadTypes.PercentageBoost
or spreadType == options.SpreadTypes.Exponential
then
-- YES, there is an error for FTT + these grid types, even though
-- FTT has implementation for these. These just doesn't work together,
-- no matter how hard you try, unless you ready for that insane spam
-- of cancel and replace of orders...
Logger:error('Cannot use FTT with spread type ['.. spreadType ..'].')
isOK = false
end
Logger:exit()
end
if safeties.enabled then
Logger:enter('Safety_Settings')
if totalBuyAmt > 0 and totalSellAmt > 0 then
if safeties.sellSide.below != options.MoveOutBelowSellActions.None then
Logger:error('Cannot use "'.. safeties.sellSide.below ..'" (or any other) when trading both sides.')
isOK = false
end
if safeties.buySide.above != options.MoveInAboveBuyActions.None then
Logger:error('Cannot use "'.. safeties.buySide.above ..'" (or any other) when trading both sides.')
isOK = false
end
if safeties.sellSide.above == options.MoveInAboveSellActions.BuyAndMove then
Logger:error('Cannot use "'.. safeties.sellSide.above ..'" when trading both sides.')
isOK = false
end
if safeties.buySide.below == options.MoveOutBelowBuyActions.SellAndMove then
Logger:error('Cannot use "'.. safeties.buySide.above ..'" when trading both sides.')
isOK = false
end
Logger:exit()
end
end
if isOK then
if Load('ci:init', true) then
LogWarning('-----------------------------------------------------------')
LogWarning('You have been warned!')
LogWarning('Make sure you have all set before starting the bot!')
LogWarning('The bot will not like that and will break if you do so.')
LogWarning('Do NOT change settings while the bot is running!')
LogWarning('!! WARNING !!')
LogWarning('-----------------------------------------------------------')
Save('ci:init', false)
end
end
Logger:exit()
return isOK
end
--===================================================================
--===================================================================
-- =============================================================
-- == Enumerations
local enums = {
SlotTypes = {
Empty = 0,
Buy = 1,
Sell = 2
}
}
-- =============================================================
-- == Start Control
local StartControl = {
IsRunning = false
}
function StartControl:update()
self:load()
local isRunning = self.IsRunning
if isRunning and InputChanged('sc.enabled', sc.enabled) and sc.enabled then
isRunning = false
Logger:warn('Start Control enabled: halting bot and cancelling orders...')
elseif not sc.enabled and not isRunning then
isRunning = true
Logger:warn('Bot starting...')
elseif not isRunning then
local cp = CurrentPrice()
local tp = sc.triggerPrice
local type = sc.type
if (cp.close > tp and type == options.StartControlTypes.AbovePrice)
or (cp.close < tp and type == options.StartControlTypes.BelowPrice)
then
isRunning = true
Logger:warn('Trigger price breached, starting...')
end
end
self.IsRunning = isRunning
self:save()
end
function StartControl:isBotRunning()
return self.IsRunning
end
function StartControl:save()
Save('sc:ir', self.IsRunning)
end
function StartControl:load()
self.IsRunning = Load('sc:ir', false)
end
-- =============================================================
-- == Follow The Trend (FTT)
local FTT = {
BasePrice = basePrice,
High = -1,
Low = -1,
LastUpdate = -1,
ShouldRebuildGrid = false,
BaseKeyMove = 0,
KeepFollowing = ftt.keepFollowing,
Grid = nil
}
function FTT:load()
self.BasePrice = Load('ftt:bp', basePrice)
self.High = Load('ftt:h', 0)
self.Low = Load('ftt:l', 0)
self.LastUpdate = Load('ftt:t', 0)
end
function FTT:save()
Save('ftt:bp', self.BasePrice)
Save('ftt:h', self.High)
Save('ftt:l', self.Low)
Save('ftt:t', self.LastUpdate)
end
function FTT:getBasePrice()
return self.BasePrice
end
function FTT:setBasePrice(newPrice)
self.BasePrice = newPrice
end
function FTT:getChannel()
local st = spreadType
local offset = 0
local size = 0
local spread = 0
if st == options.SpreadTypes.Fixed
or st == options.SpreadTypes.Percentage
then
-- get channel offset and size based on fixed grid
spread = self.Grid:getSpread(1)
offset = spread * ftt.channelOffset
size = spread * ftt.channelSize
elseif st == options.SpreadTypes.PercentageBoost
or st == options.SpreadTypes.Exponential
then
-- get channel offset and size based on percentages
local cp = CurrentPrice()
offset = SubPerc(cp.close, 100 - ftt.channelOffset2)
size = SubPerc(cp.close, 100 - ftt.channelSize2)
end
return spread, offset, size
end
function FTT:calculateChannel(offset, size)
local bp = self.BasePrice
if totalBuyAmt > 0 then
self.High = bp + offset + size
self.Low = bp + offset
elseif totalSellAmt > 0 then
self.High = bp - offset
self.Low = bp - offset - size
end
end
function FTT:plot()
local h = self.High
local l = self.Low
local bp = self.BasePrice
if ftt.enabled and h > 0 and l > 0 then
PlotBands(
Plot(0, 'Ftt:High', h, Cyan),
Plot(0, 'Ftt:Low', l, Cyan),
SkyBlue(10)
)
end
if bp > 0 then
Plot(0, 'Ftt:BasePrice', bp, Orange)
end
end
function FTT:update(isNoPos, force)
Logger:enter('FTT')
self:load()
self:plot()
if not ftt.enabled and not force then
self:save()
Logger:exit()
return
end
self.ShouldRebuildGrid = false
if not isNoPos then
self:save()
Logger:exit()
return
end
if not force and Time() < self.LastUpdate + ftt.interval * 60 then
self:save()
Logger:exit()
return
end
local spread, offset, size = self:getChannel()
self:calculateChannel(offset, size)
-- dont mess with timing if we forced update
if not force then
self.LastUpdate = Time()
end
local cp = CurrentPrice()
self.BaseKeyMove = 0
-- update?
if cp.high > self.High then
-- move grid up
if spread > 0 then
-- if spread is set, we are using fixed/percentage grid
while self.High < cp.high do
self.BasePrice = self.BasePrice + spread -- move BP by 1 step
self:calculateChannel(offset, size) -- recalc channel
self.BaseKeyMove = self.BaseKeyMove - 1
end
else
-- spread was not set, assuming boosted/exponential grid
self.BasePrice = cp.close -- we have no fixed steps so... just update BP
self:calculateChannel(offset, size) -- recalc channel
end
self.ShouldRebuildGrid = true
elseif cp.low < self.Low then
-- move grid down
if spread > 0 then
-- if spread is set, we are using fixed/percentage grid
local bp = self.BasePrice
while self.Low > cp.low do
self.BasePrice = self.BasePrice - spread -- move BP by 1 step
self:calculateChannel(offset, size) -- recalc channel
self.BaseKeyMove = self.BaseKeyMove + 1
end
else
-- spread was not set, assuming boosted/exponential grid
self.BasePrice = cp.close -- we have no fixed steps so... just update BP
self:calculateChannel(offset, size) -- recalc channel
end
self.ShouldRebuildGrid = true
end
self:save()
Logger:exit()
end
-- =============================================================
-- == Orderbook builder
local Grid = {
Loaded = false,
IfFlipped = false,
TotalBuyAmount = 0,
TotalSellAmount = 0,
BuySlotCount = 0,
SellSlotCount = 0,
PriceArray = Load('gps', {}),
}
-- since we now know what "Grid" is (as a variable),
-- we need to tell that to the FTT module.
-- And believe me; I know what you are thinking!
FTT.Grid = Grid
---------------------------
function Grid:load()
if self.Loaded then
return
end
self.IsFlipped = Load('grid:if', false)
self.TotalBuyAmount = Load('grid:tba', totalBuyAmt)
self.TotalSellAmount = Load('grid:tsa', totalSellAmt)
self.BuySlotCount = (self.TotalBuyAmount > 0 and orderSize > 0) and ArrayGet(Floor(self.TotalBuyAmount / orderSize), 1) or 0
self.SellSlotCount = (self.TotalSellAmount > 0 and orderSize > 0) and ArrayGet(Floor(self.TotalSellAmount / orderSize), 1) or 0
self.Loaded = true
end
function Grid:save()
Save('grid:if', self.IsFlipped)
Save('grid:tba', self.TotalBuyAmount)
Save('grid:tsa', self.TotalSellAmount)
end
function Grid:getTotalBuy()
if not self.Loaded then
Logger:error('Grid not initialized')
end
return self.IsFlipped and self.TotalSellAmount or self.TotalBuyAmount
end
function Grid:getTotalSell()
if not self.Loaded then
Logger:error('Grid not initialized')
end
return self.IsFlipped and self.TotalBuyAmount or self.TotalSellAmount
end
function Grid:getBuyCount()
if not self.Loaded then
Logger:error('Grid not initialized')
end
return self.IsFlipped and self.SellSlotCount or self.BuySlotCount
end
function Grid:getSellCount()
if not self.Loaded then
Logger:error('Grid not initialized')
end
return self.IsFlipped and self.BuySlotCount or self.SellSlotCount
end
-- Used to add input buttons with callbacks
function Grid:init()
-- There was some magic here, aaaaand it's gone.
self:load()
end
function Grid:flip()
self.IsFlipped = not self.IsFlipped
end
function Grid:getSpread(index, totalOrders)
local st = spreadType
local bp = basePrice
local spr = spread
local boost = spreadBoost
local mult = spreadMult
local min = spreadMin
local max = spreadMax
local ret = 0
-- Fixed spread
if st == options.SpreadTypes.Fixed then
ret = spr * index
-- Percentage-based spread
elseif st == options.SpreadTypes.Percentage then
ret = bp / 100 * spr * index
-- Percentage with boost addition
elseif st == options.SpreadTypes.PercentageBoost then
ret = bp / 100 * (spr + boost * index) * index
-- Exponential spread
elseif st == options.SpreadTypes.Exponential then
-- helper function to calculate linear interpolation
local function lerp(x, y, a)
return x + (y - x) * a
end
-- do linear interpolation between min and max
-- but using an exponential alpha value
ret = bp / 100 * lerp(min, max, Pow(index / totalOrders, mult))
end
return ret
end
-- function to build grid prices into an array
function Grid:buildGrid(bp)
--local bp = basePrice
local spr, count
local newArr = {}
-- init array
for i=1, 200 do
newArr[i] = -1
end
-- set base price in the middle
newArr[100] = bp
-- buy prices
count = self:getBuyCount()
if count > 0 then
for i = 1, count do
spr = Grid:getSpread(i, count)
newArr[100 + i] = bp - spr
end
end
-- sell prices
count = self:getSellCount()
if count > 0 then
for i = 1, count do
spr = Grid:getSpread(i, count)
newArr[100 - i] = bp + spr
end
end
Save('gps', newArr)
self.PriceArray = newArr
end
-- =============================================================
-- == Slot Object
local SlotObject = {
Index = 0,
OrderId = '',
InUse = false,
IsActive = false,
CurrentType = enums.SlotTypes.Empty,
DefaultType = enums.SlotTypes.Empty,
RefillTime = 0,
FilledAmount = 0
--[[
TODO:
* slots have to have their own corresponding position!
* create new position for "entries" and close them with "exits"
* this way dont have to fuck about with fees...
]]
}
local SlotObjectLists = {
Init = false,
Buy = {},
Sell = {}
}
function SlotObject:load(index)
Logger:enter('load')
local obj = clone(self)
local type = enums.SlotTypes.Empty
if index > 0 then
type = enums.SlotTypes.Buy
Logger:log('Load buy slot at index ' .. index)
elseif index < 0 then
type = enums.SlotTypes.Sell
Logger:log('Load sell slot at index ' .. index)
end
obj.Index = Load(index .. 'i', index)
obj.OrderId = Load(index .. 'oid', '')
obj.InUse = Load(index .. 'iu', false)
obj.CurrentType = Load(index .. 'ct', type)
obj.DefaultType = Load(index .. 'dt', type)
obj.RefillTime = Load(index .. 'rt', 0)
obj.FilledAmount = Load(index .. 'fa', 0)
Logger:exit()
return obj
end
function SlotObject:save(index)
Logger:enter('save')
if not self then
Logger:error('Cannot use save() as function; use as method.')
else
if not index then
Logger:error('SlotObject index is not set.')
Logger:exit()
return
end
end
-- DO NOT save the slot object itself!
-- ONLY save its values!
Save(index .. 'i', self.Index)
Save(index .. 'oid', self.OrderId)
Save(index .. 'iu', self.InUse)
Save(index .. 'ct', self.CurrentType)
Save(index .. 'dt', self.DefaultType)
Save(index .. 'rt', self.RefillTime)
Save(index .. 'fa', self.FilledAmount)
Logger:log('SlotObject on index ' .. index .. ' was saved.')
Logger:exit()
end
function SlotObject:loadAll(buys, sells)
if SlotObjectLists.Init then
return
end
Logger:enter('loadAll')
local buySlots = {}
local sellSlots = {}
for i = 1, buys do
buySlots[#buySlots + 1] = SlotObject:load(i)
end
for i = 1, sells do
sellSlots[#sellSlots + 1] = SlotObject:load(-i)
end
SlotObjectLists.Buy = buySlots
SlotObjectLists.Sell = sellSlots
SlotObjectLists.Init = true
Logger:exit()
end
function SlotObject:reset()
self.CurrentType = self.DefaultType
self:check(true)
end
function SlotObject:resetAll()
Logger:enter('resetAll')
local buySlots = SlotObjectLists.Buy
local sellSlots = SlotObjectLists.Sell
local buys = #buySlots
local sells = #sellSlots
for i=1, buys do
SlotObject:get(i):reset()
end
for i=1, sells do
SlotObject:get(-i):reset()
end
Logger:exit()
end
function SlotObject:saveAll()
Logger:enter('saveAll')
if not SlotObjectLists.Init then
Logger:error('SlotObjectLists not initialized.')
Logger:exit()
return
end
local buySlots = SlotObjectLists.Buy
local sellSlots = SlotObjectLists.Sell
local buys = #buySlots
local sells = #sellSlots
for i=1, buys do
SlotObject:get(i):save(i)
end
for i=1, sells do
SlotObject:get(-i):save(-i)
end
Logger:exit()
end
function SlotObject:get(index)
Logger:enter('get')
if not SlotObjectLists.Init then
Logger:error('SlotObjectLists not initialized.')
Logger:exit()
return nil
end
if index > 0 then
local listLen = #SlotObjectLists.Buy
if Grid:getBuyCount() > listLen then
local slot = SlotObject:load(index)
SlotObjectLists.Buy[listLen + 1] = slot
Logger:exit()
return slot
end
Logger:exit()
return SlotObjectLists.Buy[index]
elseif index < 0 then
local listLen = #SlotObjectLists.Sell
if Grid:getSellCount() > listLen then
local slot = SlotObject:load(index)
SlotObjectLists.Sell[listLen + 1] = slot
Logger:exit()
return slot
end
Logger:exit()
return SlotObjectLists.Sell[-index]
end
Logger:error('Cannot get() an index of '..index)
Logger:exit()
return nil
end
function SlotObject:processFill(amt)
Logger:enter('processFill')
local type = self.CurrentType
local dtype = self.DefaultType
if type == dtype then
self.FilledAmount = amt
else
self.FilledAmount = 0
end
if type == enums.SlotTypes.Buy then
self.Index = self.Index - 1
self.CurrentType = enums.SlotTypes.Sell
Logger:log('Buy order filled, preparing to place counter-sell... (filled order: "'..self.OrderId..'")')
elseif type == enums.SlotTypes.Sell then
self.Index = self.Index + 1
self.CurrentType = enums.SlotTypes.Buy
Logger:log('Sell order filled, preparing to place counter-buy... (filled order: "'..self.OrderId..'")')
end
if refillDelay > 0 then
self.RefillTime = Time() + refillDelay * 60
end
Logger:exit()
end
function SlotObject:plotOrder(order)
local ctype = self.CurrentType
local dtype = self.DefaultType
local oid = order.orderId
local price = order.price
local color = ''
local name = ''
if ctype == enums.SlotTypes.Buy then
if ctype == dtype then
color = Green
else
color = Yellow
end
name = 'Buy-' .. self.Index
elseif ctype == enums.SlotTypes.Sell then
if ctype == dtype then
color = Red
else
color = Yellow
end
name = 'Sell-' .. self.Index
end
Plot(0, name, price, {c = color, id = oid})
Logger:log(name .. ' is open order at ' .. price)
end
function SlotObject:check(shouldCancel)
Logger:enter('check')
local oid = self.OrderId
if oid != '' then
local order = OrderContainer(oid)
if order.isOpen then
if shouldCancel then
CancelOrder(oid)
end
self:plotOrder(order)
else
if order.isFilled then
self:processFill(order.filledAmount)
elseif order.isCancelled then
-- nothing to do.
Logger:log('Order was cancelled, preparing to replace...')
end
oid = ''
end
end
self.OrderId = oid
self.InUse = oid != ''
Logger:exit()
return oid == ''
end
function SlotObject:getPrice()
Logger:enter('getPrice')
local index = self.Index
local bp = FTT:getBasePrice()
local price = -1
if index == 0 then
price = bp
else
local buys = Grid:getBuyCount()
local sells = Grid:getSellCount()
if buys > 0 and index > buys then
Logger:error('Trying to place buy order out of grid.')
Logger:exit()
return nil
end
if sells > 0 and index < -sells then
Logger:error('Trying to place sell order out of grid.')
Logger:exit()
return nil
end
price = Grid.PriceArray[100 + index]
end
Logger:log('Price for index ' .. index .. ': ' .. price)
Logger:exit()
return price
end
function SlotObject:getAmount(price)
Logger:enter('getAmount')
if not price then
Logger:error('Developer error: cannot calculate order size.')
Logger:exit()
return
end
local amount = orderSize
if self.CurrentType != self.DefaultType and self.FilledAmount > 0 then
return self.FilledAmount
end
if usedCurrency == options.Currencies.Quote then
amount = amount / price
end
Logger:exit()
return ParseTradeAmount('', price, amount)
end
function SlotObject:execute()
Logger:enter('execute')
-- wait for refill delay
if refillDelay > 0 and self.RefillTime >= Time() then
Logger:exit()
return
end
--local price = ParseTradePrice('', self:getPrice())
local price = self:getPrice()
local amount = self:getAmount(price)
local ctype = self.CurrentType
if ctype == enums.SlotTypes.Buy then
-- place buy order
Logger:log('Placing BUY order at ' .. price)
self.OrderId = PlaceBuyOrder(price, amount, {type = NoTimeOutOrderType, note = 'Buy-'..self.Index})
elseif ctype == enums.SlotTypes.Sell then
-- place sell order
Logger:log('Placing SELL order at ' .. price)
self.OrderId = PlaceSellOrder(price, amount, {type = NoTimeOutOrderType, note = 'Sell-'..self.Index})
end
Logger:exit()
end
function SlotObject:shiftAll(moveKey)
Logger:enter('shiftAll')
local up = moveKey < 0
local down = moveKey > 0
local steps = Abs(moveKey)
local buySlots = SlotObjectLists.Buy
local sellSlots = SlotObjectLists.Sell
local buys = #buySlots
local sells = #sellSlots
Logger:log('attempting to shift grid (buys: '..buys..' | sells: '..sells..')')
if buys > 0 then
while steps > 0 do
Logger:log('steps: '..steps)
if up then
for i = 1, buys do
local slot = buySlots[i]
slot.Index = slot.Index + 1
if slot.Index > buys then
if slot.OrderId != '' then
CancelOrder(slot.OrderId)
slot.OrderId = ''
end
end
end
-- discard the last cell
buySlots = ArrayPop(buySlots)
local newSlot = SlotObject:load(9999)
newSlot.Index = 1
buySlots = ArrayUnshift(buySlots, newSlot)
elseif down then
for i = buys, 1, -1 do
local slot = buySlots[i]
slot.Index = slot.Index - 1
if slot.Index < 1 then
if slot.OrderId != '' then
CancelOrder(slot.OrderId)
slot.OrderId = ''
end
end
end
-- discard the first cell
buySlots = ArrayShift(buySlots)
local newSlot = SlotObject:load(9999)
newSlot.Index = buys
buySlots = ArrayAdd(buySlots, newSlot)
end
steps = steps - 1
end
end
if sells > 0 then
while steps > 0 do
Logger:log('steps: '..steps)
if up then
for i = 1, sells do
local slot = sellSlots[i]
slot.Index = slot.Index + 1
if slot.Index > -1 then
if slot.OrderId != '' then
CancelOrder(slot.OrderId)
slot.OrderId = ''
end
end
end
-- discard the first cell
sellSlots = ArrayShift(sellSlots)
local newSlot = SlotObject:load(-9999)
newSlot.Index = -sells
sellSlots = ArrayAdd(sellSlots, newSlot)
elseif down then
for i = sells, 1, -1 do
local slot = sellSlots[i]
slot.Index = slot.Index - 1
if slot.Index < -sells then
if slot.OrderId != '' then
CancelOrder(slot.OrderId)
slot.OrderId = ''
end
end
end
-- discard the last cell
sellSlots = ArrayPop(sellSlots)
local newSlot = SlotObject:load(-9999)
newSlot.Index = -1
sellSlots = ArrayUnshift(sellSlots, newSlot)
end
steps = steps - 1
end
end
SlotObjectLists.Buy = buySlots
SlotObjectLists.Sell = sellSlots
Logger:exit()
end
function SlotObject:cancelAll()
Logger:enter('SlotObject:cancelAll')
local buys = Grid:getBuyCount()
local sells = Grid:getSellCount()
-- buy orders on the positive indices
if buys > 0 then
for i=1, buys do
-- get loaded slot
local slot = SlotObject:get(i)
-- cancel order
slot:check(true)
end
end
-- sell orders on the negative indices
if sells > 0 then
for i=1, sells do
-- get slot
local slot = SlotObject:get(-i)
-- cancel order
slot:check(true)
end
end
Logger:exit()
end
local function UpdateSlots(cancelOrders, baseKeyMove)
Logger:enter('UpdateSlots')
-- slot counts
local buys = Grid:getBuyCount()
local sells = Grid:getSellCount()
-- shift grid is necessary
if baseKeyMove and baseKeyMove != 0 then
if IsAnyOrderOpen() then
Logger:log('Move grid by: ' .. baseKeyMove)
SlotObject:shiftAll(baseKeyMove)
else
Logger:log('No need to shift grid; no orders open')
end
end
-- buy orders on the positive indices
if buys > 0 then
for i=1, buys do
-- get loaded slot
local slot = SlotObject:get(i)
-- check order state and update info
if slot:check(cancelOrders) then
-- create order if none open
slot:execute()
end
end
-- get rid of excessive orders
local list = SlotObjectLists.Buy
local listLen = #list
if buys < listLen then
while buys < listLen do
local slot = SlotObject:get(listLen)
slot:check(true)
slot:reset()
list[listLen] = nil
listLen = #list
end
end
end
-- sell orders on the negative indices
if sells > 0 then
for i=1, sells do
-- get slot
local slot = SlotObject:get(-i)
-- check
if slot:check(cancelOrders) then
-- execute order
slot:execute()
end
end
-- get rid of excessive orders
local list = SlotObjectLists.Sell
local listLen = #list
if sells < listLen then
while sells < listLen do
local slot = SlotObject:get(-listLen)
slot:check(true)
slot:reset()
list[listLen] = nil
listLen = #list
end
end
end
Logger:exit()
end
-- =============================================================
-- == Position Manager
local PosMan = {
Soid = '', -- sell order id
Boid = '', -- buy order id
Note = ''
}
function PosMan:load()
self.Soid = Load('posman:soid', '')
self.Boid = Load('posman:boid', '')
self.Note = Load('posman:n', '')
end
function PosMan:save()
Save('posman:soid', self.Soid)
Save('posman:boid', self.Boid)
Save('posman:n', self.Note)
end
function PosMan:hasPosition(pid)
return GetPositionDirection(pid or '') != NoPosition
end
function PosMan:getAmount(isAbove, isBuyGrid)
if isAbove and isBuyGrid then
return Grid:getTotalBuy()
elseif not isAbove and not isBuyGrid then
return Grid:getTotalSell()
else
return GetPositionAmount()
end
return nil -- should not happen
end
-- dump
function PosMan:sell(amount, note)
local cp = CurrentPrice()
if self.Soid == '' then
self.Note = note
self.Soid = PlaceSellOrder(cp.bid, amount, {type = MarketOrderType, note = note})
else
Logger:log('PosMan already has open sell order.')
end
end
-- buy back
function PosMan:buy(amount, note)
local cp = CurrentPrice()
if self.Boid == '' then
self.Note = note
self.Boid = PlaceBuyOrder(cp.ask, amount, {type = MarketOrderType, note = note})
else
Logger:log('PosMan already has open buy order.')
end
end
function PosMan:update()
if self.Soid != '' and not IsOrderOpen(self.Soid) then
self.Soid = ''
end
if self.Boid != '' and not IsOrderOpen(self.Boid) then
self.Boid = ''
end
end
-- =============================================================
-- == Safeties
local SafetyControl = {}
function SafetyControl:load()
end
function SafetyControl:save()
end
function SafetyControl:stop(isAbove, isBuyGrid)
Logger:enter('stop')
-- build msg
local part1 = isAbove and 'above ' or 'below '
local part2 = isBuyGrid and 'Buy ' or 'Sell '
local msg = 'SafetyControl: Stop triggered '
.. part1
.. part2
.. 'grid.'
-- cancel all slots
SlotObject:cancelAll()
-- deactivate
DeactivateBot(msg, false)
Logger:exit()
end
function SafetyControl:tradeAndStop(isAbove, isBuyGrid)
Logger:enter('tradeAndStop')
local amount = PosMan:getAmount(isAbove, isBuyGrid)
if isBuyGrid then
if isAbove then
PosMan:buy(amount, 'Buy & Stop')
SlotObject:resetAll()
self:stop(isAbove, isBuyGrid)
else
PosMan:sell(amount, 'Sell Bought & Stop')
SlotObject:resetAll()
self:stop(isAbove, isBuyGrid)
end
else
if isAbove then
PosMan:buy(amount, 'Buy Sold & Stop')
SlotObject:resetAll()
self:stop(isAbove, isBuyGrid)
else
PosMan:sell(amount, 'Sell & Stop')
SlotObject:resetAll()
self:stop(isAbove, isBuyGrid)
end
end
Logger:exit()
end
function SafetyControl:tradeAndMove(isAbove, isBuyGrid)
Logger:enter('tradeAndMove')
local amount = PosMan:getAmount(isAbove, isBuyGrid)
if isBuyGrid then
if isAbove then
Logger:error('Cannot buy and move a buy grid.')
return
end
PosMan:sell(amount, 'Sell Bought & Move')
SlotObject:resetAll()
FTT:update(true, true)
else
if not isAbove then
Logger:error('Cannot sell and move a sell grid.')
return
end
PosMan:buy(amount, 'Buy Sold & Move')
SlotObject:resetAll()
FTT:update(true, true)
end
Logger:exit()
end
function SafetyControl:tradeAndFlip(isAbove, isBuyGrid, triggerPrice)
Logger:enter('tradeAndFlip')
local amount = PosMan:getAmount(isAbove, isBuyGrid)
if isBuyGrid then
if not isAbove then
Logger:error('Cannot sell a buy grid and flip it into a buy grid.')
return
end
-- we are above a buy grid
-- we wanted to buy total buy amount using buy grid
-- we need to buy total buy amount
-- and flip grid into a sell grid
-- TODO:
-- 1. buy total buy amount
-- 2. flip buy-grid to sell-grid
-- 3. continue normal FCB
PosMan:buy(amount, 'Buy & Flip')
Grid:flip()
SlotObject:resetAll()
FTT:setBasePrice(triggerPrice)
FTT:update(true, true)
Grid:buildGrid(FTT:getBasePrice())
else
if isAbove then
Logger:error('Cannot buy a sell grid and flip into a sell grid.')
return
end
-- we are below a sell grid
-- we wanted to sell total sell amount using sell grid
-- we need to sell total sell amount
-- and flip grid into a buy grid
-- TODO:
-- 1. sell total sell amount
-- 2. flip sell-grid to buy-grid
-- 3. continue normal FCB
PosMan:sell(amount, 'Sell & Flip')
Grid:flip()
SlotObject:resetAll()
FTT:setBasePrice(triggerPrice)
FTT:update(true, true)
Grid:buildGrid(FTT:getBasePrice())
end
Logger:exit()
end
function SafetyControl:update()
Logger:enter('SafetyControl::update')
if not safeties.enabled then
return
end
-- load SafetyControl settings
self:load()
-- Initiate Position Manager
PosMan:load()
PosMan:update()
-- get settings
local buySlots = Grid:getBuyCount()
local sellSlots = Grid:getSellCount()
local bp = FTT:getBasePrice()
local buySpreadMax = Grid:getSpread(buySlots, buySlots)
local sellSpreadMax = Grid:getSpread(sellSlots, sellSlots)
local trigger = safeties.trigger
local cp = CurrentPrice()
local pos_id = PositionContainer().positionId
-- set stuff
local above = {
sells = {
mode = safeties.sellSide.above,
level = AddPerc(bp + sellSpreadMax, trigger)
},
buys = {
mode = safeties.buySide.above,
level = AddPerc(bp, trigger)
}
}
local below = {
sells = {
mode = safeties.sellSide.below,
level = SubPerc(bp, trigger)
},
buys = {
mode = safeties.buySide.below,
level = SubPerc(bp - buySpreadMax, trigger)
}
}
if above.sells.mode != options.MoveInAboveSellActions.None and sellSlots > 0 then
-- plot
Plot(0, 'Safety-3.1', above.sells.level, {c = Red, w = 2, id = pos_id})
end
if below.sells.mode != options.MoveOutBelowSellActions.None and sellSlots > 0 then
-- plot
Plot(0, 'Safety-3.2', below.sells.level, {c = Red, w = 2, id = pos_id})
end
if above.buys.mode != options.MoveInAboveBuyActions.None and buySlots > 0 then
-- plot
Plot(0, 'Safety-4.1', above.buys.level, {c = Red, w = 2, id = pos_id})
end
if below.buys.mode != options.MoveOutBelowBuyActions.None and buySlots > 0 then
-- plot
Plot(0, 'Safety-4.2', below.buys.level, {c = Red, w = 2, id = pos_id})
end
if sellSlots > 0 then
-- above sell grid
if cp.high >= above.sells.level then
-- stop
if above.sells.mode == options.MoveInAboveSellActions.Stop then
SafetyControl:stop(true, false)
-- buy and stop
elseif above.sells.mode == options.MoveInAboveSellActions.BuyAndStop then
SafetyControl:tradeAndStop(true, false)
-- buy and move
elseif above.sells.mode == options.MoveInAboveSellActions.BuyAndMove then
SafetyControl:tradeAndMove(true, false)
end
-- below sell grid
elseif cp.low <= below.sells.level then
-- stop
if below.sells.mode == options.MoveOutBelowSellActions.Stop then
SafetyControl:stop(false, false)
-- sell and stop
elseif below.sells.mode == options.MoveOutBelowSellActions.SellAndStop then
SafetyControl:tradeAndStop(false, false)
-- sell and flip
elseif below.sells.mode == options.MoveOutBelowSellActions.SellAndFlip then
SafetyControl:tradeAndFlip(false, false, below.sells.level)
end
end
end
if buySlots > 0 then
-- above buy grid
if cp.high >= above.buys.level then
-- stop
if above.buys.mode == options.MoveInAboveBuyActions.Stop then
SafetyControl:stop(true, true)
-- buy and stop
elseif above.buys.mode == options.MoveInAboveBuyActions.BuyAndStop then
SafetyControl:tradeAndStop(true, true)
-- buy and flip
elseif above.buys.mode == options.MoveInAboveBuyActions.BuyAndFlip then
SafetyControl:tradeAndFlip(true, true, above.buys.level)
end
-- below buy grid
elseif cp.low <= below.buys.level then
-- stop
if below.buys.mode == options.MoveOutBelowBuyActions.Stop then
SafetyControl:stop(false, true)
-- sell and stop
elseif below.buys.mode == options.MoveOutBelowBuyActions.SellAndStop then
SafetyControl:tradeAndStop(false, true)
-- sell and move
elseif below.buys.mode == options.MoveOutBelowBuyActions.SellAndMove then
SafetyControl:tradeAndMove(false, true)
end
end
end
-- save SafetyControl settings
self:save()
-- save Position Manager
PosMan:save()
Logger:exit()
end
-- =============================================================
-- == Custom Reports
-- TODO:
-- * report start settings ???
-- * report last safety triggered and action used for that
-- * report current bought/sold (slots filled) and selling/buying (slots waiting for counter-trades) amounts
-- =============================================================
-- == Run function
local function Run()
Logger:enter('Run')
local shouldInit = Load('init', true)
-- load all slots
local buys, sells = Grid:getBuyCount(), Grid:getSellCount()
SlotObject:loadAll(buys, sells)
-- update Follow The Trend
local updateFtt = false
if FTT.KeepFollowing then
local pos_amt = GetPositionAmount()
updateFtt = pos_amt <= 0 --MinimumTradeAmount()
else
local pos_dir = GetPositionDirection()
updateFtt = pos_dir == NoPosition
end
FTT:update(updateFtt)
-- check if should init
if shouldInit or FTT.ShouldRebuildGrid then
Logger:warn('Rebuilding GRID...')
Grid:buildGrid(FTT:getBasePrice())
Save('init', false)
end
-- update slots
UpdateSlots(false, FTT.BaseKeyMove)
-- update safeties
SafetyControl:update()
-- save all slots
SlotObject:saveAll()
end
-- =============================================================
-- == Main Logic
-- initialize Grid
Grid:init()
-- only if inputs are fine, proceed with update
if CheckInputs() then
-- update Start Control system
StartControl:update()
-- Run() if we are... running
if StartControl:isBotRunning() then
Run()
Grid:save()
else
-- if not running, spam notification msg every 5 mins
local wtimer = Load('wtimer', Time())
if wtimer < Time() then
Logger:warn('Bot is running, but actions halted...')
Save('wtimer', Time() + 5 * 60)
end
-- we dont want active orders here
if IsAnyOrderOpen() then
SlotObject:cancelAll()
end
end
else
if IsAnyOrderOpen() then
SlotObject:cancelAll()
SlotObject:resetAll()
end
-- ¯\_(ツ)_/¯
DeactivateBot('Cannot run FCB because of bad settings. Please clear the bot, reconfigure and restart.')
end
8 Comments
Sign in to leave a comment.
ERROR: Backtest has failed. There is a execution error.
ERROR: Circular reference detected.
Any idea what could cause that? Probably settings, but which one?
Please write the approximate settings for this bot, deposit $ 2000
No idea.. That's the error I hate the most, that's all I know.
Set "1. Used Currency" to Quote
Set "2.1. Total Buy Amount" to for example 2000
Set "3. Order Size" to for example 20
Now, your setup will create 100 buy orders, all worth 20 bucks. All you need to do next is to setup the grid the way you want it.
Hello pshai, great work thank you.
My version is v3.3.39.0 RELEASE, so does that mean this script won't run on my system? I also couldn't find a version 4 of the HTS, even beta version is v3.3.42.0?
Thank you.
For v4 (beta testing) you will need to join our Discord server. I have not done any tests in v3 for this, so I do not know if it works or doesn't work. That is why I labelled it "v4 only".
It's a nice bot. I like how it works.
The only things I would like on it would be a Stop Loss parameter, and perhaps a take profit value, where the bot would sell any remaining tokens.
The stop loss would allow me to step away from the chart and not worry about a flash crash that drops below my risk level.
Other than that, I really enjoy the simplicity and functionality.
:bow: :bow: great script!