Advertisement IC Markets

Aggregating Liquidity: How to Scale Into Sweeps Using Cost Averaging

Share, develop, and backtest custom MQL4/MQL5 Expert Advisors, Python data-scraping scripts, trading bots, and automated market alert systems.
PTScalper
Site Admin
Posts: 4687
Joined: Mon Jul 20, 2026 1:28 pm

Re: Aggregating Liquidity: How to Scale Into Sweeps Using Cost Averaging

Post by PTScalper »

The Pro C# cBot Code

Code: Select all

using System;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Internals;
using cAlgo.API.UI;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class LiquidityAggregatorPro : Robot
    {
        [Parameter("Trade Direction", Group = "Core Execution", DefaultValue = TradeType.Buy)]
        public TradeType Direction { get; set; }

        [Parameter("Bot Label", Group = "Core Execution", DefaultValue = "LiqAggregator_Pro")]
        public string BotLabel { get; set; }

        [Parameter("Initial Volume (Units)", Group = "Sizing", DefaultValue = 10000, MinValue = 1000, Step = 1000)]
        public double InitialVolume { get; set; }

        [Parameter("Volume Multiplier", Group = "Sizing", DefaultValue = 1.5, MinValue = 1.0, Step = 0.1)]
        public double VolumeMultiplier { get; set; }

        [Parameter("Max Tiers", Group = "Sizing", DefaultValue = 5, MinValue = 1)]
        public int MaxTiers { get; set; }

        [Parameter("Scale-In Distance (Pips)", Group = "Grid Parameters", DefaultValue = 10.0)]
        public double DistancePips { get; set; }

        [Parameter("Target Net Profit (Pips)", Group = "Grid Parameters", DefaultValue = 5.0)]
        public double TakeProfitPips { get; set; }

        [Parameter("Max Spread (Pips)", Group = "Safety", DefaultValue = 2.0)]
        public double MaxSpreadPips { get; set; }

        [Parameter("Max Slippage (Pips)", Group = "Safety", DefaultValue = 1.0)]
        public double MaxSlippage { get; set; }

        // State variables
        private bool _isActive = false;
        private double _breakEvenPrice = 0;
        private double _targetPrice = 0;

        // UI Elements
        private TextBlock _uiTiers;
        private TextBlock _uiVolume;
        private TextBlock _uiBreakEven;
        private TextBlock _uiTarget;
        private TextBlock _uiPnL;
        private TextBlock _uiStatus;

        protected override void OnStart()
        {
            DrawHUD();

            // Subscribe to position events
            Positions.Closed += OnPositionsClosed;

            // Execute initial tier asynchronously
            _uiStatus.Text = "Executing Tier 1...";
            _uiStatus.ForegroundColor = Color.Orange;
            
            ExecuteMarketOrderAsync(Direction, SymbolName, InitialVolume, BotLabel, MaxSlippage, null, null, "Tier 1", OnOrderExecuted);
        }

        protected override void OnTick()
        {
            UpdateHUD();

            if (!_isActive) return;

            var positions = Positions.FindAll(BotLabel, SymbolName, Direction);
            if (positions.Length == 0 || positions.Length >= MaxTiers) return;

            // Check Spread Safety
            double currentSpread = Symbol.Spread / Symbol.PipSize;
            if (currentSpread > MaxSpreadPips)
            {
                _uiStatus.Text = $"Spread too high ({Math.Round(currentSpread, 1)} pips)";
                _uiStatus.ForegroundColor = Color.Red;
                return; // Wait for spread to normalize
            }

            _uiStatus.Text = "Monitoring Excursion...";
            _uiStatus.ForegroundColor = Color.LightGreen;

            // Calculate excursion
            double lastPrice = Direction == TradeType.Buy 
                ? positions.Min(p => p.EntryPrice) 
                : positions.Max(p => p.EntryPrice);

            bool shouldScale = Direction == TradeType.Buy 
                ? Symbol.Ask <= lastPrice - (DistancePips * Symbol.PipSize)
                : Symbol.Bid >= lastPrice + (DistancePips * Symbol.PipSize);

            if (shouldScale)
            {
                var lastPosition = positions.OrderBy(p => p.EntryTime).Last();
                double newVolume = Symbol.NormalizeVolumeInUnits(lastPosition.VolumeInUnits * VolumeMultiplier);
                
                int nextTier = positions.Length + 1;
                ExecuteMarketOrderAsync(Direction, SymbolName, newVolume, BotLabel, MaxSlippage, null, null, $"Tier {nextTier}", OnOrderExecuted);
            }
        }

        private void OnOrderExecuted(TradeResult result)
        {
            if (result.IsSuccessful)
            {
                _isActive = true;
                UpdateTakeProfitAsync();
            }
            else
            {
                Print($"Execution Failed: {result.Error}");
            }
        }

        private void UpdateTakeProfitAsync()
        {
            var positions = Positions.FindAll(BotLabel, SymbolName, Direction);
            if (positions.Length == 0) return;

            double totalVolume = positions.Sum(p => p.VolumeInUnits);
            double weightedPriceSum = positions.Sum(p => p.EntryPrice * p.VolumeInUnits);

            _breakEvenPrice = weightedPriceSum / totalVolume;
            
            _targetPrice = Direction == TradeType.Buy 
                ? _breakEvenPrice + (TakeProfitPips * Symbol.PipSize)
                : _breakEvenPrice - (TakeProfitPips * Symbol.PipSize);

            _targetPrice = Math.Round(_targetPrice, Symbol.Digits);

            // Asynchronously update TP for all positions to avoid thread blocking
            foreach (var pos in positions)
            {
                if (pos.TakeProfit != _targetPrice)
                {
                    ModifyPositionAsync(pos, pos.StopLoss, _targetPrice);
                }
            }
        }

        private void OnPositionsClosed(PositionClosedEventArgs args)
        {
            if (args.Position.Label != BotLabel || args.Position.SymbolName != SymbolName) return;

            var remainingPositions = Positions.FindAll(BotLabel, SymbolName, Direction);
            
            // If all positions in our basket are closed, shut down the bot
            if (remainingPositions.Length == 0 && _isActive)
            {
                Print("Basket target hit or closed manually. Terminating cBot.");
                Stop();
            }
            else
            {
                // A partial close happened (e.g. manual intervention), recalculate TP
                UpdateTakeProfitAsync();
            }
        }

        #region UI Rendering and Updates

        private void DrawHUD()
        {
            var border = new Border
            {
                BackgroundColor = Color.FromArgb(200, 20, 20, 20),
                BorderColor = Color.FromArgb(255, 60, 60, 60),
                BorderThickness = 1,
                CornerRadius = 5,
                Margin = new Thickness(10),
                HorizontalAlignment = HorizontalAlignment.Right,
                VerticalAlignment = VerticalAlignment.Bottom,
                Padding = new Thickness(10)
            };

            var stackPanel = new StackPanel { Orientation = Orientation.Vertical };

            // Title
            stackPanel.AddChild(new TextBlock { Text = "LIQUIDITY AGGREGATOR", FontWeight = FontWeight.Bold, ForegroundColor = Color.White, Margin = new Thickness(0, 0, 0, 10) });

            // Create UI elements
            _uiTiers = CreateUIRow(stackPanel, "Active Tiers:");
            _uiVolume = CreateUIRow(stackPanel, "Total Volume:");
            _uiBreakEven = CreateUIRow(stackPanel, "Break Even:");
            _uiTarget = CreateUIRow(stackPanel, "Target Price:");
            _uiPnL = CreateUIRow(stackPanel, "Current PnL:");
            
            _uiStatus = new TextBlock { Text = "Initializing...", ForegroundColor = Color.Gray, Margin = new Thickness(0, 10, 0, 0), FontStyle = FontStyle.Italic };
            stackPanel.AddChild(_uiStatus);

            border.Child = stackPanel;
            Chart.AddControl(border);
        }

        private TextBlock CreateUIRow(StackPanel parent, string labelText)
        {
            var row = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 2) };
            row.AddChild(new TextBlock { Text = labelText, Width = 90, ForegroundColor = Color.Gray });
            
            var valueText = new TextBlock { Text = "-", ForegroundColor = Color.White };
            row.AddChild(valueText);
            
            parent.AddChild(row);
            return valueText;
        }

        private void UpdateHUD()
        {
            var positions = Positions.FindAll(BotLabel, SymbolName, Direction);
            
            _uiTiers.Text = $"{positions.Length} / {MaxTiers}";
            
            if (positions.Length > 0)
            {
                double totalVol = positions.Sum(p => p.VolumeInUnits);
                double netProfit = positions.Sum(p => p.NetProfit);
                
                _uiVolume.Text = (totalVol / 100000).ToString("0.00") + " Lots"; // Assumes standard lot size
                _uiBreakEven.Text = _breakEvenPrice.ToString($"F{Symbol.Digits}");
                _uiTarget.Text = _targetPrice.ToString($"F{Symbol.Digits}");
                
                _uiPnL.Text = netProfit >= 0 ? $"+${netProfit:F2}" : $"-${Math.Abs(netProfit):F2}";
                _uiPnL.ForegroundColor = netProfit >= 0 ? Color.LimeGreen : Color.Tomato;
            }
            else
            {
                _uiVolume.Text = "-";
                _uiBreakEven.Text = "-";
                _uiTarget.Text = "-";
                _uiPnL.Text = "-";
                _uiPnL.ForegroundColor = Color.White;
            }
        }

        #endregion
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Recommended broker for automated trading & scalping IC Markets
PTScalper
Site Admin
Posts: 4687
Joined: Mon Jul 20, 2026 1:28 pm

