Smokybot MK4

stable
By smokyho in Trading Bots Published January 2024 👁 2,284 views 💬 4 comments

Description

Hi guys...thank you for everyone who use my previous bots and everyone who inspired me with their snippets and sharing their knowledge in DIscord. So..version 4 now. The basic trading risk management is the same as MK3. Still using balance risk to determine the order size with some additions/improvements. Now let's jump to NEW features: 1. Easy replaceable indicators. You can easily use any indicators as many as you like! By default it has 3 entries but you can add as many as you like. All you need to do is go to line 170 and do a little modification there (somehow i can't upload screenshots). 2. Auto adjustment for exchange limits. To avoid order rejected because over size or above exchange's trading rule MK4 has entry where you have to (if you want to) enter the exchange limits. More about it in the Discord post. If you enter the limits parameter (position limit and order limit) then bot will always check the risk based calculated size against it and make adjustments to make sure the size sent to exchange is within limit. You will be notified in the log if this happen and then you can decide to lower the leverage to gain more limits or just let it run which makes it like ProfitHodl feature. Remember, more risk more profit, less risk less profit. 3. Multiple TP Logic Like MK3 you can use RR for TP. Risking $5 for potential getting $10 at TP means 2RR. You can trail you profit but unlike MK3 the trailing now based on RR or TDST (from TD Sequential). Another option is using price change % if you just want to scalp or have fixed % TP level. That is the main thing in MK4. There are some small changes/updates/improvements which going to be too long to explain here (assuming i remember it). Have fun with it and 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
HaasScript
--[[
MK4.2.2
If you would like to buy me a cup of coffee or diamond
ENS: smokyho.eth
--]]

EnableHighSpeedUpdates(true)
HideOrderSettings()
HideTradeAmountSettings()
 
--INPUTS
    InputGroupHeader('Trade Settings')
        local mainInterval = CurrentInterval()
        local okLong = Input('Long Entry', false, 'Allow bot to open Long')
        local okProfitL = Input('Long Exit', false, 'Allow bot to exit Long')
        local okShort = Input('Short Entry', false, 'Allow bot to open Short')
        local okProfitS = Input('Short Exit', false, 'Allow bot to exit Short')
        local okReduce = Input('BalRatio Reduction', false, 'Allow Bal Ratio based size reduction')
        local okDerisk = Input('Risk Reduction', false, 'Allow Signal based size reduction')
        local oneWay = Input("Not Hedge", false, "Trading one way even if your position mode in exchange is Hedge Mode. If your position mode in exchange is One-Way the bot will automatically trading One-Way.")
        local okCycle = Input('Position Cycle', false, 'If activated = 1 position per Indicator(easy1) cycle')
 
    InputGroupHeader('Bot Settings')
        local showDetails = Input("Show Details", false, "Show trading details in Log.")
        local wtfStop = Input('Stop at no position', false, 'Deactivate bot when there is no open position.')
        local FOL = Input('Force Open Long', false, 'Force open LONG position. Turn off position cycle when activated and set the settings back after position opened.')
        local FEL = Input('Force Exit Long', false, 'Force exit LONG position.')
        local FOS = Input('Force Open Short', false, 'Force open SHORT position. Turn off position cycle when activated and set the settings back after position opened.')
        local FES = Input('Force Exit Short', false, 'Force exit SHORT position.')
 
    InputGroupHeader('BACKTEST SETTINGS')
        local wtfBal = Input('Deactivate on Over Budget', false, 'Disable bot when Balance Ratio hit trigger or Trading Balance is 5% from Starting Balance.')
        local wtfSize = Input('Deactivate on Over Size', false, 'Disable bot when order size need adjustment.')
 
    InputGroupHeader('Budget')
        local startingBalance = Input('Starting Balance', 0, 'Starting trading balance which also maximum COST to limit working balance (margin cost + unrealised loss)')
        local isolated = GetMarginMode() == IsolatedMarginMode
        local leverage = GetLeverage()
        local contVal = ContractValue()
        local strictBudget = Input('Strict Budget', false,  'When applied bot will close all position and stop if highest bot balance - current bot balance > starting balance.')
 
    InputGroupHeader("Exchange Order & Position Limits")
        local positionLimitTypes = {base = AmountLabel(), quote = ProfitLabel()}
        local positionLimitType = InputOptions("POSITION Limit Type", positionLimitTypes.quote, positionLimitTypes)
        local positionLimitAmount = Input("POSITION Limit Amount", 10000)
        local orderLimitTypes = {base = AmountLabel(), quote = ProfitLabel()}
        local orderLimitType = InputOptions("ORDER Limit Type", orderLimitTypes.base, orderLimitTypes)
        local orderLimitAmount = Input("ORDER Limit Amount", 10000000)
 
    InputGroupHeader('Safety')
        local slTrigger = Input('Fix SL %', 0, '0 is disable, else DISREGARDING RISK REDUCTION option and signal bot will derisking when % price change againts average entry.')
        local reduceTrigger = Input('Bal Ratio Trigger', 0.8, 'Ratio between working balance and trading balance to trigger size reduction')
        local profitHodl = Input('Profit Hodl %', 100, '% of the bot profit taken out or not compounded into trading balance calculation')
        local dynamicHodl = Input('Dynamic Hodl', false, 'After in profit will only take out profit if current trading balance < 80% of highest trading balance.')
        local delay = Input('SL Delay (Min)', 0, 'Stop the bot from opening new long/short order for X minutes after close at loss.')
 
    InputGroupHeader('Order')
        local posRisk = Input('Risk % per POSITION', 1, '10 = risk losing ~10% of trading balance PER POSITION if SL hit from current order price.')
        local slotSizeD = Input('Min. Order Size ', 0, 'Minimum order size if Dynamic Slot Size activated. Static amount if Dynamic Slot Size disabled.')
        local autoSlot = Input('Dynamic Order Size', true, 'Dynamically change order size. Calculated based on distance to SL price.')

    InputGroupHeader('Profit')
        local RR = Input('Target Reward to Risk', 1, '2 = reward 2x risk. At this level bot will close position.')
        local trailingTDST = Input('Trailing TDST', false, 'Moving SL following TDST.')
        local RRtrailing = Input('RR Trailing', 0, 'RR to start trailing SL. 0 is disable. Only working if Trailing TDST is off. EXAMPLE: If 2 = at 2RR SL move to Entry, at 4RR SL move to 2RR from entry and so on.')
        local sizeHalving = Input('Size Halving', false, 'Halving the position and its remaining size at trailing event. With Trailing TDST only when TDST crossing entry. With RR Trailing at every trailing point.')
        local okScalping = Input('Scalping', false, 'Take profit minimum at % below as an alternative to RR method.')
        local minScalping = Input('Min. scalping profit %', 0.1, 'TP when profit (price change) above this value.')
 
    if startingBalance == 0 then
        DeactivateBot('Please enter Starting Balance', true)
    end
 
    if GetPositionMode() == OneWayPositionMode then 
        oneWay = true 
    end
 

