Advertisement IC Markets

The 1% Rule Isn't Optional

Discuss 1-minute to 15-minute price action setups, fading intraday momentum, key support/resistance zones, and proven short-term trading methodologies.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

Architectural Nuances for MT5

Net Edge Filtering (DEAL_ENTRY_OUT): In CalculatePositionSize(), the history parser strictly looks for DEAL_ENTRY_OUT (which represents closed trades). It extracts gross profit, commission, and swap to determine your true net edge. If you don't account for commissions in MQL5, your EA will oversize based on gross metrics while your account bleeds from spread friction.

Dynamic Spreads: ExecuteTrade() utilizes Ask() for buys and Bid() for sells dynamically. In scalping, a sudden spread widening can instantly trigger your stop if you hardcode mid-prices.

Liquidation Protocol: If either the Daily Drawdown or Peak-to-Trough circuit breaker trips in CheckCircuitBreakers(), the EA immediately calls trade.PositionClose(_Symbol) to flatten the portfolio and halts execution until startOfDayEquity resets at midnight.
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: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

Translating institutional mechanics from MQL5 to MQL4 requires fundamentally shifting how you handle order management. MQL4 is not position-based; it is order-based.

The most critical change is how MQL4 handles partial scale-outs. When you execute a partial OrderClose in MQL4, the engine sends the original ticket to history and creates a brand new ticket number for the remaining volume. If you close the partial before modifying the stop loss, you lose reference to the trade and your break-even stop will fail. The algorithm below modifies the stop loss to break-even first, and then slices the volume, forcing the new ticket to inherit the protected stop loss.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

Here is the fully adapted MQL4 Expert Advisor. Save this as an .mq4 file.

Code: Select all

//+------------------------------------------------------------------+
//|                                     Inst_Kelly_Engine_Scalper.mq4|
//+------------------------------------------------------------------+
#property strict

// =========================================================================
// 1. INPUT PARAMETERS
// =========================================================================
input string   grp1 = "--- Dynamic Edge (Kelly) ---";
input double   InpBootstrapWinRate = 50.0;    // Bootstrap Win Rate (%)
input double   InpBootstrapRR      = 2.0;     // Bootstrap Reward/Risk
input double   InpKellyFraction    = 0.5;     // Kelly Fraction (0.5 = Half)
input int      InpLookback         = 100;     // Rolling Trade Window

input string   grp2 = "--- Risk & Execution Limits ---";
input int      InpAtrPeriod        = 14;      // ATR Period
input double   InpAtrMultiplier    = 1.5;     // ATR Stop Multiplier
input double   InpMaxRisk          = 5.0;     // Max Risk per Trade (%)
input double   InpMaxLeverage      = 10.0;    // Max Leverage (Margin Cap)

input string   grp3 = "--- Circuit Breakers ---";
input double   InpMaxDailyDD       = 3.0;     // Max Daily Drawdown (%)
input double   InpHwmDD            = 15.0;    // Max Peak-to-Trough DD (%)

// =========================================================================
// 2. GLOBAL STATE VARIABLES
// =========================================================================
int            MagicNumber = 1337;
double         hwmEquity;
double         startOfDayEquity;
int            currentDay;
bool           systemHalted = false;

// Trade Management State
bool           tp1Hit = false;
double         entryPrice = 0.0;
double         tp1Level = 0.0;

// =========================================================================
// INITIALIZATION
// =========================================================================
int OnInit()
{
    hwmEquity = AccountEquity();
    startOfDayEquity = hwmEquity;
    currentDay = Day();
    
    return(INIT_SUCCEEDED);
}

// =========================================================================
// CORE EVENT LOOP
// =========================================================================
void OnTick()
{
    // 1. Update Circuit Breakers & High-Water Mark
    CheckCircuitBreakers();
    if(systemHalted) return;

    // 2. Dynamic Trade Management (Scale-out & Break-even)
    ManageOpenPositions();

    // 3. Entry Logic (Only if no open positions)
    if(CountOpenOrders() == 0)
    {
        // Reset state
        tp1Hit = false; 

        // MQL4 fetches indicators directly, no handles needed
        double fastSMA1 = iMA(Symbol(), 0, 10, 0, MODE_SMA, PRICE_CLOSE, 1);
        double fastSMA0 = iMA(Symbol(), 0, 10, 0, MODE_SMA, PRICE_CLOSE, 0);
        double slowSMA1 = iMA(Symbol(), 0, 20, 0, MODE_SMA, PRICE_CLOSE, 1);
        double slowSMA0 = iMA(Symbol(), 0, 20, 0, MODE_SMA, PRICE_CLOSE, 0);
        double atrVal   = iATR(Symbol(), 0, InpAtrPeriod, 0);
        
        bool longTrigger = (fastSMA1 <= slowSMA1 && fastSMA0 > slowSMA0);
        bool shortTrigger = (fastSMA1 >= slowSMA1 && fastSMA0 < slowSMA0);

        if(longTrigger || shortTrigger)
        {
            double stopDist = atrVal * InpAtrMultiplier;
            double lotSize = CalculatePositionSize(stopDist);
            
            if(lotSize > 0)
            {
                if(longTrigger) ExecuteTrade(OP_BUY, lotSize, stopDist);
                if(shortTrigger) ExecuteTrade(OP_SELL, lotSize, stopDist);
            }
        }
    }
}