Re: Aggregating Liquidity: How to Scale Into Sweeps Using Cost Averaging

Post by PTScalper »

Pro Features Explained:

The BotLabel Parameter: This ensures you can run this script multiple times on different charts, or even long and short simultaneously on the same pair, without the bots mixing up each other's trades. It also ignores any trades you place manually.

The OnOrderExecuted Callback: Instead of freezing the bot while it waits for the broker to confirm the trade, the bot sends the order and keeps doing its job. When the broker finally replies, it triggers this callback to adjust the Take Profit limits seamlessly.

Partial Close Resiliency: If you intervene manually and close one of the tiers early (perhaps to reduce risk), the OnPositionsClosed event will instantly recalculate the Break Even and Take Profit for the remaining open trades automatically.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4687
Joined: Mon Jul 20, 2026 1:28 pm

Re: Aggregating Liquidity: How to Scale Into Sweeps Using Cost Averaging

Post by PTScalper »

Translating these institutional features from C# to MQL requires adapting to how MetaTrader handles threading and events.

Since MetaTrader’s standard OrderSend runs synchronously and can block the main thread, the HUD is decoupled to update independently. Instead of cTrader's Positions.Closed event, the MT5 version uses the native OnTrade() event handler to instantly recalculate the basket if you manually close a tier, while MT4 uses an optimized state-tracking loop.

