[pshaiBot] Bot Blueprint (ALPHA TEST)

alpha
By pshai in Miscellaneous Published August 2022 👁 1,495 views 💬 1 comments

Description

Here is my take on bot blueprint!! It has quite a lot of inner works and I intend on adding more later on. Unfortunately I cannot upload screenshots of the bot... Will add them later! Read the full script file carefully to understand what happens and why, I have written lots of comments to help with that. Features:
  1. Ease-of-Use - All trade commands have default values so no matter what skill-level you are, you can easily start using this blueprint! If you want to skip/use default input values of any trading commands, simply give it a nil as input.
  2. Customizable "events" - Whether an order is open, filled, partially filled of cancelled, there is a built-in "even" function that fires accordingly. These functions are empty at the moment and be customized to update custom stats or to run any other actions you need.
  3. Chart Plots - Depending if you are running a backtest in the editor or a live bot, the plots done by bot are not forgotten; they are transformed into log messages when running a live bot and plotted normally in editor backtests!
  4. "You cannot exit more than you have" - The exit functions will check your position size before putting through the exit orders. Consider it as "automatic reduce-only".
Missing features:
  1. Built-in inputs
  2. Wallet checks
  3. TP and SL systems
  4. Proper custom reports and useful stats
Example of use (also included at the end of the script):

-- init/load our 'testBot'
local testBot = bot('testBot')

-- prices and simple moving average with plotting
local c = ClosePrices()
local sma = SMA(c, 50)
testBot.PlotLine(0, 'sma', sma)

-- some simple strategy to control how bot trades
if CrossOver(c, sma) then
    -- enable long and disable short entries
    testBot.EnableLongs()
    testBot.DisableShorts()
    
    -- exit open short position
    testBot.ExitShort()
elseif CrossUnder(c, sma) then
    -- enable short and disable long entries
    testBot.DisableLongs()
    testBot.EnableShorts()

    -- exit open long position
    testBot.ExitLong()
end

-- run the inner works of the bot
testBot.Run()

-- just endlessly entry longs and shorts when allowed
testBot.GoLong()
testBot.GoShort()

-- save bot info for next update
testBot.SaveBot()
Another example:

-- init/load our 'testBot'
local testBot = bot('testBot')

-- prices and simple moving average with plotting
local c = ClosePrices()
local sma = SMA(c, 50)
testBot.PlotLine(0, 'sma', sma)

-- some simple strategy to control how bot trades
if CrossOver(c, sma) then
    -- entry long
    testBot.GoLong()

    -- exit open short position
    testBot.ExitShort()
elseif CrossUnder(c, sma) then
    -- entry short
    testBot.GoShort()

    -- exit open long position
    testBot.ExitLong()
end

-- run the inner works of the bot
testBot.Run()

-- save bot info for next update
testBot.SaveBot()
Inspired by romdisc's Blueprint of Trading Bot.
HaasScript
-- [pshaiBot] Bot Blueprint
-- Author: pshai


---- if you are using custom order type and trade amount settings,
---- uncomment these accordingly
--HideOrderSettings()
--HideTradeAmountSettings()
EnableHighSpeedUpdates(true) ----- DO NOT REMOVE!! -----