// =========================================================================
// DYNAMIC KELLY & LOT SIZING
// =========================================================================
double CalculatePositionSize(double stopDistancePoints)
{
    double dynWinRate = InpBootstrapWinRate / 100.0;
    double dynRR = InpBootstrapRR;
    
    // Parse History for Net Edge Calculation
    int totalHistory = OrdersHistoryTotal();
    int analyzed = 0, wins = 0, losses = 0;
    double grossProfit = 0, grossLoss = 0;
    
    for(int i = totalHistory - 1; i >= 0 && analyzed < InpLookback; i--)
    {
        if(OrderSelect(i, SELECT_BY_POS, MODE_HISTORY))
        {
            if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
            {
                // Net Profit (includes broker friction)
                double profit = OrderProfit() + OrderCommission() + OrderSwap();
                
                if(profit > 0) { wins++; grossProfit += profit; }
                else if(profit < 0) { losses++; grossLoss += MathAbs(profit); }
                analyzed++;
            }
        }
    }
    
    if(analyzed > 0)
    {
        dynWinRate = (double)wins / analyzed;
        double avgWin = (wins > 0) ? grossProfit / wins : 0;
        double avgLoss = (losses > 0) ? grossLoss / losses : 0;
        if(avgLoss > 0) dynRR = avgWin / avgLoss;
    }
    
    // Kelly Math
    double fullKelly = dynWinRate - ((1.0 - dynWinRate) / dynRR);
    double fracKelly = MathMax(0.0, fullKelly * InpKellyFraction);
    double appliedRisk = MathMin(InpMaxRisk / 100.0, fracKelly);
    
    if(appliedRisk <= 0) return 0.0;

    // Convert Risk $ to Lot Size
    double riskAmount = AccountEquity() * appliedRisk;
    double tickValue = MarketInfo(Symbol(), MODE_TICKVALUE);
    double tickSize = MarketInfo(Symbol(), MODE_TICKSIZE);
    
    if(tickValue == 0 || tickSize == 0) return 0.0;
    
    double pointsLoss = stopDistancePoints / tickSize;
    double calcLots = riskAmount / (pointsLoss * tickValue);
    
    // Normalize and apply lot constraints
    double minLot = MarketInfo(Symbol(), MODE_MINLOT);
    double maxLot = MarketInfo(Symbol(), MODE_MAXLOT);
    double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);
    
    calcLots = MathFloor(calcLots / lotStep) * lotStep;
    calcLots = MathMax(minLot, MathMin(maxLot, calcLots));
    
    return calcLots;
}

// =========================================================================
// EXECUTION & DYNAMIC MANAGEMENT
// =========================================================================
void ExecuteTrade(int type, double volume, double stopDist)
{
    double price = (type == OP_BUY) ? Ask : Bid;
    double sl = (type == OP_BUY) ? price - stopDist : price + stopDist;
    
    // Normalize for broker submission
    price = NormalizeDouble(price, Digits);
    sl = NormalizeDouble(sl, Digits);
    
    int ticket = OrderSend(Symbol(), type, volume, price, 3, sl, 0, "Kelly Algo", MagicNumber, 0, (type == OP_BUY) ? clrBlue : clrRed);
    
    if(ticket > 0)
    {
        entryPrice = price;
        tp1Level = (type == OP_BUY) ? price + stopDist : price - stopDist;
        tp1Hit = false;
    }
}

