Institutional Upgrades Applied for a .NET Environment:
The C# switch State Machine: Notice how OnTick() is completely decoupled. Instead of a messy block of if/else statements checking boolean flags, the tick loop simply routes to SearchForSetup(), ManageRiskPhase(), or ManageRunnerPhase() based on the TradeState enum. This makes Phase 1 logic physically impossible to execute once Phase 2 has been entered.
Native Event Delegates (Positions.Closed += OnPositionClosed): You don't have to check if a position was stopped out on every single tick. By hooking into the native C# event delegate in OnStart(), the API will proactively notify you the millisecond the broker closes your position (whether by your Time Stop, a manual closure, or hitting your trailing stop), allowing your bot to reset its FSM asynchronously and cleanly.
LINQ-Powered Structural Analysis: Bars.LowPrices.Minimum(TrailBars) replaces all the archaic for-loops you would need in MQL to find recent structural pivots.
Cost-Aware Break-Even Execution: Modifying a runner's stop loss to pos.EntryPrice is a retail trap because the spread and commission will cause your "free" trade to lose money. This calculates pos.EntryPrice + (CostBufferPips * Symbol.PipSize) to ensure a break-even trade is mathematically zero-loss.
Beating cutting winners early as a scalper: rules that stuck
Re: Beating cutting winners early as a scalper: rules that stuck
To push this from a functional Pro script to a true Enterprise-Grade Algorithmic Architecture, we must address the realities of live-market execution. In high-stakes C# environments, blocking the main thread on an API call is unacceptable, and failing to account for network latency or spread-widening causes slippage that ruins scalping expectancy.
We elevate the engine using four advanced .NET paradigms:
Asynchronous Execution (Async Callbacks): We replace all synchronous order commands with non-blocking Async methods. This prevents the OnTick thread from freezing while waiting for broker latency.
Concurrency/State Locking: A volatile _isProcessing lock prevents the FSM from spamming the broker with multiple partial-close requests on the same tick before the first network response returns.
Microstructure Safeguards: We implement a MaxSpreadPips filter. If the spread blows out (which ruins the R-multiple math on tight PA sweeps), the engine refuses to fire.
Local Telemetry & Chart Rendering: The bot natively draws its execution intent on the cTrader UI—rendering the entry, target, and trailing stop lines dynamically without needing external indicators.
We elevate the engine using four advanced .NET paradigms:
Asynchronous Execution (Async Callbacks): We replace all synchronous order commands with non-blocking Async methods. This prevents the OnTick thread from freezing while waiting for broker latency.
Concurrency/State Locking: A volatile _isProcessing lock prevents the FSM from spamming the broker with multiple partial-close requests on the same tick before the first network response returns.
Microstructure Safeguards: We implement a MaxSpreadPips filter. If the spread blows out (which ruins the R-multiple math on tight PA sweeps), the engine refuses to fire.
Local Telemetry & Chart Rendering: The bot natively draws its execution intent on the cTrader UI—rendering the entry, target, and trailing stop lines dynamically without needing external indicators.
Re: Beating cutting winners early as a scalper: rules that stuck
Here is the final, institutional-grade C# framework.
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_Enterprise_Engine : Robot
{
// ==============================================================================
// 1. RISK & MICROSTRUCTURE CONFIGURATION
// ==============================================================================
[Parameter("Risk Per Trade (%)", Group = "Risk Management", DefaultValue = 1.0, Step = 0.1)]
public double RiskPct { get; set; }
[Parameter("Max Spread (Pips)", Group = "Risk Management", DefaultValue = 1.5, ToolTip = "Aborts entry if spread is too wide")]
public double MaxSpreadPips { get; set; }
[Parameter("Cost Buffer (Pips)", Group = "Risk Management", DefaultValue = 0.5)]
public double CostBufferPips { get; set; }
// ==============================================================================
// 2. FSM MANAGEMENT CONFIGURATION
// ==============================================================================
[Parameter("Partial Target (R)", Group = "FSM Rules", DefaultValue = 1.0)]
public double PartialTargetR { get; set; }
[Parameter("Partial Size (%)", Group = "FSM Rules", DefaultValue = 50)]
public double PartialPct { get; set; }
[Parameter("Time Stop (Bars)", Group = "FSM Rules", DefaultValue = 12)]
public int TimeStopBars { get; set; }
[Parameter("Trailing Lookback (Bars)", Group = "FSM Rules", DefaultValue = 5)]
public int TrailBars { get; set; }
// ==============================================================================
// 3. THREAD-SAFE STATE MACHINE
// ==============================================================================
private enum TradeState { Flat, RiskPhase, RunnerPhase }
private TradeState _currentState = TradeState.Flat;
// Concurrency lock for async broker communication
private volatile bool _isProcessing = false;
private const string TradeLabel = "PA_Enterprise";
private double _partialTargetPrice;
private double _initialVolume;
private int _entryBarIndex;
protected override void OnStart()
{
Positions.Closed += OnPositionClosed;
}
private void OnPositionClosed(PositionClosedEventArgs args)
{
if (args.Position.Label == TradeLabel && args.Reason != PositionCloseReason.Partial)
{
_currentState = TradeState.Flat;
_isProcessing = false;
Chart.RemoveAllObjects(); // Clean chart on exit
Print($"[Telemetry] Trade Closed. Reason: {args.Reason}. P&L: {args.Position.GrossProfit}");
}
}
// ==============================================================================
// 4. MAIN ASYNC EVENT LOOP
// ==============================================================================
protected override void OnTick()
{
// Abort if waiting on a callback from the broker
if (_isProcessing) return;
var pos = Positions.Find(TradeLabel, SymbolName);
// FSM Recovery/Sync
if (pos == null && _currentState != TradeState.Flat)
{
_currentState = TradeState.Flat;
Chart.RemoveAllObjects();
}
switch (_currentState)
{
case TradeState.Flat:
SearchForSetup();
break;
case TradeState.RiskPhase:
ManageRiskPhase(pos);
break;
case TradeState.RunnerPhase:
ManageRunnerPhase(pos);
break;
}
RenderVisuals(pos);
}
// ==============================================================================
// 5. SETUP & ASYNC EXECUTION
// ==============================================================================
private void SearchForSetup()
{
// Microstructure Check: Deny entry if spread destroys our RR math
if ((Symbol.Ask - Symbol.Bid) / Symbol.PipSize > MaxSpreadPips) return;
// Sweeping PA structure (Dynamic array slicing)
double swingLow = Bars.LowPrices.Skip(Math.Max(0, Bars.Count - 6)).Take(5).Min();
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;
double volume = Symbol.NormalizeVolumeInUnits((Account.Balance * (RiskPct / 100.0)) / (riskPips * Symbol.PipValue), RoundingMode.Down);
if (volume < Symbol.VolumeInUnitsMin) return;
_initialVolume = volume;
_partialTargetPrice = Symbol.Ask + (riskPips * PartialTargetR * Symbol.PipSize);
_entryBarIndex = Bars.Count;
_isProcessing = true; // Lock FSM
// Fire & Forget Async Execution
ExecuteMarketOrderAsync(TradeType.Buy, SymbolName, volume, TradeLabel, riskPips, null, (result) =>
{
_isProcessing = false; // Unlock FSM
if (result.IsSuccessful)
{
_currentState = TradeState.RiskPhase;
Print($"[FSM] Setup Validated. Async Entry Filled at {result.Position.EntryPrice}");
}
else Print($"[Error] Execution Failed: {result.Error}");
});
}
}
// ==============================================================================
// 6. PHASE 1: ASYNC PARTIALS & BREAK-EVEN
// ==============================================================================
private void ManageRiskPhase(Position pos)
{
if (Bars.Count - _entryBarIndex >= TimeStopBars)
{
_isProcessing = true;
ClosePositionAsync(pos, (res) => _isProcessing = false);
return;
}
if (pos.TradeType == TradeType.Buy && Symbol.Bid >= _partialTargetPrice)
{
_isProcessing = true; // Lock to prevent multiple partials firing
double closeVol = Symbol.NormalizeVolumeInUnits(_initialVolume * (PartialPct / 100.0), RoundingMode.Down);
ClosePositionAsync(pos, closeVol, (res) =>
{
if (res.IsSuccessful)
{
double costBE = pos.EntryPrice + (CostBufferPips * Symbol.PipSize);
ModifyPositionAsync(pos, costBE, pos.TakeProfit, (modRes) =>
{
_isProcessing = false;
if (modRes.IsSuccessful)
{
_currentState = TradeState.RunnerPhase;
Print("[FSM] Partial Secured. Runner locked at Cost-Aware BE.");
}
});
}
else _isProcessing = false;
});
}
}
// ==============================================================================
// 7. PHASE 2: STRUCTURAL TRAIL
// ==============================================================================
private void ManageRunnerPhase(Position pos)
{
if (pos.TradeType == TradeType.Buy)
{
double structLow = Bars.LowPrices.Skip(Math.Max(0, Bars.Count - TrailBars - 1)).Take(TrailBars).Min();
double trailPrice = structLow - (CostBufferPips * Symbol.PipSize);
if (pos.StopLoss.HasValue && trailPrice > pos.StopLoss.Value + (Symbol.PipSize * 0.5))
{
_isProcessing = true;
ModifyPositionAsync(pos, trailPrice, pos.TakeProfit, (res) => _isProcessing = false);
}
}
}
// ==============================================================================
// 8. UI OVERLAY ENGINE
// ==============================================================================
private void RenderVisuals(Position pos)
{
if (_currentState == TradeState.RiskPhase)
{
Chart.DrawHorizontalLine("PartialTarget", _partialTargetPrice, Color.DeepSkyBlue, 2, LineStyle.Lines);
}
else if (_currentState == TradeState.RunnerPhase)
{
Chart.RemoveObject("PartialTarget");
}
if (pos != null && pos.StopLoss.HasValue)
{
Color slColor = _currentState == TradeState.RiskPhase ? Color.Crimson : Color.MediumSeaGreen;
Chart.DrawHorizontalLine("DynamicSL", pos.StopLoss.Value, slColor, 2, LineStyle.Dots);
}
}
}
}Re: Beating cutting winners early as a scalper: rules that stuck
The Architectural Leaps:
The _isProcessing Concurrency Lock: In synchronous code, the broker executes your request instantly in a vacuum. In live markets, taking a partial exit via ClosePositionAsync requires a round-trip to the broker server. Without a concurrency lock (_isProcessing = true), the OnTick loop would evaluate the next tick 10 milliseconds later, see the partial target is still breached, and fire another close request before the first one completes—resulting in accidental full closures.
Callbacks & State Promotion: State transitions only occur inside the asynchronous callback. The FSM is never allowed to upgrade to TradeState.RunnerPhase until the broker explicitly confirms the partial position was successfully removed from the market.
Array Slicing via LINQ (Skip.Take.Min): Using Bars.LowPrices.Skip(...).Take(...).Min() is an ultra-clean, memory-safe approach to defining rolling structure without declaring custom iterators.
The Overlay Engine: Rather than guessing what the bot is doing, the RenderVisuals method hooks into cTrader's native chart drawing API. When in RiskPhase, it projects a Deep Sky Blue line at your exact partial target and a Crimson dotted line at your initial stop. Upon transition to RunnerPhase, the blue line automatically vanishes, and the stop-loss line turns Medium Sea Green as it trails your market structure.
The _isProcessing Concurrency Lock: In synchronous code, the broker executes your request instantly in a vacuum. In live markets, taking a partial exit via ClosePositionAsync requires a round-trip to the broker server. Without a concurrency lock (_isProcessing = true), the OnTick loop would evaluate the next tick 10 milliseconds later, see the partial target is still breached, and fire another close request before the first one completes—resulting in accidental full closures.
Callbacks & State Promotion: State transitions only occur inside the asynchronous callback. The FSM is never allowed to upgrade to TradeState.RunnerPhase until the broker explicitly confirms the partial position was successfully removed from the market.
Array Slicing via LINQ (Skip.Take.Min): Using Bars.LowPrices.Skip(...).Take(...).Min() is an ultra-clean, memory-safe approach to defining rolling structure without declaring custom iterators.
The Overlay Engine: Rather than guessing what the bot is doing, the RenderVisuals method hooks into cTrader's native chart drawing API. When in RiskPhase, it projects a Deep Sky Blue line at your exact partial target and a Crimson dotted line at your initial stop. Upon transition to RunnerPhase, the blue line automatically vanishes, and the stop-loss line turns Medium Sea Green as it trails your market structure.