[HaasOnline] Order Bot

beta
By pshai in Trading Bots Published October 2021 👁 2,809 views 💬 5 comments

Description

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:
            &lt; 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 *', '', '&lt;', ''),
    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       = '&lt;',
        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 &lt;= target)
            or (self:isSellOrder() and cp.bid &lt;= 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 &lt; 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.

K
Kobalt over 4 years ago

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]]

P
pshai over 4 years ago

Thanks man! I really kinda appreciate it.

K
Kobalt over 4 years ago

XD

D
davidhetfield about 4 years ago

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)

P
pshai almost 4 years ago

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.