Page 2 of 3

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:46 am
by FTtrader
Pine Script handles state natively on historical bars. In MetaTrader, we are operating an event-driven loop (OnTick).

To enforce your "No Negotiation" rules (Partials, Break-Even, and Time Stops) reliably on a local terminal, we handle the execution differently across the two platforms due to their distinct architectures.

1. The MT4 Base Version (MQL4)

MT4 is strictly an order-based system. The cleanest algorithmic way to manage partials in MT4 without losing track of state after a terminal restart is the Split-Ticket Execution. We send two separate orders simultaneously: one assigned to the Partial, one to the Runner. The broker handles the partial take-profit automatically, and our EA simply listens for the partial to close before moving the runner to Break-Even.

Code: Select all

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

input double InpLotSize       = 0.1;      // Total Position Size (Split in half)
input double InpPartialR      = 1.0;      // Partial Target (R-Multiple)
input double InpRunnerR       = 3.0;      // Runner Target (R-Multiple)
input int    InpTimeStopBars  = 12;       // Time Stop in Bars
input int    BaseMagic        = 10000;    // Magic Number Base

int MAGIC_PARTIAL = BaseMagic + 1;
int MAGIC_RUNNER  = BaseMagic + 2;

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    // 1. Check if we have open trades
    int partialTickets = 0;
    int runnerTickets = 0;
    
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
        if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES) && OrderSymbol() == _Symbol)
        {
            if(OrderMagicNumber() == MAGIC_PARTIAL) partialTickets++;
            if(OrderMagicNumber() == MAGIC_RUNNER)  runnerTickets++;
        }
    }
    
    bool hasActiveTrades = (partialTickets > 0 || runnerTickets > 0);

    // 2. ENTRY LOGIC (Placeholder for your PA setup)
    if(!hasActiveTrades)
    {
        // Simple Bullish Engulfing for demonstration
        bool isBullish = Close[1] > Open[1] && Close[2] < Open[2] && Close[1] > Open[2];
        
        if(isBullish)
        {
            double sl = Low[iLowest(_Symbol, _Period, MODE_LOW, 3, 1)];
            double risk = Ask - sl;
            
            double tpPartial = Ask + (risk * InpPartialR);
            double tpRunner  = Ask + (risk * InpRunnerR);
            
            double splitLot = NormalizeDouble(InpLotSize / 2.0, 2);
            
            // Execute Split Tickets
            OrderSend(_Symbol, OP_BUY, splitLot, Ask, 3, sl, tpPartial, "Partial", MAGIC_PARTIAL);
            OrderSend(_Symbol, OP_BUY, splitLot, Ask, 3, sl, tpRunner,  "Runner",  MAGIC_RUNNER);
        }
    }
    
    // 3. TRADE MANAGEMENT ENGINE
    if(hasActiveTrades)
    {
        for(int i = OrdersTotal() - 1; i >= 0; i--)
        {
            if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES) && OrderSymbol() == _Symbol)
            {
                int barsInTrade = iBarShift(_Symbol, _Period, OrderOpenTime());
                
                // RULE 1: TIME STOP (Momentum died, flatten everything)
                if(barsInTrade >= InpTimeStopBars && partialTickets > 0)
                {
                    bool closed = OrderClose(OrderTicket(), OrderLots(), OrderType() == OP_BUY ? Bid : Ask, 3, clrRed);
                    Print("Time Stop Triggered. Closing trade.");
                    continue; 
                }
                
                // RULE 2: MOVE TO BREAK-EVEN
                // If the Partial ticket is gone (TP hit) but Runner is open, move SL to Entry
                if(partialTickets == 0 && runnerTickets > 0 && OrderMagicNumber() == MAGIC_RUNNER)
                {
                    // Check if SL is not already at Break-Even
                    if(OrderStopLoss() != OrderOpenPrice())
                    {
                        bool modified = OrderModify(OrderTicket(), OrderOpenPrice(), OrderOpenPrice(), OrderTakeProfit(), 0, clrBlue);
                        if(modified) Print("Partial hit. Runner moved to Break-Even.");
                    }
                }
            }
        }
    }
}

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:47 am
by FTtrader
2. The MT5 Base Version (MQL5)

MT5 handles positions differently (especially in netting accounts, but standard for modern scalping is hedging). Instead of splitting tickets, we can programmatically slice a single position using the standard <Trade\Trade.mqh> library.

This script tracks the state in memory, calculates the real-time R-multiple, and slices the position by InpPartialPct when the target is reached, instantly modifying the remaining volume to Break-Even.

