[pshaiBot] Futures Band-It

stable
By pshai in Trading Bots Published April 2020 👁 7,302 views 💬 11 comments

Description

Hello everyone! I'm excited to release this new awesome grid-bot script with you! It is a very powerful and dynamic grid-bot that utilizes ATR (Average True Range) for order placement, ADX (Average Directional Index) to limit when and where to trade and it offers a good bunch of settings for you to tweak it the way you like! I hope this massive beast can generate you the income you deserve! Parameters: - BASIC - - 1. Max. consecutive orders: Maximum amount of consecutive orders. Once last order is filled, stop-loss is activated - - 2. First order size: The size of the first order. Also works as the base for next order sizes (see [Order size multiplier]) - - 3. Order size multiplier: Multiplier for how much order sizes grow, example with first order 100 and multiplier of 3; 100, 300, 900, 2700 and so on - - 4. Take-profit %: Take-profit is calculated based on the current average entry price of open position. If your exchange doesn't offer rebate fees, you need to take those paid fees into account!! - GRID - - 1. Grid type: The type of grid. [ATR] type uses Average True Range, [StdDev] type uses Standard Deviation and [Fixed] type uses fixed stepping for order placement. - - 2. Grid period length: Period length of grid. Doesn't affect FIXED type. - - 3. Grid base multiplier: Base multiplier, controls the distance of the orders. - - 4. Grid fixed step: Grid fixed step defines the step size for the [FIXED] grid type. - - 5. Grid timeframe: Interval used for grid data. Doesn't affect FIXED type. - SAFETY - - 1. ADX period length: Period length of ADX - - 2. ADX shutdown limit: If ADX rises above this value, the bot will shutdown and exit any position it has open, using market order. Use this setting to control what volatility levels this bot should trade in. Value ignored if set to zero. - - 3. ADX timeframe: Interval used for ADX data - - 4. Stop-loss offset %: Stop-loss is offsetted based on the last filled entry price, NOT the current average entry price of open position - - 5. Cooldown length (min): The length of cooldown (in minutes) after ADX-shutdown or stop-loss "May the profits be with you..." ~pshai
HaasScript
EnableHighSpeedUpdates(true)
HideOrderSettings()
HideTradeAmountSettings()
SetFee(-0.025) -- for backtesting
 
 
-- grid types
    local gridTypes = {
        atr = 'ATR',
        stddev = 'StdDev',
        fixed = 'Fixed Step'
        }
 
-- inputs
    local orderCount = Input('1. Max. consecutive orders', 10, 'Maximum amount of consecutive orders. Once last order is filled, stop-loss is activated', 'BASIC')
    local initialAmount = Input('2. First order size', 100, 'The size of the first order. Also works as the base for next order sizes (see [Order size multiplier])', 'BASIC')
    local sizeMult = Input('3. Order size multiplier', 1, 'Multiplier for how much order sizes grow, example with first order 100 and multiplier of 3; 100, 300, 900, 2700 and so on', 'BASIC')
    local takeProfit = Input('4. Take-profit %', 0.25, 'Take-profit is calculated based on the current average entry price of open position. If your exchange doesn\'t offer rebate fees, you need to take those paid fees into account!!', 'BASIC')
    local gridType = InputOptions('1. Grid type', gridTypes.atr, {gridTypes.atr, gridTypes.stddev, gridTypes.fixed}, 'The type of grid. [ATR] type uses Average True Range, [StdDev] type uses Standard Deviation and [Fixed] type uses fixed stepping for order placement.', 'GRID')
    local atrLen = Input('2. Grid period length', 20, 'Period length of grid. Doesn\'t affect FIXED type.', 'GRID')
    local baseMult = Input('3. Grid multiplier', 1, 'Grid multiplier controls the distance of the orders (ATR & StdDev types). For FIXED type, this value is the price gap between orders.', 'GRID')
    local fixedStep = Input('4. Grid fixed step', 10, 'Grid fixed step defines the step size for the [FIXED] grid type.', 'GRID')
    local atrInt = InputInterval('5. Grid timeframe', 15, 'Interval used for grid data. Doesn\'t affect FIXED type.', 'GRID')
    local adxLen = Input('1. ADX period length', 20, 'Period length of ADX', 'SAFETY')
    local adxLimit = Input('2. ADX shutdown limit', 0, 'If ADX rises above this value, the bot will shutdown and exit any position it has open, using market order. Use this setting to control what volatility levels this bot should trade in. Value ignored if set to zero.', 'SAFETY')
    local adxInt = InputInterval('3. ADX timeframe', 15, 'Interval used for ADX data', 'SAFETY')
    local stopLossOffset = Input('4. Stop-loss offset %', 0.25, 'Stop-loss is offsetted based on the last filled entry price, NOT the current average entry price of open position', 'SAFETY')
    local timeoutLen = Input('5. Cooldown length (min)', 60, 'The length of cooldown (in minutes) after ADX-shutdown or stop-loss', 'SAFETY')
 
