Mad Hatter Unmanaged Template

alpha
By bIyni3 in Trading Bots Published August 2025 👁 689 views 💬 1 comments

Description

Its a work in progress that currently already works in Spot market in backtesting. Using typical BBands, and standard rsi as well, so no Mad Hatter logic there yet.
HaasScript
-- Mad Hatter Bot (Unmanaged Trading, No Bot Container)

-- 1. Define Inputs
-- Trading Parameters
local tradeAmount = Input("Trade Amount (Spot)", 100) -- Base-asset amount used in Spot mode
local stopLossPct = Input("Stop Loss (%)", 2.0) -- 2% stop loss
local takeProfitPct = Input("Take Profit (%)", 4.0) -- 4% take profit
local tradeMode = InputOptions("Trade Mode", "Spot", {"Spot", "Futures"})
local useContractsFutures = Input("Futures: Use Contracts", true)
local contractsPerTrade = Input("Futures: Contracts per Trade", 100)
local orderTimeoutSec = Input("Order Timeout (seconds)", 120)

-- BBands Parameters
local bbandsPeriod = Input("BBands Length", 12)
local bbandsDevUp = Input("BBands DevUp", 2)
local bbandsDevDn = Input("BBands DevDown", 2)
local bbandsMaType = InputMaTypes("BBands MA Type", "sma")

-- MACD Parameters
local macdFastLength = Input("MACD Fast Length", 12)
local macdSlowLength = Input("MACD Slow Length", 26)
local macdSignalLength = Input("MACD Signal Length", 9)

-- RSI Parameters
local rsiLength = Input("RSI Length", 14)
local rsiOverbought = Input("RSI Overbought", 70)
local rsiOversold = Input("RSI Oversold", 30)

-- 2. Retrieve Data
local prices = ClosePrices()
local cp = CurrentPrice()
local currentPrice = cp.close

-- 3. Calculate Indicators
-- BBands
local bbandsData = BBANDS(prices, bbandsPeriod, bbandsDevUp, bbandsDevDn, bbandsMaType)
local bbandsUpper = bbandsData[1]
local bbandsMiddle = bbandsData[2]
local bbandsLower = bbandsData[3]

-- MACD
local macdValues = MACD(prices, macdFastLength, macdSlowLength, macdSignalLength)
local macdLine = macdValues.macd
local signalLine = macdValues.signal

-- RSI
local rsi = RSI(prices, rsiLength)

-- 4. Generate Individual Signals (1 = Buy, -1 = Sell, 0 = None)
local bbandsSignal = 0
-- Simple BBands strategy: Buy when price touches lower band, Sell when price touches upper band
if currentPrice <= bbandsLower[1] then
    bbandsSignal = 1 -- Buy
elseif currentPrice >= bbandsUpper[1] then
    bbandsSignal = -1 -- Sell
end

local macdSignal = 0
-- Simple MACD strategy: Buy on crossover above signal, Sell on crossover below signal
if CrossOver(macdLine, signalLine) then
    macdSignal = 1 -- Buy
elseif CrossUnder(macdLine, signalLine) then
    macdSignal = -1 -- Sell
end

local rsiSignal = 0
-- Simple RSI strategy: Buy when oversold, Sell when overbought
if rsi <= rsiOversold then
    rsiSignal = 1 -- Buy
elseif rsi >= rsiOverbought then
    rsiSignal = -1 -- Sell
end

-- 5. Aggregate Signals (Majority Vote)
local buyVotes = 0
local sellVotes = 0

if bbandsSignal == 1 then buyVotes = buyVotes + 1 elseif bbandsSignal == -1 then sellVotes = sellVotes + 1 end
if macdSignal == 1 then buyVotes = buyVotes + 1 elseif macdSignal == -1 then sellVotes = sellVotes + 1 end
if rsiSignal == 1 then buyVotes = buyVotes + 1 elseif rsiSignal == -1 then sellVotes = sellVotes + 1 end

local finalSignal = 0 -- 1 for Buy, -1 for Sell, 0 for None
if buyVotes > sellVotes then
    finalSignal = 1 -- Majority Buy
elseif sellVotes > buyVotes then
    finalSignal = -1 -- Majority Sell
end

-- 6. Implement Unmanaged Trading Logic
local lastOrderId = Load("madHatterLastOrderId", "")
local longPositionId = Load("madHatterLongPositionId", NewGuid())
local shortPositionId = Load("madHatterShortPositionId", NewGuid())
local entryPrice = Load("madHatterEntryPrice", 0)
local currentPositionId = "" -- Will be set based on active position