Here are the Pro implementations for both platforms, complete with the Heads-Up Display, spread filters, slippage control, and dynamic PnL tracking.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4687
Joined: Mon Jul 20, 2026 1:28 pm

Re: Aggregating Liquidity: How to Scale Into Sweeps Using Cost Averaging

Post by PTScalper »

MetaTrader 4 (MQL4) Pro Version

Code: Select all

//+------------------------------------------------------------------+
//|                                       LiquidityAggregatorPro.mq4 |
//+------------------------------------------------------------------+
#property strict
#property description "Pro Discretionary Liquidity Scaling EA with HUD"

enum ENUM_TRADE_DIR {
    DIR_BUY = 0,  // Buy
    DIR_SELL = 1  // Sell
};

input string           BotLabel         = "LiqAggregator_Pro";
input ENUM_TRADE_DIR   TradeDirection   = DIR_BUY;
input double           InitialVolume    = 0.1;
input double           VolumeMultiplier = 1.5;
input int              MaxTiers         = 5;
input double           DistancePips     = 10.0;
input double           TakeProfitPips   = 5.0;
input double           MaxSpreadPips    = 2.0;
input int              MaxSlippagePips  = 1;
input int              MagicNumber      = 123456;

bool   isActive = false;
double pipSize;
int    slippagePoints;
double breakEvenPrice = 0;
double targetPrice = 0;
string hudPrefix = "HUD_LAP_";

int GetVolumeDecimals(double step) {
    if (step <= 0.001) return 3;
    if (step <= 0.01) return 2;
    if (step <= 0.1) return 1;
    return 0;
}

int OnInit() {
    pipSize = Point;
    if (Digits == 3 || Digits == 5) pipSize = Point * 10;
    slippagePoints = (int)(MaxSlippagePips * (pipSize / Point));

    DrawHUD();
    UpdateStatus("Executing Tier 1...", clrOrange);

    double price = (TradeDirection == DIR_BUY) ? Ask : Bid;
    int cmd = (TradeDirection == DIR_BUY) ? OP_BUY : OP_SELL;
    
    int ticket = OrderSend(Symbol(), cmd, InitialVolume, price, slippagePoints, 0, 0, BotLabel, MagicNumber, 0, clrDodgerBlue);
    if (ticket > 0) {
        isActive = true;
        UpdateTakeProfit();
        UpdateStatus("Monitoring Excursion...", clrLimeGreen);
    } else {
        Print("Initial execution failed. Error: ", GetLastError());
        ExpertRemove();
    }
    return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason) {
    ObjectsDeleteAll(0, hudPrefix);
}

void OnTick() {
    UpdateHUD();
    if (!isActive) return;

    int posCount = 0;
    double lastPrice = (TradeDirection == DIR_BUY) ? 9999999.0 : 0.0;
    double lastLot = 0.0;
    datetime lastTime = 0;

    for (int i = OrdersTotal() - 1; i >= 0; i--) {
        if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES) && OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber) {
            posCount++;
            if (OrderOpenTime() > lastTime) {
                lastTime = OrderOpenTime();
                lastLot = OrderLots();
            }
            if (TradeDirection == DIR_BUY && OrderOpenPrice() < lastPrice) lastPrice = OrderOpenPrice();
            else if (TradeDirection == DIR_SELL && OrderOpenPrice() > lastPrice) lastPrice = OrderOpenPrice();
        }
    }

    if (posCount == 0) {
        Print("Basket target hit or closed manually. Terminating EA.");
        ExpertRemove();
        return;
    }

    // Partial close detection (forces TP recalculation)
    static int lastPosCount = 0;
    if (posCount != lastPosCount) {
        UpdateTakeProfit();
        lastPosCount = posCount;
    }

    if (posCount >= MaxTiers) return;

    double spreadPips = MarketInfo(Symbol(), MODE_SPREAD) * Point / pipSize;
    if (spreadPips > MaxSpreadPips) {
        UpdateStatus("Spread too high (" + DoubleToStr(spreadPips, 1) + " pips)", clrRed);
        return;
    } else {
        UpdateStatus("Monitoring Excursion...", clrLimeGreen);
    }

    bool shouldScaleIn = false;
    if (TradeDirection == DIR_BUY && Ask <= lastPrice - (DistancePips * pipSize)) shouldScaleIn = true;
    else if (TradeDirection == DIR_SELL && Bid >= lastPrice + (DistancePips * pipSize)) shouldScaleIn = true;

    if (shouldScaleIn) {
        double step = MarketInfo(Symbol(), MODE_LOTSTEP);
        double newVol = NormalizeDouble(MathRound((lastLot * VolumeMultiplier) / step) * step, GetVolumeDecimals(step));
        newVol = MathMax(MarketInfo(Symbol(), MODE_MINLOT), MathMin(MarketInfo(Symbol(), MODE_MAXLOT), newVol));

        double price = (TradeDirection == DIR_BUY) ? Ask : Bid;
        int cmd = (TradeDirection == DIR_BUY) ? OP_BUY : OP_SELL;
        
        int ticket = OrderSend(Symbol(), cmd, newVol, price, slippagePoints, 0, 0, BotLabel, MagicNumber, 0, clrRed);
        if (ticket > 0) UpdateTakeProfit();
    }
}