-- usefulness
    local adxShutdownCount = Load('adxsc', 0)
    local slHitCount = Load('slhc', 0)
    local tpHitCount = Load('tphc', 0)
    local orderHitCount = Load('ohc', 0)
    local maxOrderHitCount = Load('mohc', 0)
    local totalOrderHitCount = Load('tohc', 0)
    local positionCount = Load('pc', 0)
 
    local shutdownTypes = {
        stoploss = 1,
        adx = 2
        }
    
    if Load('init', true) then
        Save('startBtc', WalletAmount(AccountGuid(), BaseCurrency()))
        Save('init', false)
    end
 
-- data
    local ob = GetOrderbook()
    local h0 = HighPrices(1)
    local l0 = LowPrices(1)
    local c0 = ClosePrices(1)
    local h1 = HighPrices(atrInt)
    local l1 = LowPrices(atrInt)
    local c1 = ClosePrices(atrInt)
    local h2 = HighPrices(adxInt)
    local l2 = LowPrices(adxInt)
    local c2 = ClosePrices(adxInt)
    local cp = CurrentPrice()
    local baseMa = TYPPRICE(h0, l0, c0)
    local baseStd
    local adx = ADX(h2, l2, c2, adxLen)
 
    if gridType == gridTypes.atr then
        baseStd = ATR(h1, l1, c1, atrLen)
    elseif gridType == gridTypes.stddev then
        baseStd = STDDEV(c1, atrLen, 1)
    elseif gridType == gridTypes.fixed then
        baseStd = fixedStep
    end
 
-- plot
    Plot(1, 'ADX', adx)
    PlotHorizontalLine(1, 'ADX Limit', Red, adxLimit, Dashed)
    Plot(2, 'Displacement', baseStd)
 
-- positions
    local posId = Load('posId', NewGuid())
    local position = PositionContainer(posId)
    local posDD = Load('posDD', 0)
    local posDDs = Load('posDDs', {})
    local maxDD = Load('maxDD', 0)
 
    posDD = ArrayGet(Min(posDD, position.roi), 1)
 
-- memory
    local index = Load('index', 0)
    local leid = Load('leid', '')
    local seid = Load('seid', '')
    local xeid = Load('xeid', '')
    local timeout = Load('timeout', 0)
 
-- price adjustment helpers
    function adjustBuyPrice(price)
        if ob.bidPrices[1] <= price then
            price = SubPerc(ob.bidPrices[1], 0.05)
        end
 
        return price
    end
 
    function adjustSellPrice(price)
        if ob.askPrices[1] >= price then
            price = AddPerc(ob.askPrices[1], 0.05)
        end
 
        return price
    end
 
-- price helper
    function getPrice(index, isLong)
        return isLong 
            and adjustBuyPrice( baseMa - baseStd * Pow(baseMult, index) )
            or adjustSellPrice( baseMa + baseStd * Pow(baseMult, index) )
    end
 
    function getExitPrice(isLong)
        return isLong
            and adjustSellPrice( AddPerc(position.enterPrice, takeProfit) )
            or adjustBuyPrice( SubPerc(position.enterPrice, takeProfit) )
    end
 
-- order amount helper
    function getAmount(index, price)
        return ParseTradeAmount(PriceMarket(), price, initialAmount * Pow(sizeMult, index))
    end
 
-- position helper
    function newPosition()
        index = 0
        xeid = ''
        leid = ''
        seid = ''
        posId = NewGuid()
        orderHitCount = 0
        positionCount = positionCount + 1
    end
 