---------------------------------------------------------------------------------
-- LABS STUFF
    local in_labs = Input('In Labs?', false, '', 'BACKTEST SETTINGS')
    local market = PriceMarket()
    local markets = CC_All_BF_Markets()
    local selected = InputOptions('Market', markets[1], markets, '', 'BACKTEST SETTINGS')

    if in_labs then
        market = CreateMarket({baseCurrency = selected})
        priceSource = market
        labPlot = 99
        PlotPrice(labPlot, market)
        ChartSetOptions(0, '', 0.002)
        ChartSetOptions(labPlot, selected, 0.4)

        if not init_lev then
            local max_lev = GetMaxLeverage(market)
            SetLeverage(max_lev, market)
            init_lev = true
            LogWarning('--- MAX LEVERAGE SET: '..max_lev..' ---')
        end
    else
        labPlot = 0
        --ChartSetOptions(0, 'Market')
        priceSource = InputPriceSourceMarket("Indicator Price Source", market, {group = 'Trade Settings'})
    end


---------------------
--DATA
    local getAccount = AccountGuid()
    local report = GetTradingReport()
    local highestP = report.highestPointInProfit
    local lowestP = report.lowestPointInProfit
    local highestBal = startingBalance + highestP
    local lowestBal = startingBalance + lowestP
 
 
---------------------
-- PERSISTENT STORAGE
    if not inited then
        bot = Load('bot', {})
 
        if Count(bot) == 0 then
            -- positions
            bot.longPosId = NewGuid()
            bot.shortPosId = NewGuid()
            bot.timerL = Time()
            bot.timerS = Time()
 
            -- prices
            bot.targetPriceL = 0
            bot.targetPriceS = 0
            bot.orderDistanceL = 0
            bot.orderDistanceS = 0
            bot.longCycle = 1
            bot.shortCycle = 1
            bot.longHalving = 0
            bot.shortHalving = 0
 
            -- TD
            bot.bullCount = 0
            bot.bearCount = 0
            bot.longTDST = 0
            bot.longSL = l
            bot.shortTDST = 0
            bot.shortSL = h
            bot.bullish = false
            bot.bearish = false
 
            -- stat
            bot.SRCounter = 0
            bot.BRCounter = 0
            bot.longFilled = 0
            bot.shortFilled = 0
            bot.longTrail = 0
            bot.shortTrail = 0
            bot.TP_rr = 0
            bot.TP_trailing = 0
            bot.TP_scalping = 0
            bot.orderCountL = 1 
            bot.orderCountS = 1
        end
 
        inited = true
    end
 
 