-- 6.a Order handling: check open/filled/cancelled orders
if lastOrderId ~= "" then
    -- If order is still open, enforce timeout
    if IsOrderOpen(lastOrderId) then
        local openTime = GetOrderOpenTime(lastOrderId)
        if openTime > orderTimeoutSec then
            CancelOrder(lastOrderId)
            LogWarning("Order " .. lastOrderId .. " timed out (".. openTime .."s). Cancelled.")
            Save("madHatterLastOrderId", "")
            -- clear any pending info
            Save("madHatterPendingSide", "")
            Save("madHatterPendingPid", "")
            Save("madHatterPendingAmount", 0)
            Save("madHatterPendingPrice", 0)
        end
    else
        -- Not open anymore -> either filled or cancelled/failed
        if IsOrderFilled(lastOrderId) then
            Log("Order filled: " .. lastOrderId)
            local pendingSide = Load("madHatterPendingSide", "")
            local pendingPid = Load("madHatterPendingPid", "")
            local pendingAmount = Load("madHatterPendingAmount", tradeAmount)
            local pendingPrice = Load("madHatterPendingPrice", currentPrice)
            if pendingSide == "long" and pendingPid ~= "" and pendingAmount > 0 then
                CreatePosition(PositionLong, pendingPrice, pendingAmount, PriceMarket(), 0, pendingPid)
                Save("madHatterEntryPrice", pendingPrice)
                Log("Virtual LONG position created: " .. pendingPid .. " at " .. pendingPrice)
            elseif pendingSide == "short" and pendingPid ~= "" and pendingAmount > 0 then
                CreatePosition(PositionShort, pendingPrice, pendingAmount, PriceMarket(), 0, pendingPid)
                Save("madHatterEntryPrice", pendingPrice)
                Log("Virtual SHORT position created: " .. pendingPid .. " at " .. pendingPrice)
            end
            -- clear pending info
            Save("madHatterPendingSide", "")
            Save("madHatterPendingPid", "")
            Save("madHatterPendingAmount", 0)
            Save("madHatterPendingPrice", 0)
        else
            LogWarning("Order cancelled or failed: " .. lastOrderId)
        end
        Save("madHatterLastOrderId", "")
    end
end

-- Check if we have an open position
local activeLongPosition = not IsPositionClosed(longPositionId) and GetPositionAmount(longPositionId) > 0
local activeShortPosition = not IsPositionClosed(shortPositionId) and GetPositionAmount(shortPositionId) > 0

if activeLongPosition or activeShortPosition then
    if activeLongPosition then
        currentPositionId = longPositionId
    elseif activeShortPosition then
        currentPositionId = shortPositionId
    end
    local positionDirection = GetPositionDirection(currentPositionId)
    local positionAmount = GetPositionAmount(currentPositionId)
    local positionProfit = GetPositionProfit(currentPositionId) -- This is ROI in quote currency

    Log("Current Position: " .. positionDirection .. ", Amount: " .. positionAmount .. ", Profit: " .. positionProfit)

    -- Check Stop Loss / Take Profit
    if positionDirection == PositionLong then
        local profitPct = (currentPrice - entryPrice) / entryPrice * 100
        if profitPct <= -stopLossPct then
            Log("STOP LOSS triggered for LONG position. Exiting.")
            PlaceExitPositionOrder(longPositionId, currentPrice, MarketOrderType, "Stop Loss Exit")
            longPositionId = NewGuid() -- Reset for next trade
            Save("madHatterLongPositionId", longPositionId)
            Save("madHatterEntryPrice", 0)
        elseif profitPct >= takeProfitPct then
            Log("TAKE PROFIT triggered for LONG position. Exiting.")
            PlaceExitPositionOrder(longPositionId, currentPrice, MarketOrderType, "Take Profit Exit")
            longPositionId = NewGuid() -- Reset for next trade
            Save("madHatterLongPositionId", longPositionId)
            Save("madHatterEntryPrice", 0)
        end
    elseif positionDirection == PositionShort then
        local profitPct = (entryPrice - currentPrice) / entryPrice * 100
        if profitPct <= -stopLossPct then
            Log("STOP LOSS triggered for SHORT position. Exiting.")
            PlaceExitPositionOrder(shortPositionId, currentPrice, MarketOrderType, "Stop Loss Exit")
            shortPositionId = NewGuid() -- Reset for next trade
            Save("madHatterShortPositionId", shortPositionId)
            Save("madHatterEntryPrice", 0)
        elseif profitPct >= takeProfitPct then
            Log("TAKE PROFIT triggered for SHORT position. Exiting.")
            PlaceExitPositionOrder(shortPositionId, currentPrice, MarketOrderType, "Take Profit Exit")
            shortPositionId = NewGuid() -- Reset for next trade
            Save("madHatterShortPositionId", shortPositionId)
            Save("madHatterEntryPrice", 0)
        end
    end

    -- If position is closed by SL/TP or manually, clear state
    if IsPositionClosed(longPositionId) and activeLongPosition then
        Log("Long Position " .. longPositionId .. " is closed.")
        longPositionId = NewGuid() -- Reset for next trade
        Save("madHatterLongPositionId", longPositionId)
        Save("madHatterEntryPrice", 0)
    end
    if IsPositionClosed(shortPositionId) and activeShortPosition then
        Log("Short Position " .. shortPositionId .. " is closed.")
        shortPositionId = NewGuid() -- Reset for next trade
        Save("madHatterShortPositionId", shortPositionId)
        Save("madHatterEntryPrice", 0)
    end