void UpdateTakeProfit() {
    double totalVol = 0, weightedPrice = 0;
    for (int i = OrdersTotal() - 1; i >= 0; i--) {
        if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES) && OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber) {
            totalVol += OrderLots();
            weightedPrice += OrderOpenPrice() * OrderLots();
        }
    }
    if (totalVol == 0) return;

    breakEvenPrice = weightedPrice / totalVol;
    targetPrice = (TradeDirection == DIR_BUY) ? breakEvenPrice + (TakeProfitPips * pipSize) : breakEvenPrice - (TakeProfitPips * pipSize);
    targetPrice = NormalizeDouble(targetPrice, Digits);

    for (int i = OrdersTotal() - 1; i >= 0; i--) {
        if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES) && OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber) {
            if (NormalizeDouble(OrderTakeProfit(), Digits) != targetPrice) {
                OrderModify(OrderTicket(), OrderOpenPrice(), OrderStopLoss(), targetPrice, 0, clrGreen);
            }
        }
    }
}

// --- HUD RENDERING ---
void CreateLabel(string name, int yDist, string text, color clr, int size=10, bool bold=false) {
    ObjectCreate(0, hudPrefix + name, OBJ_LABEL, 0, 0, 0);
    ObjectSetInteger(0, hudPrefix + name, OBJPROP_CORNER, CORNER_RIGHT_LOWER);
    ObjectSetInteger(0, hudPrefix + name, OBJPROP_XDISTANCE, 20);
    ObjectSetInteger(0, hudPrefix + name, OBJPROP_YDISTANCE, yDist);
    ObjectSetString(0, hudPrefix + name, OBJPROP_TEXT, text);
    ObjectSetInteger(0, hudPrefix + name, OBJPROP_COLOR, clr);
    ObjectSetInteger(0, hudPrefix + name, OBJPROP_FONTSIZE, size);
    ObjectSetString(0, hudPrefix + name, OBJPROP_FONT, bold ? "Arial Bold" : "Arial");
}

void DrawHUD() {
    CreateLabel("Title", 140, "LIQUIDITY AGGREGATOR", clrWhite, 11, true);
    CreateLabel("Tiers", 120, "Active Tiers: -", clrLightGray);
    CreateLabel("Vol", 100, "Total Volume: -", clrLightGray);
    CreateLabel("BE", 80, "Break Even: -", clrLightGray);
    CreateLabel("TP", 60, "Target Price: -", clrLightGray);
    CreateLabel("PnL", 40, "Current PnL: -", clrLightGray);
    CreateLabel("Status", 20, "Initializing...", clrGray, 9);
}

void UpdateHUD() {
    int posCount = 0;
    double totalVol = 0, netProfit = 0;
    for (int i = OrdersTotal() - 1; i >= 0; i--) {
        if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES) && OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber) {
            posCount++;
            totalVol += OrderLots();
            netProfit += OrderProfit() + OrderSwap() + OrderCommission();
        }
    }

    ObjectSetString(0, hudPrefix + "Tiers", OBJPROP_TEXT, "Active Tiers: " + IntegerToString(posCount) + " / " + IntegerToString(MaxTiers));
    if (posCount > 0) {
        ObjectSetString(0, hudPrefix + "Vol", OBJPROP_TEXT, "Total Volume: " + DoubleToStr(totalVol, 2) + " Lots");
        ObjectSetString(0, hudPrefix + "BE", OBJPROP_TEXT, "Break Even: " + DoubleToStr(breakEvenPrice, Digits));
        ObjectSetString(0, hudPrefix + "TP", OBJPROP_TEXT, "Target Price: " + DoubleToStr(targetPrice, Digits));
        
        string pnlStr = "Current PnL: " + (netProfit >= 0 ? "+$" : "-$") + DoubleToStr(MathAbs(netProfit), 2);
        ObjectSetString(0, hudPrefix + "PnL", OBJPROP_TEXT, pnlStr);
        ObjectSetInteger(0, hudPrefix + "PnL", OBJPROP_COLOR, netProfit >= 0 ? clrLimeGreen : clrTomato);
    }
}