---------------------
-- PRICES & INDICATORS
    local cp = CurrentPrice(priceSource)
    local cpDefault = CurrentPrice(market)
 
    -- Function for adjusting position limits in hedge mode
        local function adjustPositionLimits()
            if GetPositionMode() == HedgePositionMode then
                positionLimitAmount = positionLimitAmount / 2
            end
        end
 
    -- Function for calculating dynamic percentage
        local function calculateDynamicPercentage(h, l, c)
            local tr = TRANGE(h, l, c)
            return Max(tr, DEMA(tr, 7), SMA(tr, 7), TRIMA(tr, 7)) / c * 100
        end
 
    -- Function for calculating skew
        local function calculateSkew(c)
            local rsi = RSI(c, 14)
            return Max(Abs(50 - rsi) / 50, DEMA(Abs(50 - rsi) / 50, 20))
        end
 
    -- Combined function for calculating Indicators
        InputGroupHeader('Indicators')
            local atrTDM = Input("SL ATR Multiplier", 1.5)
            local value1 = Input("Custom Value 1", 0, "Custom value for indicator's parameter.")
            local value2 = Input("Custom Value 2", 0, "Custom value for indicator's parameter.")
            local value3 = Input("Custom Value 3", 0, "Custom value for indicator's parameter.")
            -- Add mode if needed 

            local consensus = Input("Consensus Signal Decision", false, "Activate to make majority decides else the decicion has to be unanimous.")

        local function calculateIndicators(h, l, c)
            ----------------------------------------------------------------------
            --You can change this to any EASY Indicators or Custom Indicators as long as the Return is SIGNAL
            --https://help.haasonline.com/api/haasscript/commands/easy-indicators
            --If you are using Custom Indicator then you can use the Custome Value for parameters
        
            local easy1 = CC_EasySuperTrend(0,'', value1)
            local easy2 = EasyMA(0) --example usage of custom value
            local easy3 = easy1
            if consensus then
                easy = GetConsensusSignal(easy1, easy2, easy3)
            else 
                easy = GetUnanimousSignal(easy1, easy2, easy3)
            end
            --Log(easy)
 
            ----------------------------------------------------------------------
            
            local atr = ATR(h, l, c, 9)
            return easy1, easy, atr
        end
 
    -- Function for updating TD Sequential Count
        local function updateTDSequential(c, h, l, bot, offset)
            if offset == nil then
                offset = 1
            end
            
            bullCount = c[offset] > c[offset + 4]
            bearCount = c[offset] < c[offset + 4]
            if bot.bullCount == 0 and bot.bearCount == 0  then
                if bullCount and c[offset + 1] < c[offset + 5] then
                    bot.bullCount = 1
                elseif bearCount and c[offset + 1] > c[offset + 5] then
                    bot.bearCount = 1
                end
            end
 
            if bot.bullCount >= 1 then
                if bullCount then
                    bot.bullCount = bot.bullCount + 1
                elseif bearCount then
                    bot.bullCount = 0
                    bot.bearCount = 1
                end
            elseif bot.bearCount >= 1 then
                if bearCount then
                    bot.bearCount =  bot.bearCount + 1
                elseif bullCount then
                    bot.bullCount = 1
                    bot.bearCount = 0
                end
            end
 
            return bot
        end
 
    -- Function for plotting shapes based on TD Sequential
        local function plotTDSequentialShapes(c, h, l, bot, offset)
            if offset == nil then
                offset = 1
            end
            
            if bot.bullCount == 1 then
                PlotShape(0, ShapeText, Green(50), 1, true, bot.bullCount, Green(50))
            elseif bot.bearCount == 1 or bot.bearCount == 9 then
                PlotShape(0, ShapeText, Red(50), 1, false, bot.bearCount, Red(50))
            end
 
            if bot.bullCount == 9 then
                bot.longTDST = GetLow(l, 9, offset)
                if h[offset] > h[offset + 2] and h[offset] > h[offset + 3] then
                    PlotShape(0, ShapeTriangleDown, Green(50), 3, true, bot.bullCount, Green(50))
                else
                    PlotShape(0, ShapeText, Green(50), 1, true, bot.bullCount, Green(50))
                end
            elseif bot.bearCount == 9 then
                bot.shortTDST = GetHigh(h, 9, offset)
                if l[offset] < l[offset + 2] and l[offset] < l[offset + 3] then
                    PlotShape(0, ShapeTriangleUp, Red(50), 3, false, bot.bearCount, Red(50))
                else
                    PlotShape(0, ShapeText, Red(50), 1, false, bot.bearCount, Red(50))
                end
            end
        end
 
    -- Function for calculating and checking signals
        local function checkSignals(bot)
            local above = l > bot.longTDST and easy == SignalLong 
            local below = h < bot.shortTDST and easy == SignalShort 

            if (above and bot.bearCount == 0 and bot.bullCount < 9) or FOL then
                bot.bullish = true
            else
                bot.bullish = false
            end
 
            if (below and bot.bullCount == 0 and bot.bearCount < 9) or FOS then
                bot.bearish = true
            else
                bot.bearish = false
            end
  
            return bot
        end
 
    -- Function for plotting indicators
        local function plotIndicators(bot)
            if bot.longTDST > 0 then
                Plot(labPlot, 'Long TDST', bot.longTDST, {c = Green, d = Dotted})
            end
            if bot.shortTDST > 0 then
                Plot(labPlot, 'Short TDST', bot.shortTDST, {c = Red, d = Dotted})
            end
        end
 
    -- Main execution function
        OptimizedForInterval(0, function()
            h = HighPrices(mainInterval, true, priceSource)
            l = LowPrices(mainInterval, true, priceSource)
            c = ClosePrices(mainInterval, true, priceSource)
 
            adjustPositionLimits()
 
            diff5 = calculateDynamicPercentage(h, l, c)
            skew = calculateSkew(c)
            easy1, easy, atr = calculateIndicators(h, l, c, value1, value3, value3)
 
            -- Warmup
            if Load('warmup') == nil then
                LogWarning('Warming up TD Sequential...')

                local wm_len = ArrayGet(Min(500, #c, #atr), 1)

                for i=wm_len, 1, -1 do
                    bot = updateTDSequential(c, h, l, bot, i)
                    plotTDSequentialShapes(c, h, l, bot, i)
                end

                LogWarning('Warmup completed.')

                Save('warmup', false)
            else
                -- only update current step
                bot = updateTDSequential(c, h, l, bot)
                plotTDSequentialShapes(c, h, l, bot) 
            end
            -----
            
            bot = checkSignals(bot)
            plotIndicators(bot)
        end)
 
 
---------------------
-- POSITIONS
    local dir_l = GetPositionDirection(bot.longPosId)
    local aep_l = GetPositionEnterPrice(bot.longPosId)
    local pamt_l = GetPositionAmount(bot.longPosId)
    local delta_l = pamt_l > 0 and (cpDefault.close - aep_l) / aep_l * 100 or 0

    local dir_s = GetPositionDirection(bot.shortPosId)
    local aep_s = GetPositionEnterPrice(bot.shortPosId)
    local pamt_s = GetPositionAmount(bot.shortPosId)
    local delta_s = pamt_s > 0 and (aep_s - cpDefault.close) / aep_s * 100 or 0

    -- manage position ids
        if pamt_l == 0 and IsPositionClosed(bot.longPosId) then
            if IsAnyOrderOpen(bot.longPosId) then
                CancelAllOrders(bot.longPosId)
            else
                if okCycle then
                    bot.longCycle = 1
                end
                
                if LastLongProfit(bot.longPosId) > 0 then
                    bot.TP_trailing = bot.TP_trailing + 1
                end
                bot.longPosId = NewGuid()
                dir_l = GetPositionDirection(bot.longPosId)
                aep_l = GetPositionEnterPrice(bot.longPosId)
                pamt_l = GetPositionAmount(bot.longPosId)
                bot.longTrail = 0
                bot.longFilled = 0
                bot.longHalving = 0
                bot.targetPriceL = 0
                bot.orderCountL = 1 
            end
        end
 
        if pamt_s == 0 and IsPositionClosed(bot.shortPosId) then
            if IsAnyOrderOpen(bot.shortPosId) then
                CancelAllOrders(bot.shortPosId)
            else
                if okCycle then
                    bot.shortCycle = 1
                end
                
                if LastShortProfit(bot.shortPosId) > 0 then
                    bot.TP_trailing = bot.TP_trailing + 1
                end
                bot.shortPosId = NewGuid()
                dir_s = GetPositionDirection(bot.shortPosId)
                aep_s = GetPositionEnterPrice(bot.shortPosId)
                pamt_s = GetPositionAmount(bot.shortPosId)
                bot.shortTrail = 0
                bot.shortFilled = 0
                bot.shortHalving = 0
                bot.targetPriceS = 0
                bot.orderCountS = 1
            end
        end
 
    -- get pos id
        local getPositionId = function(isLong)
            return isLong and bot.longPosId or bot.shortPosId
        end
 
 
---------------------
-- WALLET CHECK
    local profitLabel = ProfitLabel(market)
    if profitLabel == nil then profitLabel = QuoteCurrency(market) end
 
    -- inverse or not
        if profitLabel == 'USD' or profitLabel == 'USDT' or profitLabel == 'BUSD' or profitLabel == 'USDC' or profitLabel == 'TUSD' then
            isInverse = false
        else
            isInverse = true
        end
 
    -- check balance usage
        local usedLong = UsedMargin(market, aep_l, pamt_l, leverage)
        local getProfitL = GetCurrentProfit(PositionLong, market)
        local usedShort = UsedMargin(market, aep_s, pamt_s, leverage)
        local getProfitS = GetCurrentProfit(PositionShort, market)
 
        local getProfit = getProfitL + getProfitS
        local botProfit = GetBotProfit(market, false)
        local netbotProfit = GetBotProfit(market, true)
 
        local walletBal = WalletAmount(getAccount, profitLabel, market)
        local workBal = usedLong + usedShort - getProfit
 
        local botBalance = startingBalance + botProfit
        local netBalance = startingBalance + netbotProfit - usedLong - usedShort
        if dynamicHodl and botBalance >= highestBal then
            tradeBal = AddPerc(startingBalance, SubPerc(botProfit, profitHodl) / startingBalance * 100)
            --PlotSignalBar(-99, White)
        else
            tradeBal = IfElse(profitHodl > 0 and botProfit > 0, startingBalance + SubPerc(botProfit, profitHodl),
            botBalance)
        end
 
    ChartSetOptions(5, 'Balance Monitor')
    Plot(5, 'Net Balance', netBalance, {c=Orange, s=Step})
    Plot(5, 'Trading Balance', tradeBal, {c=White, s=Step})
    Plot(5, 'WorkBal', workBal, {c=Red, s=Step})
    Plot(5, 'Bot Profit', botProfit, {c=DarkGreen, s=Step})
    Plot(5, 'Bot Balance', botBalance, {c=Yellow, s=Step})
    Plot(5, 'Highest Balance', highestBal, {c=Green, s=Step})
    Plot(5, 'Lowest Balance', lowestBal, {c=Fuchsia, s=Step})--]]