-- stop-loss helper
-- loaded variables above fall into this function,
-- so there's no need for input parameters
    function execStopLossExit(type)
        if IsAnyOrderOpen() then
            CancelAllOrders(posId)
 
        elseif position.isLong or position.isShort then
            LogWarning('Executing a hard exit!')
            PlaceExitPositionOrder(posId, {type = MarketOrderType, note='SL'})
            newPosition()
 
            if type == shutdownTypes.adx then
                adxShutdownCount = adxShutdownCount + 1
                LogWarning('--- ADX cutoff')
            elseif type == shutdownTypes.stoploss then
                slHitCount = slHitCount + 1
                LogWarning('--- SL cutoff')
            end
        end
 
        if timeout < Time() then
            timeout = Time() + timeoutLen * 60
        end
    end
 
-- order update helper
    function updateOrders(isLong)
        local mainId = isLong and leid or seid
        local secId = isLong and seid or leid
 
        if mainId != '' and secId != '' and IsOrderFilled(mainId) and IsOrderFilled(secId) then
            newPosition()
            LogWarning('Both orders got filled?')
        end
 
        if secId != '' and IsOrderOpen(secId) then
            CancelOrder(secId)
            secId = ''
        end
 
        local postfix = isLong and 'L' or 'S'
        local entryCmd = isLong and PlaceGoLongOrder or PlaceGoShortOrder
 
 
        if xeid != '' and IsOrderFilled(xeid) then
            if mainId != '' and IsOrderOpen(mainId) then CancelOrder(mainId) end
 
            newPosition()
            tpHitCount = tpHitCount + 1
            return
 
        elseif mainId != '' and IsOrderOpen(mainId) == false then
            if xeid != '' and IsOrderOpen(xeid) then
                CancelOrder(xeid)
            
            else
                if IsOrderFilled(mainId) == true then
                    
                    Save('lastEntryPrice', OrderContainer(mainId).price)
 
                    index = index + 1
 
                    orderHitCount = orderHitCount + 1
                    totalOrderHitCount = totalOrderHitCount + 1
                    if orderHitCount > maxOrderHitCount then
                        maxOrderHitCount = orderHitCount
                    end
                end
 
                local entryPrice = getPrice(index, isLong)
                local entrySize = getAmount(index, entryPrice)
                
                -- only add new orders if allowed
                if index < orderCount then
                    mainId = entryCmd(
                        entryPrice,
                        entrySize,
                        {
                            type=MakerOrCancelOrderType,
                            note=postfix..(index+1),
                            timeout=604800,
                            positionId=posId
                        })
                else
                    mainId = ''
                end
            end
        end
 
        -- update id's
        leid = isLong and mainId or secId
        seid = isLong and secId or mainId
 
        -- update draw-down
        posDD = ArrayGet(Min(posDD, position.roi), 1)
 
        -- if all orders are in use, then we want to check for stop-loss
        if index >= orderCount then
            local lastEntryPrice = Load('lastEntryPrice', 0)
            local slPrice = isLong
                and SubPerc(lastEntryPrice, stopLossOffset)
                or AddPerc(lastEntryPrice, stopLossOffset)
 
            Plot(0, 'SL ('..stopLossOffset..'%)', slPrice, {c=Red, id=posId})
 
            if (isLong and cp.bid < slPrice)
            or (not isLong and cp.ask > slPrice)
            then
                execStopLossExit(shutdownTypes.stoploss)
            end
        end
 
        -- placing exit order here to ensure it is where it has to be.
        if (xeid == '' or (xeid != '' and IsOrderOpen(xeid) == false and IsOrderFilled(xeid) == false)) then
            local exitPrice = getExitPrice(isLong)
 
            xeid = PlaceExitPositionOrder({
                positionId = posId,
                price = exitPrice,
                type=MakerOrCancelOrderType,
                note=postfix..'X'..(index+1),
                timeout=604800
                })
        end
    end
 
 
-- da trading logic
    local adxAllows = (adxLimit > 0 and adx <= adxLimit) or (adxLimit <= 0)
    if adxAllows then
        -- No position detected, place initial orders
        if not (position.isLong or position.isShort) and timeout < Time() then
            local longPrice = getPrice(index, true)
            local shortPrice = getPrice(index, false)
            local longSize = getAmount(index, longPrice)
            local shortSize = getAmount(index, shortPrice)
            
            if leid == '' or (leid != '' and IsOrderOpen(leid) == false) then
                leid = PlaceGoLongOrder(longPrice, longSize, {type=MakerOrCancelOrderType, note='L'..(index+1), timeout=604800, positionId=posId})
            end
 
            if seid == '' or (seid != '' and IsOrderOpen(seid) == false) then
                seid = PlaceGoShortOrder(shortPrice, shortSize, {type=MakerOrCancelOrderType, note='S'..(index+1), timeout=604800, positionId=posId})
            end
        elseif position.isLong then
            updateOrders(true)
 
        elseif position.isShort then
            updateOrders(false)
        end
    else
        -- execute forced exit
        execStopLossExit(shutdownTypes.adx)
    end
 
