Remote Signals with Limit-Chasing Maker Order Functionality

stable
By Strvinmarvin in Trading Bots Published November 2023 👁 1,146 views 💬 0 comments

Description

This is a simple SIGNALS bot but it demonstrates how to use 'target position size' in a cycle function that constantly tries to close the Delta between what you have now, and what you want to have. It uses Maker Orders only to try and save on fees, and "chases" the price with them until price jitter fills them. You will need remote signals set up for this. It currently only supports 1 coin per bot code, this is an area you may want to improve upon.
HaasScript
-- Author: Strvinmarvin
-- Date: 2023-11-29
-- Notes: This only uses Signals 1, 2, 4, 5
-- It is designed to be a simple signal bot, but to use limit-chasing to secure Maker orders if possible.
-- go here to watch the video on setting up signals in TradingView: https://help.haasonline.com/docs/usage/remote-signals
----------------------------------------------------------------------------------------------------------

-- Your signal details are listed below:

-- Name: your_signal_name_here
-- Identifier: abcdabcdabcdabcd
-- Secret: wxyzwxyzwxyzwxyz
-- Webhooks
-- To make things a little easier for webhook users. We prepared the following copy and paste ready links for you;

-- Long (or buy)

-- https://production.hcdn.web.haasapi.com/SignalAPI.php?channel=STORE_SIGNAL&id=abcdabcdabcdabcd&secret=wxyzwxyzwxyzwxyz&signal=1

-- Short (or sell)

-- https://production.hcdn.web.haasapi.com/SignalAPI.php?channel=STORE_SIGNAL&id=abcdabcdabcdabcd&secret=wxyzwxyzwxyzwxyz&signal=2

-- Exit all positions

-- https://production.hcdn.web.haasapi.com/SignalAPI.php?channel=STORE_SIGNAL&id=abcdabcdabcdabcd&secret=wxyzwxyzwxyzwxyz&signal=3

-- Exit long

-- https://production.hcdn.web.haasapi.com/SignalAPI.php?channel=STORE_SIGNAL&id=abcdabcdabcdabcd&secret=wxyzwxyzwxyzwxyz&signal=4

-- Exit short

-- https://production.hcdn.web.haasapi.com/SignalAPI.php?channel=STORE_SIGNAL&id=abcdabcdabcdabcd&secret=wxyzwxyzwxyzwxyz&signal=5

-- Custom signal 1

-- https://production.hcdn.web.haasapi.com/SignalAPI.php?channel=STORE_SIGNAL&id=abcdabcdabcdabcd&secret=wxyzwxyzwxyzwxyz&signal=6

-- Custom signal 2

-- https://production.hcdn.web.haasapi.com/SignalAPI.php?channel=STORE_SIGNAL&id=abcdabcdabcdabcd&secret=wxyzwxyzwxyzwxyz&signal=7

-- High speed update are recommended for a quick responce, but its not essential
EnableHighSpeedUpdates(true)
HideTradeAmountSettings()
HideOrderSettings()

local notional_fiat_per_side = Input('Notional FIAT Per Side', 2000)
local target_leverage = Input('Target Lev', 10)
 
-- Use your own signal identifier for this field
signal = GetRemoteSignal('abcdabcdabcdabcd')

local current_signal = Load('current_signal', {
    ['LONG'] = SignalNone,
    ['SHORT'] = SignalNone
})

-- Process Long signals
if current_signal['LONG'] ~= SignalExitLong and signal == SignalExitLong then
    current_signal['LONG'] = SignalExitLong
    Log('[LONG] Signal Received: '..current_signal['LONG'], Green)

elseif current_signal['LONG'] ~= SignalLong and signal == SignalLong then
    current_signal['LONG'] = SignalLong
    Log('[LONG] Signal Received: '..current_signal['LONG'], Green)
end

-- Process Short signals
if current_signal['SHORT'] ~= SignalExitShort and signal == SignalExitShort then
    current_signal['SHORT'] = SignalExitShort
    Log('[SHORT] Signal Received: '..current_signal['SHORT'], Fuchsia)

elseif current_signal['SHORT'] ~= SignalShort and signal == SignalShort then
    current_signal['SHORT'] = SignalShort
    Log('[SHORT] Signal Received: '..current_signal['SHORT'], Fuchsia)
end


local hard_exit_now = Input('Hard Exit Now', false, '', 'Control')
local soft_exit_now = Input('Soft Exit Now', false, '', 'Control')

local log_inposition_holding = Input('Log when in position and holding?', false)
local log_noposition_holding = Input('Log when not in position and waiting?', false)

local mkt = PriceMarket()
local tmkt = TradeMarketContainer(mkt)
local cp = CurrentPrice(mkt)