---------------------
-- BALANCE WARNING
    local balRatio = workBal / tradeBal
    local okBalance = walletBal + workBal >= botBalance
 
    if not okBalance then
        DeactivateBot('TRADING BALANCE < EXCHANGE BALANCE', true)
    end
 
    if tradeBal <= 0 then
        DeactivateBot('OUT of TRADING BALANCE', true)
    end
 
    if balRatio > 0.8 then
        LogWarning('Working balance is > 80% of Budget!!!')
    end
 
    if balRatio > bot.BRCounter then
        bot.BRCounter = balRatio
    end
 
    if strictBudget and highestBal - botBalance >= startingBalance then
        okLong = false
        FEL = true
        okShort = false
        FES = true
        if pamt_l == 0 and pamt_s == 0 then
            DeactivateBot('LOSS < STARTING BALANCE', true)
        end
    end
 
 
---------------------
-- WTF
    if wtfBal then
        if balRatio > reduceTrigger or botBalance <= startingBalance * 0.05 then
            DeactivateBot('Deactivated because over budget or bot balance is 5% left.', true)
            Log('STOP at over budget safety activated. Is this a backtest ?', Yellow)
        end
    end
 
    if wtfStop then
        if pamt_l == 0 and pamt_s == 0 then
            DeactivateBot('Deactivated by No Position', true)
        end
        Log('Deactivate on No Position is active.', Yellow)
    end
 
 
---------------------
-- DYNAMICS
    -- ENTRY
        local longPrice = cpDefault.bid
        local shortPrice = cpDefault.ask
 
        if okCycle then
            if pamt_l == 0 and bot.longCycle == 1 and easy1 == SignalShort then
                bot.longCycle = 0
            end
 
            if pamt_s == 0 and bot.shortCycle == 1 and easy1 == SignalLong then
                bot.shortCycle = 0
            end
        end
 
    -- SL
        -- Calculate SL
            OptimizedForInterval(0, function()
                if pamt_l == 0 then
                    if slTrigger == 0 then
                        if longPrice < bot.shortTDST then
                            longRisk = bot.longTDST - (atr * atrTDM)
                        else
                            longRisk = GetLow(l, 9) - (atr * atrTDM)
                        end
                    else
                        longRisk = SubPerc(longPrice, slTrigger)
                    end

                    longSLP = longRisk
                    longRisk_range = pamt_l == 0 and longPrice - longRisk or aep_l - longRisk
                    longSL_range = longPrice - longRisk
                    longTrailPrice = 0
                end

                if pamt_s == 0 then
                    if slTrigger == 0 then
                        if shortPrice > bot.longTDST then
                            shortRisk = bot.shortTDST + (atr * atrTDM)
                        else
                            shortRisk = GetHigh(h, 9) + (atr * atrTDM)
                        end
                    else
                        shortRisk = AddPerc(shortPrice, slTrigger)
                    end

                    shortSLP = shortRisk
                    shortRisk_range = pamt_s == 0 and shortRisk - shortPrice or shortRisk - aep_s
                    shortSL_range = shortRisk - shortPrice
                    shortTrailPrice = 0
                end
            end)

        -- Check SL
            -- Function to adjust long position SL
            local function adjustLongSL(bot, cp, pamt_l, aep_l, longRisk_range, atr, atrTDM, trailingTDST, RRtrailing)
                if pamt_l > 0 then
                    longSLP = longRisk
                    longTrailPrice = bot.longTrail >= 1 and aep_l + (longRisk_range * bot.longTrail) or aep_l
                    local LT = Max(bot.longTrail, bot.longHalving)

                    if trailingTDST and bot.longTDST > aep_l then 
                        if sizeHalving and bot.longHalving == 0 then
                            PlaceExitLongOrder(cpDefault.ask, pamt_l / 2, {note = 'TDST Halving'})
                            bot.longHalving = 1 
                        end
                        longSLP = bot.longTDST - (atr * atrTDM)

                    elseif RRtrailing > 0 and LT >= RRtrailing then
                        longSLP = longTrailPrice - (RRtrailing * longRisk_range)
                    end
                end
            end

            -- Function to adjust short position SL
            local function adjustShortSL(bot, cp, pamt_s, aep_s, shortRisk_range, atr, atrTDM, trailingTDST, RRtrailing)
                if pamt_s > 0 then
                    shortSLP = shortRisk
                    shortTrailPrice = bot.shortTrail >= 1 and aep_s - (shortRisk_range * bot.shortTrail) or aep_s
                    local ST = Max(bot.shortTrail, bot.shortHalving)

                    if trailingTDST and bot.shortTDST < aep_s then
                        if sizeHalving and bot.shortHalving == 0 then
                            PlaceExitShortOrder(cpDefault.bid, pamt_s / 2, {note = 'TDST Halving'})
                            bot.shortHalving = 1 
                        end
                        shortSLP = bot.shortTDST + (atr * atrTDM)

                    elseif RRtrailing > 0 and ST >= RRtrailing then
                        shortSLP = shortTrailPrice + (RRtrailing * shortRisk_range)
                    end
                end
            end

            adjustLongSL(bot, cp, pamt_l, aep_l, longRisk_range, atr, atrTDM, trailingTDST, RRtrailing)
            adjustShortSL(bot, cp, pamt_s, aep_s, shortRisk_range, atr, atrTDM, trailingTDST, RRtrailing)

    -- TP RR
        -- LONG
            if pamt_l != 0 and bot.longTrail == 0 and bot.targetPriceL == 0 then
                bot.targetPriceL = aep_l + (longRisk_range * RR)
            end
 
        -- SHORT
            if pamt_s != 0 and bot.shortTrail == 0 and bot.targetPriceS == 0 then
                bot.targetPriceS = aep_s - (shortRisk_range * RR)
            end--]]
 
    -- the force
        if FEL then
            okLong = false
            okProfitL = false
            exitPriceL = cpDefault.ask
            LET = 'Forced'
            Log('FORCE EXIT LONG POSITION ACTIVATED', Red)
        end
 
        if FES then
            okShort = false
            okProfitS = false
            exitPriceS = cpDefault.bid
            SET = 'Forced'
            Log('FORCE EXIT SHORT POSITION ACTIVATED', Red)
        end
 
    --TRAILING
        --LONG
            local longRRT = pamt_l > 0 and cp.close > aep_l + (longRisk_range * (bot.longTrail + 1))
 
            if longRRT then
                bot.longTrail = bot.longTrail + 1
            end
 
        --SHORT
            local shortRRT = pamt_s > 0 and cp.close < aep_s - (shortRisk_range * (bot.shortTrail + 1))
 
            if shortRRT then
                bot.shortTrail = bot.shortTrail + 1
            end
 
 
