Mad Hatter Unmanaged Template
alphaDescription
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.
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