-------------------------------------------------------------------------------
-- the beast itself
function bot(name)
    -------------------------------------------------------------------------------
    -- private variables
        local can_run = Load(name..':cr', false)
        local init = Load(name..':init', true)
        local long_pid = Load(name..':lpid', NewGuid())
        local short_pid = Load(name..':spid', NewGuid())
        local long_pos = PositionContainer(long_pid)
        local short_pos = PositionContainer(short_pid)
        local long_eids = Load(name..':leids', {}) -- long entry order ids
        local short_eids = Load(name..':seids', {}) -- short entry order ids
        local long_xids = Load(name..':lxids', {}) -- long exit order ids
        local short_xids = Load(name..':sxids', {}) -- short exit order ids
        local allow_longs = Load(name..':al', false)
        local allow_shorts = Load(name..':as', false)
        local isBT = Load(name..':ibt', false)
        local plot_msgs = {} -- when we are live, we log plot info
        local log = |msg, color| Log('['..name..']: ' .. msg, color or '')
        local cp = CurrentPrice()
        local mktType = MarketType()
        local goLong_cmd = mktType == SpotTrading and PlaceBuyOrder or PlaceGoLongOrder
        local goShort_cmd = mktType == SpotTrading and PlaceSellOrder or PlaceGoShortOrder
        local exLong_cmd = mktType == SpotTrading and PlaceSellOrder or PlaceExitLongOrder
        local exShort_cmd = mktType == SpotTrading and PlaceBuyOrder or PlaceExitShortOrder
        
        -------------------------------------------------------------------------------
        -- single/multi order mode. if enabled, we only allow 1 open order at a time,
        -- otherwise we use max_open_orders (see below)
            local single_order_mode = true
        
        -------------------------------------------------------------------------------
        -- max open entries/exits allowed (entries and exits do not block each other!)
        -- change this to your liking
            local max_open_orders = 3

    -------------------------------------------------------------------------------
    -- short-hand function for: if v then return v else return d end
        local def = |v, d| v or d

    -------------------------------------------------------------------------------
    -- colors used for logging
        local color = {
            info = DarkGray,
            blocked = Purple,
            warn = Yellow,
            fail = Red,
            success = Green
        }

    -------------------------------------------------------------------------------
    -- customizable functions

        -------------------------------------------------------------------------------
        -- initialization "event"
            function onInit()
                -- do initialization. runs only once when bot starts
            end

        -------------------------------------------------------------------------------
        -- open order "event"
            function onOpenOrder(order)
                -- do smth when order is open.
                -- [order] input is an OrderContainer object
            end

        -------------------------------------------------------------------------------
        -- filled order "event"
            function onFilledOrder(order)
                -- do smth when order is filled.
                -- [order] input is an OrderContainer object

                log('Order was filled!', color.success)
            end

        -------------------------------------------------------------------------------
        -- partially filled order "event"
            function onPartiallyFilledOrder(order, isOpen)
                -- do smth when order is partially filled.
                -- [order] input is an OrderContainer object

                local extra = isOpen and ' (order is open)' or ' (order is not open)'
                log('Order was partially filled.' .. extra, color.info)
            end

        -------------------------------------------------------------------------------
        -- cancelled/failed order "event"
            function onFailedOrder(order)
                -- do smth when order is failed/cancelled.
                -- [order] input is an OrderContainer object

                log('Order was cancelled!', color.warn)
            end


    -------------------------------------------------------------------------------
    -- built-in private functions, getters and setters

        -------------------------------------------------------------------------------
        -- empty function to mask all output functions of bot unless we can run
            function empty_function() end

        -------------------------------------------------------------------------------
        -- check if we are in backtest or not
        -- will not work properly if high-speed mode is not used!
            function check_is_bt()
                -- dont check if we can already run
                if can_run then
                    return
                end

                local t = Time() -- current time
                local rt = t - (t % 60) -- get fraction
                local dt = t - rt -- delta time
                local dc = Load(name..':btdc', 0) -- delta count
                local c = Load(name..':btc', 0) -- run count

                if c < 3 then
                    if c > 0 and dt > 0 then
                        can_run = true
                    end

                    dc = dc + dt
                    c = c + 1
                else
                    if dc == 0 then
                        isBT = true
                    end

                    can_run = true
                end

                if can_run then
                    log('(this is ' .. (not isBT and 'not' or '') .. ' a backtest...)', color.info)
                end

                Save(name..':btdc', dc)
                Save(name..':btc', c)

                CustomReport('Is backtest', (isBT and 'true' or 'false'))
            end

        -------------------------------------------------------------------------------
        -- the juice that keeps this thing going
            function saveBot()
                if can_run then
                    Save(name..':init', false)
                end
                Save(name..':ibt', isBT)
                Save(name..':cr', can_run)
                Save(name..':lpid', long_pid)
                Save(name..':spid', short_pid)
                Save(name..':leids', long_eids) -- long entry order ids
                Save(name..':seids', short_eids) -- short entry order ids
                Save(name..':lxids', long_xids) -- long exit order ids
                Save(name..':sxids', short_xids) -- short exit order ids
                Save(name..':al', allow_longs)
                Save(name..':as', allow_shorts)
            end

        -------------------------------------------------------------------------------
        -- enable/disable longs/shorts
            function enableLongs()
                allow_longs = true
            end

            function disableLongs()
                allow_longs = false
            end

            function enableShorts()
                allow_shorts = true
            end

            function disableShorts()
                allow_shorts = false
            end

        -------------------------------------------------------------------------------
        -- when multi-order mode, get index of an empty slot
            function getNewOID(list)
                local count = Count(list)
                
                for i = 1, max_open_orders do
                    if not list[i] or list[i] == '' then
                        return i
                    end
                end
                
                return -1
            end

        -------------------------------------------------------------------------------
        -- update a single order by id
            function updateOrder(id)
                if id != '' then
                    local o = OrderContainer(id)

                    if o.isOpen then
                        onOpenOrder(o)
                        
                        if o.executedAmount - o.filledAmount < o.executedAmount then
                            onPartiallyFilledOrder(o, true)
                        end
                    else
                        if o.executedAmount > o.filledAmount then
                            onPartiallyFilledOrder(o, false)
                        elseif o.isFilled then
                            onFilledOrder(o)
                        elseif o.isCancelled then
                            onFailedOrder(o)
                        end

                        id = '' -- id is reset
                    end
                end

                return id
            end

        -------------------------------------------------------------------------------
        -- update order list
            function updateOrderList(list)
                local count = Count(list)

                for i = 1, count do
                    local id = list[i] -- get id

                    -- only update ids that contain an order
                    if id != '' then 
                        id = updateOrder(id) -- update order
                        list[i] = id -- update id
                    end
                end

                return list
            end

        -------------------------------------------------------------------------------
        -- plot command. adds msg into queue if not a backtest
            function plot(id, name, value, options)
                -- if we doing backtest, plot the line
                if isBT then
                    Plot(id, name, value, options)
                    return
                end
                
                -- if we live, queue a message of the plot info
                addPlotMsg(id, name, value, options)
            end

        -------------------------------------------------------------------------------
        -- add plot as a message in queue
            function addPlotMsg(id, name, value, options)
                options = def(options, SkyBlue)

                local parsed_options = Parse(options, StringType)
                local color

                if Count(parsed_options) > 1 then
                    color = parsed_options[1]
                else
                    color = parsed_options
                end

                local len = Count(plot_msgs)
                local index = len + 1

                plot_msgs[index] = {
                    msg = 'chartId: ' .. id .. ', ' ..
                    'name: ' .. name .. ', ' .. 
                    'value: ' .. value,
                    color = color
                }
            end

        -------------------------------------------------------------------------------
        -- log chart plots if any and if not a backtest
            function plotMsgs()
                -- skip if backtest
                if isBT then
                    return
                end

                -- count of messages in queue
                local count = Count(plot_msgs)

                -- skip if not any messages
                if count == 0 then
                    return
                end

                -- log messages
                log('---------------------', color.info)
                for i = 1, count do
                    local item = plot_msgs[i]
                    log(item.msg, item.color)
                end
                log('-- Plots as Logs -------------------', color.info)
            end

        -------------------------------------------------------------------------------
        -- long entry
            function goLong(price, amount, type, note, timeout)
                if not can_run then
                    return
                end

                if not allow_longs then
                    log('Not allowed to entry long. (bot.allow_longs = false)', color.blocked)
                    return
                end

                local oid = getNewOID(long_eids)

                if not single_order_mode and oid == -1 then
                    log('Maximum amount of open long entries already in use, skipping entry.', color.fail)
                    return
                end

                price = def(price, cp.bid)
                amount = def(amount, TradeAmount())
                type = def(type, GetOrderType())
                note = def(note, 'Long Entry')
                timeout = def(timeout, 600)

                if single_order_mode then
                    if not long_eids[1] or long_eids[1] == '' then
                        long_eids[1] = goLong_cmd(price, amount, {timeout = timeout, type = type, note = note, positionId = long_pid})

                        log('Long entry placed: price: ' .. price .. ', amount: ' .. amount .. ', note: "'..note..'".', color.info)
                    else
                        log('Long entry order already open, skipping entry...', color.info)
                    end
                else
                    long_eids[oid] = goLong_cmd(price, amount, {timeout = timeout, type = type, note = note, positionId = long_pid})

                    log('Long entry placed: price: ' .. price .. ', amount: ' .. amount .. ', note: "'..note..'".', color.info)
                end
            end

        -------------------------------------------------------------------------------
        -- long exit
            function exitLong(price, amount, type, note, timeout)
                if not can_run then
                    return
                end

                -- get slot index for empty slot, if any (-1 if not any)
                local oid = getNewOID(long_xids)

                if not single_order_mode and oid == -1 then
                    log('Maximum amount of open long exits already in use, skipping exit.', color.fail)
                    return
                end

                price = def(price, cp.ask)
                amount = def(amount, long_pos.amount) -- defaults to full position size
                type = def(type, GetOrderType())
                note = def(note, 'Long Exit')
                timeout = def(timeout, 600)


                -- dont allow exiting more than we have
                if amount > long_pos.amount then
                    log('Long exit amount set to higher than position size, reducing to position size.', color.info)

                    amount = long_pos.amount
                end

                if single_order_mode then
                    if not long_xids[1] or long_xids[1] == '' then
                        long_xids[1] = exLong_cmd(price, amount, {timeout = timeout, type = type, note = note, positionId = long_pid})

                        log('Long exit placed: price: ' .. price .. ', amount: ' .. amount .. ', note: "'..note..'".', color.info)
                    else
                        log('Long exit order already open, skipping exit...', color.info)
                    end
                else
                    long_xids[oid] = exLong_cmd(price, amount, {timeout = timeout, type = type, note = note, positionId = long_pid})

                    log('Long exit placed: price: ' .. price .. ', amount: ' .. amount .. ', note: "'..note..'".', color.info)
                end
            end

        -------------------------------------------------------------------------------
        -- short entry
            function goShort(price, amount, type, note, timeout)
                if not allow_shorts then
                    log('Not allowed to entry short. (bot.allow_shorts = false)', color.blocked)
                    return
                end 

                local oid = getNewOID(short_eids)

                if not single_order_mode and oid == -1 then
                    log('Maximum amount of open short entries already in use, skipping entry.', color.fail)
                    return
                end

                price = def(price, cp.ask)
                amount = def(amount, TradeAmount())
                type = def(type, GetOrderType())
                note = def(note, 'Short Entry')
                timeout = def(timeout, 600)

                if single_order_mode then
                    if not short_eids[1] or short_eids[1] == '' then
                        short_eids[1] = goShort_cmd(price, amount, {timeout = timeout, type = type, note = note, positionId = short_pid})

                        log('Short entry placed: price: ' .. price .. ', amount: ' .. amount .. ', note: "'..note..'".', color.info)
                    else
                        log('Short entry order already open, skipping entry....', color.info)
                    end
                else
                    short_eids[oid] = goShort_cmd(price, amount, {timeout = timeout, type = type, note = note, positionId = short_pid})

                    log('Short entry placed: price: ' .. price .. ', amount: ' .. amount .. ', note: "'..note..'".', color.info)
                end
            end

        -------------------------------------------------------------------------------
        -- short exit
            function exitShort(price, amount, type, note, timeout)
                if not can_run then
                    return
                end

                -- get slot index for empty slot, if any (-1 if not any)
                local oid = getNewOID(short_xids)

                if not single_order_mode and oid == -1 then
                    log('Maximum amount of open short exits already in use, skipping exit.', color.fail)
                    return
                end

                price = def(price, cp.ask)
                amount = def(amount, short_pos.amount) -- defaults to full position size
                type = def(type, GetOrderType())
                note = def(note, 'Short Exit')
                timeout = def(timeout, 600)


                -- dont allow exiting more than we have
                if amount > short_pos.amount then
                    log('Short exit amount set to higher than position size, reducing to position size.', color.info)

                    amount = short_pos.amount
                end

                if single_order_mode then
                    if not short_xids[1] or short_xids[1] == '' then
                        short_xids[1] = exShort_cmd(price, amount, {timeout = timeout, type = type, note = note, positionId = short_pid})

                        log('Short exit placed: price: ' .. price .. ', amount: ' .. amount .. ', note: "'..note..'".', color.info)
                    else
                        log('Short exit order already open, skipping exit...', color.info)
                    end
                else
                    short_xids[oid] = exShort_cmd(price, amount, {timeout = timeout, type = type, note = note, positionId = short_pid})

                    log('Short exit placed: price: ' .. price .. ', amount: ' .. amount .. ', note: "'..note..'".', color.info)
                end
            end

        -------------------------------------------------------------------------------
        -- main run function
            function run()
                if not can_run then
                    return
                end

                -- update order lists
                long_eids = updateOrderList(long_eids)
                short_eids = updateOrderList(short_eids)

                -- log plot messages
                plotMsgs()
            end

            -- check if we are in backtest or not
            check_is_bt()

            -- run initialization
            if can_run and init then
                log('Startup complete!', color.success)

                -- fire custom init function
                onInit()

            elseif not can_run then
                log('Bot is starting, please wait...', color.info)
            end


    -------------------------------------------------------------------------------
    -- output functions and stuff to public.
    -- if we are starting up (checking if we are in backtest), we
    -- output empty functions so the bot logic can still run errorless,
    -- but doesnt really do anything. once we are running, we output
    -- the correct functions and bot will start working.
    -- NOTE that saveBot gets out no matter what. this is to make
    -- sure everything still gets saved, even though it doesnt really
    -- matter... idk.
        return {
            EnableLongs = can_run and enableLongs or empty_function,
            EnableShorts = can_run and enableShorts or empty_function,
            DisableLongs = can_run and disableLongs or empty_function,
            DisableShorts = can_run and disableShorts or empty_function,
            GoLong = can_run and goLong or empty_function,
            GoShort = can_run and goShort or empty_function,
            ExitLong = can_run and exitLong or empty_function,
            ExitShort = can_run and exitShort or empty_function,
            Run = can_run and run or empty_function,
            PlotLine = can_run and plot or empty_function,
            SaveBot = saveBot
        }
