SMM 2.0 SMOD
stableDescription
Modification of Phsai's SMM 2.0
---Camarilla Levels---
Adding Camarilla levels for dynamic range based in daily, weekly or monthly intervals. When activated bot will only trade between the Third Support and Third Resistance and take action (if selected) at Fourth Support or Fourth Resistance.
More about Camarilla https://pivotboss.com/2010/05/31/the-camarilla-equation-explained/
---Balance Management---
As usual with my script, you have to enter the amount you allocated for bot to work a.k.a Trading Balance. With this bot has new stop loss trigger which is Balance Ratio. This is the default SL type with default value 1 which means if Working Balance (Used balance for positions + position PNL ) is >= Trading Balance then all positions will be closed. If you set it to 0.8 then SL will triggered when working balance is 80% of trading balance.
For backtest there is an option to stop when the Balance Ration SL triggered.
The profit is not compounded into Trading Balance but if the wallet balance is < trading balance then you will get a notification in the log about it and bot will use the wallet balance for Balance Ratio. It is better to enter trading balance lower than wallet balance to give some room (loss).
---Order at New Candle Open---
Pay attention to the Order Timeout. Orders will be place on candle close (or new candle open) in this interval and will be cancelled if not filled within the forming candle. For example, if you set it to 60 then bot will be placing order at new 1H candle and will cancel it if it is not filled within 1 hour. If the order is canceled by % value then new one will be placed at the next candle.
---FIX 1.0---
1. Fixing position ID. The mistake causing miscalculation in balance usage.
2. Fixing time interval misconfiguration for Camarilla and giving more options.
3. Adding position entry plot to better visualize the trades in back test.
May the profits be with you!
More questions ? Find me in Haasonline Discord.
If you would like to buy me a cup of coffee or diamond:
ENS: smokyho.eth
Lightning: [email protected]
HaasScript
-- [pshaiBot] Simple Market Maker 2.0
-- Author: pshai
-- Mods by: smokyho
-- FIX 1.0
EnableHighSpeedUpdates(true)
HideOrderSettings()
HideTradeAmountSettings()
-- =======================================================================================================
-- == SMOKYHO MOD
-- =======================================================================================================
InputGroupHeader('Bot Settings')
local tradeBal = Input("Trading Balance", 0, "Trading balance in QUOTE currency")
if WalletAmount() < tradeBal then
tradeBal = WalletAmount()
Log("Trading balance adjusted because your wallet balance < trading balance", Yellow)
end
local useCam = Input('Use Camarilla for range', false, "Use Camarilla levels to define trade range. If activated it will ignore ONLY value in upper/lower Price Limit entry")
local rangeInterval = InputInterval('Camarilla Interval', 1440, 'Time interval for Camarilla levels') --InputOptions('Camarilla Interval', 'Daily', camIntervals, 'Time interval for Camarilla levels')
local showInfo = Input('Details in Log', false, 'Show detail information in logs about the bot condition')
InputGroupHeader('Backtest Settings')
local wtfBal = Input('Deactivate on Over Budget', false, 'Deactivate when Working Balance >= Starting Balance')
-- price and data
local getMarket = PriceMarket()
local cp = CurrentPrice()
if useCam then
s3, r3, s4, r4 = OptimizedForInterval(CurrentInterval(), function()
local high = HighPrices(rangeInterval)
local low = LowPrices(rangeInterval)
local close = ClosePrices(rangeInterval)
local range = high - low
local r3 = close + (range * 1.1 / 4)
local s3 = close - (range * 1.1 / 4)
local r4 = close + (range * 1.1 / 2)
local s4 = close - (range * 1.1 / 2)
return s3, r3, s4, r4
end)
end
-- =======================================================================================================
-- == INIT
-- =======================================================================================================
if not init then
bot = Load('bot', {})
-- id
bot.longPosId = NewGuid()
bot.shortPosId = NewGuid()
-- stat
bot.SRCounter = 0
bot.BRCounter = 0
bot.SACounter = 0
bot.slotCounter = 0
-- create config objects
long_config = {}
short_config = {}
init = true
end
__isSpot = MarketType() == SpotTrading
-- budget management
local realizedBotProfit = GetBotProfit()
local longProfit = GetCurrentProfit(PositionLong)
local shortProfit = GetCurrentProfit(PositionShort)
local usedLong = UsedMargin(getMarket, GetPositionEnterPrice(bot.longPosId), LongAmount(getMarket), GetLeverage())
local usedShort = UsedMargin(getMarket, GetPositionEnterPrice(bot.shortPosId), ShortAmount(getMarket), GetLeverage())
local workingBalance = (usedLong + usedShort - longProfit - shortProfit)
local balRatio = workingBalance / tradeBal
-- =======================================================================================================
-- == OPTIONS ARRAYS
-- =======================================================================================================
local orderTypes = {
limit = 'LIMIT',
postLimit = 'LIMIT (Post-Only)',
market = 'MARKET'
}
local marginTypes = {
base = 'BASE',
quote = 'QUOTE'
}
local tpTypes = {
none = 'NONE',
priceChange = 'PRICE CHANGE %',
pnl = 'TRADING BALANCE %'
}
local slTypes = {
br = 'BALANCE RATIO',
priceChange = 'PRICE CHANGE %',
pnl = 'OPEN PNL'
}
local stopActions = {
none = 'NONE',
exit = 'CLOSE POSITION'
}
-- =======================================================================================================
-- == INPUTS
-- =======================================================================================================
---------------------------------------------------------------------------------
-- CONFIG BUILDER
function buildConfigs(config, prefix)
local group = prefix .. ' Settings'
InputButton('------ '..prefix..' ENTRY SETTINGS ------', ||nil, '', group)
config.enabled = Input('Trade '..prefix, false, 'If true, bot will be placing long entries.', group)
config.marginType = InputOptions(prefix..' Margin Type', marginTypes.quote, marginTypes, 'Margin type.', group)
config.orderMargin = Input(prefix..' Order Margin', 0, 'Order size, based on the margin type. This value DOES NOT account for leverage, but is rather the notional size. 10 USDT order margin on 10x leverage would result in 1 USDT used margin per order.', group)
config.orderMarginMult = Input(prefix..' Order Margin Multiplier', 1, 'If set to 1, order size is constant. If higher than 1, order size will INCREASE per entry. If lower than 1, order size will DECREASE per entry.', group)
config.triggerOffset = Input(prefix..' Trigger Offset %', 0, 'Price change required (downwards for longs, up for shorts) before an entry order is placed. Negative values will allow longs traded upwards and shorts down. Set this to 0 to disable.', group)
config.orderOffset = Input(prefix..' Order Offset %', 0.382, 'How far the new order is placed from either current prices or its trigger price.', group)
config.orderTimeout = Input(prefix..' Order Timeout (MINUTES)', 60, 'Entry order timeout in MINUTES (for LIMIT order type). Set this to -1 to disable.', group)
config.orderDelta = Input(prefix..' Order Delta Cancel %', 0, 'How far the prices can run before the entry order is cancelled and replaced.', group)
config.orderType = InputOptions(prefix..' Order Type', orderTypes.limit, orderTypes, 'Entry order type.', group)
InputButton('------ '..prefix..' TAKE-PROFIT SETTINGS ------', ||nil, '', group)
config.tpType = InputOptions(prefix..' Take-profit Type', tpTypes.priceChange, tpTypes, 'Take-profit type.', group)
config.tpValue = Input(prefix..' Take-profit Value', 0.382, 'Take-profit value for the Take-profit Type.', group)
config.tpOrderType = InputOptions(prefix..' Order Type', orderTypes.limit, orderTypes, 'Take-profit order type.', group)
InputButton('------ '..prefix..' STOP-LOSS SETTINGS ------', ||nil, '', group)
config.slType = InputOptions(prefix..' Stop-loss Type', slTypes.br, slTypes, 'Stop-loss type.', group)
config.slValue = Input(prefix..' Stop-loss Value', 1, 'Stop-loss value for the Stop-loss Type.', group)
config.slOrderType = InputOptions(prefix..' Order Type', orderTypes.limit, orderTypes, 'Stop-loss order type.', group)
InputButton('------ '..prefix..' RANGE SETTINGS ------', ||nil, '', group)
config.upperPrice = Input(prefix..' Upper Price Limit', 0, 'Upper price level where the bot will stop trading and goes into PAUSE regardless of stop-action (no deactivation).', group)
config.upperAction = InputOptions(prefix..' Upper Limit Action', stopActions.none, stopActions, 'Upper price limit stop-action.', group)
config.lowerPrice = Input(prefix..' Lower Price Limit', 0, 'Lower price level where the bot will stop trading and goes into PAUSE regardless of stop-action (no deactivation).', group)
config.lowerAction = InputOptions(prefix..' Lower Limit Action', stopActions.none, stopActions, 'Lower price limit stop-action.', group)
config.reset = Input(prefix..' Re-init Config', false, 'If true, configs will be re-initialized in next update (allows to continue trading without having to restart bot). It is advised to let bot only update once when this is enabled.', group)
--range mod
config.upperPrice = useCam and r4 or config.upperPrice
config.lowerPrice = useCam and s4 or config.lowerPrice
--plot the range
if config.upperPrice > 0 then
Plot(0, 'Upper Limit', config.upperPrice, SkyBlue)
end
if config.lowerPrice > 0 then
Plot(0, 'Lower Limit', config.lowerPrice, Orange)
end
end
---------------------------------------------------------------------------------
-- BUILD CONFIGS
buildConfigs(long_config, 'Long')
buildConfigs(short_config, 'Short')
-- =======================================================================================================
-- == FUNCTIONS
-- =======================================================================================================
---------------------------------------------------------------------------------
-- NEW SLOT OBJECT
function newSlot()
return {
id = '',
oid = '',
triggerPrice = 0,
price = 0,
size = 0,
timeoutTime = 0
}
end
---------------------------------------------------------------------------------
-- CONVERT ORDER TYPE
function getOrderType(strType)
if strType == orderTypes.limit then
return LimitOrderType
elseif strType == orderTypes.postLimit then
return MakerOrCancelOrderType
elseif strType == orderTypes.market then
return MarketOrderType
end
LogError('Order type cannot be converted: ' .. strType)
end
---------------------------------------------------------------------------------
-- CONVERT TRADE AMOUNT
function getOrderSize(config)
-- get order margin from config
local margin = config.orderMargin
local res = 0
-- multiply the amount
for i = 1, config.entryCount do
if i == 1 then
res = margin
else
res = res * config.orderMarginMult
end
end
-- convert order size
if config.marginType == marginTypes.quote then
res = res / ContractValue()
end
return res
end
---------------------------------------------------------------------------------
-- UPDATE POSITION
function updatePid(config, cp, isLong)
-- dont update if position ID empty
if not config.pid then
return
end
-- get position info
local pos = PositionContainer(config.pid)
-- reset position
if pos.amount == 0 and pos.enterPrice > 0 then
if IsAnyOrderOpen(config.pid) then
CancelAllOrders(config.pid)
else
config.pid = NewGuid()
config.entryCount = 1
config.startPrice = isLong and cp.bid or cp.ask
if isLong then
bot.longPosId = config.pid
else
bot.shortPosId = config.pid
end
LogWarning('-- New PID generated for '..(isLong and 'LONG' or 'SHORT')..' position. --')
end
end
end
---------------------------------------------------------------------------------
-- RUN CONFIGS
function runConfigs(config, isLong)
-- dont run if disabled
if not config.enabled then
return
end
-- reset
if config.reset then
config.run = nil
config.pid = nil
config.startPrice = nil
config.entryCount = nil
config.entrySlot = nil
LogWarning('-- Bot was RESET. --')
end
-- init some variables
if not config.run then config.run = true end
if not config.pid then
config.pid = NewGuid()
if isLong then
bot.longPosId = config.pid
else
bot.shortPosId = config.pid
end
end
if not config.startPrice then config.startPrice = isLong and cp.bid or cp.ask end
if not config.entryCount then config.entryCount = 1 end
if not config.entrySlot then config.entrySlot = newSlot() end
-- run everything
updatePid(config, cp, isLong)
runRange(config, cp)
runStoploss(config, cp, isLong)
runTakeprofit(config, cp, isLong)
-- range
local rangeSet = config.lowerPrice > 0 and config.upperPrice > 0
if useCam then
inRange = rangeSet and cp.bid > s3 and cp.ask < r3
else
inRange = rangeSet and cp.bid > config.lowerPrice and cp.ask < config.upperPrice
end
local aboveLower = cp.bid > config.lowerPrice
local belowUpper = cp.ask < config.upperPrice
local okLong = false
local okShort = false
if rangeSet then
okLong = inRange
okShort = inRange
else
if config.lowerPrice > 0 and config.upperPrice == 0 then
okLong = cp.bid > config.lowerPrice
end
if config.lowerPrice == 0 and config.upperPrice > 0 then
okShort = cp.ask < config.upperPrice
end
end
if isLong and MinutesTillCandleClose(config.orderTimeout) == 0 and IfElse(config.lowerPrice > 0, okLong, true) then
runEntry(config, cp, isLong)
elseif not isLong and MinutesTillCandleClose(config.orderTimeout) == 0 and IfElse(config.upperPrice > 0, okShort, true) then
runEntry(config, cp, isLong)
end
end
---------------------------------------------------------------------------------
-- RUN RANGE
function runRange(config, cp, isLong)
-- return if not configs running
if not config.run then
return
end
-- get position info
local pos = PositionContainer(config.pid)
-- if upper price set and last traded price goes above, we stop running configs
if config.upperPrice > 0 and cp.close > config.upperPrice then
PlotSignalBar(-2, White)
CancelAllOrders(config.pid)
-- stop action
if config.upperAction == stopActions.exit and pos.amount > 0 then
PlaceExitPositionOrder(config.pid, {type = MarketOrderType, note = 'Stop-Action Exit'})
bot.SACounter = bot.SACounter + 1
end
-- inform the user
--LogWarning((isLong and 'LONG' or 'SHORT')..' CONFIG PAUSED: Price breached UPPER price limit of '..config.upperPrice..'.')
end
-- if lower price set and last traded price goes below, we stop running configs
if config.lowerPrice > 0 and cp.close < config.lowerPrice then
PlotSignalBar(-2, White)
CancelAllOrders(config.pid)
-- stop action
if config.lowerAction == stopActions.exit and pos.amount > 0 then
PlaceExitPositionOrder(config.pid, {type = MarketOrderType, note = 'Stop-Action Exit'})
bot.SACounter = bot.SACounter + 1
end
-- inform the user
--LogWarning((isLong and 'LONG' or 'SHORT')..' CONFIG PAUSED: Price breached LOWER price limit of '..config.lowerPrice..'.')
end
end
---------------------------------------------------------------------------------
-- RUN STOP-LOSS
function runStoploss(config, cp, isLong)
if not config.run then
return
end
if wtfBal and workingBalance != 0 then
if workingBalance >= tradeBal then
DeactivateBot('Over Budget', true)
end
end
local direction = isLong and PositionLong or PositionShort
local trigger = false
----------
-- balance ratio
if config.slType == slTypes.br then
trigger = workingBalance / tradeBal >= config.slValue
----------
-- price change % based stop-loss
elseif config.slType == slTypes.priceChange then
trigger = StopLoss(config.slValue, config.pid, direction)
----------
-- unrealized PnL based stop-loss
elseif config.slType == slTypes.pnl then
local pos = PositionContainer(config.pid)
trigger = pos.profit <= -config.slValue
end
if trigger then
-- inform user
LogWarning('-- Stop-Loss triggered. --')
bot.SRCounter = bot.SRCounter + 1
-- cancel all open orders
CancelAllOrders(config.pid)
-- set the price
local price = isLong
and cp.bid
or cp.ask
-- offset price a bit of using post-only limit
if config.slOrderType == orderTypes.postLimit then
price = isLong
and AddPerc(price, 0.01)
or SubPerc(price, 0.01)
end
-- place exit position order
PlaceExitPositionOrder(
config.pid,
price,
getOrderType( config.slOrderType ),
(isLong and 'Long' or 'Short')..' Stop-Loss'
)
config.run = false
end
end
---------------------------------------------------------------------------------
-- RUN TAKE-PROFIT
function runTakeprofit(config, cp, isLong)
if not config.run then
return
end
local direction = isLong and PositionLong or PositionShort
local trigger = false
----------
-- no TP used, return
if config.tpType == tpTypes.none then
return
----------
-- price change % based take-profit
elseif config.tpType == tpTypes.priceChange then
trigger = TakeProfit(config.tpValue, config.pid, direction)
----------
-- unrealized PnL based take-profit
elseif config.tpType == tpTypes.pnl then
local pos = PositionContainer(config.pid)
trigger = pos.profit >= config.tpValue * tradeBal / 100
end
if trigger then
-- inform user
LogWarning('-- Take-Profit triggered. --')
-- cancel all open orders
CancelAllOrders(config.pid)
-- set the price
local price = isLong
and cp.ask
or cp.bid
-- offset price a bit of using post-only limit
if config.tpOrderType == orderTypes.postLimit then
price = isLong
and price + PriceStep(getMarket)
or price - PriceStep(getMarket)
end
-- place exit position order
PlaceExitPositionOrder(
config.pid,
price,
getOrderType( config.tpOrderType ),
(isLong and 'Long' or 'Short')..' Take-Profit'
)
--config.run = false
end
end
---------------------------------------------------------------------------------
-- RUN ENTRY
function runEntry(config, cp, isLong)
if not config.run then
return
end
-- flag-variable for if we can place new entry or not
local canPlace = false
local pos = PositionContainer(config.pid)
local LLP = LastLongPrice()
local LSP = LastShortPrice()
----------
-- limit order type logic
if config.orderType != orderTypes.market then
-- is triggerOffset used?
if config.triggerOffset != 0 then
-- get last recorded price (value updated when order has filled, resets when position exits)
local triggerLevel = config.startPrice
-- calculate delta accordingly
local delta = isLong
and Delta(triggerLevel, cp.bid)
or Delta(cp.ask, triggerLevel)
-- allow new entry
if delta <= -config.triggerOffset then
-- inform user
LogWarning('-- New '..(isLong and 'LONG' or 'SHORT')..' entry allowed. --')
canPlace = true
-- set entry order price
if pos.amount > 0 then
config.entrySlot.price = isLong
and SubPerc(LLP, (config.orderOffset + config.triggerOffset))
or AddPerc(LSP, (config.orderOffset + config.triggerOffset))
else
config.entrySlot.price = isLong
and SubPerc(cp.bid, config.orderOffset)
or AddPerc(cp.ask, config.orderOffset)
end
-- set entry order size
config.entrySlot.size = getOrderSize( config )
end
else
canPlace = true
-- set entry order price
if pos.amount > 0 then
config.entrySlot.price = isLong
and SubPerc(LLP, config.orderOffset)
or AddPerc(LSP, config.orderOffset)
else
config.entrySlot.price = isLong
and SubPerc(cp.bid, config.orderOffset)
or AddPerc(cp.ask, config.orderOffset)
end
-- set entry order size
config.entrySlot.size = getOrderSize( config )
end
----------
-- ...market order type logic
elseif config.orderType == orderTypes.market then
-- get last recorded price (value updated when order has filled, resets when position exits)
local triggerLevel = config.startPrice
-- combine triggerOffset and orderOffset to trigger market order entry
local combinedOffset = 0
-- add trigger offset if used
if config.triggerOffset >= 0 then
combinedOffset = config.triggerOffset
end
-- add order offset
combinedOffset = combinedOffset + config.orderOffset
-- calculate delta accordingly
local delta = isLong
and Delta(triggerLevel, cp.bid)
or Delta(cp.ask, triggerLevel)
-- allow new entry
if delta <= -combinedOffset then
-- inform user
LogWarning('-- New '..(isLong and 'LONG' or 'SHORT')..' entry allowed. --')
canPlace = true
-- set entry order price
config.entrySlot.price = isLong
and cp.bid
or cp.ask
-- set entry order size
config.entrySlot.size = getOrderSize( config )
end
end
-- continue execution in other function
updateEntry(config, cp, canPlace, isLong)
end
---------------------------------------------------------------------------------
-- UPDATE ENTRY
function updateEntry(config, cp, canPlace, isLong)
-- define proper entry order command
local cmd = isLong
and (__isSpot and PlaceBuyOrder or PlaceGoLongOrder)
or (__isSpot and PlaceSellOrder or PlaceGoShortOrder)
-- set informative name for order
local orderName = isLong
and 'Long Entry '..config.entryCount
or 'Short Entry '..config.entryCount
-- order id is empty
if config.entrySlot.oid == '' then
-- place new order if allowed
if canPlace then
config.entrySlot.oid = cmd(
config.entrySlot.price,
config.entrySlot.size,
{
note = orderName,
timeout = config.orderTimeout * 60,
type = getOrderType( config.orderType ),
positionId = config.pid
}
)
end
else
-- get the order
local order = OrderContainer(config.entrySlot.oid)
-- order not open
if not order.isOpen then
config.entrySlot.oid = '' -- reset order ID
-- order was fully filled
if order.isFilled then
config.startPrice = order.price -- update last price
config.entryCount = config.entryCount + 1 -- increment entry count
if config.entryCount > bot.slotCounter then
bot.slotCounter = config.entryCount
end
end
else
-- plot order as line if open
Plot(0, orderName, order.price, {c = isLong and Green or Red, id = ''..order.price})
-- order delta cancellation
if config.orderDelta > 0 then
-- calculate delta
local delta = isLong
and Delta(order.price, cp.bid)
or Delta(cp.ask, order.price)
-- cancel order
if delta > config.orderDelta then
CancelOrder(config.entrySlot.oid)
end
end
end
end
end
-- =======================================================================================================
-- == RUN
-- =======================================================================================================
runConfigs(long_config, true)
runConfigs(short_config, false)
-- =======================================================================================================
-- == BOT PERFORMANCE REPORT
-- =======================================================================================================
if balRatio > bot.BRCounter then bot.BRCounter = balRatio end
local profitPercent = realizedBotProfit / tradeBal * 100
local profit2bal = realizedBotProfit / tradeBal / bot.BRCounter
ChartSetOptions(1, "Bot Performance")
Plot(1, "Working Balance", workingBalance, Red)
Plot(1, "Bot Balance", tradeBal + realizedBotProfit, White)
Plot(1, "Trading Balance", tradeBal, Green)--]]
--AEP Plot
if usedLong != 0 then
Plot(0, 'AvgEP Long', GetPositionEnterPrice(bot.longPosId), {id=bot.longPosId, c=Green, w=2})
--Log('GetPositionEnterPrice(bot.longPosId) '..GetPositionEnterPrice(bot.longPosId))
--Log('Long PID '..bot.longPosId)
end
if usedShort != 0 then
Plot(0, 'AvgEP Short', GetPositionEnterPrice(bot.shortPosId), {id=bot.shortPosId, c=Red, w=2})
--Log('GetPositionEnterPrice(bot.shortPosId) '..GetPositionEnterPrice(bot.shortPosId))
--Log('Short PID '..bot.shortPosId)
end
-- detailed log
if showInfo then
Log('Balance Ratio: '..balRatio)
Log('Working Balance: '..workingBalance)
Log('Bot Realized Profit: '..realizedBotProfit)
Log('Trading Balance: '..tradeBal)
Log('Exchange Balance: '..WalletAmount())
Log(' ')
end
-- custom reports
Finalize(function()
CustomReport('Max Entry', bot.slotCounter..' entries')
CustomReport('Stop-Action', bot.SACounter..' times')
CustomReport('Stop Loss', bot.SRCounter..' times')
CustomReport('Current Balance Ratio', balRatio)
CustomReport('Highest Balance Ratio', bot.BRCounter)
CustomReport('Profit to Highest Balance Ratio', profit2bal)
CustomReport('Bot Profit %', profitPercent..'%')
CustomReport('BACKTEST', 'WAS SUCCESSFUL!')
end)
Save('bot', bot)
0 Comments
Sign in to leave a comment.
No comments yet. Be the first!