void UpdateStatus(string msg, color clr) {
    ObjectSetString(0, hudPrefix + "Status", OBJPROP_TEXT, msg);
    ObjectSetInteger(0, hudPrefix + "Status", OBJPROP_COLOR, clr);
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4687
Joined: Mon Jul 20, 2026 1:28 pm

Re: Aggregating Liquidity: How to Scale Into Sweeps Using Cost Averaging

Post by PTScalper »

MetaTrader 5 (MQL5) Pro Version

Note: MT5 utilizes OnTrade() which cleanly handles recalculating the Break-Even and limits if a position is partially closed or stopped out manually.

Code: Select all

//+------------------------------------------------------------------+
//|                                       LiquidityAggregatorPro.mq5 |
//+------------------------------------------------------------------+
#property strict
#property description "Pro Discretionary Liquidity Scaling EA with HUD"

#include <Trade\Trade.mqh>

enum ENUM_TRADE_DIR {
    DIR_BUY = 0,  // Buy
    DIR_SELL = 1  // Sell
};

input string           BotLabel         = "LiqAggregator_Pro";
input ENUM_TRADE_DIR   TradeDirection   = DIR_BUY;
input double           InitialVolume    = 0.1;
input double           VolumeMultiplier = 1.5;
input int              MaxTiers         = 5;
input double           DistancePips     = 10.0;
input double           TakeProfitPips   = 5.0;
input double           MaxSpreadPips    = 2.0;
input int              MaxSlippagePips  = 1;
input ulong            MagicNumber      = 123456;

CTrade trade;
bool   isActive = false;
double pipSize;
double breakEvenPrice = 0;
double targetPrice = 0;
string hudPrefix = "HUD_LAP_";

int GetVolumeDecimals(double step) {
    if (step <= 0.001) return 3;
    if (step <= 0.01) return 2;
    if (step <= 0.1) return 1;
    return 0;
}

int OnInit() {
    if ((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_NETTING) {
        Print("Requires Hedging account to track separate tiers.");
        return(INIT_FAILED);
    }

    pipSize = _Point;
    if (_Digits == 3 || _Digits == 5) pipSize = _Point * 10;
    
    trade.SetExpertMagicNumber(MagicNumber);
    trade.SetDeviationInPoints((ulong)(MaxSlippagePips * (pipSize / _Point)));

    DrawHUD();
    UpdateStatus("Executing Tier 1...", clrOrange);

    double price = (TradeDirection == DIR_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
    bool res = (TradeDirection == DIR_BUY) ? trade.Buy(InitialVolume, _Symbol, price, 0, 0, BotLabel) : trade.Sell(InitialVolume, _Symbol, price, 0, 0, BotLabel);
        
    if (res) {
        isActive = true;
        UpdateTakeProfit();
        UpdateStatus("Monitoring Excursion...", clrLimeGreen);
    } else {
        Print("Initial execution failed. Code: ", trade.ResultRetcode());
        ExpertRemove();
    }
    return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason) {
    ObjectsDeleteAll(0, hudPrefix);
}

// Native event handler for position changes (mimics cTrader Positions.Closed)
void OnTrade() {
    if (!isActive) return;
    
    int posCount = 0;
    for (int i = PositionsTotal() - 1; i >= 0; i--) {
        if (PositionGetTicket(i) > 0 && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber) {
            posCount++;
        }
    }
    
    if (posCount == 0) {
        Print("Basket target hit or closed manually. Terminating EA.");
        ExpertRemove();
    } else {
        UpdateTakeProfit();
    }
}

void OnTick() {
    UpdateHUD();
    if (!isActive) return;
    
    int posCount = 0;
    double lastPrice = (TradeDirection == DIR_BUY) ? 9999999.0 : 0.0;
    double lastLot = 0.0;
    
    for (int i = PositionsTotal() - 1; i >= 0; i--) {
        if (PositionGetTicket(i) > 0 && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber) {
            posCount++;
            lastLot = PositionGetDouble(POSITION_VOLUME);
            double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
            if (TradeDirection == DIR_BUY && openPrice < lastPrice) lastPrice = openPrice;
            else if (TradeDirection == DIR_SELL && openPrice > lastPrice) lastPrice = openPrice;
        }
    }
    
    if (posCount >= MaxTiers || posCount == 0) return;
    
    double spreadPips = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * _Point / pipSize;
    if (spreadPips > MaxSpreadPips) {
        UpdateStatus("Spread too high (" + DoubleToStr(spreadPips, 1) + " pips)", clrRed);
        return;
    } else {
        UpdateStatus("Monitoring Excursion...", clrLimeGreen);
    }
    
    double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
    double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    
    bool shouldScaleIn = false;
    if (TradeDirection == DIR_BUY && ask <= lastPrice - (DistancePips * pipSize)) shouldScaleIn = true;
    else if (TradeDirection == DIR_SELL && bid >= lastPrice + (DistancePips * pipSize)) shouldScaleIn = true;
        
    if (shouldScaleIn) {
        double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
        double newVol = NormalizeDouble(MathRound((lastLot * VolumeMultiplier) / step) * step, GetVolumeDecimals(step));
        newVol = MathMax(SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN), MathMin(SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX), newVol));
        
        bool res = (TradeDirection == DIR_BUY) ? trade.Buy(newVol, _Symbol, ask, 0, 0, BotLabel) : trade.Sell(newVol, _Symbol, bid, 0, 0, BotLabel);
        if (res) UpdateTakeProfit(); // Backup update, though OnTrade will also catch it
    }
}

void UpdateTakeProfit() {
    double totalVol = 0, weightedPrice = 0;
    for (int i = PositionsTotal() - 1; i >= 0; i--) {
        if (PositionGetTicket(i) > 0 && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber) {
            double vol = PositionGetDouble(POSITION_VOLUME);
            totalVol += vol;
            weightedPrice += PositionGetDouble(POSITION_PRICE_OPEN) * vol;
        }
    }
    if (totalVol == 0) return;
    
    breakEvenPrice = weightedPrice / totalVol;
    targetPrice = (TradeDirection == DIR_BUY) ? breakEvenPrice + (TakeProfitPips * pipSize) : breakEvenPrice - (TakeProfitPips * pipSize);
    targetPrice = NormalizeDouble(targetPrice, _Digits);
    
    for (int i = PositionsTotal() - 1; i >= 0; i--) {
        ulong ticket = PositionGetTicket(i);
        if (ticket > 0 && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber) {
            if (NormalizeDouble(PositionGetDouble(POSITION_TP), _Digits) != targetPrice) {
                trade.PositionModify(ticket, PositionGetDouble(POSITION_SL), targetPrice);
            }
        }
    }
}

// --- HUD RENDERING ---
void CreateLabel(string name, int yDist, string text, color clr, int size=10, bool bold=false) {
    ObjectCreate(0, hudPrefix + name, OBJ_LABEL, 0, 0, 0);
    ObjectSetInteger(0, hudPrefix + name, OBJPROP_CORNER, CORNER_RIGHT_LOWER);
    ObjectSetInteger(0, hudPrefix + name, OBJPROP_XDISTANCE, 20);
    ObjectSetInteger(0, hudPrefix + name, OBJPROP_YDISTANCE, yDist);
    ObjectSetString(0, hudPrefix + name, OBJPROP_TEXT, text);
    ObjectSetInteger(0, hudPrefix + name, OBJPROP_COLOR, clr);
    ObjectSetInteger(0, hudPrefix + name, OBJPROP_FONTSIZE, size);
    ObjectSetString(0, hudPrefix + name, OBJPROP_FONT, bold ? "Arial Bold" : "Arial");
}

void DrawHUD() {
    CreateLabel("Title", 140, "LIQUIDITY AGGREGATOR", clrWhite, 11, true);
    CreateLabel("Tiers", 120, "Active Tiers: -", clrLightGray);
    CreateLabel("Vol", 100, "Total Volume: -", clrLightGray);
    CreateLabel("BE", 80, "Break Even: -", clrLightGray);
    CreateLabel("TP", 60, "Target Price: -", clrLightGray);
    CreateLabel("PnL", 40, "Current PnL: -", clrLightGray);
    CreateLabel("Status", 20, "Initializing...", clrGray, 9);
}

void UpdateHUD() {
    int posCount = 0;
    double totalVol = 0, netProfit = 0;
    for (int i = PositionsTotal() - 1; i >= 0; i--) {
        if (PositionGetTicket(i) > 0 && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber) {
            posCount++;
            totalVol += PositionGetDouble(POSITION_VOLUME);
            netProfit += PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP); // Commission handling varies by broker in MT5, often applied to balance directly
        }
    }

    ObjectSetString(0, hudPrefix + "Tiers", OBJPROP_TEXT, "Active Tiers: " + IntegerToString(posCount) + " / " + IntegerToString(MaxTiers));
    if (posCount > 0) {
        ObjectSetString(0, hudPrefix + "Vol", OBJPROP_TEXT, "Total Volume: " + DoubleToStr(totalVol, 2) + " Lots");
        ObjectSetString(0, hudPrefix + "BE", OBJPROP_TEXT, "Break Even: " + DoubleToStr(breakEvenPrice, _Digits));
        ObjectSetString(0, hudPrefix + "TP", OBJPROP_TEXT, "Target Price: " + DoubleToStr(targetPrice, _Digits));
        
        string pnlStr = "Current PnL: " + (netProfit >= 0 ? "+$" : "-$") + DoubleToStr(MathAbs(netProfit), 2);
        ObjectSetString(0, hudPrefix + "PnL", OBJPROP_TEXT, pnlStr);
        ObjectSetInteger(0, hudPrefix + "PnL", OBJPROP_COLOR, netProfit >= 0 ? clrLimeGreen : clrTomato);
    }
}