---------------------
-- SLOT SIZE
    if isolated and not oneWay then
        tradeBal = tradeBal / 2
    end
    local risk = posRisk / 100 * tradeBal
    local slotSizeL = slotSizeD
    local slotSizeS = slotSizeD

    if autoSlot then
        if bot.bullish then
            local sizeL = isInverse and risk * longRisk / longSL_range * longPrice / contVal
                            or risk / longSL_range
            slotSizeL = sizeL > slotSizeD and sizeL or slotSizeD
        end

        if bot.bearish then
            local sizeS = isInverse and risk * shortRisk / shortSL_range * shortPrice / contVal 
                            or risk / shortSL_range
            slotSizeS = sizeS > slotSizeD and sizeS or slotSizeD
        end
    else
        slotSizeL = slotSizeD
        slotSizeS = slotSizeD
    end

    --Log("slotSizeL "..slotSizeL)
    --Log("slotSizeS "..slotSizeS)

    -- LIMIT CALCULATION
        adjustedPositionL = false
        adjustedPositionS = false
        if positionLimitAmount > 0 and orderLimitAmount > 0 then
            -- Function to set POSITION LIMITS
            if positionLimitType == positionLimitTypes.quote then
                positionLimitAmount = isInverse and positionLimitAmount * cp.close / contVal or positionLimitAmount / cp.close
            end 

            -- Function to set ORDER LIMITS
            local function setOrderLimits(orderLimitAmount, orderLimitType, longPrice, shortPrice, contVal, isInverse)
                local order_limitAmountL, order_limitAmountS

                if orderLimitType == orderLimitTypes.quote then
                    order_limitAmountL = isInverse and orderLimitAmount * longPrice / contVal or orderLimitAmount / longPrice
                    order_limitAmountS = isInverse and orderLimitAmount * shortPrice / contVal or orderLimitAmount / shortPrice
                else
                    order_limitAmountL = orderLimitAmount
                    order_limitAmountS = orderLimitAmount
                end

                return order_limitAmountL, order_limitAmountS
            end

            -- Function to adjust long position
            local function adjustLongPosition(bot, slotSizeL, positionLimitAmountL, order_limitAmountL)
                local slotSizeLADJ, orderCountL, adjustedPositionL

                if bot.bullish and pamt_l == 0 then
                    if slotSizeL > positionLimitAmountL then
                        totalSizeL = positionLimitAmountL * 0.95
                        adjustedPositionL = true
                    else
                        totalSizeL = slotSizeL
                    end

                    if totalSizeL > order_limitAmountL then
                        bot.orderCountL = Round(totalSizeL / order_limitAmountL, 0) + 1
                        splitSizeL = totalSizeL / bot.orderCountL
                        adjustedPositionL = true
                    else
                        splitSizeL = totalSizeL
                    end
                    return splitSizeL, bot.orderCountL, adjustedPositionL
                elseif bot.orderCountL > 1 then 
                    return splitSizeL, bot.orderCountL, true
                end
            end

            -- Function to adjust short position
            local function adjustShortPosition(bot, slotSizeS, positionLimitAmountS, order_limitAmountS)
                local slotSizeSADJ, orderCountS, adjustedPositionS

                if bot.bearish and pamt_s == 0 then 
                    if slotSizeS > positionLimitAmountS then
                        totalSizeS = positionLimitAmountS * 0.95
                        adjustedPositionS = true
                    else
                        totalSizeS = slotSizeS
                    end

                    if totalSizeS > order_limitAmountS then
                        bot.orderCountS = Round(totalSizeS / order_limitAmountS, 0) + 1
                        splitSizeS = totalSizeS / bot.orderCountS
                        adjustedPositionS = true
                    else
                        splitSizeS = totalSizeS
                    end
                    return splitSizeS, bot.orderCountS, adjustedPositionS
                elseif bot.orderCountS > 1 then 
                    return splitSizeS, bot.orderCountS, true
                end
            end

            -- Main Execution
            local order_limitAmountL, order_limitAmountS = setOrderLimits(orderLimitAmount, orderLimitType, longPrice, shortPrice, contVal, isInverse)
            slotSizeLADJ, orderCountL, adjustedPositionL = adjustLongPosition(bot, slotSizeL, positionLimitAmount, order_limitAmountL)
            slotSizeSADJ, orderCountS, adjustedPositionS = adjustShortPosition(bot, slotSizeS, positionLimitAmount, order_limitAmountS)            
            
            slotSizeL = slotSizeLADJ != nil and slotSizeLADJ or slotSizeL
            slotSizeS = slotSizeSADJ != nil and slotSizeSADJ or slotSizeS
        end 

    -- Cost Check
        local potentialCostL = isInverse and slotSizeL * contVal / longPrice / leverage or slotSizeL * longPrice / leverage
        local maxUsedL = bot.bullish and longRisk_range > 0 and potentialCostL + risk or 0

        local potentialCostS = isInverse and slotSizeS * contVal / shortPrice / leverage or slotSizeS * shortPrice / leverage
        local maxUsedS = bot.bearish and shortRisk_range > 0 and potentialCostS + risk or 0
        --Log('maxUsedL '..maxUsedL)
        --Log('maxUsedS '..maxUsedS)

        local okCostL = maxUsedL > 0 and maxUsedL < tradeBal * 0.8 or false
        local okCostS = maxUsedS > 0 and maxUsedS < tradeBal * 0.8 or false
 
 
