Page 2 of 2
Re: Consistency Isn’t the Whole Story 🧪
Posted: Sat Sep 26, 2026 9:59 am
by PTScalper
Here are the complete Expert Advisor templates for both MQL5 and MQL4.
To make this mathematically bulletproof regardless of account currency or lot size, these templates isolate the 1R absolute price distance at the exact moment of execution.
(Note: If your liquidity sweep strategies use trailing stops or break-even adjustments, MQL natively overwrites the initial Stop Loss in the history pool. To prevent this from breaking the R-multiple math, both scripts utilize a standard tracking architecture: storing the initial 1R price distance directly inside the Order Comment at execution, allowing the history parser to extract the true baseline risk long after the trade is closed.)
1. MQL5 Expectancy & R-Multiple Tracker (MT5)
Code: Select all
Here are the complete Expert Advisor templates for both MQL5 and MQL4.
To make this mathematically bulletproof regardless of account currency or lot size, these templates isolate the 1R absolute price distance at the exact moment of execution.
(Note: If your liquidity sweep strategies use trailing stops or break-even adjustments, MQL natively overwrites the initial Stop Loss in the history pool. To prevent this from breaking the R-multiple math, both scripts utilize a standard tracking architecture: storing the initial 1R price distance directly inside the Order Comment at execution, allowing the history parser to extract the true baseline risk long after the trade is closed.)
1. MQL5 Expectancy & R-Multiple Tracker (MT5)
Re: Consistency Isn’t the Whole Story 🧪
Posted: Sat Sep 26, 2026 9:59 am
by PTScalper
2. MQL4 Expectancy & R-Multiple Tracker (MT4)
Code: Select all
//+------------------------------------------------------------------+
//| Expectancy_Tracker_MT4.mq4 |
//+------------------------------------------------------------------+
#property copyright "Expectancy Framework"
#property version "1.00"
#property strict
extern int FastEmaLength = 9;
extern int SlowEmaLength = 21;
extern double RiskAtrMultiplier = 1.5;
extern double RewardAtrMultiplier = 3.0;
extern int MagicNumber = 123456;
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
if(OrdersTotal() == 0)
{
double atr = iATR(Symbol(), 0, 14, 1);
double fastEma1 = iMA(Symbol(), 0, FastEmaLength, 0, MODE_EMA, PRICE_CLOSE, 1);
double slowEma1 = iMA(Symbol(), 0, SlowEmaLength, 0, MODE_EMA, PRICE_CLOSE, 1);
double fastEma2 = iMA(Symbol(), 0, FastEmaLength, 0, MODE_EMA, PRICE_CLOSE, 2);
double slowEma2 = iMA(Symbol(), 0, SlowEmaLength, 0, MODE_EMA, PRICE_CLOSE, 2);
bool longSweep = (fastEma1 > slowEma1 && fastEma2 <= slowEma2);
bool shortSweep = (fastEma1 < slowEma1 && fastEma2 >= slowEma2);
if(longSweep)
{
double sl = Bid - (atr * RiskAtrMultiplier);
double tp = Ask + (atr * RewardAtrMultiplier);
double riskDist = Ask - sl;
string comment = "1R:" + DoubleToString(riskDist, Digits);
OrderSend(Symbol(), OP_BUY, 0.1, Ask, 3, sl, tp, comment, MagicNumber, 0, clrGreen);
}
else if(shortSweep)
{
double sl = Ask + (atr * RiskAtrMultiplier);
double tp = Bid - (atr * RewardAtrMultiplier);
double riskDist = sl - Bid;
string comment = "1R:" + DoubleToString(riskDist, Digits);
OrderSend(Symbol(), OP_SELL, 0.1, Bid, 3, sl, tp, comment, MagicNumber, 0, clrRed);
}
}
UpdateExpectancyDashboard();
}
//+------------------------------------------------------------------+
//| Calculate and Render Expectancy |
//+------------------------------------------------------------------+
void UpdateExpectancyDashboard()
{
int trades = 0, wins = 0, losses = 0;
double sumWinR = 0, sumLossR = 0;
int historyTotal = OrdersHistoryTotal();
for(int i = 0; i < historyTotal; i++)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_HISTORY))
{
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
{
string comment = OrderComment();
int idx = StringFind(comment, "1R:");
if(idx >= 0)
{
double initialRiskPoints = StringToDouble(StringSubstr(comment, idx + 3));
double profit = OrderProfit() + OrderCommission() + OrderSwap();
double tickValue = MarketInfo(Symbol(), MODE_TICKVALUE);
double tickSize = MarketInfo(Symbol(), MODE_TICKSIZE);
double monetaryRisk = (initialRiskPoints / tickSize) * tickValue * OrderLots();
if(monetaryRisk == 0) continue;
double rMultiple = profit / monetaryRisk;
trades++;
if(rMultiple > 0) {
wins++;
sumWinR += rMultiple;
} else {
losses++;
sumLossR += MathAbs(rMultiple);
}
}
}
}
}
double winRate = trades > 0 ? (double)wins / trades : 0.0;
double lossRate = 1.0 - winRate;
double avgWin = wins > 0 ? sumWinR / wins : 0.0;
double avgLoss = losses > 0 ? sumLossR / losses : 0.0;
double expectancy = (winRate * avgWin) - (lossRate * avgLoss);
string dash = "--- EXPECTANCY FRAMEWORK ---\n";
dash += "Total Trades: " + IntegerToString(trades) + "\n";
dash += "Win Rate: " + DoubleToString(winRate * 100, 1) + "%\n";
dash += "Avg Win: +" + DoubleToString(avgWin, 2) + "R\n";
dash += "Avg Loss: -" + DoubleToString(avgLoss, 2) + "R\n";
dash += "Expectancy per Trade: " + DoubleToString(expectancy, 2) + "R\n";
Comment(dash);
}
Re: Consistency Isn’t the Whole Story 🧪
Posted: Sat Sep 26, 2026 10:01 am
by PTScalper
Since cTrader’s API is natively built on C# and .NET, we can abandon the clunky for loops required in MQL and leverage System.Linq to process the historical R-multiples cleanly.
To ensure this cBot functions flawlessly regardless of your Windows regional settings (crucial for European locales where commas are used as decimals), this template enforces CultureInfo.InvariantCulture when serializing the 1R distance into the position comment.
Here is the complete cAlgo implementation:
Code: Select all
using System;
using System.Linq;
using System.Globalization;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;
namespace cAlgo.Robots
{
[Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
public class ExpectancyTracker : Robot
{
[Parameter("Fast EMA", DefaultValue = 9, Group = "Price Action Triggers")]
public int FastEmaLength { get; set; }
[Parameter("Slow EMA", DefaultValue = 21, Group = "Price Action Triggers")]
public int SlowEmaLength { get; set; }
[Parameter("Risk ATR Multiplier", DefaultValue = 1.5, Group = "Risk Management")]
public double RiskAtrMultiplier { get; set; }
[Parameter("Reward ATR Multiplier", DefaultValue = 3.0, Group = "Risk Management")]
public double RewardAtrMultiplier { get; set; }
[Parameter("Volume", DefaultValue = 1000, Group = "Risk Management")]
public double Volume { get; set; }
private ExponentialMovingAverage _fastEma;
private ExponentialMovingAverage _slowEma;
private AverageTrueRange _atr;
protected override void OnStart()
{
_fastEma = Indicators.ExponentialMovingAverage(Bars.ClosePrices, FastEmaLength);
_slowEma = Indicators.ExponentialMovingAverage(Bars.ClosePrices, SlowEmaLength);
_atr = Indicators.AverageTrueRange(14, MovingAverageType.Simple);
Positions.Closed += OnPositionClosed;
UpdateExpectancyDashboard();
}
protected override void OnBar()
{
// Prevent multiple entries
if (Positions.Count(p => p.Label == "ExpectancyTracker") > 0) return;
// PA TRIGGER LOGIC (Replace with your Liquidity Sweep Logic)
bool longSweep = _fastEma.Result.Last(1) > _slowEma.Result.Last(1) && _fastEma.Result.Last(2) <= _slowEma.Result.Last(2);
bool shortSweep = _fastEma.Result.Last(1) < _slowEma.Result.Last(1) && _fastEma.Result.Last(2) >= _slowEma.Result.Last(2);
if (!longSweep && !shortSweep) return;
double atrValue = _atr.Result.Last(1);
double slDistancePrice = atrValue * RiskAtrMultiplier;
double tpDistancePrice = atrValue * RewardAtrMultiplier;
// cTrader ExecuteMarketOrder requires StopLoss and TakeProfit in Pips, not absolute price
double slPips = slDistancePrice / Symbol.PipSize;
double tpPips = tpDistancePrice / Symbol.PipSize;
// Embed the 1R structural distance directly into the comment (culture invariant to avoid comma/dot parse errors)
string riskStr = Math.Round(slDistancePrice, Symbol.Digits).ToString(CultureInfo.InvariantCulture);
string comment = $"1R:{riskStr}";
if (longSweep)
{
ExecuteMarketOrder(TradeType.Buy, SymbolName, Volume, "ExpectancyTracker", slPips, tpPips, comment);
}
else if (shortSweep)
{
ExecuteMarketOrder(TradeType.Sell, SymbolName, Volume, "ExpectancyTracker", slPips, tpPips, comment);
}
}
private void OnPositionClosed(PositionClosedEventArgs args)
{
UpdateExpectancyDashboard();
}
private void UpdateExpectancyDashboard()
{
// Evaluate history strictly for trades managed by this bot that contain the 1R stamp
var rMultiples = History
.Where(t => t.Label == "ExpectancyTracker" && !string.IsNullOrEmpty(t.Comment) && t.Comment.StartsWith("1R:"))
.Select(t =>
{
string riskString = t.Comment.Substring(3);
double initialRiskPrice = double.Parse(riskString, CultureInfo.InvariantCulture);
// Pure structural price delta (isolates the price action edge from lot sizes)
double priceDelta = t.TradeType == TradeType.Buy
? t.ClosingPrice - t.EntryPrice
: t.EntryPrice - t.ClosingPrice;
return initialRiskPrice > 0 ? (priceDelta / initialRiskPrice) : 0;
})
.ToList();
int totalTrades = rMultiples.Count;
if (totalTrades == 0) return;
var wins = rMultiples.Where(r => r > 0).ToList();
var losses = rMultiples.Where(r => r <= 0).ToList();
double winRate = (double)wins.Count / totalTrades;
double lossRate = 1.0 - winRate;
double avgWin = wins.Any() ? wins.Average() : 0.0;
double avgLoss = losses.Any() ? Math.Abs(losses.Average()) : 0.0;
double expectancy = (winRate * avgWin) - (lossRate * avgLoss);
string dash = $"--- EXPECTANCY FRAMEWORK ---\n" +
$"Total Trades: {totalTrades}\n" +
$"Win Rate: {winRate * 100:F1}%\n" +
$"Avg Win: +{avgWin:F2}R\n" +
$"Avg Loss: -{avgLoss:F2}R\n" +
$"Expectancy per Trade: {expectancy:F2}R";
Chart.DrawStaticText("ExpectancyDash", dash, VerticalAlignment.Bottom, HorizontalAlignment.Right, Color.White);
}
}
}
Re: Consistency Isn’t the Whole Story 🧪
Posted: Sat Sep 26, 2026 10:01 am
by PTScalper
Architectural Notes
Structural Price Delta: The LINQ .Select() block calculates the R-multiple using purely the entry/exit price delta (ClosingPrice - EntryPrice). This guarantees your expectancy score reflects the raw mathematical edge of the price action setups, stripping out arbitrary lot sizes or margin scaling.
Culture Invariance: CultureInfo.InvariantCulture is applied during both string injection and string parsing. If your Windows OS in Czechia defaults to comma decimals, this prevents double.Parse() from throwing formatting exceptions when reading the history log.
Re: Consistency Isn’t the Whole Story 🧪
Posted: Sat Sep 26, 2026 10:04 am
by PTScalper
Because cTrader Automate runs entirely on the .NET runtime, you can pull standard NuGet packages directly into your cBot workspace.
For high-performance trading—especially when executing rapid liquidity sweeps on 1-minute or 15-minute charts—RabbitMQ is the superior architectural choice. Writing directly to an MS SQL database from the cBot introduces I/O latency and risks blocking the main trading thread during database locks or network timeouts.
By publishing trade outcomes to a local RabbitMQ exchange as fire-and-forget messages, you keep the cBot's execution thread completely unblocked. You can then use a standard .NET Core worker service to consume those messages and handle the heavy lifting of parsing and inserting them into MS SQL.
Here is how to implement the RabbitMQ publisher directly inside the cTrader Expectancy framework.
Re: Consistency Isn’t the Whole Story 🧪
Posted: Sat Sep 26, 2026 10:04 am
by PTScalper
1. Configure the cBot
In the cTrader Automate editor, go to Manage References -> NuGet Packages and install RabbitMQ.Client and System.Text.Json.
2. Implement the Message Publisher
Add the connection lifecycle and the fire-and-forget publishing logic to your existing cBot.
Code: Select all
using System;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Globalization;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;
using RabbitMQ.Client; // Added via NuGet
namespace cAlgo.Robots
{
[Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.FullAccess)]
public class ExpectancyTracker : Robot
{
// ... (Keep existing parameters and EMA/ATR variables) ...
private IConnection _rmqConnection;
private IModel _rmqChannel;
private const string QueueName = "cTrader_Expectancy_Data";
protected override void OnStart()
{
// Initialize RabbitMQ Connection (adjust HostName/Credentials for your local environment)
var factory = new ConnectionFactory() { HostName = "localhost" };
_rmqConnection = factory.CreateConnection();
_rmqChannel = _rmqConnection.CreateModel();
_rmqChannel.QueueDeclare(queue: QueueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
// ... (Keep existing indicator initialization) ...
Positions.Closed += OnPositionClosed;
}
protected override void OnStop()
{
// Clean up connections when the cBot stops
_rmqChannel?.Close();
_rmqConnection?.Close();
}
protected override void OnBar()
{
// ... (Keep existing entry logic) ...
}
private void OnPositionClosed(PositionClosedEventArgs args)
{
var pos = args.Position;
// Only process our specific trades
if (pos.Label != "ExpectancyTracker" || string.IsNullOrEmpty(pos.Comment) || !pos.Comment.StartsWith("1R:"))
return;
// Calculate the R-Multiple
string riskString = pos.Comment.Substring(3);
double initialRiskPrice = double.Parse(riskString, CultureInfo.InvariantCulture);
double priceDelta = pos.TradeType == TradeType.Buy
? pos.EntryPrice - pos.ClosingPrice
: pos.ClosingPrice - pos.EntryPrice; // Assuming loss is negative delta
double rMultiple = initialRiskPrice > 0 ? (priceDelta / initialRiskPrice) : 0;
// Build the data payload
var tradeData = new
{
Ticket = pos.Id,
Symbol = pos.SymbolName,
Direction = pos.TradeType.ToString(),
EntryTime = pos.EntryTime.ToString("o"),
ExitTime = Server.Time.ToString("o"),
RMultiple = Math.Round(rMultiple, 2),
NetProfit = pos.NetProfit,
InitialRiskPrice = initialRiskPrice
};
// Offload the serialization and network I/O to a background task
Task.Run(() => PublishToQueue(tradeData));
UpdateExpectancyDashboard();
}
private void PublishToQueue(object dataPayload)
{
try
{
string json = JsonSerializer.Serialize(dataPayload);
var body = Encoding.UTF8.GetBytes(json);
var properties = _rmqChannel.CreateBasicProperties();
properties.Persistent = true;
_rmqChannel.BasicPublish(exchange: "",
routingKey: QueueName,
basicProperties: properties,
body: body);
}
catch (Exception ex)
{
Print($"RabbitMQ Publish Error: {ex.Message}");
}
}
private void UpdateExpectancyDashboard()
{
// ... (Keep existing dashboard logic) ...
}
}
}
Re: Consistency Isn’t the Whole Story 🧪
Posted: Sat Sep 26, 2026 10:04 am
by PTScalper
3. Alternative: Direct Entity Framework / SQL Client
If you decide to skip the message broker and write directly to MS SQL from the cBot, you can install Microsoft.Data.SqlClient via NuGet.
To prevent blocking the cTrader thread, you must wrap the SQL connection in a background task. Replace the Task.Run(() => PublishToQueue(tradeData)) with:
Code: Select all
Task.Run(async () =>
{
string connString = "Server=localhost;Database=TradingDb;Integrated Security=True;TrustServerCertificate=True;";
using var connection = new Microsoft.Data.SqlClient.SqlConnection(connString);
string query = @"INSERT INTO TradeHistory (Ticket, Symbol, RMultiple, NetProfit, ClosedAt)
VALUES (@Ticket, @Symbol, @RMultiple, @NetProfit, @ClosedAt)";
using var command = new Microsoft.Data.SqlClient.SqlCommand(query, connection);
command.Parameters.AddWithValue("@Ticket", pos.Id);
command.Parameters.AddWithValue("@Symbol", pos.SymbolName);
command.Parameters.AddWithValue("@RMultiple", Math.Round(rMultiple, 2));
command.Parameters.AddWithValue("@NetProfit", pos.NetProfit);
command.Parameters.AddWithValue("@ClosedAt", Server.Time);
await connection.OpenAsync();
await command.ExecuteNonQueryAsync();
});
Re: Consistency Isn’t the Whole Story 🧪
Posted: Sat Sep 26, 2026 10:04 am
by PTScalper
(Note: cTrader requires AccessRights = AccessRights.FullAccess in the [Robot] attribute whenever a cBot makes external network calls, accesses the local file system, or opens socket connections to local services like RabbitMQ or MS SQL).