else -- No open position, look for new trades
    -- Prevent new entries while an order is pending
    if lastOrderId ~= "" and IsOrderOpen(lastOrderId) then
        Log("An order is already pending (".. lastOrderId .."). Skipping new entries until it resolves.")
        return
    end
    -- Optional: margin/balance checks could be added here if needed


    if finalSignal == 1 then
        Log("Aggregated signal: BUY. Attempting to place order at price: " .. currentPrice .. ", amount: " .. tradeAmount)
        local placeAmount = tradeAmount
        if tradeMode == "Futures" and useContractsFutures then
            placeAmount = contractsPerTrade -- contracts
        end
        -- Enforce exchange minimums
        local minAmt = MinimumTradeAmount(PriceMarket(), currentPrice)
        if placeAmount < minAmt then
            LogWarning('Adjusted amount up to exchange minimum: '.. minAmt)
            placeAmount = minAmt
        end
        if not IsTradeAmountEnough(PriceMarket(), currentPrice, placeAmount, false) then
            LogError("Trade amount " .. placeAmount .. " is not enough for market " .. PriceMarket() .. " at price " .. currentPrice)
        else
            -- Spot wallet pre-check: need enough quote currency to buy base amount
            if tradeMode == "Spot" then
                local fee = TakersFee(PriceMarket())
                local neededQuote = currentPrice * tradeAmount * (1 + fee)
                local haveQuote = WalletAmount('', QuoteCurrency(PriceMarket()))
                if haveQuote < neededQuote then
                    LogWalletError('Insufficient '.. QuoteCurrency(PriceMarket()) ..' balance. Need '.. neededQuote ..', have '.. haveQuote)
                    return
                end
            end
            local orderId = ""
            local priceArg = 1 -- For MarketOrderType, pass 1 to indicate market
            if tradeMode == "Spot" then
                orderId = PlaceBuyOrder(priceArg, placeAmount, { type = MarketOrderType, note = "MadHatter Buy", positionId = longPositionId, timeout = orderTimeoutSec })
            elseif tradeMode == "Futures" then
                orderId = PlaceGoLongOrder(priceArg, placeAmount, { type = MarketOrderType, note = "MadHatter Long", positionId = longPositionId, timeout = orderTimeoutSec })
            end

            if orderId ~= "" then
                Log("Buy/Long order placed: " .. orderId)
                Save("madHatterLastOrderId", orderId)
                -- Save pending details, create position once filled in order handling
                Save("madHatterPendingSide", "long")
                Save("madHatterPendingPid", longPositionId)
                Save("madHatterPendingAmount", placeAmount)
                Save("madHatterPendingPrice", currentPrice)
            else
                LogError("Failed to place buy/long order.")
            end
        end
    elseif finalSignal == -1 then
        Log("Aggregated signal: SELL. Attempting to place order at price: " .. currentPrice .. ", amount: " .. tradeAmount)
        local placeAmount = tradeAmount
        if tradeMode == "Futures" and useContractsFutures then
            placeAmount = contractsPerTrade -- contracts
        end
        -- Enforce exchange minimums
        local minAmt = MinimumTradeAmount(PriceMarket(), currentPrice)
        if placeAmount < minAmt then
            LogWarning('Adjusted amount up to exchange minimum: '.. minAmt)
            placeAmount = minAmt
        end
        if not IsTradeAmountEnough(PriceMarket(), currentPrice, placeAmount, false) then
            LogError("Trade amount " .. placeAmount .. " is not enough for market " .. PriceMarket() .. " at price " .. currentPrice)
        else
            -- Spot wallet pre-check: need enough base currency to sell
            if tradeMode == "Spot" then
                local haveBase = WalletAmount('', BaseCurrency(PriceMarket()))
                if haveBase < tradeAmount then
                    LogWalletError('Insufficient '.. BaseCurrency(PriceMarket()) ..' balance. Need '.. tradeAmount ..', have '.. haveBase)
                    return
                end
            end
            local orderId = ""
            local priceArg = 1 -- For MarketOrderType, pass 1 to indicate market
            if tradeMode == "Spot" then
                orderId = PlaceSellOrder(priceArg, placeAmount, { type = MarketOrderType, note = "MadHatter Sell", positionId = shortPositionId, timeout = orderTimeoutSec })
            elseif tradeMode == "Futures" then
                orderId = PlaceGoShortOrder(priceArg, placeAmount, { type = MarketOrderType, note = "MadHatter Short", positionId = shortPositionId, timeout = orderTimeoutSec })
            end

            if orderId ~= "" then
                Log("Sell/Short order placed: " .. orderId)
                Save("madHatterLastOrderId", orderId)
                -- Save pending details, create position once filled in order handling
                Save("madHatterPendingSide", "short")
                Save("madHatterPendingPid", shortPositionId)
                Save("madHatterPendingAmount", placeAmount)
                Save("madHatterPendingPrice", currentPrice)
            else
                LogError("Failed to place sell/short order.")
            end
        end
    else
        Log("No strong aggregated signal. Waiting.")
    end