void ManageOpenPositions()
{
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
        if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
            if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
            {
                if(!tp1Hit)
                {
                    double currentPrice = (OrderType() == OP_BUY) ? Bid : Ask;
                    bool hitTarget = (OrderType() == OP_BUY) ? (currentPrice >= tp1Level) : (currentPrice <= tp1Level);
                    
                    if(hitTarget)
                    {
                        // 1. Move Stop to Break-Even FIRST (before ticket ID changes)
                        bool modified = OrderModify(OrderTicket(), OrderOpenPrice(), NormalizeDouble(entryPrice, Digits), OrderTakeProfit(), 0);
                        
                        // 2. Execute 50% Scale-Out
                        double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);
                        double partialVol = MathFloor((OrderLots() / 2.0) / lotStep) * lotStep;
                        double minLot = MarketInfo(Symbol(), MODE_MINLOT);
                        
                        if(partialVol >= minLot)
                        {
                            if(OrderType() == OP_BUY) OrderClose(OrderTicket(), partialVol, Bid, 3);
                            else OrderClose(OrderTicket(), partialVol, Ask, 3);
                        }
                        tp1Hit = true;
                    }
                }
            }
        }
    }
}

// =========================================================================
// CIRCUIT BREAKER & UTILITIES
// =========================================================================
void CheckCircuitBreakers()
{
    double equity = AccountEquity();
    hwmEquity = MathMax(hwmEquity, equity);
    
    int today = Day();
    if(today != currentDay)
    {
        startOfDayEquity = equity;
        currentDay = today;
        systemHalted = false; // Reset daily
    }
    
    double dailyDD = (equity - startOfDayEquity) / startOfDayEquity;
    double peakDD = (equity - hwmEquity) / hwmEquity;
    
    if(dailyDD <= -(InpMaxDailyDD / 100.0) || peakDD <= -(InpHwmDD / 100.0))
    {
        systemHalted = true;
        LiquidateAll();
    }
}

void LiquidateAll()
{
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
        if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
            if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
            {
                if(OrderType() == OP_BUY) OrderClose(OrderTicket(), OrderLots(), Bid, 3);
                else if(OrderType() == OP_SELL) OrderClose(OrderTicket(), OrderLots(), Ask, 3);
            }
        }
    }
}

int CountOpenOrders()
{
    int count = 0;
    for(int i = 0; i < OrdersTotal(); i++)
    {
        if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
            if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber) count++;
        }
    }
    return count;
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

Moving this algorithmic architecture into cTrader allows you to leverage native C# and the .NET framework. Because cTrader is fundamentally position-based (like MQL5) rather than order-based (like MQL4), handling partial scale-outs is significantly cleaner. Furthermore, we can utilize LINQ to parse the history pool elegantly, and cache our Kelly calculations to preserve CPU cycles on the OnTick thread.

Here is the complete cBot utilizing professional C# structural patterns.