Code: Select all

//+------------------------------------------------------------------+
//|                                        PA_Engine_Base_MT5.mq5    |
//+------------------------------------------------------------------+
#property strict
#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>

input double InpLotSize       = 0.2;      // Position Size
input double InpPartialPct    = 50.0;     // Percentage to close at partial (%)
input double InpPartialR      = 1.0;      // Partial Target (R-Multiple)
input double InpRunnerR       = 3.0;      // Runner Target (R-Multiple)
input int    InpTimeStopBars  = 12;       // Time Stop in Bars
input ulong  BaseMagic        = 20000;    // Magic Number

CTrade         trade;
CPositionInfo  posInfo;

// In-memory state tracking
ulong    activeTicket  = 0;
bool     partialHit    = false;
double   entryPrice    = 0;
double   initialSL     = 0;
double   partialPrice  = 0;
datetime entryTime     = 0;

int OnInit()
{
    trade.SetExpertMagicNumber(BaseMagic);
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    bool hasPosition = PositionSelectByTicket(activeTicket);

    // 1. ENTRY LOGIC
    if(!hasPosition)
    {
        // Reset state
        activeTicket = 0;
        partialHit = false;

        // Simple setup logic placeholder
        double close1 = iClose(_Symbol, _Period, 1);
        double open1  = iOpen(_Symbol, _Period, 1);
        double close2 = iClose(_Symbol, _Period, 2);
        double open2  = iOpen(_Symbol, _Period, 2);
        
        bool isBullish = close1 > open1 && close2 < open2 && close1 > open2;
        
        if(isBullish)
        {
            double sl = iLow(_Symbol, _Period, iLowest(_Symbol, _Period, MODE_LOW, 3, 1));
            double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
            double risk = ask - sl;
            
            double tp = ask + (risk * InpRunnerR); // TP set to runner distance
            
            if(trade.Buy(InpLotSize, _Symbol, ask, sl, tp, "PA Entry"))
            {
                activeTicket = trade.ResultOrder();
                entryPrice   = ask;
                initialSL    = sl;
                partialPrice = ask + (risk * InpPartialR);
                entryTime    = TimeCurrent();
                partialHit   = false;
            }
        }
    }

    // 2. TRADE MANAGEMENT ENGINE
    if(hasPosition)
    {
        double currentPrice = PositionGetDouble(POSITION_PRICE_CURRENT);
        long posType = PositionGetInteger(POSITION_TYPE);
        
        // Calculate bars passed
        int barsInTrade = 0;
        datetime timeArr[];
        if(CopyTime(_Symbol, _Period, entryTime, TimeCurrent(), timeArr) > 0)
            barsInTrade = ArraySize(timeArr);

        // RULE 1: TIME STOP (If partial not hit)
        if(!partialHit && barsInTrade >= InpTimeStopBars)
        {
            trade.PositionClose(activeTicket);
            Print("Time Stop Triggered.");
            return;
        }

        // RULE 2: PARTIAL & BREAK-EVEN
        if(!partialHit)
        {
            bool triggerPartial = (posType == POSITION_TYPE_BUY && currentPrice >= partialPrice) || 
                                  (posType == POSITION_TYPE_SELL && currentPrice <= partialPrice);
                                  
            if(triggerPartial)
            {
                double currentVol = PositionGetDouble(POSITION_VOLUME);
                double closeVol = NormalizeDouble(currentVol * (InpPartialPct / 100.0), 2);
                
                // Close the partial amount
                if(trade.PositionClosePartial(activeTicket, closeVol))
                {
                    partialHit = true;
                    
                    // Move remaining runner to Break-Even
                    double newSL = entryPrice; // You can add buffer here (e.g. entryPrice + spread)
                    double currentTP = PositionGetDouble(POSITION_TP);
                    
                    trade.PositionModify(activeTicket, newSL, currentTP);
                    Print("Partial Triggered. Runner moved to Break-Even.");
                }
            }
        }
    }
}

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:47 am
by FTtrader
Key Differences from Pine Script:

No historical retro-calculation: Unlike Pine Script which evaluates every bar instantly to draw chart data, these EAs operate strictly in the present moment on incoming ticks.

MT4 "Split" vs MT5 "Slice": MQL4 doesn't support partial position closing cleanly, hence sending two distinct orders is the most reliable way to enforce your framework. MT5 supports PositionClosePartial(), keeping your trade journal cleaner by treating it as a single execution that scales out.