local g = Load('g', {})
if Count(g) == 0 then
    g.target_amt = {
        ['LONG'] = 0,
        ['SHORT'] = 0,
    }
    SetLeverage(target_leverage, mkt)
    SetMarginMode(IsolatedMarginMode, mkt)
    SetPositionMode(HedgePositionMode, '', mkt)
    
    -- Check if we already have open positions.
    local sides = {'LONG', 'SHORT'}
    local pos_enum = {
        ['LONG'] = PositionLong,
        ['SHORT'] = PositionShort
    }
    local pos = {}
    for side in sides do
        pos[side] = UserPositionContainer({market = mkt, direction = pos_enum[side]})
        if pos[side].amount > tmkt.calculatedMinTradeAmount then
            g.target_amt[side] = pos[side].amount
            Log('**** Found existing '..side..' Position, setting this as the starting state.', SkyBlue)
            if side == 'LONG' then
                current_signal[side] = SignalLong
                CreatePosition(PositionLong, pos[side].enterPrice, pos[side].amount, mkt, target_leverage, BaseCurrency(mkt)..'-'..side)
            elseif side == 'SHORT' then
                current_signal[side] = SignalShort
                CreatePosition(PositionLong, pos[side].enterPrice, pos[side].amount, mkt, target_leverage, BaseCurrency(mkt)..'-'..side)
            end
        end
    end
end

local coin = BaseCurrency(mkt)


local sides = {'LONG', 'SHORT'}
local pos_enum = {
    ['LONG'] = PositionLong,
    ['SHORT'] = PositionShort
}

local pos = {}
for side in sides do
    pos[side] = UserPositionContainer({market = mkt, direction = pos_enum[side]})
end

function heartbeat()
    Log('.  .  '..coin..' - '..'[HeartBeat] '..'LONG Current: '..pos['LONG'].amount..' '..coin..' (PNL: $'..pos['LONG'].profit..'), SHORT Current: '..pos['SHORT'].amount..' '..coin..' (PNL: $'..pos['SHORT'].profit..')', DarkGray)
end

OptimizedForInterval(15, heartbeat)