---------------------
-- EXECUTION
    --LONG
        --OPEN
            local longOpen1 = pamt_l == 0 
                                and bot.bullish 
                                and l > longRisk
                                and okBalance
                                and okCostL
 
            local longDCA = pamt_l != 0 
                            and bot.orderCountL > 1
 
            if longOpen1 then
                LT = FOL and 'ForceLong' or '1stOrder'
            elseif longDCA then
                LT = 'Splits'
            end
 
            local longOpen = okLong
                                and bot.longFilled < bot.orderCountL
                                and longSLP != 0
                                and IfElse(oneWay, pamt_s == 0, true)
                                and IfElse(okCycle, bot.longCycle == 0, true)
                                and (longOpen1 or longDCA)
        
        if pamt_l > 0 then 
            --EXIT
            local longExit1 = cp.close > bot.targetPriceL
  
            local longExit2 = okScalping 
                                and delta_l > Max(minScalping, diff5) + skew
 
            if longExit1 then
                LET = 'RR'
            elseif longExit2 then
                LET = 'Scalping'
            end
 
            longExit = okProfitL
                        and pamt_l > 0
                        and not FEL
                        and delta_l > 0.1
                        and (longExit1 or longExit2)
 
            --DERISK
            local okLongDerisk1 = IfElse(delta_l < 0, cp.close <= Average(aep_l, longSLP), 
                                    cp.close <= Average(longTrailPrice, longSLP)) 
 
            local okLongDerisk2 = RRtrailing > 0
                                    and sizeHalving
                                    and delta_l > 0
                                    and cp.close < bot.targetPriceL
                                    and not okLongDerisk1
 
            if okLongDerisk1 then
                LDP = longSLP
                LDT = 'Stop'
            elseif okLongDerisk2 then
                LDPc = aep_l + (RRtrailing * longRisk_range * (bot.longHalving + 1))
                LDP = IfElse(cp.close < LDPc, LDPc, cpDefault.ask)
                LDT = 'Halving'
            else
                LDP = longSLP
            end
 
            okLongDerisk = okDerisk
                            and pamt_l > 0
                            and not longExit
                            and (okLongDerisk1 or okLongDerisk2)
        
        else 
            longExit = false 
            okLongDerisk = false
        end
 
    --SHORT
        --OPEN
            local shortOpen1 = pamt_s == 0
                                and bot.bearish
                                and h < shortRisk
                                and okBalance
                                and okCostS
 
            local shortDCA = pamt_s != 0
                                and bot.orderCountS > 1
 
            if shortOpen1 then
                ST = FOS and 'ForceShort' or '1stOrder'
            elseif shortDCA then
                ST = 'Splits'
            end
  
            local shortOpen = okShort
                                and bot.shortFilled < bot.orderCountS
                                and shortSLP != 0
                                and IfElse(oneWay, pamt_l == 0, true)
                                and IfElse(okCycle, bot.shortCycle == 0, true)
                                and (shortOpen1 or shortDCA)

        if pamt_s > 0 then
            --EXIT
            local shortExit1 = cp.close < bot.targetPriceS
  
            local shortExit2 = okScalping 
                                and delta_s > Max(minScalping, diff5) + skew
 
            if shortExit1 then
                SET = 'RR'
            elseif shortExit2 then
                SET = 'Scalping'
            end
 
            shortExit = okProfitS
                        and pamt_s > 0
                        and not FES
                        and delta_s > 0.1
                        and (shortExit1 or shortExit2)
 
            --DERISK
            local okShortDerisk1 = IfElse(delta_s < 0, cp.close >= Average(aep_s, shortSLP),
                                    cp.close >= Average(shortTrailPrice, shortSLP)) 
 
            local okShortDerisk2 = RRtrailing > 0
                                    and sizeHalving
                                    and delta_s > 0
                                    and cp.close > bot.targetPriceS
                                    and not okShortDerisk1
 
            if okShortDerisk1 then
                SDP = shortSLP
                SDT = 'Stop'
            elseif okShortDerisk2 then
                SDPc = aep_s - (RRtrailing * shortRisk_range * (bot.shortHalving + 1))
                SDP = IfElse(cp.close > SDPc, SDPc, cpDefault.bid)
                SDT = 'Halving'
            else
                SDP = shortSLP
            end
 
            okShortDerisk = okDerisk
                            and pamt_s > 0
                            and not shortExit
                            and (okShortDerisk1 or okShortDerisk2)

        else 
            shortExit = false 
            okShortDerisk = false
        end

 