Journal Logging: In these versions, simply checking your terminal's "Account History" will automatically segment the trades. In MT4, you can filter by the Magic Numbers (Partial vs. Runner) to see exactly how much money the runner strategy is pulling compared to the structural partials.

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:49 am
by FTtrader
Transitioning the Finite State Machine (FSM), dynamic risk sizing, cost-aware execution, and structural trailing from Pine Script to native MQL4/MQL5 requires an architectural shift.

In Pine Script, state is evaluated historically on every bar. In MetaTrader EAs, we are building an event-driven state machine that operates on live ticks.

To make these "pro," we must implement:

Dynamic Volumetric Sizing: Calculating exact lot sizes based on Account Balance, Risk %, and the tick-distance to the stop loss.

State Inference & Recovery: If your terminal reboots, boolean variables reset. A professional EA infers its FSM state directly from the broker's active order pool or position volumes.

Cost-Aware Break-Even: Factoring in the spread/commission buffer into the break-even math.

Structural Trailing: Using iHighest/iLowest arrays to dynamically trail the runner strictly based on market structure.

Here are the enterprise-grade architectures for both platforms.

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:49 am
by FTtrader
1. The Pro MT4 Engine (MQL4)

MT4 is an order-centric system. The most resilient way to handle a strict FSM in MT4 without complex local file storage is Split-Ticket State Inference. We assign two different Magic Numbers. The EA scans the live order pool: if both are open, we are in STATE_RISK. If only the runner is open, the broker has executed the partial, and we transition to STATE_RUNNER.

Code: Select all

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

// --- FSM & EXECUTION SETTINGS ---
input double InpRiskPct       = 1.0;      // Risk Per Trade (%)
input int    InpBEBufferPts   = 20;       // BE Buffer (Points/Pipettes) to cover cost
input double InpPartialR      = 1.0;      // First Partial (R-Multiple)
input int    InpTrailBars     = 5;        // Trailing Structure Lookback (Bars)
input int    InpTimeStopBars  = 12;       // Time Stop (Bars)

// --- SESSION KILLZONES (Broker Server Time) ---
input int    InpSessionStart  = 8;        // Active Start Hour (e.g., London Open)
input int    InpSessionEnd    = 16;       // Active End Hour (e.g., NY Close)

int MAGIC_PARTIAL = 80001;
int MAGIC_RUNNER  = 80002;

//+------------------------------------------------------------------+
//| DYNAMIC SIZING ENGINE                                            |
//+------------------------------------------------------------------+
double GetDynamicLots(double stopLossDistPoints)
{
    double riskAmount = AccountFreeMargin() * (InpRiskPct / 100.0);
    double tickValue = MarketInfo(_Symbol, MODE_TICKVALUE);
    double tickSize = MarketInfo(_Symbol, MODE_TICKSIZE);
    
    // Normalize distance to ticks
    double distTicks = stopLossDistPoints / tickSize;
    if(distTicks <= 0) return MarketInfo(_Symbol, MODE_MINLOT);
    
    double lots = riskAmount / (distTicks * tickValue);
    
    double minLot = MarketInfo(_Symbol, MODE_MINLOT);
    double maxLot = MarketInfo(_Symbol, MODE_MAXLOT);
    double stepLot= MarketInfo(_Symbol, MODE_LOTSTEP);
    
    lots = MathFloor(lots / stepLot) * stepLot;
    if(lots < minLot) lots = minLot;
    if(lots > maxLot) lots = maxLot;
    
    return lots;
}