-- plot open orders for backtesting
    if leid != '' and IsOrderOpen(leid) then
        Plot(0, 'leid', OrderContainer(leid).price, {c=Green, id=posId})
    end
 
    if seid != '' and IsOrderOpen(seid) then
        Plot(0, 'seid', OrderContainer(seid).price, {c=Red, id=posId})
    end
 
    if xeid != '' and IsOrderOpen(xeid) then
        Plot(0, 'xeid', OrderContainer(xeid).price, {c=Purple, id=posId})
    end
 
    if position.amount > 0 or position.amount < 0 then
        Plot(0, 'position', position.enterPrice, {c=Blue, id=posId})
    end
 
 
-- update draw-downs
    local lastPosId = Load('posId', '')
    if #lastPosId > 0 and lastPosId != posId then
        posDDs[#posDDs+1] = posDD
        maxDD = ArrayGet(Min(maxDD, posDD), 1)
        posDD = 0
    end
 
-- save memory
    Save('posId', posId)
    Save('index', index)
    Save('leid', leid)
    Save('seid', seid)
    Save('xeid', xeid)
    Save('timeout', timeout)
    Save('posDD', posDD)
    Save('posDDs', posDDs)
    Save('maxDD', maxDD)
    Save('adxsc', adxShutdownCount)
    Save('slhc', slHitCount)
    Save('tphc', tpHitCount)
    Save('ohc', orderHitCount)
    Save('mohc', maxOrderHitCount)
    Save('tohc', totalOrderHitCount)
    Save('pc', positionCount)
 
 
-- finalize
    Finalize(function()
        local adxSlRatio = slHitCount > 0 and adxShutdownCount/slHitCount or 1
        local tpSlRatio = slHitCount > 0 and tpHitCount/slHitCount or 1
        local t = Round((timeout-Time())/60, 0)
        local startBtc = Load('startBtc', 0)
        local endBtc = WalletAmount(AccountGuid(), BaseCurrency())
        CustomReport('Max. DrawDown', maxDD..' %', 'Performance')
        CustomReport('Avg. DrawDown', Round(Average(posDDs), 4)..' %', 'Performance')
        CustomReport('Last DrawDown', posDD..' %', 'Performance')
        CustomReport('ADX Shutdowns', adxShutdownCount, 'Performance')
        CustomReport('StopLoss hits', slHitCount, 'Performance')
        CustomReport('TakeProfit hits', tpHitCount, 'Performance')
        CustomReport('ADX-StopLoss ratio', slHitCount > 0 and adxSlRatio or 1, 'Performance')
        CustomReport('TakeProfit-StopLoss ratio', slHitCount > 0 and tpSlRatio or 1, 'Performance')
        CustomReport('Total positions', positionCount, 'Performance')
        CustomReport('Total orders hit', totalOrderHitCount, 'Performance')
        CustomReport('Max. orders hit/pos', maxOrderHitCount, 'Performance')
        CustomReport('Avg. orders hit/pos', totalOrderHitCount/positionCount, 'Performance')
        CustomReport('ADX', Round(adx,2)..'/'..Round(adxLimit,2), 'Safeties')
        CustomReport('Timeout', t > 0 and (t..' minutes') or ('No active timeout'), 'Safeties')
        CustomReport('Start '..BaseCurrency(), Round(startBtc,8), 'Wallet')
        CustomReport('End '..BaseCurrency(), Round(endBtc,8), 'Wallet')
        CustomReport('Delta', Round(Delta(startBtc, endBtc),4)..'%', 'Wallet')
        CustomReport('Difference', Round(endBtc-startBtc,8)..' '..BaseCurrency(), 'Wallet')
    end)

11 Comments

Sign in to leave a comment.

B
Busera about 6 years ago

Hi, I started to test your script (simulated Kraken Future Accounts with 1 BTC) and I have encounter two questions:
- I am getting the following log messages: "WARNING: No maker template available for KrakenFutures. Falling back to normal limit order."
-- Q: Do I need to create the respective maker template?

- Although the bot is showing +26% , the overall wallet amount dropped to "0.99758157" (from 1.0).

Cheers
Andre

P
pshai about 6 years ago

Hey @Busera.

1) With simulated accounts there are no Maker templates, so the warning is nothing to be worried about.