Code: Select all

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

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class InstKellyEngineScalper : Robot
    {
        // =========================================================================
        // 1. INPUT PARAMETERS
        // =========================================================================
        [Parameter("Bootstrap Win Rate (%)", DefaultValue = 50.0, MinValue = 1.0, MaxValue = 99.0, Group = "Dynamic Edge (Kelly)")]
        public double InpBootstrapWinRate { get; set; }

        [Parameter("Bootstrap Reward/Risk", DefaultValue = 2.0, MinValue = 0.1, Group = "Dynamic Edge (Kelly)")]
        public double InpBootstrapRR { get; set; }

        [Parameter("Kelly Fraction (0.5 = Half)", DefaultValue = 0.5, MinValue = 0.1, MaxValue = 1.0, Group = "Dynamic Edge (Kelly)")]
        public double InpKellyFraction { get; set; }

        [Parameter("Rolling Trade Window", DefaultValue = 100, MinValue = 10, Group = "Dynamic Edge (Kelly)")]
        public int InpLookback { get; set; }

        [Parameter("ATR Period", DefaultValue = 14, Group = "Risk & Execution Limits")]
        public int InpAtrPeriod { get; set; }

        [Parameter("ATR Stop Multiplier", DefaultValue = 1.5, Group = "Risk & Execution Limits")]
        public double InpAtrMultiplier { get; set; }

        [Parameter("Max Risk per Trade (%)", DefaultValue = 5.0, MaxValue = 20.0, Group = "Risk & Execution Limits")]
        public double InpMaxRisk { get; set; }

        [Parameter("Max Daily Drawdown (%)", DefaultValue = 3.0, MinValue = 0.5, MaxValue = 10.0, Group = "Circuit Breakers")]
        public double InpMaxDailyDD { get; set; }

        [Parameter("Max Peak-to-Trough DD (%)", DefaultValue = 15.0, MinValue = 5.0, MaxValue = 50.0, Group = "Circuit Breakers")]
        public double InpHwmDD { get; set; }

        // =========================================================================
        // 2. GLOBAL STATE VARIABLES
        // =========================================================================
        private readonly string _label = "InstKellyEngine";
        
        // Indicators
        private SimpleMovingAverage _fastSma;
        private SimpleMovingAverage _slowSma;
        private AverageTrueRange _atr;

        // Circuit Breaker State
        private double _hwmEquity;
        private double _startOfDayEquity;
        private int _currentDayOfYear;
        private bool _systemHalted;

        // Trade Management State
        private bool _tp1Hit;
        private double _tp1Price;
        
        // Optimization Cache
        private int _lastHistoryCount = -1;
        private double _cachedAppliedRisk = 0;
        private double _cachedDynRR = 2.0;

        // =========================================================================
        // INITIALIZATION
        // =========================================================================
        protected override void OnStart()
        {
            _fastSma = Indicators.SimpleMovingAverage(Bars.ClosePrices, 10);
            _slowSma = Indicators.SimpleMovingAverage(Bars.ClosePrices, 20);
            _atr = Indicators.AverageTrueRange(InpAtrPeriod, MovingAverageType.Simple);

            _hwmEquity = Account.Equity;
            _startOfDayEquity = _hwmEquity;
            _currentDayOfYear = Server.Time.DayOfYear;
            _cachedDynRR = InpBootstrapRR;
        }

        // =========================================================================
        // CORE EVENT LOOP
        // =========================================================================
        protected override void OnTick()
        {
            CheckCircuitBreakers();
            if (_systemHalted) return;

            ManageOpenPositions();

            // Entry Logic (Only execute if flat)
            var openPositions = Positions.FindAll(_label, SymbolName);
            if (openPositions.Length == 0)
            {
                _tp1Hit = false;

                bool isLongTrigger = _fastSma.Result.Last(1) <= _slowSma.Result.Last(1) && _fastSma.Result.Last(0) > _slowSma.Result.Last(0);
                bool isShortTrigger = _fastSma.Result.Last(1) >= _slowSma.Result.Last(1) && _fastSma.Result.Last(0) < _slowSma.Result.Last(0);

                if (isLongTrigger || isShortTrigger)
                {
                    double stopDistPips = (_atr.Result.Last(0) * InpAtrMultiplier) / Symbol.PipSize;
                    double volume = CalculatePositionVolume(stopDistPips);

                    if (volume > 0)
                    {
                        var tradeType = isLongTrigger ? TradeType.Buy : TradeType.Sell;
                        ExecuteTrade(tradeType, volume, stopDistPips);
                    }
                }
            }
        }

        // =========================================================================
        // DYNAMIC KELLY & VOLUME SIZING (Optimized via LINQ)
        // =========================================================================
        private double CalculatePositionVolume(double stopLossPips)
        {
            // Only recalculate edge if a trade has closed (saves CPU on OnTick)
            if (History.Count != _lastHistoryCount)
            {
                double dynWinRate = InpBootstrapWinRate / 100.0;
                double dynRR = InpBootstrapRR;

                // Extract history natively utilizing LINQ
                var recentTrades = History
                    .Where(t => t.SymbolName == SymbolName && t.Label == _label)
                    .OrderByDescending(t => t.ClosingTime)
                    .Take(InpLookback)
                    .ToList();

                if (recentTrades.Any())
                {
                    double wins = recentTrades.Count(t => t.NetProfit > 0);
                    double losses = recentTrades.Count(t => t.NetProfit < 0);
                    
                    dynWinRate = wins / recentTrades.Count;

                    double avgWin = wins > 0 ? recentTrades.Where(t => t.NetProfit > 0).Average(t => t.NetProfit) : 0;
                    double avgLoss = losses > 0 ? Math.Abs(recentTrades.Where(t => t.NetProfit < 0).Average(t => t.NetProfit)) : 0;
                    
                    if (avgLoss > 0) dynRR = avgWin / avgLoss;
                }

                // Kelly Math
                double fullKelly = dynWinRate - ((1.0 - dynWinRate) / dynRR);
                double fracKelly = Math.Max(0.0, fullKelly * InpKellyFraction);
                
                _cachedAppliedRisk = Math.Min(InpMaxRisk / 100.0, fracKelly);
                _cachedDynRR = dynRR;
                _lastHistoryCount = History.Count;
            }

            if (_cachedAppliedRisk <= 0) return 0;

            // Translate Risk $ into Asset Volume
            double riskAmount = Account.Equity * _cachedAppliedRisk;
            double exactRiskPerUnit = stopLossPips * Symbol.PipValue;
            
            if (exactRiskPerUnit == 0) return 0;

            double rawVolume = riskAmount / exactRiskPerUnit;
            
            // cTrader best-practice: always normalize volume to broker step increments
            return Symbol.NormalizeVolumeInUnits(rawVolume, RoundingMode.Down);
        }

        // =========================================================================
        // EXECUTION & DYNAMIC MANAGEMENT
        // =========================================================================
        private void ExecuteTrade(TradeType type, double volume, double slPips)
        {
            // We use ExecuteMarketOrder to place the trade, and calculate absolute targets based on dynamic R/R
            var result = ExecuteMarketOrder(type, SymbolName, volume, _label, slPips, null);
            
            if (result.IsSuccessful)
            {
                double absoluteStopDist = slPips * Symbol.PipSize;
                _tp1Price = type == TradeType.Buy 
                    ? result.Position.EntryPrice + absoluteStopDist 
                    : result.Position.EntryPrice - absoluteStopDist;
            }
        }

        private void ManageOpenPositions()
        {
            var positions = Positions.FindAll(_label, SymbolName);
            if (positions.Length > 0 && !_tp1Hit)
            {
                var pos = positions[0];
                double currentPrice = pos.TradeType == TradeType.Buy ? Symbol.Bid : Symbol.Ask;
                
                bool hitTarget = pos.TradeType == TradeType.Buy 
                    ? currentPrice >= _tp1Price 
                    : currentPrice <= _tp1Price;

                if (hitTarget)
                {
                    // 1. Calculate Half Volume & Normalize
                    double halfVolume = Symbol.NormalizeVolumeInUnits(pos.VolumeInUnits / 2.0, RoundingMode.Down);
                    
                    if (halfVolume >= Symbol.VolumeInUnitsMin)
                    {
                        // Partially close the position. (cTrader handles this without changing the Position ID)
                        ClosePosition(pos, halfVolume);
                    }

                    // 2. Move Stop Loss to Break-Even (Entry Price)
                    ModifyPosition(pos, pos.EntryPrice, pos.TakeProfit);
                    _tp1Hit = true;
                }
            }
        }

        // =========================================================================
        // CIRCUIT BREAKER UTILITY
        // =========================================================================
        private void CheckCircuitBreakers()
        {
            double equity = Account.Equity;
            _hwmEquity = Math.Max(_hwmEquity, equity);

            // Detect new trading day
            if (Server.Time.DayOfYear != _currentDayOfYear)
            {
                _startOfDayEquity = equity;
                _currentDayOfYear = Server.Time.DayOfYear;
                _systemHalted = false; 
            }

            double dailyDD = (equity - _startOfDayEquity) / _startOfDayEquity;
            double peakDD = (equity - _hwmEquity) / _hwmEquity;

            if (dailyDD <= -(InpMaxDailyDD / 100.0) || peakDD <= -(InpHwmDD / 100.0))
            {
                _systemHalted = true;
                LiquidateAll();
            }
        }

        private void LiquidateAll()
        {
            foreach (var pos in Positions.FindAll(_label, SymbolName))
            {
                ClosePosition(pos);
            }
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

Architectural Upgrades over MQL

LINQ History Parsing: In MQL, history loops require fragile index counters and raw ticket fetching. By using LINQ History.Where(...).Take(InpLookback), we extract the Win Rate and the precise NetProfit cleanly.

State Caching for OnTick Efficiency: C# allows us to cache our Kelly parameters efficiently. Since OnTick() fires potentially hundreds of times per second, the algorithm actively monitors History.Count != _lastHistoryCount. It will only perform the LINQ calculation when a trade actually closes, freeing up CPU overhead to focus strictly on spread monitoring and scale-out execution.

Clean Partial Closes: Note how ManageOpenPositions() handles the scale-out. In MQL4, closing half the volume spawns a new ticket ID that breaks tracking loops. In cTrader, ClosePosition(pos, halfVolume) leaves the Position object intact. You just issue a subsequent ModifyPosition(pos, pos.EntryPrice, pos.TakeProfit) seamlessly on the same reference.

Native Volume Normalization: The CalculatePositionVolume() method utilizes Symbol.NormalizeVolumeInUnits(rawVolume, RoundingMode.Down). This uses the broker's underlying API to guarantee your calculated lot size exactly matches the micro-lot requirements of your specific account tier, avoiding "Invalid Volume" execution errors.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

To execute a single algorithm across multiple symbols simultaneously, you must discard the procedural programming model and embrace Object-Oriented Encapsulation.

If you attempt to run multiple symbols using global variables, a partial scale-out on EURUSD will overwrite the state variables for GBPUSD, causing the system to crash or mismanage orders.

The professional C# architectural solution is the "Engine Wrapper" pattern. You build a nested SymbolEngine class that acts as an independent mini-robot. It holds its own specific symbol data, its own indicators, and its own Kelly state caching. The main cBot simply acts as a master controller that feeds inputs, manages the global equity circuit breaker, and loops through your individual symbol engines.

Here is the complete, multi-symbol architecture.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

Ctrader multicurency version 1.00

Code: Select all

using System;
using System.Linq;
using System.Collections.Generic;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class InstKellyMultiScalper : Robot
    {
        // =========================================================================
        // 1. MULTI-SYMBOL ROUTING INPUT
        // =========================================================================
        [Parameter("Target Symbols (Comma Separated)", DefaultValue = "EURUSD,GBPUSD", Group = "Multi-Symbol Routing")]
        public string InpSymbols { get; set; }

        // =========================================================================
        // 2. INPUT PARAMETERS (Passed to Engines)
        // =========================================================================
        [Parameter("Bootstrap Win Rate (%)", DefaultValue = 50.0, MinValue = 1.0, MaxValue = 99.0, Group = "Dynamic Edge (Kelly)")]
        public double InpBootstrapWinRate { get; set; }

        [Parameter("Bootstrap Reward/Risk", DefaultValue = 2.0, MinValue = 0.1, Group = "Dynamic Edge (Kelly)")]
        public double InpBootstrapRR { get; set; }

        [Parameter("Kelly Fraction (0.5 = Half)", DefaultValue = 0.5, MinValue = 0.1, MaxValue = 1.0, Group = "Dynamic Edge (Kelly)")]
        public double InpKellyFraction { get; set; }

        [Parameter("Rolling Trade Window", DefaultValue = 100, MinValue = 10, Group = "Dynamic Edge (Kelly)")]
        public int InpLookback { get; set; }

        [Parameter("ATR Period", DefaultValue = 14, Group = "Risk & Execution Limits")]
        public int InpAtrPeriod { get; set; }

        [Parameter("ATR Stop Multiplier", DefaultValue = 1.5, Group = "Risk & Execution Limits")]
        public double InpAtrMultiplier { get; set; }

        [Parameter("Max Risk per Trade (%)", DefaultValue = 5.0, MaxValue = 20.0, Group = "Risk & Execution Limits")]
        public double InpMaxRisk { get; set; }

        [Parameter("Max Daily Drawdown (%)", DefaultValue = 3.0, MinValue = 0.5, MaxValue = 10.0, Group = "Circuit Breakers")]
        public double InpMaxDailyDD { get; set; }

        [Parameter("Max Peak-to-Trough DD (%)", DefaultValue = 15.0, MinValue = 5.0, MaxValue = 50.0, Group = "Circuit Breakers")]
        public double InpHwmDD { get; set; }

        // =========================================================================
        // 3. GLOBAL MASTER STATE
        // =========================================================================
        private List<SymbolEngine> _engines = new List<SymbolEngine>();
        
        private double _hwmEquity;
        private double _startOfDayEquity;
        private int _currentDayOfYear;
        
        public bool SystemHalted { get; private set; } // Read-only access for engines

        protected override void OnStart()
        {
            _hwmEquity = Account.Equity;
            _startOfDayEquity = _hwmEquity;
            _currentDayOfYear = Server.Time.DayOfYear;

            // Instantiate an independent engine for each requested symbol
            string[] symbols = InpSymbols.Split(',').Select(s => s.Trim()).ToArray();
            foreach (string symName in symbols)
            {
                _engines.Add(new SymbolEngine(this, symName));
                Print($"[System] Initialized Engine for {symName}");
            }
        }

        protected override void OnTick()
        {
            // 1. Evaluate the Global Circuit Breaker First
            CheckCircuitBreakers();

            // 2. Delegate execution to all independent engines
            foreach (var engine in _engines)
            {
                engine.Evaluate();
            }
        }

        private void CheckCircuitBreakers()
        {
            double equity = Account.Equity;
            _hwmEquity = Math.Max(_hwmEquity, equity);

            if (Server.Time.DayOfYear != _currentDayOfYear)
            {
                _startOfDayEquity = equity;
                _currentDayOfYear = Server.Time.DayOfYear;
                SystemHalted = false; 
            }

            double dailyDD = (equity - _startOfDayEquity) / _startOfDayEquity;
            double peakDD = (equity - _hwmEquity) / _hwmEquity;

            if (dailyDD <= -(InpMaxDailyDD / 100.0) || peakDD <= -(InpHwmDD / 100.0))
            {
                SystemHalted = true;
                foreach (var pos in Positions) ClosePosition(pos);
            }
        }
    }

    // =========================================================================
    // 4. THE ENCAPSULATED SYMBOL ENGINE
    // =========================================================================
    public class SymbolEngine
    {
        private readonly InstKellyMultiScalper _algo;
        private readonly Symbol _symbol;
        private readonly Bars _bars;
        private readonly string _label = "InstKellyMulti";

        // Isolated Indicators
        private readonly SimpleMovingAverage _fastSma;
        private readonly SimpleMovingAverage _slowSma;
        private readonly AverageTrueRange _atr;

        // Isolated State Tracking
        private bool _tp1Hit;
        private double _tp1Price;
        private int _lastHistoryCount = -1;
        private double _cachedAppliedRisk = 0;
        private double _cachedDynRR = 2.0;

        public SymbolEngine(InstKellyMultiScalper algo, string symbolName)
        {
            _algo = algo;
            
            // Request the specific market data for this isolated symbol
            _symbol = _algo.Symbols.GetSymbol(symbolName);
            _bars = _algo.MarketData.GetBars(_algo.TimeFrame, symbolName);

            // Bind indicators strictly to this symbol's data series
            _fastSma = _algo.Indicators.SimpleMovingAverage(_bars.ClosePrices, 10);
            _slowSma = _algo.Indicators.SimpleMovingAverage(_bars.ClosePrices, 20);
            _atr = _algo.Indicators.AverageTrueRange(_bars, _algo.InpAtrPeriod, MovingAverageType.Simple);

            _cachedDynRR = _algo.InpBootstrapRR;
        }

        // Called continually by the main Robot's OnTick event
        public void Evaluate()
        {
            if (_algo.SystemHalted) return;

            ManageOpenPositions();

            // Find positions specifically for this engine's symbol
            var openPositions = _algo.Positions.FindAll(_label, _symbol.Name);
            if (openPositions.Length == 0)
            {
                _tp1Hit = false;

                bool isLongTrigger = _fastSma.Result.Last(1) <= _slowSma.Result.Last(1) && _fastSma.Result.Last(0) > _slowSma.Result.Last(0);
                bool isShortTrigger = _fastSma.Result.Last(1) >= _slowSma.Result.Last(1) && _fastSma.Result.Last(0) < _slowSma.Result.Last(0);

                if (isLongTrigger || isShortTrigger)
                {
                    double stopDistPips = (_atr.Result.Last(0) * _algo.InpAtrMultiplier) / _symbol.PipSize;
                    double volume = CalculatePositionVolume(stopDistPips);

                    if (volume > 0)
                    {
                        var tradeType = isLongTrigger ? TradeType.Buy : TradeType.Sell;
                        ExecuteTrade(tradeType, volume, stopDistPips);
                    }
                }
            }
        }

        private double CalculatePositionVolume(double stopLossPips)
        {
            if (_algo.History.Count != _lastHistoryCount)
            {
                double dynWinRate = _algo.InpBootstrapWinRate / 100.0;
                double dynRR = _algo.InpBootstrapRR;

                // Natively filter history for this exact symbol engine
                var recentTrades = _algo.History
                    .Where(t => t.SymbolName == _symbol.Name && t.Label == _label)
                    .OrderByDescending(t => t.ClosingTime)
                    .Take(_algo.InpLookback)
                    .ToList();

                if (recentTrades.Any())
                {
                    double wins = recentTrades.Count(t => t.NetProfit > 0);
                    double losses = recentTrades.Count(t => t.NetProfit < 0);
                    
                    dynWinRate = wins / recentTrades.Count;
                    double avgWin = wins > 0 ? recentTrades.Where(t => t.NetProfit > 0).Average(t => t.NetProfit) : 0;
                    double avgLoss = losses > 0 ? Math.Abs(recentTrades.Where(t => t.NetProfit < 0).Average(t => t.NetProfit)) : 0;
                    
                    if (avgLoss > 0) dynRR = avgWin / avgLoss;
                }

                double fullKelly = dynWinRate - ((1.0 - dynWinRate) / dynRR);
                double fracKelly = Math.Max(0.0, fullKelly * _algo.InpKellyFraction);
                
                _cachedAppliedRisk = Math.Min(_algo.InpMaxRisk / 100.0, fracKelly);
                _cachedDynRR = dynRR;
                _lastHistoryCount = _algo.History.Count;
            }

            if (_cachedAppliedRisk <= 0) return 0;

            double riskAmount = _algo.Account.Equity * _cachedAppliedRisk;
            double exactRiskPerUnit = stopLossPips * _symbol.PipValue;
            
            if (exactRiskPerUnit == 0) return 0;

            double rawVolume = riskAmount / exactRiskPerUnit;
            return _symbol.NormalizeVolumeInUnits(rawVolume, RoundingMode.Down);
        }

        private void ExecuteTrade(TradeType type, double volume, double slPips)
        {
            var result = _algo.ExecuteMarketOrder(type, _symbol.Name, volume, _label, slPips, null);
            
            if (result.IsSuccessful)
            {
                double absoluteStopDist = slPips * _symbol.PipSize;
                _tp1Price = type == TradeType.Buy 
                    ? result.Position.EntryPrice + absoluteStopDist 
                    : result.Position.EntryPrice - absoluteStopDist;
            }
        }

        private void ManageOpenPositions()
        {
            var positions = _algo.Positions.FindAll(_label, _symbol.Name);
            if (positions.Length > 0 && !_tp1Hit)
            {
                var pos = positions[0];
                double currentPrice = pos.TradeType == TradeType.Buy ? _symbol.Bid : _symbol.Ask;
                
                bool hitTarget = pos.TradeType == TradeType.Buy 
                    ? currentPrice >= _tp1Price 
                    : currentPrice <= _tp1Price;

                if (hitTarget)
                {
                    double halfVolume = _symbol.NormalizeVolumeInUnits(pos.VolumeInUnits / 2.0, RoundingMode.Down);
                    
                    if (halfVolume >= _symbol.VolumeInUnitsMin)
                    {
                        _algo.ClosePosition(pos, halfVolume);
                    }
                    _algo.ModifyPosition(pos, pos.EntryPrice, pos.TakeProfit);
                    _tp1Hit = true;
                }
            }
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

The Architectural Blueprint

Indicator Decoupling: In the SymbolEngine constructor, notice that _algo.Indicators.SimpleMovingAverage is passed _bars.ClosePrices, not Bars.ClosePrices. Bars is a global property of the chart you attach the cBot to; _bars is dynamically fetched via MarketData.GetBars(), ensuring EURUSD indicator calculations are completely sealed off from GBPUSD price action.

Synchronized Execution Array: The master OnTick() loop calls engine.Evaluate() in sequence. Because cTrader is single-threaded, if a massive EURUSD liquidity sweep occurs at the exact millisecond a GBPUSD news event drops, the List<SymbolEngine> evaluates them sequentially. You completely avoid asynchronous memory-locking errors (Race Conditions) that plague poorly designed multi-currency bots.

Segmented Kelly Optimization: The LINQ query filters history using Where(t => t.SymbolName == _symbol.Name). This means if EURUSD hits a brutal losing streak, the EURUSD engine automatically throttles its position size down to fractional safety levels, while the GBPUSD engine (if performing well) is allowed to independently scale its size up using the exact same equity pool.

Operational Note: In cTrader, OnTick() fires exclusively when the chart symbol you attach the cBot to receives a tick. When trading highly correlated majors (like EURUSD/GBPUSD), attaching the bot to either chart works perfectly as they tick almost simultaneously.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

To implement a global portfolio cap, you centralize the exposure state in the Master Robot and expose it as a public read-only property. Because the SymbolEngine instances already hold a reference to the Master _algo object, they can query this global state in real-time before executing a trade.

Because cTrader operates on a single thread per cBot instance, the OnTick() loop evaluates each engine sequentially. This inherently prevents race conditions—if EURUSD executes a trade, the global position count instantly increments before the GBPUSD engine is evaluated milliseconds later.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

Here is the architectural update.

1. Update the Master Robot (Centralized State)

Add the new input parameter to your InstKellyMultiScalper class, and expose a TotalOpenPositions property that counts every active trade tagged with your algorithm's label, regardless of symbol.

Code: Select all

// Add this to your Parameters section
[Parameter("Max Global Open Trades", DefaultValue = 2, MinValue = 1, Group = "Portfolio Risk Limits")]
public int InpMaxGlobalPositions { get; set; }

// Add this below your SystemHalted property
public int TotalOpenPositions => Positions.FindAll("InstKellyMulti").Length;
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Post Reply