//+------------------------------------------------------------------+
//| MAIN FSM EVENT LOOP                                              |
//+------------------------------------------------------------------+
void OnTick()
{
    int partialTicket = -1, runnerTicket = -1;
    double runnerSL = 0, runnerOpen = 0, partialTP = 0;
    int type = -1;
    datetime openTime = 0;

    // 1. STATE INFERENCE (Query Broker)
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
        if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES) && OrderSymbol() == _Symbol)
        {
            if(OrderMagicNumber() == MAGIC_PARTIAL) partialTicket = OrderTicket();
            if(OrderMagicNumber() == MAGIC_RUNNER) 
            {
                runnerTicket = OrderTicket();
                runnerSL = OrderStopLoss();
                runnerOpen = OrderOpenPrice();
                openTime = OrderOpenTime();
                type = OrderType();
            }
        }
    }
    
    int state = 0; // STATE_FLAT
    if(partialTicket != -1 && runnerTicket != -1) state = 1; // STATE_RISK
    if(partialTicket == -1 && runnerTicket != -1) state = 2; // STATE_RUNNER

    // 2. ENTRY CONTROLLER (Killzones & Setup)
    if(state == 0)
    {
        if(Hour() >= InpSessionStart && Hour() < InpSessionEnd)
        {
            // PRO SETUP: Liquidity Sweep Check (Sweep of 5-bar low, close above)
            int lowestBar = iLowest(_Symbol, _Period, MODE_LOW, 5, 2);
            double swingLow = iLow(_Symbol, _Period, lowestBar);
            bool bullishSweep = (iLow(_Symbol, _Period, 1) < swingLow && iClose(_Symbol, _Period, 1) > swingLow);
            
            if(bullishSweep)
            {
                double sl = swingLow - (InpBEBufferPts * _Point);
                double riskPts = (Ask - sl) / _Point;
                double tp = Ask + (riskPts * InpPartialR * _Point);
                
                double splitLots = GetDynamicLots(riskPts) / 2.0;
                splitLots = MathMax(splitLots, MarketInfo(_Symbol, MODE_MINLOT));

                // Send Split Tickets
                OrderSend(_Symbol, OP_BUY, splitLots, Ask, 3, sl, tp, "Partial", MAGIC_PARTIAL);
                OrderSend(_Symbol, OP_BUY, splitLots, Ask, 3, sl, 0,  "Runner",  MAGIC_RUNNER); // No TP on runner
            }
        }
    }

    // 3. PHASE 1: RISK & TIME STOPS
    if(state == 1)
    {
        int barsInTrade = iBarShift(_Symbol, _Period, openTime);
        if(barsInTrade >= InpTimeStopBars)
        {
            if(OrderSelect(partialTicket, SELECT_BY_TICKET)) OrderClose(OrderTicket(), OrderLots(), OrderType()==OP_BUY?Bid:Ask, 3, clrOrange);
            if(OrderSelect(runnerTicket, SELECT_BY_TICKET))  OrderClose(OrderTicket(), OrderLots(), OrderType()==OP_BUY?Bid:Ask, 3, clrOrange);
            Print("FSM: Time Stop Triggered. Momentum failed.");
        }
    }

    // 4. PHASE 2: COST-AWARE BE & STRUCTURAL TRAILING
    if(state == 2)
    {
        if(OrderSelect(runnerTicket, SELECT_BY_TICKET))
        {
            if(type == OP_BUY)
            {
                // Move to BE + Cost Buffer
                double bePrice = runnerOpen + (InpBEBufferPts * _Point);
                double trailTarget = MathMax(runnerSL, bePrice);
                
                // Calculate Structural Trail
                double structLow = iLow(_Symbol, _Period, iLowest(_Symbol, _Period, MODE_LOW, InpTrailBars, 1));
                double structuralTrail = structLow - (InpBEBufferPts * _Point);
                
                double newSL = MathMax(trailTarget, structuralTrail);
                
                if(newSL > runnerSL + (_Point * 5)) // Prevent micro-modifications
                {
                    OrderModify(runnerTicket, runnerOpen, newSL, 0, 0, clrGreen);
                    Print("FSM: Trailing Runner Stop to: ", newSL);
                }
            }
        }
    }
}

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:49 am
by FTtrader
2. The Pro MT5 Engine (MQL5)

Because MT5 is position-centric, we can build a much cleaner C#-style Object-Oriented structure. MT5 supports native volumetric partial closes (PositionClosePartial). The FSM class tracks our exact entry prices and execution states locally, slicing the position volume natively without needing separate tickets.

Code: Select all

//+------------------------------------------------------------------+
//|                                     PA_FSM_Engine_Pro_MT5.mq5    |
//+------------------------------------------------------------------+
#property strict
#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>

input double InpRiskPct       = 1.0;      // Risk Per Trade (%)
input int    InpBEBufferPts   = 20;       // Cost Buffer (Points)
input double InpPartialR      = 1.0;      // First Partial (R-Multiple)
input double InpPartialPct    = 50.0;     // Partial Size (%)
input int    InpTrailBars     = 5;        // Structural Trail Lookback
input int    InpTimeStopBars  = 12;       // Time Stop (Bars)

input int    InpSessionStart  = 8;        
input int    InpSessionEnd    = 16;       

CTrade         trade;
CPositionInfo  pos;

// --- FSM STRUCT (C#-style Context) ---
struct TradeContext {
    int      state;          // 0 = FLAT, 1 = RISK, 2 = RUNNER
    ulong    ticket;
    double   entryPrice;
    double   targetPrice;
    datetime entryTime;
    double   initialVol;
};
TradeContext ctx;