end

--[[ Example #1
    -- init/load our 'testBot'
    local testBot = bot('testBot')

    -- prices and simple moving average with plotting
    local c = ClosePrices()
    local sma = SMA(c, 50)
    testBot.PlotLine(0, 'sma', sma)


    -- some simple strategy to control how bot trades
    if CrossOver(c, sma) then
        -- enable long and disable short entries
        testBot.EnableLongs()
        testBot.DisableShorts()
        
        -- exit open short position
        testBot.ExitShort()
    elseif CrossUnder(c, sma) then
        -- enable short and disable long entries
        testBot.DisableLongs()
        testBot.EnableShorts()

        -- exit open long position
        testBot.ExitLong()
    end

    -- run the inner works of the bot
    testBot.Run()

    -- just endlessly entry longs and shorts when allowed
    testBot.GoLong()
    testBot.GoShort()

    -- save bot info for next update
    testBot.SaveBot()
]]


---------------------------------------------------------------
-- Example #2
    -- init/load our 'testBot'
    local testBot = bot('testBot')
    
    -- prices and simple moving average with plotting
    local c = ClosePrices()
    local sma = SMA(c, 50)
    testBot.PlotLine(0, 'sma', sma)

    testBot.EnableLongs()
    testBot.EnableShorts()
    
    -- some simple strategy to control how bot trades
    if CrossOver(c, sma) then
        -- entry long
        testBot.GoLong()
    
        -- exit open short position
        testBot.ExitShort()
    elseif CrossUnder(c, sma) then
        -- entry short
        testBot.GoShort()
    
        -- exit open long position
        testBot.ExitLong()
    end
    
    -- run the inner works of the bot
    testBot.Run()
    
    -- save bot info for next update
    testBot.SaveBot()

1 Comment

Sign in to leave a comment.

H
Hedgehog1729 over 3 years ago

Nice work on this one