end

-- Plotting (Optional, but good for visualization)
Plot(0, "Current Price", currentPrice)
Plot(1, "BBands Upper", bbandsUpper[1], Red)
Plot(1, "BBands Middle", bbandsMiddle[1], Blue)
Plot(1, "BBands Lower", bbandsLower[1], Green)
Plot(2, "MACD Line", macdLine[1], Purple)
Plot(2, "Signal Line", signalLine[1], Orange)
Plot(3, "RSI", rsi[1], Cyan)
PlotHorizontalLine(3, "RSI Overbought", Red, rsiOverbought)
PlotHorizontalLine(3, "RSI Oversold", Green, rsiOversold)

-- Custom Reports
Finalize(function()
    CustomReport("BBands Signal", bbandsSignal)
    CustomReport("MACD Signal", macdSignal)
    CustomReport("RSI Signal", rsiSignal)
    CustomReport("Final Aggregated Signal", finalSignal)
    CustomReport("Current Long Position ID", Load("madHatterLongPositionId", "None"))
    CustomReport("Current Short Position ID", Load("madHatterShortPositionId", "None"))
    CustomReport("Current Entry Price", Load("madHatterEntryPrice", 0))
end)

1 Comment

Sign in to leave a comment.

W
wawan 10 months ago

local buyPrice = Input('Buy Price')
local sellPrice = Input('Sell Price')
local buyWindowPercent = Input('Buy Price Window (%)', 1)
local sellWindowPercent = Input('Sell Price Window (%)', 1)
-- local TradeAmount() = Input('Trade amount')
-- tradeam


local lastOrderType = Load('last_order_type', 'none')

HideOrderSettings()
-- HideTradeAmount()Settings()
-- TradeAmount()

function updateLastOrderType(type)
lastOrderType = type
Save('last_order_type', type)
end

function placeBuyOrderAndWait()
local cp = CurrentPrice()
local buyWindow = buyPrice * buyWindowPercent / 100

if cp.bid >= buyPrice - buyWindow and cp.bid <= buyPrice + buyWindow then
PlaceBuyOrder(buyPrice, TradeAmount(),PriceMarket(),LimitOrderType,"","",-1)
updateLastOrderType('buy')
repeat until IsAnyOrderOpen()
end
end

function placeSellOrderAndWait()
local cp = CurrentPrice()
local sellWindow = sellPrice * sellWindowPercent / 100

if cp.ask >= sellPrice - sellWindow and cp.ask <= sellPrice + sellWindow then
PlaceSellOrder(sellPrice, TradeAmount(),PriceMarket(),LimitOrderType,"","",-1)
updateLastOrderType('sell')
repeat until IsAnyOrderOpen()
end
end

-- Skip very first update cycle
if Load('first_update', true) then
Save('first_update', false)
else
if lastOrderType == 'none' then
placeBuyOrderAndWait()
end

if lastOrderType == 'buy' then
if not IsAnyOrderOpen() then
placeSellOrderAndWait()
end
end

if lastOrderType == 'sell' then
if not IsAnyOrderOpen() then
placeBuyOrderAndWait()
end
end
end