---------------------
-- FUNCTIONS
    -- ENTRY
        function slot (isLong, price, amount, timer, trigger, canPlace)
            local name = isLong and 'L' or 'S'
            local cmd = isLong and PlaceGoLongOrder or PlaceGoShortOrder
 
            local posId = getPositionId(isLong)
            local filled = isLong and bot.longFilled + 1 or bot.shortFilled + 1
 
            local oid = Load(name..'oid', '') -- order id
            if oid != '' then
                local order = OrderContainer(oid)
 
                if order.isOpen then
                    if not canPlace then
                        CancelOrder(oid)
                        LogWarning('Not allowed right now '..name)
                    end
 
                    if MinutesTillCandleClose(mainInterval) == 0 then
                        CancelAllOrders()
                        Log("Canceled by new candle", Yellow)
                    end
 
                elseif order.isFilled then
                    CancelAllOrders()
                    if isLong then
                        bot.longFilled = bot.longFilled + 1
                        Log('Target TP: '..bot.targetPriceL)
                        Log("SL: "..longSLP)
                    else
                        bot.shortFilled = bot.shortFilled + 1
                        Log('Target TP '..bot.targetPriceS)
                        Log("SL: "..shortSLP)
                    end
                    oid = ''
 
                else
                    oid = ''
 
                end
            else
                if canPlace and TradeOncePerBar(1, posId) and Time() >= timer then
                    oid = cmd(price, amount, {market = market, timeout = mainInterval * 60, type = LimitOrderType, note = name..filled..'-'..trigger, positionId = posId})
                    if isLong then
                        if pamt_l == 0 then
                            Save('longRisk', longRisk)
                        end
 
                        if adjustedPositionL then
                            Log("Long order size adjusted to exchange limits.", Gold)
                            ChartSetOptions(-2, "Order Size Adjustment")
                            PlotSignalBar(-2, Gold)
                            if wtfSize and adjustedPositionL then 
                                DeactivateBot("Over Size", true)
                            end
                        end
                    else
                        if pamt_s == 0 then
                            Save('shortRisk', shortRisk)
                        end
 
                        if adjustedPositionS then
                            Log("Short order size adjusted to exchange limits.", Gold)
                            ChartSetOptions(-2, "Order Size Adjustment")
                            PlotSignalBar(-2, Gold)
                            if wtfSize and adjustedPositionS then 
                                DeactivateBot("Over Size", true)
                            end
                        end
                    end
                end
            end
 
            Save(name..'oid', oid)
        end
 
    -- EXIT
        function updateTakeProfit (isLong, ET, canExit)
            local prefix = isLong and 'L' or 'S'
            local name = prefix .. ' TP'
            local oid = Load(prefix .. 'tp_oid', '')
            local posId = getPositionId(isLong)
            local amount = isLong and pamt_l or pamt_s
            local price = isLong and cp.ask or cp.bid
            local cmd = isLong and PlaceExitLongOrder or PlaceExitShortOrder

            if orderLimitAmount > 0 and amount > orderLimitAmount then 
                amount = orderLimitAmount
            end
 
            if oid != '' then
                local order = OrderContainer(oid)
 
                if order.isOpen then
                    if not canExit or MinutesTillCandleClose(mainInterval) == 0 then
                        CancelOrder(oid)
                        LogWarning('Exit cancelled '..name)
                    end
                elseif order.isFilled then
                    if ET == 'RR' then
                        bot.TP_rr = bot.TP_rr + 1
                    elseif ET == 'Scalping' then
                        bot.TP_scalping = bot.TP_scalping + 1
                    end
                    oid = ''
                else
                    oid = ''
                end
            else
                if canExit then
                    local pDelta = isLong and Round(delta_l, 2) or Round(delta_s, 2)
                    oid = cmd(price, amount, {market = market, timeout = mainInterval * 60, type = LimitOrderType, note = name..'-'..ET..' '..pDelta..'%', positionId = posId})
                end
            end
 
            Save(prefix .. 'tp_oid', oid)
        end
 
    -- Balance Ratio REDUCTION
        function updatePositionManagement (isLong, amount, canReduce)
            local price = isLong and cpDefault.bid - PriceStep() or cpDefault.ask + PriceStep()
            local prefix = isLong and 'Long' or 'Short'
            local name = prefix .. ' Size Reduction'
            local oid = Load(prefix .. 'pos_oid', '')
            local posId = getPositionId(isLong)
            local cmd = isLong and PlaceExitLongOrder or PlaceExitShortOrder
            local timer = Load(prefix .. 'pos_timer', Time())

            if orderLimitAmount > 0 and amount > orderLimitAmount then 
                amount = orderLimitAmount
            end
 
            if oid != '' then
                local order = OrderContainer(oid)
 
                if order.isOpen then
                    if not canReduce or MinutesTillCandleClose(mainInterval) == 0 then
                        CancelOrder(oid)
                        LogWarning('Reduction cancelled '..name)
                    end
                else
                    bot.SRCounter = bot.SRCounter + 1
                    oid = ''
                end
            else
                if canReduce and Time() >= timer then
                    CancelAllOrders()
                    local pDelta = isLong and Round(delta_l, 2) or Round(delta_s, 2)
                    oid = cmd(price, amount, {market = market, type = MarketOrderType, note = name..' '..pDelta..'%', timeout = mainInterval * 60, positionId = posId})
                end 
            end
 
            Save(prefix .. 'pos_oid', oid)
            Save(prefix .. 'pos_timer', timer)
        end
 
    -- Derisking REDUCTION
        function derisking(isLong, size, price, DT, canDerisk)
            local openDT = Load('openDT', DT)
            local prefix = isLong and 'Long ' or 'Short '
            local halvingCount = isLong and bot.longHalving + 1 or bot.shortHalving + 1
            local name = prefix..' Derisking'
            local oid = Load(prefix .. 'derisk_oid', '')
            local posId = getPositionId(isLong)
            local cmd = isLong
            and PlaceExitLongOrder
            or PlaceExitShortOrder
            local trigger = isLong and longSLP or shortSLP
 
        -- order check
            if DT == 'Halving' then
                deriskOrderType = LimitOrderType
                name = prefix..' '..DT..'-'..halvingCount
                amount = size / 2
                if amount < MinimumTradeAmount(market, price) then
                    amount = size
                end
            else
                if isLong then
                    deriskOrderType = IfElse(cp.close > longSLP, StopMarketOrderType, MarketOrderType)
                else
                    deriskOrderType = IfElse(cp.close < shortSLP, StopMarketOrderType, MarketOrderType)
                end
                amount = size
            end
 
            if orderLimitAmount > 0 and amount > orderLimitAmount then 
                amount = orderLimitAmount
            end
        
        -- placing order
            if oid != '' then
                local order = OrderContainer(oid)
 
                if order.isOpen then
                    if not canDerisk or MinutesTillCandleClose(mainInterval) == 0 then
                        CancelOrder(oid)
                        LogWarning('Reduction cancelled '..name)
                    end
                    if openDT != DT  then
                        CancelOrder(oid)
                    end
                elseif order.isFilled then
                    if isLong then
                        if LDT == 'Halving' and sizeHalving then
                            bot.longHalving = bot.longHalving + 1
                        end
                        bot.timerL = Time() + (delay * 60)
                    else
                        if SDT == 'Halving' and sizeHalving then
                            bot.shortHalving = bot.shortHalving + 1
                        end
                        bot.timerS = Time() + (delay * 60)
                    end
                    oid = ''
                else
                    oid = ''
                end
            else
                if canDerisk then
                    CancelAllOrders()
                    local pDelta = isLong and Round(delta_l, 2) or Round(delta_s, 2)
                    oid = cmd(price, amount, {market = market, triggerPrice = trigger, type = deriskOrderType, note = name..'-'..DT..' '..pDelta..'%', timeout = -1, positionId = posId})
                    Save('openDT', DT)
                end
            end
            Save(prefix .. 'derisk_oid', oid)
        end
 
    --CORE LOGIC HEDGE MODE
        slot(true, longPrice, slotSizeL, bot.timerL, LT, longOpen or FOL) -- long slot
        slot(false, shortPrice, slotSizeS, bot.timerS, ST, shortOpen or FOS) -- short slot

        if pamt_l > 0 then 
            updateTakeProfit(true, LET, longExit or FEL)
            updatePositionManagement(true, pamt_l, okReduce and pamt_l > 0 and balRatio > reduceTrigger and delta_l < delta_s)
            derisking(true, pamt_l, LDP, LDT, okLongDerisk)
        end 

        if pamt_s > 0 then 
            updateTakeProfit(false, SET, shortExit or FES)
            updatePositionManagement(false, pamt_s, okReduce and pamt_s > 0 and balRatio > reduceTrigger and delta_s < delta_l)
            derisking(false, pamt_s, SDP, SDT, okShortDerisk)
        end
 
 