function AdjustPositionSizes(f_side, f_pos)
    local target_size = ParseTradeAmount(mkt, cp.bid, g.target_amt[f_side])
    local side = f_side
    local pos = f_pos[side]
    local pid = coin..'-'..side
    local open_orders = GetAllOpenOrders(pid)
    local open_pos = GetAllOpenPositions()

    if hard_exit_now then
        Log('* '..coin..' HARD-EXIT enabled - Overriding Positions to Target of 0...', Orange)
        target_size = 0 -- overrride to 0
    end

    local delta
    if target_size == 0 and pos.amount == 0 then
        if log_noposition_holding then
            Log('.  .  '..coin..' - '..'[PosAdj] '..side..' Current: '..pos.amount..' '..coin..', Target: '..target_size..' '..coin..', No Position and No Target (Zero)', DarkGray)
        end
        if Count(open_orders) > 0 then
            CancelAllOrders(pid)
        end
        return
    elseif target_size == pos.amount then
        if log_inposition_holding then
            Log('.  .  '..coin..' - '..'[PosAdj] '..side..' Current: '..pos.amount..' '..coin..', Target: '..target_size..' '..coin..', No Delta (No Change)', SkyBlue)
        end
        if Count(open_orders) > 0 then
            CancelAllOrders(pid)
        end
        return
    elseif target_size > pos.amount then
        delta = ParseTradeAmount(mkt, cp.bid, target_size - pos.amount)
        Log('.  .  '..coin..' - '..'[PosAdj] '..side..' Current: '..pos.amount..' '..coin..', Target: '..target_size..' '..coin..', Delta: + '..delta..' (Accumulate)', Green)
    else
        delta = ParseTradeAmount(mkt, cp.bid, pos.amount - target_size)
        Log('.  .  '..coin..' - '..'[PosAdj] '..side..' Current: '..pos.amount..' '..coin..', Target: '..target_size..' '..coin..', Delta: - '..delta..' (Distribute)', Purple)
    end

    if IsTradeAmountEnough(mkt, cp.bid, delta, false) then
        if side == 'LONG' then
            if target_size > pos.amount then --accumulate
                if soft_exit_now then
                    Log('* '..coin..' SOFT-EXIT enabled - blocking new Long position...', Orange)
                -- elseif Count(open_pos) > max_open_positions then
                --     Log('* '..coin..' Max Open Positions [max: '..max_open_positions..'] exceeded [open now: '..Count(open_pos)..'] - blocking new Long position...', Yellow)
                else
                    for key, ord in pairs(open_orders) do
                        if ord.isEnterOrder then
                            if ord.price == cp.bid then
                                Log('.  .  '..coin..' - '..'[PosAdj] Enter '..side..' order already exists at Bid price...')
                                return
                            else
                                Log('.  .  '..coin..' - '..'[PosAdj] Enter '..side..' order already exists but NOT at Bid price. Cancelling...')
                                CancelOrder(ord.orderId)
                            end
                        end
                    end
                    PlaceGoLongOrder(cp.bid, delta, mkt, MakerOrCancelOrderType, 'Accumulate '..side, pid, 120)
                end

            else --distribute
                for key, ord in pairs(open_orders) do
                    if ord.isExitOrder then
                        if ord.price == cp.ask then
                            Log('.  .  '..coin..' - '..'[PosAdj] Exit '..side..' order already exists at Ask price...')
                            return
                        else
                            Log('.  .  '..coin..' - '..'[PosAdj] Exit '..side..' order already exists but NOT at Ask price. Cancelling...')
                            CancelOrder(ord.orderId)
                        end
                    end
                end

                if pos.amount < tmkt.calculatedMinTradeAmount then
                    PlaceGoLongOrder(cp.bid, tmkt.calculatedMinTradeAmount, mkt, MarketOrderType, 'Position too small - market buy '..side, pid)
                else
                    PlaceExitLongOrder(cp.ask, delta, mkt, MakerOrCancelOrderType, 'Distribute '..side, pid, 120)
                end
            end

        elseif side == 'SHORT' then
            if target_size > pos.amount then --accumulate
                if soft_exit_now then
                    Log('* '..coin..' SOFT-EXIT enabled - blocking new Short position...', Orange)
                --  elseif Count(open_pos) > max_open_positions then
                --     Log('* '..coin..' Max Open Positions [max: '..max_open_positions..'] exceeded [open now: '..Count(open_pos)..'] - blocking new Short position...', Yellow)
                else
                    for key, ord in pairs(open_orders) do
                        if ord.isEnterOrder then
                            if ord.price == cp.ask then
                                Log('.  .  '..coin..' - '..'[PosAdj] Enter '..side..' order already exists at Ask price...')
                                return
                            else
                                Log('.  .  '..coin..' - '..'[PosAdj] Enter '..side..' order already exists but NOT at Ask price. Cancelling...')
                                CancelOrder(ord.orderId)
                            end
                        end
                    end
                    PlaceGoShortOrder(cp.ask, delta, mkt, MakerOrCancelOrderType, 'Accumulate '..side, pid, 120)
                end

            else --distribute
                for key, ord in pairs(open_orders) do
                    if ord.isExitOrder then
                        if ord.price == cp.bid then
                            Log('.  .  '..coin..' - '..'[PosAdj] Exit '..side..' order already exists at Bid price...')
                            return
                        else
                            Log('.  .  '..coin..' - '..'[PosAdj] Exit '..side..' order already exists but NOT at Bid price. Cancelling...')
                            CancelOrder(ord.orderId)
                        end
                    end
                end

                if pos.amount < tmkt.calculatedMinTradeAmount then
                    PlaceGoShortOrder(cp.ask, tmkt.calculatedMinTradeAmount, mkt, MarketOrderType, 'Position too small - market buy '..side, pid)
                else 
                    PlaceExitShortOrder(cp.bid, delta, mkt, MakerOrCancelOrderType, 'Distribute '..side, pid, 120)
                end
            end
        end

    else
        Log('.  .  '..coin..' - '..'[PosAdj] '..side..' Trade Amount too Small! ['..delta..' '..coin..']')
        return
    end

end                  

local qty_to_trade = CC_GetQtyFromFiat(notional_fiat_per_side, mkt).coin_amount

-- Process Long position
if current_signal['LONG'] == SignalLong then
    if pos['LONG'].amount < qty_to_trade * 0.95 and qty_to_trade - pos['LONG'].amount >= tmkt.calculatedMinTradeAmount then
        g.target_amt['LONG'] = qty_to_trade
    end

elseif current_signal['LONG'] == SignalExitLong then
    g.target_amt['LONG'] = 0
end

-- Process Short position
if current_signal['SHORT'] == SignalShort then
    if pos['SHORT'].amount < qty_to_trade * 0.95 and qty_to_trade - pos['SHORT'].amount >= tmkt.calculatedMinTradeAmount then
        g.target_amt['SHORT'] = qty_to_trade
    end

elseif current_signal['SHORT'] == SignalExitShort then
    g.target_amt['SHORT'] = 0
end

for side in sides do
    AdjustPositionSizes(side, pos)
end
    

Save('g', g)
Save('current_signal', current_signal)

0 Comments

Sign in to leave a comment.

No comments yet. Be the first!