int OnInit() {
    trade.SetExpertMagicNumber(90001);
    ctx.state = 0;
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| UTILITIES                                                        |
//+------------------------------------------------------------------+
double GetDynamicLots(double slDistPts) {
    double riskAmt = AccountInfoDouble(ACCOUNT_BALANCE) * (InpRiskPct / 100.0);
    double tickVal = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
    double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
    
    double distTicks = slDistPts / tickSize;
    if(distTicks <= 0) return 0;
    
    double lots = riskAmt / (distTicks * tickVal);
    double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
    
    lots = MathFloor(lots / step) * step;
    return MathMax(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN));
}

//+------------------------------------------------------------------+
//| MAIN FSM TICK LOOP                                               |
//+------------------------------------------------------------------+
void OnTick() {
    bool hasPos = PositionSelectByTicket(ctx.ticket);
    
    // Safety check: Did broker close us out?
    if(!hasPos && ctx.state != 0) {
        ctx.state = 0; 
        Print("FSM: Position flat. Resetting engine.");
    }

    // 1. STATE 0: ENTRY CONTROLLER
    if(ctx.state == 0) {
        MqlDateTime dt; TimeCurrent(dt);
        if(dt.hour >= InpSessionStart && dt.hour < InpSessionEnd) {
            
            // Sweep Check Logic
            double lows[]; CopyLow(_Symbol, _Period, 2, 5, lows);
            int minIdx = ArrayMinimum(lows);
            double swingLow = lows[minIdx];
            
            double c1 = iClose(_Symbol, _Period, 1);
            double l1 = iLow(_Symbol, _Period, 1);
            
            if(l1 < swingLow && c1 > swingLow) {
                double sl = swingLow - (InpBEBufferPts * _Point);
                double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
                double riskPts = (ask - sl) / _Point;
                
                double lots = GetDynamicLots(riskPts);
                
                if(trade.Buy(lots, _Symbol, ask, sl, 0, "PA Entry")) {
                    ctx.ticket      = trade.ResultOrder();
                    ctx.entryPrice  = ask;
                    ctx.targetPrice = ask + (riskPts * InpPartialR * _Point);
                    ctx.entryTime   = TimeCurrent();
                    ctx.initialVol  = lots;
                    ctx.state       = 1; // Transition to Phase 1
                }
            }
        }
    }

    // 2. IN-TRADE MANAGEMENT
    if(ctx.state > 0 && pos.SelectByTicket(ctx.ticket)) {
        
        datetime timeArr[];
        int barsHeld = CopyTime(_Symbol, _Period, ctx.entryTime, TimeCurrent(), timeArr);
        
        // --- PHASE 1: RISK EXPOSURE ---
        if(ctx.state == 1) {
            // Time Stop Failsafe
            if(barsHeld >= InpTimeStopBars) {
                trade.PositionClose(ctx.ticket);
                Print("FSM: Momentum faded. Time stop executed.");
                return;
            }
            
            // Partial Target Check
            double currentPrice = pos.PriceCurrent();
            if(pos.PositionType() == POSITION_TYPE_BUY && currentPrice >= ctx.targetPrice) {
                
                double closeVol = NormalizeDouble(ctx.initialVol * (InpPartialPct / 100.0), 2);
                closeVol = MathMax(closeVol, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN));
                
                if(trade.PositionClosePartial(ctx.ticket, closeVol)) {
                    ctx.state = 2; // Transition to Phase 2 (Runner)
                    
                    double beCostSL = ctx.entryPrice + (InpBEBufferPts * _Point);
                    trade.PositionModify(ctx.ticket, beCostSL, 0);
                    Print("FSM: Partial Secured. SL to Cost-Aware BE.");
                }
            }
        }
        
        // --- PHASE 2: STRUCTURAL TRAIL ---
        if(ctx.state == 2) {
            if(pos.PositionType() == POSITION_TYPE_BUY) {
                double lows[]; CopyLow(_Symbol, _Period, 1, InpTrailBars, lows);
                int minIdx = ArrayMinimum(lows);
                double structLow = lows[minIdx];
                
                double trailSL = structLow - (InpBEBufferPts * _Point);
                double currentSL = pos.StopLoss();
                
                // Only trail forward
                if(trailSL > currentSL + (5 * _Point)) {
                    trade.PositionModify(ctx.ticket, trailSL, 0);
                }
            }
        }
    }
}

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:50 am
by FTtrader
Institutional Upgrades Applied Here:

Volumetric Math GetDynamicLots(): Retail scripts use fixed lot sizes (e.g., 0.1). This algorithm queries the MODE_TICKVALUE, normalizes the stop loss points into ticks, and divides it against the exact monetary risk allowed on your account balance.

Cost-Aware Trailing (InpBEBufferPts): "Break-Even" is a myth if you don't account for spread and commissions. The script moves the stop to EntryPrice + Buffer, ensuring your "scratch" trades don't bleed capital via broker fees.

The State Machine Reset: Notice the safety check at the start of the MT5 tick loop. If the broker manually closes your position (or you hit a trailing stop), the FSM detects the ticket is gone, formally closes out STATE_RUNNER, and arms the system for a fresh sweep entry.

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:51 am
by FTtrader
More importantly, cTrader handles partial position closures natively. You don't need the "Split-Ticket" hack from MT4, and the API is much cleaner than MT5's CTrade library.

Here is the base "No Negotiation" PA engine written idiomatically for cTrader.

Code: Select all

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

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class PA_Engine_Base : Robot
    {
        // ==============================================================================
        // 1. INPUTS & CONFIGURATION
        // ==============================================================================
        [Parameter("Total Volume (Units)", Group = "Risk", DefaultValue = 100000)]
        public double TotalVolume { get; set; }

        [Parameter("Partial Target (R)", Group = "Management", DefaultValue = 1.0)]
        public double PartialTargetR { get; set; }

        [Parameter("Runner Target (R)", Group = "Management", DefaultValue = 3.0)]
        public double RunnerTargetR { get; set; }

        [Parameter("Partial Size (%)", Group = "Management", DefaultValue = 50, MinValue = 10, MaxValue = 90)]
        public double PartialPct { get; set; }

        [Parameter("Time Stop (Bars)", Group = "Management", DefaultValue = 12)]
        public int TimeStopBars { get; set; }

        // ==============================================================================
        // 2. IN-MEMORY STATE TRACKING
        // ==============================================================================
        private const string TradeLabel = "PA_Engine";
        private bool _isPartialHit;
        private double _partialTargetPrice;
        private int _entryBarIndex;
        private double _initialVolume;

        // ==============================================================================
        // 3. MAIN EVENT LOOP
        // ==============================================================================
        protected override void OnTick()
        {
            // Find our active position managed by this specific bot
            var position = Positions.Find(TradeLabel, SymbolName);

            // --- ENTRY ENGINE ---
            if (position == null)
            {
                // Reset state on flat
                _isPartialHit = false;

                // Mock Trigger: Simple Bullish Engulfing using LINQ on the Bars collection
                bool isBullish = Bars.ClosePrices.Last(1) > Bars.OpenPrices.Last(1) && 
                                 Bars.ClosePrices.Last(2) < Bars.OpenPrices.Last(2) && 
                                 Bars.ClosePrices.Last(1) > Bars.OpenPrices.Last(2);

                if (isBullish)
                {
                    // Calculate absolute SL price
                    double slPrice = Bars.LowPrices.Minimum(3);
                    double riskDistance = Symbol.Ask - slPrice;
                    
                    // Convert absolute price targets to Pips for cTrader's execution method
                    double slPips = riskDistance / Symbol.PipSize;
                    double runnerTpPips = (riskDistance * RunnerTargetR) / Symbol.PipSize;

                    // Set state variables
                    _partialTargetPrice = Symbol.Ask + (riskDistance * PartialTargetR);
                    _entryBarIndex = Bars.Count;
                    
                    // Normalize volume to broker's allowed step limits safely
                    _initialVolume = Symbol.NormalizeVolumeInUnits(TotalVolume, RoundingMode.Down);

                    // Execute
                    ExecuteMarketOrder(TradeType.Buy, SymbolName, _initialVolume, TradeLabel, slPips, runnerTpPips);
                }
            }

            // --- MANAGEMENT ENGINE (No Mid-Trade Negotiation) ---
            if (position != null)
            {
                int barsInTrade = Bars.Count - _entryBarIndex;

                // RULE 1: TIME STOP (Momentum died)
                if (!_isPartialHit && barsInTrade >= TimeStopBars)
                {
                    ClosePosition(position);
                    Print("Time Stop Triggered: Structure failed to validate in time.");
                    return;
                }

                // RULE 2: PRE-COMMITTED PARTIAL & BREAK-EVEN
                if (!_isPartialHit)
                {
                    // Did price cross the exact price line?
                    bool triggerPartial = (position.TradeType == TradeType.Buy && Symbol.Bid >= _partialTargetPrice) ||
                                          (position.TradeType == TradeType.Sell && Symbol.Ask <= _partialTargetPrice);

                    if (triggerPartial)
                    {
                        // Calculate volume to slice based on Percentage
                        double rawVolumeToClose = _initialVolume * (PartialPct / 100.0);
                        double volumeToClose = Symbol.NormalizeVolumeInUnits(rawVolumeToClose, RoundingMode.Down);
                        
                        // Close Partial natively
                        ClosePosition(position, volumeToClose);
                        _isPartialHit = true;
                        
                        // Move remaining Runner to Break-Even (cTrader accepts absolute price for modification)
                        ModifyPosition(position, position.EntryPrice, position.TakeProfit);
                        Print($"Partial Hit at {_partialTargetPrice}. Runner SL moved to Break-Even: {position.EntryPrice}");
                    }
                }
            }
        }
    }
}

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:51 am
by FTtrader
Why cTrader (C#) excels for this logic over MQL:

LINQ on Time Series: Notice the Bars.LowPrices.Minimum(3). Instead of MQL's clunky iLowest() loops, you can use standard C# LINQ extensions natively on the market data arrays.

Native Fractional Execution: ClosePosition(position, volumeToClose) is a native, synchronous call. The broker handles the slice immediately, avoiding the race conditions and ticket-management headaches of MT4.

Pips vs Price Separation: cTrader explicitly forces you to think in PipSize when passing Stop Losses to ExecuteMarketOrder, but allows you to use absolute prices when updating via ModifyPosition(). It makes calculating risk distances explicitly clearer than MQL's rigid _Point math.

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:52 am
by FTtrader
To make this a "Pro" algorithmic engine in C#, we implement a Finite State Machine (FSM) Pattern, decouple our entry logic into distinct methods, use LINQ for our structural lookbacks, and hook into native .NET event delegates to handle state teardown cleanly.

Here is the institutional-grade cTrader architecture:

Code: Select all

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

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class PA_FSM_Engine_Pro : Robot
    {
        // ==============================================================================
        // 1. INPUTS & CONFIGURATION
        // ==============================================================================
        [Parameter("Risk Per Trade (%)", Group = "Risk Management", DefaultValue = 1.0, MinValue = 0.1, Step = 0.1)]
        public double RiskPct { get; set; }

        [Parameter("Cost Buffer (Pips)", Group = "Risk Management", DefaultValue = 1.0, ToolTip = "Spread/Comm buffer for Break-Even")]
        public double CostBufferPips { get; set; }

        [Parameter("Partial Target (R)", Group = "Trade Management", DefaultValue = 1.0)]
        public double PartialTargetR { get; set; }

        [Parameter("Partial Size (%)", Group = "Trade Management", DefaultValue = 50)]
        public double PartialPct { get; set; }

        [Parameter("Trailing Lookback (Bars)", Group = "Trade Management", DefaultValue = 5)]
        public int TrailBars { get; set; }

        [Parameter("Time Stop (Bars)", Group = "Trade Management", DefaultValue = 12)]
        public int TimeStopBars { get; set; }

        [Parameter("Session Start (Hour UTC)", Group = "Killzones", DefaultValue = 8)]
        public int SessionStart { get; set; }

        [Parameter("Session End (Hour UTC)", Group = "Killzones", DefaultValue = 16)]
        public int SessionEnd { get; set; }

        // ==============================================================================
        // 2. STATE MACHINE (FSM) ARCHITECTURE
        // ==============================================================================
        private enum TradeState { Flat, RiskPhase, RunnerPhase }
        private TradeState _currentState = TradeState.Flat;

        private const string TradeLabel = "PA_Pro_Engine";
        private double _partialTargetPrice;
        private double _initialVolume;
        private int _entryBarIndex;

        // ==============================================================================
        // 3. INITIALIZATION & EVENT HOOKS
        // ==============================================================================
        protected override void OnStart()
        {
            // Subscribe to position closed events to handle state teardown natively
            Positions.Closed += OnPositionClosed;
        }

        private void OnPositionClosed(PositionClosedEventArgs args)
        {
            // Only reset if our specific bot's position is fully closed (ignore partial closures)
            if (args.Position.Label == TradeLabel && args.Reason != PositionCloseReason.Partial)
            {
                Print($"[FSM Reset] Position closed. Reason: {args.Reason}. Engine returning to Flat.");
                _currentState = TradeState.Flat;
            }
        }

        // ==============================================================================
        // 4. MAIN EVENT LOOP
        // ==============================================================================
        protected override void OnTick()
        {
            var activePosition = Positions.Find(TradeLabel, SymbolName);

            // Safety catch: If state is out of sync with actual positions, force reset
            if (activePosition == null && _currentState != TradeState.Flat)
                _currentState = TradeState.Flat;

            switch (_currentState)
            {
                case TradeState.Flat:
                    SearchForSetup();
                    break;

                case TradeState.RiskPhase:
                    ManageRiskPhase(activePosition);
                    break;

                case TradeState.RunnerPhase:
                    ManageRunnerPhase(activePosition);
                    break;
            }
        }

        // ==============================================================================
        // 5. EXECUTION & SIZING LOGIC
        // ==============================================================================
        private void SearchForSetup()
        {
            // Enforce Session Killzone
            if (Server.Time.Hour < SessionStart || Server.Time.Hour >= SessionEnd) return;

            // PA Sweep Trigger using LINQ on the Time Series
            double swingLow = Bars.LowPrices.Last(5); // Baseline for last 5 bars
            bool bullishSweep = Bars.LowPrices.Last(1) < swingLow && Bars.ClosePrices.Last(1) > swingLow;

            if (bullishSweep)
            {
                double slPrice = swingLow - (CostBufferPips * Symbol.PipSize);
                double riskPips = (Symbol.Ask - slPrice) / Symbol.PipSize;

                if (riskPips <= 0) return;

                // Dynamic C# Volumetric Math
                double riskAmount = Account.Balance * (RiskPct / 100.0);
                double exactVolume = riskAmount / (riskPips * Symbol.PipValue);
                _initialVolume = Symbol.NormalizeVolumeInUnits(exactVolume, RoundingMode.Down);

                if (_initialVolume < Symbol.VolumeInUnitsMin) return;

                // State pre-configuration
                _partialTargetPrice = Symbol.Ask + (riskPips * PartialTargetR * Symbol.PipSize);
                _entryBarIndex = Bars.Count;
                _currentState = TradeState.RiskPhase; // Immediate state transition

                ExecuteMarketOrder(TradeType.Buy, SymbolName, _initialVolume, TradeLabel, riskPips, null);
            }
        }

        // ==============================================================================
        // 6. MANAGEMENT: PHASE 1 (RISK & PARTIALS)
        // ==============================================================================
        private void ManageRiskPhase(Position pos)
        {
            int barsInTrade = Bars.Count - _entryBarIndex;

            // RULE 1: Time Stop (Kill trade if momentum failed)
            if (barsInTrade >= TimeStopBars)
            {
                ClosePosition(pos);
                Print("[Time Stop] Momentum invalidation. Position scratched.");
                return; // The Positions.Closed event handler will reset the FSM
            }

            // RULE 2: Pre-Committed Partial Execution
            if (pos.TradeType == TradeType.Buy && Symbol.Bid >= _partialTargetPrice)
            {
                double volumeToClose = Symbol.NormalizeVolumeInUnits(_initialVolume * (PartialPct / 100.0), RoundingMode.Down);

                // Slice the position cleanly
                ClosePosition(pos, volumeToClose);

                // Move remaining SL to Cost-Aware Break-Even
                double costAwareBE = pos.EntryPrice + (CostBufferPips * Symbol.PipSize);
                ModifyPosition(pos, costAwareBE, pos.TakeProfit);

                // Transition FSM to Runner phase
                _currentState = TradeState.RunnerPhase;
                Print($"[Phase Transition] Partial Hit. Runner moved to Cost-Aware BE: {costAwareBE}");
            }
        }

        // ==============================================================================
        // 7. MANAGEMENT: PHASE 2 (STRUCTURAL TRAILING)
        // ==============================================================================
        private void ManageRunnerPhase(Position pos)
        {
            if (pos.TradeType == TradeType.Buy)
            {
                // Retrieve the lowest low of the last N bars using LINQ extensions
                double structLow = Bars.LowPrices.Minimum(TrailBars);
                
                // Trail strictly behind structure, plus our cost buffer
                double trailPrice = structLow - (CostBufferPips * Symbol.PipSize);

                // Only move stop forward
                if (pos.StopLoss.HasValue && trailPrice > pos.StopLoss.Value + (Symbol.PipSize * 0.5))
                {
                    ModifyPosition(pos, trailPrice, pos.TakeProfit);
                    Print($"[Trailing] Runner SL moved to structural low: {trailPrice}");
                }
            }
        }
    }
}