---------------------
--PLOT
    --AEP Plot
        if aep_l > 0 then
            local posId = getPositionId(true)
            Plot(labPlot, 'AvgEP Long', aep_l, {c=Green, id=posId, w=2})
            Plot(labPlot, 'SL Long', longSLP, {c=Green, id=posId, w=1})
            if bot.longTrail >= 1 then
                Plot(labPlot, bot.longTrail.." RR Long", longTrailPrice, {c=Cyan, id=posId, w=1})
            end
        end
 
        if aep_s > 0 then
            local posId = getPositionId(false)
            Plot(labPlot, 'AvgEP Short', aep_s, {c=Red, id=posId, w=2})
            Plot(labPlot, 'SL Short', shortSLP, {c=Red, id=posId, w=1})
            if bot.shortTrail >= 1 then
                Plot(labPlot, bot.shortTrail.." RR Short", shortTrailPrice, {c=Yellow, id=posId, w=1})
            end
        end
 
 
---------------------
-- FINAL REPORT
    Finalize(function()
        CustomReport('BalRatio Reduction', bot.SRCounter..' times')
        CustomReport('TP by RR', bot.TP_rr..' times')
        CustomReport('TP by Trailing', bot.TP_trailing..' times')
        CustomReport('TP by Scalping', bot.TP_scalping..' times')
        CustomReport('Final Bot Net Balance', Round(netBalance, 5)..' '..profitLabel)
        CustomReport('Final Bot Balance', Round(botBalance, 5)..' '..profitLabel)
        CustomReport('Highest Balance', Round(highestBal, 5)..' '..profitLabel)
        CustomReport('Lowest Balance', Round(lowestBal, 5)..' '..profitLabel)
        CustomReport('Current Balance Ratio', Round(balRatio, 2))
        CustomReport('Highest Balance Ratio', Round(bot.BRCounter, 2))
        
        local realprofitPercent = Round(botProfit / startingBalance * 100, 2)
        local runprofitPercent = Round(netbotProfit / startingBalance * 100, 2)
        CustomReport('Running Profit %: ', runprofitPercent..'%')
        CustomReport('Realized Profit %: ', realprofitPercent..'%')
        CustomReport('Profit to Highest Bal Ratio', Round(realprofitPercent / (100 * bot.BRCounter), 2))
 
        local checkBal = lowestBal / startingBalance / 0.2
        local checkRatio = bot.BRCounter > 0 and 0.8 / bot.BRCounter or 0
        local suggest = posRisk * Min(checkBal, checkRatio)
        local suggestion = IfElse(suggest > 50, 50, suggest)
        local potential = Min(checkBal, checkRatio) * realprofitPercent * suggestion / suggest
        if potential > realprofitPercent then
            CustomReport("SMOKGESTION", "Potential profit at "..potential.."%".." by changing STARTING RISK to "..suggestion)
        elseif runprofitPercent > realprofitPercent and runprofitPercent > 0 then
            CustomReport("SMOKGESTION", "The settings has good potential because Running Profit > Realized Profit")
        end
    end)
 
 
---------------------
-- INFO
    if showDetails then
        -- positions data
        if pamt_l > 0 then
            if RRtrailing > 0 then
                Log('Next LONG halving is at '..LDP)
            end
            Log('LONG Delta: '..Round(delta_l, 2)..'% | Amount: '..getProfitL)
        end

        if pamt_s > 0 then
            if RRtrailing > 0 then
                Log('Next SHORT halving is at '..SDP)
            end
            Log('SHORT Delta: '..Round(delta_s, 2)..'% | Amount: '..getProfitS)
        end

        -- balance data
            if (CurrentMinute() == 0 and CurrentSecond() == 0) or pamt_l > 0 or pamt_s > 0 then
                Log('Balance Monitor -> Wallet: '..Round(walletBal, 5)..' '..profitLabel..
                ' | Bot: '..Round(botBalance, 5)..' '..profitLabel..
                ' | Trading: '..Round(tradeBal, 5)..' '..profitLabel..
                ' | Net: '..Round(netBalance, 5)..' '..profitLabel..
                ' | Working: '..Round(workBal, 5)..' '..profitLabel..
                ' | Ratio: '..Round(balRatio, 3))
            end
    end


---------------------
Save('bot', bot)

4 Comments

Sign in to leave a comment.

C
Costa over 2 years ago

Same all of the time Greatness !

K
Keazy over 2 years ago

Thank you for sharing. Ive returned after years and seen its all on Cloud now which is pretty cool.
Will report back once I figure it out

Y
Yandeara about 2 years ago

Hi. I cant find the ref to CC_All_BF_Markets Script anywhere =/

S
smokyho about 2 years ago

have you tried the discord ?