2) The bot reserves some assets for its orders (when it has open orders). You can add the "In Wallet" value with "In Order"/"In Use" value to get the real total (or simply stop the bot and cancel open orders).

B
Busera about 6 years ago

@pshai Thx a lot for the prompt reply.

B
Busera about 6 years ago

Hi @pshai,

I am running your script now exclusively for the Kraken Future Challenge, and it generates excellent results. I adjusted the settings based on 8W backtesting, which I update every week. I also use a more conservative setting to minimize the drawdown and not to maximize the return.

However, once in a while, the script gets stuck (Rather quite often), e.g., it sells only 99 of 100 contracts. In this case, I have to sell the remaining contracts manually, need to clear the statistics and restart the bot.

Are you facing the same problem?

Update: Today, during the BTC pump, all my gains were killed because the stop loss was not triggered. I am not sure why the script stops working.

P
pshai about 6 years ago

Hey Busera,

Are you able to send me more information @ Haasonline Discord server? Anything you can provide regarding this will be very helpful!

Join the discord server by following this link: https://discord.gg/VxKxgQ

B
BlockHash about 6 years ago

Hi Pshai,

I've got these errors while trying the script

5. 05 May 2020 23:54:14 ERROR: Unknown references: adjustSellPrice
4. 05 May 2020 23:54:14 ERROR: Unknown references: adjustSellPrice

can you provide your support please ?

P
pshai about 6 years ago

Hey @BlockHash the script is now fixed. There was a bug on the site which broke the script... It is all now fixed! :)

S
saveriocastellano over 4 years ago

Hi Pshai,

I'm using this bot with Bitmex/XBTUSD and I have a few questions:

- in the settings I set "order size" to 15000 and multiplier to 1, thus I'd expect the bot to always open positions of size 15000. Instead right now
I see the bot has opened a position of 195000 usd... looking at the position on Bitmex it is saying that margin is "0,0386 XBT (Cross)", so if I understand
corretly this means that the bot opened 195000 contracts with 100x leverage, because current BTC value was 53096 usd, so the position value in BTC
is 3.61 BTC and with a 100x leverage and including commissions this corresponds to 0,0386 magin. So my question is, why did the bot opened
a position of 195000 usd with 100x leverage?

- in the safety settings under stop loss I set 0,3%. So I'd expect the bot to close positions when the value of BTC has moved 0.3% away from the starting
value in the opposite direction of the position that the bot opened. But i don't see a counter order for this being placed by the bot.


I'd like to show you the current situation of the position and orders opened by the bot, and I'd like you to explain me if it is all correct:

current position:

XBTUSD
size: -195.000 USD
value: 3,6117 XBT
entry price: 53096.31
mark price: 53990.64 57114.0
margin 0,0386 XBT (Cross)
unrealised PNL -0,0608 XBT (-165.65%)

active orders:

order #1:

XBTUSD
size 195000 USD
order price 52970.5
filled ---.--
remaining 195000 USD
order value 3,6812 XBT
type Limit

order #2:

XBTUSD
size -15000 USD
order price 55860.5
filled: ---.--
remaining 15000 USD
order value 0,2685 XBT
type Limit

So the first order seem to me the take profit order for the position that was opened... in fact i can see the limit is set at 0.3% below the entry value of the
position (I'm using a take profit setting of 0.3%). But given that there is 100x in the position then I'd expect the value of this order to be 1% in value of the
position size... so about 1950 usd instead of 195000 usd, or am i wrong?

The second order I don't understand it... does this have to do with the stop loss? Can you explain me why it is 15000 usd ?

thanks

P
pshai over 4 years ago

If the order size is set t o 15000 contracts and multiplier to 1, the orders will then be 15000 each time. If your order count is 3, your max position size will be 45000 after the 3rd order. After this, the bot will either hit its target or trigger the stoploss order. In other words, the bot does not care about your leverage setting, so the order size used is 1:1 the value you have set in the bot.

D
DMcL over 3 years ago

I really like this script!
Ive been playing around with it a bit,
Im trying to modify it so it can be short-only (or long-only ) by commenting out lines 278-280 for example...
It works for a few trades but then the price stops adjusting

Anyone want to suggest a tip as to how I would achieve this?

Thanks

D
DMcL over 3 years ago

Figured it out :)

just changed the timeout