void UpdateStatus(string msg, color clr) {
    ObjectSetString(0, hudPrefix + "Status", OBJPROP_TEXT, msg);
    ObjectSetInteger(0, hudPrefix + "Status", OBJPROP_COLOR, clr);
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4687
Joined: Mon Jul 20, 2026 1:28 pm

Re: Aggregating Liquidity: How to Scale Into Sweeps Using Cost Averaging

Post by PTScalper »

Pro Architecture Additions in Pine Script

On-Chart Heads-Up Display (HUD Table):

Uses TradingView’s table system anchored at position.bottom_right.

Renders real-time floating metrics on the last bar (barstate.islast) so the chart interface remains fast and responsive without repainting artifacts.

Tracks Active Tiers, Total Volume, Volume-Weighted Break-Even, Unified Take Profit, Floating PnL in USD, and live execution state.

Webhook Automation Engine:

Each tier generates formatted webhook strings inside alert_message on both strategy.entry and strategy.exit.

Includes selectable syntax for PineConnector (ID,action,symbol,qty=...,price=...) and Generic JSON ({"bot":"...", "action":"...", ...}) to bridge signals directly to external execution platforms.

Multi-Asset Pip Normalizer:

Automatically calculates point/pip conversions across standard FX pairs (e.g., 5-digit brokers), Gold (XAU), and Indices without manual point offsets.

Self-Locking Basket State:

Once all positions reach the pooled Take Profit limit order, the script closes out the entire pyramid and flips basket_finished := true, locking itself out to prevent unintentional re-entries during subsequent price swings.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4687
Joined: Mon Jul 20, 2026 1:28 pm

Re: Aggregating Liquidity: How to Scale Into Sweeps Using Cost Averaging

Post by PTScalper »

Pine Script Pro version 2.0

Code: Select all

//@version=5
strategy("Liquidity Aggregator Pro", overlay=true, pyramiding=20, calc_on_every_tick=true, initial_capital=100000, default_qty_type=strategy.cash, currency=currency.USD)

// =========================================================================
// INPUTS & CONFIGURATION
// =========================================================================
var grp_core   = "Core Execution"
i_direction    = input.string("Long", "Trade Direction", options=["Long", "Short"], group=grp_core)
i_bot_label    = input.string("LiqAgg_Pro", "Bot Label / ID Prefix", group=grp_core)
i_start_time   = input.time(timestamp("2026-10-05T00:00:00"), "Start Time (Set to Current Bar Time to Execute)", group=grp_core)

var grp_sizing = "Position Sizing"
i_initial_qty  = input.float(1.0, "Initial Volume (Lots / Qty)", step=0.1, minval=0.01, group=grp_sizing)
i_multiplier   = input.float(1.5, "Volume Multiplier", step=0.1, minval=1.0, group=grp_sizing)
i_max_tiers    = input.int(5, "Max Tiers", minval=1, maxval=20, group=grp_sizing)

var grp_grid   = "Grid Parameters"
i_dist_pips    = input.float(10.0, "Scale-In Distance (Pips)", step=1.0, minval=0.1, group=grp_grid)
i_tp_pips      = input.float(5.0, "Target Net Profit (Pips)", step=1.0, minval=0.1, group=grp_grid)

var grp_bridge = "Webhook & Bridge Integration"
i_use_alerts   = input.bool(true, "Generate Webhook Alerts", group=grp_bridge)
i_alert_prefix = input.string("PINECONNECTOR", "Bridge Format", options=["PINECONNECTOR", "JSON_GENERIC"], group=grp_bridge)

// =========================================================================
// PIP & SYMBOL METRICS
// =========================================================================
// Determine standard pip multiplier across FX, metals, indices, and crypto
float pip_size = syminfo.mintick * (syminfo.type == "forex" ? 10 : (str.contains(syminfo.ticker, "XAU") ? 10 : 1))
float dist_raw = i_dist_pips * pip_size
float tp_raw   = i_tp_pips * pip_size
bool  is_long  = i_direction == "Long"

// =========================================================================
// PERSISTENT STATE VARIABLES
// =========================================================================
var bool   is_active       = false
var bool   basket_finished = false
var int    active_tiers    = 0
var float  last_tier_price = na
var float  next_tier_qty   = i_initial_qty
var string status_msg      = "Idle - Awaiting Trigger Time"
var color  status_color    = color.gray

// Helper function to build webhook payloads for live bridges
get_entry_payload(string action, float qty, float price) =>
    if i_alert_prefix == "PINECONNECTOR"
        str.format("{0},{1},{2},qty={3},price={4}", i_bot_label, action, syminfo.ticker, qty, price)
    else
        str.format("{{\"bot\":\"{0}\",\"action\":\"{1}\",\"symbol\":\"{2}\",\"qty\":{3},\"price\":{4}}}", i_bot_label, action, syminfo.ticker, qty, price)

get_exit_payload(string reason) =>
    if i_alert_prefix == "PINECONNECTOR"
        str.format("{0},closetrades,{1},reason={2}", i_bot_label, syminfo.ticker, reason)
    else
        str.format("{{\"bot\":\"{0}\",\"action\":\"close_all\",\"symbol\":\"{1}\",\"reason\":\"{2}\"}}", i_bot_label, syminfo.ticker, reason)

// =========================================================================
// EXECUTION STATE MACHINE
// =========================================================================

// 1. Target Reached & Basket Closure Detection
if is_active and strategy.position_size == 0 and active_tiers > 0
    is_active       := false
    basket_finished := true
    status_msg      := "Basket Closed (TP Reached)"
    status_color    := color.lime
    if i_use_alerts
        alert(get_exit_payload("TargetReached"), alert.freq_once_per_bar_close)

// 2. Initial Tier Execution Trigger
if time >= i_start_time and not is_active and not basket_finished
    is_active       := true
    active_tiers    := 1
    last_tier_price := close
    next_tier_qty   := i_initial_qty * i_multiplier
    status_msg      := "Monitoring Excursion..."
    status_color    := color.lime
    
    tier_id = i_bot_label + "_Tier_1"
    string alert_txt = get_entry_payload(is_long ? "buy" : "sell", i_initial_qty, close)
    
    if is_long
        strategy.entry(tier_id, strategy.long, qty=i_initial_qty, alert_message=alert_txt)
    else
        strategy.entry(tier_id, strategy.short, qty=i_initial_qty, alert_message=alert_txt)
        
    if i_use_alerts
        alert(alert_txt, alert.freq_once_per_bar)

// 3. Dynamic Scale-In Logic
if is_active and active_tiers < i_max_tiers
    bool trigger_scale = false
    
    if is_long and close <= (last_tier_price - dist_raw)
        trigger_scale := true
    else if not is_long and close >= (last_tier_price + dist_raw)
        trigger_scale := true
        
    if trigger_scale
        active_tiers    += 1
        last_tier_price := close
        tier_qty        = next_tier_qty
        next_tier_qty   := next_tier_qty * i_multiplier
        
        tier_id = i_bot_label + "_Tier_" + str.tostring(active_tiers)
        string alert_txt = get_entry_payload(is_long ? "buy" : "sell", tier_qty, close)
        
        if is_long
            strategy.entry(tier_id, strategy.long, qty=tier_qty, alert_message=alert_txt)
        else
            strategy.entry(tier_id, strategy.short, qty=tier_qty, alert_message=alert_txt)
            
        if i_use_alerts
            alert(alert_txt, alert.freq_once_per_bar)
            
    if active_tiers >= i_max_tiers
        status_msg   := "Max Tiers Reached"
        status_color := color.orange

// 4. Dynamic Break-Even & Take Profit Exit
float target_price = na
if strategy.position_size != 0
    float avg_price = strategy.position_avg_price
    target_price := is_long ? (avg_price + tp_raw) : (avg_price - tp_raw)
    
    strategy.exit(i_bot_label + "_Exit", limit=target_price, alert_message=get_exit_payload("TP_Limit_Hit"))

// =========================================================================
// ON-CHART VISUALS
// =========================================================================
plot(strategy.position_size != 0 ? strategy.position_avg_price : na, "Break-Even Price", color=color.yellow, linewidth=2, style=plot.style_linebr)
plot(strategy.position_size != 0 ? target_price : na, "Target Price", color=color.green, linewidth=2, style=plot.style_linebr)

// =========================================================================
// HEADS-UP DISPLAY (HUD)
// =========================================================================
var table hud = table.new(position.bottom_right, 2, 7, bgcolor=color.new(#141414, 15), border_color=color.new(#3c3c3c, 0), border_width=1)

if barstate.islast
    // Header
    table.cell(hud, 0, 0, "LIQUIDITY AGGREGATOR", bgcolor=color.new(#242424, 0), text_color=color.white, text_size=size.small, text_halign=text.halign_left)
    table.cell(hud, 1, 0, "PRO", bgcolor=color.new(#242424, 0), text_color=color.teal, text_size=size.small, text_halign=text.halign_right)
    
    // Tiers
    table.cell(hud, 0, 1, "Active Tiers:", text_color=color.gray, text_size=size.small, text_halign=text.halign_left)
    table.cell(hud, 1, 1, str.tostring(active_tiers) + " / " + str.tostring(i_max_tiers), text_color=color.white, text_size=size.small, text_halign=text.halign_right)
    
    // Total Volume
    float open_vol = math.abs(strategy.position_size)
    table.cell(hud, 0, 2, "Total Volume:", text_color=color.gray, text_size=size.small, text_halign=text.halign_left)
    table.cell(hud, 1, 2, strategy.position_size != 0 ? str.tostring(open_vol, "#.##") + " Lots" : "-", text_color=color.white, text_size=size.small, text_halign=text.halign_right)
    
    // Break Even
    table.cell(hud, 0, 3, "Break Even:", text_color=color.gray, text_size=size.small, text_halign=text.halign_left)
    table.cell(hud, 1, 3, strategy.position_size != 0 ? str.tostring(strategy.position_avg_price, "#.#####") : "-", text_color=color.yellow, text_size=size.small, text_halign=text.halign_right)
    
    // Target Price
    table.cell(hud, 0, 4, "Target Price:", text_color=color.gray, text_size=size.small, text_halign=text.halign_left)
    table.cell(hud, 1, 4, strategy.position_size != 0 ? str.tostring(target_price, "#.#####") : "-", text_color=color.green, text_size=size.small, text_halign=text.halign_right)
    
    // Floating PnL
    float pnl = strategy.openprofit
    color pnl_color = pnl >= 0 ? color.lime : color.red
    string pnl_text = strategy.position_size != 0 ? (pnl >= 0 ? "+$" : "-$") + str.tostring(math.abs(pnl), "#.##") : "-"
    table.cell(hud, 0, 5, "Floating PnL:", text_color=color.gray, text_size=size.small, text_halign=text.halign_left)
    table.cell(hud, 1, 5, pnl_text, text_color=strategy.position_size != 0 ? pnl_color : color.white, text_size=size.small, text_halign=text.halign_right)
    
    // System Status
    table.cell(hud, 0, 6, "Status:", text_color=color.gray, text_size=size.small, text_halign=text.halign_left)
    table.cell(hud, 1, 6, status_msg, text_color=status_color, text_size=size.small, text_halign=text.halign_right)
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4687
Joined: Mon Jul 20, 2026 1:28 pm

Re: Aggregating Liquidity: How to Scale Into Sweeps Using Cost Averaging

Post by PTScalper »

If you have any question, feel free to ask :-)

Btw did you try it? Or do you have some idea how to improve it?
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Post Reply