[HaasOnline] Order Bot
betaDescription
The classic Order Bot recreated in HaasScript!
I tried my best to keep the code simple, yet professional.
The bot settings only contain an InputTable where you can build your orders. Unfortunately, due to limiting factors in HaasScript, orders cannot be reset. In other words, the bots orders are one-time triggers.
Input table order parameters:
Order directions:
For spot markets,
a buy order is set with a Direction value of +
a sell order is set with a Direction value of -
For leverage markets,
go long order is set with a Direction value of L+ and exit long with L-
go short order is set with a Direction value of S+ and exit short with S-
Trade amount is set in BASE value, which means, that if you are
trading BTC/USDT, the amount is then set as BTC.
Trigger Types:
< means "Lower Than" trigger price
> means "Higher Than" trigger price
Stop-Loss example (see default settings):
- "buy" order is set to trigger at price 62000.
- "sl" order is set to trigger when price "Less Than" 61800,
but is allowed to be monitored only After "buy" order
and Before "sell" order. "sl" is also set to be a market order.
- "sell" order is set to trigger at price 63000, but only After "buy"
order has completed.
All parameters with * are optional. Leave them empty when not used.
HaasScript
-----------------------------------------------------------------------------
-- [HaasOnline] Order Bot
-- Author: pshai
-----------------------------------------------------------------------------
--[[
Input table order parameters:
Order directions:
For spot markets,
a buy order is set with a Direction value of +
a sell order is set with a Direction value of -
For leverage markets,
go long order is set with a Direction value of L+ and exit long with L-
go short order is set with a Direction value of S+ and exit short with S-
Trade amount is set in BASE value, which means, that if you are
trading BTC/USDT, the amount is then set as BTC.
Trigger Types:
< means "Lower Than" trigger price
> means "Higher Than" trigger price
Stop-Loss example (see default settings):
- "buy" order is set to trigger at price 62000.
- "sl" order is set to trigger when price "Less Than" 61800,
but is allowed to be monitored only After "buy" order
and Before "sell" order. "sl" is also set to be a market order.
- "sell" order is set to trigger at price 63000, but only After "buy"
order has completed.
All parameters with * are optional. Leave them empty when not used.
]]
local orderTable = InputTable(
InputTableOptions('Orders'),
InputTableColumn('ID', 'buy', 'sl', 'sell'),
InputTableColumn('Market Order', false, true, false),
InputTableColumn('Direction', '+', '-', '-'),
InputTableColumn('Target Price', 62000, 61800, 63000),
InputTableColumn('Amount', 0.002, 0.002, 0.002),
InputTableColumn('Before *', '', 'sell', ''),
InputTableColumn('After *', '', 'buy', 'buy'),
InputTableColumn('Trigger Type *', '', '<', ''),
InputTableColumn('Trigger Price *', '', 61800, '')
)
local isDebug = Input('Debug Mode', false)
function debuglog(msg, color)
if not isDebug then return end
Log('[DEBUG] ' .. msg, color or '')
end
EnableHighSpeedUpdates(true)
HideOrderSettings()
HideTradeAmountSettings()
-- ===============================================================
-- Config object
local Config = {}
function Config:isSpot()
return MarketType() == SpotTrading
end
-- ===============================================================
-- Positions
local PosMan = {}
function PosMan:load()
self.long_pid = Load('pm:lpid', NewGuid())
self.short_pid = Load('pm:spid', NewGuid())
self.long_pos = PositionContainer(self.long_pid)
self.short_pos = PositionContainer(self.short_pid)
end
function PosMan:getPID(isLong)
return isLong and self.long_pid or self.short_pid
end
function PosMan:update()
local lpos = self.long_pos
local spos = self.short_pos
if lpos.enterPrice > 0 and lpos.amount == 0 then --and IsPositionClosed(self.long_pid) then
self.long_pid = NewGuid()
self.long_pos = PositionContainer(self.long_pid)
end
if spos.enterPrice > 0 and spos.amount == 0 then --and IsPositionClosed(self.long_pid) then
self.short_pid = NewGuid()
self.short_pos = PositionContainer(self.short_pid)
end
self:save()
end
function PosMan:save()
Save('pm:lpid', self.long_pid)
Save('pm:spid', self.short_pid)
end
-- ===============================================================
-- Enums
local TableItem =
{
Id = 1,
IsMarket = 2,
Direction = 3,
TargetPrice = 4,
Amount = 5,
Before = 6,
After = 7,
TriggerType = 8,
TriggerPrice = 9
}
local TriggerType =
{
LowerThan = '<',
HigherThan = '>',
Normal = ''
}
local OrderDirection =
{
-- Spot
Buy = '+',
Sell = '-',
-- Leverage
GoLong = 'L+',
GoShort = 'S+',
ExitLong = 'L-',
ExitShort = 'S-'
}
local OrderStatus =
{
Undefined = -1,
Created = 1,
Executing = 2,
Completed = 3,
Cancelled = 4,
Redundant = 5
}
-- ===============================================================
-- Handy functions
-- 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
-- trim a string
function StringTrim(str)
return str:gsub("%s+", "")
end
-- ===============================================================
-- PreOrder object
local PreOrder =
{
Id = '',
OrderId = '',
Status = OrderStatus.Undefined,
Direction = '',
Before = '',
After = '',
Amount = 0,
Price = 0,
IsMarket = false,
TriggerType = '',
TriggerPrice = 0
}
function PreOrder:load(table, index)
local item = table[ index ]
local order = clone(PreOrder)
order.Id = StringTrim( item[ TableItem.Id ])
order.OrderId = Load(order.Id .. ':oid', '')
order.Status = Load(order.Id .. 's', OrderStatus.Created)
order.Direction = StringTrim( item[ TableItem.Direction ])
order.Before = StringTrim( item[ TableItem.Before ])
order.After = StringTrim( item[ TableItem.After ])
order.Amount = Parse(item[ TableItem.Amount ], NumberType)
order.Price = Parse(item[ TableItem.TargetPrice ], NumberType)
order.IsMarket = Parse(item[ TableItem.IsMarket ], BooleanType)
order.TriggerType = StringTrim( item[ TableItem.TriggerType ])
order.TriggerPrice = Parse(item[ TableItem.TriggerPrice ], NumberType)
if not order.TriggerPrice then
order.TriggerPrice = -1
end
return order
end
function PreOrder:save()
Save(self.Id .. ':oid', self.OrderId)
Save(self.Id .. 's', self.Status)
end
function PreOrder:statusString()
local status = self.Status
if status == OrderStatus.Undefined then
return 'Undefined'
elseif status == OrderStatus.Created then
return 'Awaiting'
elseif status == OrderStatus.Executing then
return 'Executing'
elseif status == OrderStatus.Completed then
return 'Completed'
elseif status == OrderStatus.Cancelled then
return 'Cancelled'
end
return '[Wrong status enum: ' .. status .. ']'
end
function PreOrder:isBuyOrder()
local dir = self.Direction
return dir == OrderDirection.Buy
or dir == OrderDirection.GoLong
or dir == OrderDirection.ExitShort
end
function PreOrder:isSellOrder()
local dir = self.Direction
return dir == OrderDirection.Sell
or dir == OrderDirection.GoShort
or dir == OrderDirection.ExitLong
end
function PreOrder:isEntryOrder()
local dir = self.Direction
return dir == OrderDirection.GoShort
or dir == OrderDirection.GoLong
end
function PreOrder:isExitOrder()
local dir = self.Direction
return dir == OrderDirection.ExitShort
or dir == OrderDirection.ExitLong
end
function PreOrder:isSpotDirection()
local dir = self.Direction
return dir == OrderDirection.Buy
or dir == OrderDirection.Sell
end
function PreOrder:isLeverageDirection()
return not self:isSpotDirection()
end
function PreOrder:directionToSignal()
local dir = self.Direction
if dir == OrderDirection.Buy or dir == OrderDirection.GoLong then
return SignalBuy
elseif dir == OrderDirection.Sell or dir == OrderDirection.GoShort then
return SignalSell
elseif dir == OrderDirection.ExitLong then
return SignalExitLong
elseif dir == OrderDirection.ExitShort then
return SignalExitShort
end
return SignalNone
end
function PreOrder:checkDirection()
local isSpot = Config:isSpot()
debuglog('order direction is: ' .. self.Direction)
if isSpot and self:isLeverageDirection() then
LogError('Order "' .. self.Id .. '" has incorrect direction of "'..self.Direction..'"; for leverage markets, use L+/L- and S+/S-')
return false
elseif not isSpot and self:isSpotDirection() then
LogError('Order "' .. self.Id .. '" has incorrect direction of "'..self.Direction..'"; for spot markets, use + for buy and - for sell')
return false
end
return true
end
function PreOrder:execute()
local directionSignal = self:directionToSignal()
local isLong = Config:isSpot() or (directionSignal == SignalBuy or directionSignal == SignalExitLong)
local pid = PosMan:getPID(isLong)
-- check if we have a position already and we are not adding to it
if (GetPositionDirection(pid) == PositionBought and directionSignal != SignalBuy)
or (GetPositionDirection(pid) == PositionSold and directionSignal != SignalSell)
then
self.Amount = ArrayGet(Min(self.Amount, GetPositionAmount(pid)), 1)
end
if self.Price == 0 then
if self:isBuyOrder() then
self.Price = CurrentPrice().bid
else
self.Price = CurrentPrice().ask
end
end
local itae = IsTradeAmountEnough(PriceMarket(), self.Price, self.Amount, false)
if not itae then
LogError('Blocking trade; trade amount is too small. (min: ' .. MinimumTradeAmount()..' '..AmountLabel()..')')
return
end
local walletLabel = Config:isSpot() and AmountLabel() or UnderlyingAsset()
if not WalletCheck(AccountGuid(), walletLabel, self.Amount) then
LogError('Blocking trade; not enough funds ('..walletLabel..') in wallet.')
return
end
local dir = self.Direction
local cmd = nil
if self:isBuyOrder() then
debuglog('Order is a buy/golong/exitshort order: ' .. self.Direction)
if Config:isSpot() then
cmd = PlaceBuyOrder
else
if dir == OrderDirection.GoLong then
cmd = PlaceGoLongOrder
elseif dir == OrderDirection.ExitShort then
cmd = PlaceExitShortOrder
end
end
elseif self:isSellOrder() then
debuglog('Order is a sell/goshort/exitlong order: ' .. self.Direction)
if Config:isSpot() then
cmd = PlaceSellOrder
else
if dir == OrderDirection.GoShort then
cmd = PlaceGoShortOrder
elseif dir == OrderDirection.ExitLong then
cmd = PlaceExitLongOrder
end
end
end
if cmd == nil then
LogError('Something went wrong; unable to determine correct trading command for order direction: "' .. dir .. '".')
return
end
local settings = {timeout = 0, note = self.Id, positionId = pid}
settings.type = self.IsMarket and MarketOrderType or LimitOrderType
self.OrderId = cmd(self.Price, self.Amount, settings)
self.Status = OrderStatus.Executing
end
function PreOrder:update(cp)
local target = self.Price
local tt = self.TriggerType
if self.TriggerPrice > 0 then
target = self.TriggerPrice
end
if tt == TriggerType.LowerThan then
if (self:isBuyOrder() and cp.ask <= target)
or (self:isSellOrder() and cp.bid <= target)
then
self:execute()
end
elseif tt == TriggerType.HigherThan then
if (self:isBuyOrder() and cp.ask >= target)
or (self:isSellOrder() and cp.bid >= target)
then
self:execute()
end
else
if self:isBuyOrder() then
if self.IsMarket then
self.Price = cp.ask
elseif self.Price < cp.ask then
return
end
self:execute()
elseif self:isSellOrder() then
if self.IsMarket then
self.Price = cp.bid
elseif self.Price > cp.bid then
return
end
self:execute()
end
end
end
function PreOrder:plotOrder()
local price = self.Price
local name = self.Id
local oid = self.OrderId
local color = self:isBuyOrder() and Green or Red
Plot(0, 'Order: ' .. name, price, {c = color, id = oid})
end
function PreOrder:checkOrder()
local oid = self.OrderId
if oid != '' then
local order = OrderContainer(oid)
if order.isOpen then
self:plotOrder()
return
else
oid = ''
if order.filledAmount > 0 then
self.Status = OrderStatus.Completed
elseif order.isCancelled then
self.Status = OrderStatus.Cancelled
end
end
end
self.OrderId = oid
end
-- ===============================================================
-- Order Manager object
local OrderMan =
{
Orders = {}
}
function OrderMan:load()
local table = orderTable
local count = #table
for i = 1, count do
local order = PreOrder:load(table, i)
self.Orders[i] = order
debuglog('Loaded order at index ' .. i)
end
end
function OrderMan:getById(id)
local orders = self.Orders
local count = #orders
for i = 1, count do
local order = orders[i]
if order.Id == id then
return order
end
end
return nil
end
function OrderMan:update()
local orders = self.Orders
local count = #orders
local cp = CurrentPrice()
local mayFire, correctDirection,
correctStatus, dependedCheck,
dependedNotExecutedCheck, status1, status2
for i = 1, count do
local order = orders[i]
debuglog('Checking order "' .. order.Id .. '" (status: ' .. order:statusString() .. ')')
if order.Status == OrderStatus.Completed
or order.Status == OrderStatus.Redundant
then
goto skip
end
correctDirection = order:checkDirection()
correctStatus = order.Status == OrderStatus.Created
dependedCheck = order.After == ''
dependedNotExecutedCheck = order.Before == ''
if not dependedCheck then
local order2 = OrderMan:getById(order.After)
debuglog('after: ' .. order.After)
if order2 != nil then
status1 = order2.Status
dependedCheck = status1 == OrderStatus.Completed
end
end
if dependedCheck and not dependedNotExecutedCheck then
local order2 = OrderMan:getById(order.Before)
debuglog('before: ' .. order.Before)
if order2 != nil then
status2 = order2.Status
dependedNotExecutedCheck = status2 == OrderStatus.Created
end
if not dependedNotExecutedCheck then
if (status1 == OrderStatus.Completed or status1 == OrderStatus.Redundant)
and (status2 == OrderStatus.Completed or status2 == OrderStatus.Redundant)
then
order.Status = OrderStatus.Redundant
end
goto skip
end
end
mayFire = correctDirection and correctStatus and dependedCheck and dependedNotExecutedCheck
debuglog('direction: ' .. (correctDirection and 'OK' or '-')
.. ' | status: ' .. (correctStatus and 'OK' or '-')
.. ' | check1: ' .. (dependedCheck and 'OK' or '-')
.. ' | check2: ' .. (dependedNotExecutedCheck and 'OK' or '-'))
if mayFire then
debuglog('mayFire: yes')
order:update(cp)
end
order:checkOrder()
order:save()
::skip::
end
end
-- Skip very first update cycle
if Load('first_update', true) then
Save('first_update', false)
else
PosMan :load()
PosMan :update()
OrderMan :load()
OrderMan :update()
end
5 Comments
Sign in to leave a comment.
NICE
kudos, kudus and more kudos to you just doing the impossible aka magic: bringing Order Bot to the cloud
AWESOME you kinda rock [if not killing it]]
Thanks man! I really kinda appreciate it.
XD
Thanks, this is very useful!
I just registered yesterday - is it possible to put the 'order ID' of a previous order into the 'After*' Box? So that the orders are put on in sequence as with the original Order Bot?
(sorry not much of a coder)
Yes it is! You can create a chain of orders using the After boxes. The order that has an ID set in the After box will only be executed when ever the previous one has filled.