Page 2 of 2
Re: cTrader Hotkey Profiles: Mapping Buy, Sell, Close-All and Reverse
Posted: Fri Oct 09, 2026 8:58 am
by FTtrader
This modification introduces a partial scale-out function mapped to the Spacebar.
Handling partial closes in MQL5 requires strict volume normalization. Because a broker will reject an order to close 0.015 lots if the minimum volume step is 0.01, the script calculates exactly half of the active position size, rounds it down to the nearest valid lot step, and ensures the resulting volume meets the broker's minimum lot size before executing.
Code: Select all
//+------------------------------------------------------------------+
//| DynamicRiskHotkeyScalper.mq5 |
//+------------------------------------------------------------------+
#property copyright "Custom MQL5 Script"
#property version "1.20"
#include <Trade\Trade.mqh>
//--- Input parameters
input double InpRiskPercent = 1.0; // Account Risk per Trade (%)
input int InpAtrPeriod = 14; // ATR Period
input double InpAtrMultiplier = 1.0; // ATR Multiplier for Stop Loss
input ulong InpMagic = 123456; // Magic Number
input ulong InpSlippage = 3; // Max Slippage (Points)
CTrade trade;
int atrHandle;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
trade.SetExpertMagicNumber(InpMagic);
trade.SetDeviationInPoints(InpSlippage);
atrHandle = iATR(_Symbol, PERIOD_CURRENT, InpAtrPeriod);
if(atrHandle == INVALID_HANDLE)
{
Print("Failed to create ATR indicator handle.");
return(INIT_FAILED);
}
Print("Dynamic Risk Hotkeys Initialized.");
Print("'B'=Buy, 'S'=Sell, 'C'=Close All, 'Space'=Scale Out 50%.");
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| ChartEvent function |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
const long &lparam,
const double &dparam,
const string &sparam)
{
if(id == CHARTEVENT_KEYDOWN)
{
switch((int)lparam)
{
case 66: // 'B' key
ExecuteBuy();
break;
case 83: // 'S' key
ExecuteSell();
break;
case 67: // 'C' key
CloseAllCurrentSymbol();
break;
case 32: // Spacebar
ScaleOutCurrentSymbol();
break;
}
}
}
//+------------------------------------------------------------------+
//| Lot Size Calculation |
//+------------------------------------------------------------------+
double CalculateLotSize(double slDistanceInPrice)
{
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskAmount = balance * (InpRiskPercent / 100.0);
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
if(tickSize == 0 || tickValue == 0 || slDistanceInPrice == 0)
return 0.0;
double riskPerLot = (slDistanceInPrice / tickSize) * tickValue;
double rawLot = riskAmount / riskPerLot;
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double stepLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double finalLot = MathFloor(rawLot / stepLot) * stepLot;
if(finalLot < minLot) finalLot = minLot;
if(finalLot > maxLot) finalLot = maxLot;
return finalLot;
}
//+------------------------------------------------------------------+
//| Execution Functions |
//+------------------------------------------------------------------+
void ExecuteBuy()
{
double atrArray[];
ArraySetAsSeries(atrArray, true);
if(CopyBuffer(atrHandle, 0, 1, 1, atrArray) <= 0)
{
Print("Error getting ATR data.");
return;
}
double currentAtr = atrArray[0];
double slDistance = currentAtr * InpAtrMultiplier;
double volume = CalculateLotSize(slDistance);
if(volume <= 0) return;
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = ask - slDistance;
if(trade.Buy(volume, _Symbol, ask, sl, 0, "ATR Risk Buy"))
PrintFormat("Buy executed. Vol: %.2f, SL: %.5f", volume, sl);
else
Print("Buy failed. Error: ", GetLastError());
}
void ExecuteSell()
{
double atrArray[];
ArraySetAsSeries(atrArray, true);
if(CopyBuffer(atrHandle, 0, 1, 1, atrArray) <= 0)
{
Print("Error getting ATR data.");
return;
}
double currentAtr = atrArray[0];
double slDistance = currentAtr * InpAtrMultiplier;
double volume = CalculateLotSize(slDistance);
if(volume <= 0) return;
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double sl = bid + slDistance;
if(trade.Sell(volume, _Symbol, bid, sl, 0, "ATR Risk Sell"))
PrintFormat("Sell executed. Vol: %.2f, SL: %.5f", volume, sl);
else
Print("Sell failed. Error: ", GetLastError());
}
void CloseAllCurrentSymbol()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
string position_symbol = PositionGetSymbol(i);
if(position_symbol == _Symbol)
{
ulong ticket = PositionGetInteger(POSITION_TICKET);
trade.PositionClose(ticket);
}
}
}
void ScaleOutCurrentSymbol()
{
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double stepLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
string position_symbol = PositionGetSymbol(i);
if(position_symbol == _Symbol)
{
ulong ticket = PositionGetInteger(POSITION_TICKET);
double currentVolume = PositionGetDouble(POSITION_VOLUME);
// Calculate half volume and round down to valid broker increment
double closeVolume = MathFloor((currentVolume / 2.0) / stepLot) * stepLot;
// Safety check: ensure we don't try to close 0 lots or less than broker minimum
if(closeVolume >= minLot)
{
if(trade.PositionClosePartial(ticket, closeVolume))
PrintFormat("Position %d scaled out. Closed volume: %.2f", ticket, closeVolume);
else
PrintFormat("Scale-out failed for ticket %d. Error: %d", ticket, GetLastError());
}
else
{
PrintFormat("Remaining volume (%.2f) on position %d is too small to scale out further.", currentVolume, ticket);
}
}
}
}
Re: cTrader Hotkey Profiles: Mapping Buy, Sell, Close-All and Reverse
Posted: Fri Oct 09, 2026 8:59 am
by FTtrader
Key Mechanical Additions
PositionClosePartial: The standard CTrade library handles the complex backend mechanics of partially closing a position ticket. This replaces the need to manually send counter-orders, working cleanly on both hedging and netting accounts.
Minimum Lot Safety Net: If a position is already down to 0.01 lots and you hit the Spacebar, 0.01 / 2 = 0.005. The MathFloor function rounds this to 0.00. The script checks if(closeVolume >= minLot) and safely aborts rather than throwing a server rejection error.
Re: cTrader Hotkey Profiles: Mapping Buy, Sell, Close-All and Reverse
Posted: Fri Oct 09, 2026 8:59 am
by FTtrader
This modification introduces a "Zero Risk" function mapped to the Z key (ASCII 90).
Moving a Stop Loss to Break-Even plus the spread requires a few critical mechanical checks. The script must calculate the exact, real-time spread using current Bid/Ask prices, verify that the position is far enough in profit to legally place the new Stop Loss, and normalize the new price level to the broker's digit requirements to prevent "Invalid Stops" server rejections.
Code: Select all
//+------------------------------------------------------------------+
//| DynamicRiskHotkeyScalper.mq5 |
//+------------------------------------------------------------------+
#property copyright "Custom MQL5 Script"
#property version "1.30"
#include <Trade\Trade.mqh>
//--- Input parameters
input double InpRiskPercent = 1.0; // Account Risk per Trade (%)
input int InpAtrPeriod = 14; // ATR Period
input double InpAtrMultiplier = 1.0; // ATR Multiplier for Stop Loss
input ulong InpMagic = 123456; // Magic Number
input ulong InpSlippage = 3; // Max Slippage (Points)
CTrade trade;
int atrHandle;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
trade.SetExpertMagicNumber(InpMagic);
trade.SetDeviationInPoints(InpSlippage);
atrHandle = iATR(_Symbol, PERIOD_CURRENT, InpAtrPeriod);
if(atrHandle == INVALID_HANDLE)
{
Print("Failed to create ATR indicator handle.");
return(INIT_FAILED);
}
Print("Dynamic Risk Hotkeys Initialized.");
Print("'B'=Buy, 'S'=Sell, 'C'=Close All, 'Space'=Scale 50%, 'Z'=BE+Spread.");
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| ChartEvent function |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
const long &lparam,
const double &dparam,
const string &sparam)
{
if(id == CHARTEVENT_KEYDOWN)
{
switch((int)lparam)
{
case 66: // 'B' key
ExecuteBuy();
break;
case 83: // 'S' key
ExecuteSell();
break;
case 67: // 'C' key
CloseAllCurrentSymbol();
break;
case 32: // Spacebar
ScaleOutCurrentSymbol();
break;
case 90: // 'Z' key (Zero Risk)
MoveToBreakEvenCurrentSymbol();
break;
}
}
}
//+------------------------------------------------------------------+
//| Lot Size Calculation |
//+------------------------------------------------------------------+
double CalculateLotSize(double slDistanceInPrice)
{
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskAmount = balance * (InpRiskPercent / 100.0);
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
if(tickSize == 0 || tickValue == 0 || slDistanceInPrice == 0)
return 0.0;
double riskPerLot = (slDistanceInPrice / tickSize) * tickValue;
double rawLot = riskAmount / riskPerLot;
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double stepLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double finalLot = MathFloor(rawLot / stepLot) * stepLot;
if(finalLot < minLot) finalLot = minLot;
if(finalLot > maxLot) finalLot = maxLot;
return finalLot;
}
//+------------------------------------------------------------------+
//| Execution Functions |
//+------------------------------------------------------------------+
void ExecuteBuy()
{
double atrArray[];
ArraySetAsSeries(atrArray, true);
if(CopyBuffer(atrHandle, 0, 1, 1, atrArray) <= 0) return;
double currentAtr = atrArray[0];
double slDistance = currentAtr * InpAtrMultiplier;
double volume = CalculateLotSize(slDistance);
if(volume <= 0) return;
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = ask - slDistance;
if(trade.Buy(volume, _Symbol, ask, sl, 0, "ATR Risk Buy"))
PrintFormat("Buy executed. Vol: %.2f, SL: %.5f", volume, sl);
}
void ExecuteSell()
{
double atrArray[];
ArraySetAsSeries(atrArray, true);
if(CopyBuffer(atrHandle, 0, 1, 1, atrArray) <= 0) return;
double currentAtr = atrArray[0];
double slDistance = currentAtr * InpAtrMultiplier;
double volume = CalculateLotSize(slDistance);
if(volume <= 0) return;
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double sl = bid + slDistance;
if(trade.Sell(volume, _Symbol, bid, sl, 0, "ATR Risk Sell"))
PrintFormat("Sell executed. Vol: %.2f, SL: %.5f", volume, sl);
}
void CloseAllCurrentSymbol()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
string position_symbol = PositionGetSymbol(i);
if(position_symbol == _Symbol)
{
ulong ticket = PositionGetInteger(POSITION_TICKET);
trade.PositionClose(ticket);
}
}
}
void ScaleOutCurrentSymbol()
{
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double stepLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
string position_symbol = PositionGetSymbol(i);
if(position_symbol == _Symbol)
{
ulong ticket = PositionGetInteger(POSITION_TICKET);
double currentVolume = PositionGetDouble(POSITION_VOLUME);
double closeVolume = MathFloor((currentVolume / 2.0) / stepLot) * stepLot;
if(closeVolume >= minLot) trade.PositionClosePartial(ticket, closeVolume);
}
}
}
void MoveToBreakEvenCurrentSymbol()
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double spread = ask - bid;
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
string position_symbol = PositionGetSymbol(i);
if(position_symbol == _Symbol)
{
ulong ticket = PositionGetInteger(POSITION_TICKET);
long type = PositionGetInteger(POSITION_TYPE);
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double currentSl = PositionGetDouble(POSITION_SL);
double currentTp = PositionGetDouble(POSITION_TP);
double newSl = 0.0;
bool shouldModify = false;
if(type == POSITION_TYPE_BUY)
{
newSl = openPrice + spread;
// Check if price has moved far enough into profit to allow this SL placement
if(bid > newSl && (currentSl < newSl || currentSl == 0)) shouldModify = true;
}
else if(type == POSITION_TYPE_SELL)
{
newSl = openPrice - spread;
if(ask < newSl && (currentSl > newSl || currentSl == 0)) shouldModify = true;
}
if(shouldModify)
{
newSl = NormalizeDouble(newSl, digits);
if(trade.PositionModify(ticket, newSl, currentTp))
PrintFormat("Position %d SL moved to Break-Even + Spread (%.5f)", ticket, newSl);
else
PrintFormat("Failed to move SL for ticket %d. Error: %d", ticket, GetLastError());
}
}
}
}
Re: cTrader Hotkey Profiles: Mapping Buy, Sell, Close-All and Reverse
Posted: Fri Oct 09, 2026 9:00 am
by FTtrader
Key Mechanical Additions
Dynamic Raw Spread Calculation: The script calculates ask - bid exactly when the Z key is pressed. This ensures you cover the live market spread at that exact millisecond, preventing a situation where spread widening during high volatility turns a Break-Even trade into a net loss.
Profitability Gate: The if(bid > newSl) and if(ask < newSl) conditions ensure the script only attempts to move the Stop Loss if the position is already deeply enough in profit. Attempting to place a Stop Loss inside the current spread or on the wrong side of the current market price will trigger a server error.
Double Normalization: NormalizeDouble(newSl, digits) ensures the calculated newSl exactly matches the broker's pricing architecture (e.g., 5 decimal places for spot forex, 2 for equities).
Re: cTrader Hotkey Profiles: Mapping Buy, Sell, Close-All and Reverse
Posted: Fri Oct 09, 2026 9:00 am
by FTtrader
When scalping 1-minute and 5-minute price action, raw execution speed dictates profitability. While a high-performance local environment like an HP Z1 workstation processing scripts on NVMe storage ensures local execution happens in fractions of a millisecond, the network round-trip to the broker is entirely out of your control.
Re: cTrader Hotkey Profiles: Mapping Buy, Sell, Close-All and Reverse
Posted: Fri Oct 09, 2026 9:00 am
by FTtrader
This modification implements high-resolution microsecond benchmarking using GetMicrosecondCount() and extracts deep server response data via trade.ResultRetcode() to expose exactly where delays or requotes are happening.
Code: Select all
//+------------------------------------------------------------------+
//| DynamicRiskHotkeyScalper.mq5 |
//+------------------------------------------------------------------+
#property copyright "Custom MQL5 Script"
#property version "1.40"
#include <Trade\Trade.mqh>
//--- Input parameters
input double InpRiskPercent = 1.0; // Account Risk per Trade (%)
input int InpAtrPeriod = 14; // ATR Period
input double InpAtrMultiplier = 1.0; // ATR Multiplier for Stop Loss
input ulong InpMagic = 123456; // Magic Number
input ulong InpSlippage = 3; // Max Slippage (Points)
CTrade trade;
int atrHandle;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
trade.SetExpertMagicNumber(InpMagic);
trade.SetDeviationInPoints(InpSlippage);
atrHandle = iATR(_Symbol, PERIOD_CURRENT, InpAtrPeriod);
if(atrHandle == INVALID_HANDLE)
{
Print("Failed to create ATR indicator handle.");
return(INIT_FAILED);
}
Print("Dynamic Risk Hotkeys Initialized (Diagnostics Enabled).");
Print("'B'=Buy, 'S'=Sell, 'C'=Close All, 'Space'=Scale 50%, 'Z'=BE+Spread.");
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| ChartEvent function |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
const long &lparam,
const double &dparam,
const string &sparam)
{
if(id == CHARTEVENT_KEYDOWN)
{
switch((int)lparam)
{
case 66: // 'B' key
ExecuteBuy();
break;
case 83: // 'S' key
ExecuteSell();
break;
case 67: // 'C' key
CloseAllCurrentSymbol();
break;
case 32: // Spacebar
ScaleOutCurrentSymbol();
break;
case 90: // 'Z' key (Zero Risk)
MoveToBreakEvenCurrentSymbol();
break;
}
}
}
//+------------------------------------------------------------------+
//| Diagnostics & Logging Helper |
//+------------------------------------------------------------------+
void LogExecutionResult(ulong startTimeMicro, string action, double requestedPrice, bool success)
{
ulong endTimeMicro = GetMicrosecondCount();
double latencyMs = (endTimeMicro - startTimeMicro) / 1000.0;
uint retcode = trade.ResultRetcode();
string retDesc = trade.ResultRetcodeDescription();
if(success)
{
double executedPrice = trade.ResultPrice();
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double slippagePoints = (tickSize > 0) ? MathAbs(requestedPrice - executedPrice) / tickSize : 0;
PrintFormat("[EXECUTION] %s | Latency: %.2f ms | Requested: %.5f | Executed: %.5f | Slippage: %.1f points | Retcode: %u (%s)",
action, latencyMs, requestedPrice, executedPrice, slippagePoints, retcode, retDesc);
}
else
{
PrintFormat("[REJECTION] %s | Latency: %.2f ms | Requested: %.5f | Retcode: %u (%s) | LastError: %d",
action, latencyMs, requestedPrice, retcode, retDesc, GetLastError());
}
}
//+------------------------------------------------------------------+
//| Lot Size Calculation |
//+------------------------------------------------------------------+
double CalculateLotSize(double slDistanceInPrice)
{
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskAmount = balance * (InpRiskPercent / 100.0);
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
if(tickSize == 0 || tickValue == 0 || slDistanceInPrice == 0) return 0.0;
double riskPerLot = (slDistanceInPrice / tickSize) * tickValue;
double rawLot = riskAmount / riskPerLot;
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double stepLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double finalLot = MathFloor(rawLot / stepLot) * stepLot;
if(finalLot < minLot) finalLot = minLot;
if(finalLot > maxLot) finalLot = maxLot;
return finalLot;
}
//+------------------------------------------------------------------+
//| Execution Functions |
//+------------------------------------------------------------------+
void ExecuteBuy()
{
double atrArray[];
ArraySetAsSeries(atrArray, true);
if(CopyBuffer(atrHandle, 0, 1, 1, atrArray) <= 0) return;
double currentAtr = atrArray[0];
double slDistance = currentAtr * InpAtrMultiplier;
double volume = CalculateLotSize(slDistance);
if(volume <= 0) return;
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = ask - slDistance;
ulong startTime = GetMicrosecondCount();
bool success = trade.Buy(volume, _Symbol, ask, sl, 0, "ATR Risk Buy");
LogExecutionResult(startTime, "Buy", ask, success);
}
void ExecuteSell()
{
double atrArray[];
ArraySetAsSeries(atrArray, true);
if(CopyBuffer(atrHandle, 0, 1, 1, atrArray) <= 0) return;
double currentAtr = atrArray[0];
double slDistance = currentAtr * InpAtrMultiplier;
double volume = CalculateLotSize(slDistance);
if(volume <= 0) return;
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double sl = bid + slDistance;
ulong startTime = GetMicrosecondCount();
bool success = trade.Sell(volume, _Symbol, bid, sl, 0, "ATR Risk Sell");
LogExecutionResult(startTime, "Sell", bid, success);
}
void CloseAllCurrentSymbol()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
string position_symbol = PositionGetSymbol(i);
if(position_symbol == _Symbol)
{
ulong ticket = PositionGetInteger(POSITION_TICKET);
ulong startTime = GetMicrosecondCount();
bool success = trade.PositionClose(ticket);
if(success)
{
double latencyMs = (GetMicrosecondCount() - startTime) / 1000.0;
PrintFormat("[EXECUTION] Close All | Ticket: %d | Latency: %.2f ms", ticket, latencyMs);
}
else
{
PrintFormat("[REJECTION] Close All | Ticket: %d | Retcode: %u | Error: %d",
ticket, trade.ResultRetcode(), GetLastError());
}
}
}
}
void ScaleOutCurrentSymbol()
{
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double stepLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
string position_symbol = PositionGetSymbol(i);
if(position_symbol == _Symbol)
{
ulong ticket = PositionGetInteger(POSITION_TICKET);
double currentVolume = PositionGetDouble(POSITION_VOLUME);
double closeVolume = MathFloor((currentVolume / 2.0) / stepLot) * stepLot;
if(closeVolume >= minLot)
{
ulong startTime = GetMicrosecondCount();
bool success = trade.PositionClosePartial(ticket, closeVolume);
if(success)
PrintFormat("[EXECUTION] Scale Out %.2f | Ticket: %d | Latency: %.2f ms", closeVolume, ticket, (GetMicrosecondCount() - startTime) / 1000.0);
else
PrintFormat("[REJECTION] Scale Out | Ticket: %d | Retcode: %u", ticket, trade.ResultRetcode());
}
}
}
}
void MoveToBreakEvenCurrentSymbol()
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double spread = ask - bid;
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
string position_symbol = PositionGetSymbol(i);
if(position_symbol == _Symbol)
{
ulong ticket = PositionGetInteger(POSITION_TICKET);
long type = PositionGetInteger(POSITION_TYPE);
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double currentSl = PositionGetDouble(POSITION_SL);
double currentTp = PositionGetDouble(POSITION_TP);
double newSl = 0.0;
bool shouldModify = false;
if(type == POSITION_TYPE_BUY)
{
newSl = openPrice + spread;
if(bid > newSl && (currentSl < newSl || currentSl == 0)) shouldModify = true;
}
else if(type == POSITION_TYPE_SELL)
{
newSl = openPrice - spread;
if(ask < newSl && (currentSl > newSl || currentSl == 0)) shouldModify = true;
}
if(shouldModify)
{
newSl = NormalizeDouble(newSl, digits);
ulong startTime = GetMicrosecondCount();
bool success = trade.PositionModify(ticket, newSl, currentTp);
if(success)
PrintFormat("[EXECUTION] BE+Spread Moved | Ticket: %d | Latency: %.2f ms", ticket, (GetMicrosecondCount() - startTime) / 1000.0);
else
PrintFormat("[REJECTION] BE+Spread | Ticket: %d | Retcode: %u", ticket, trade.ResultRetcode());
}
}
}
}
Re: cTrader Hotkey Profiles: Mapping Buy, Sell, Close-All and Reverse
Posted: Fri Oct 09, 2026 9:01 am
by FTtrader
Key Diagnostic Features
Microsecond Precision Benchmarking: Wrapping the core order calls in GetMicrosecondCount() measures strictly the time spent communicating with the trade server. Anything consistently over 50–80 ms indicates you may need to migrate to a VPS closer to the broker's liquidity provider.
Slippage Calculation: The script captures trade.ResultPrice()—the actual price filled on the broker's backend—and subtracts the requested price, divided by SYMBOL_TRADE_TICK_SIZE. This logs exactly how many points you slipped between pushing the hotkey and being filled.
Retcode Isolation: trade.ResultRetcodeDescription() outputs the exact server objection (e.g., "Requote", "Invalid Stops", "Market Closed", "Off Quotes") rather than the generic integer returned by GetLastError().
Re: cTrader Hotkey Profiles: Mapping Buy, Sell, Close-All and Reverse
Posted: Fri Oct 09, 2026 9:02 am
by FTtrader
Unlike MetaTrader's OnChartEvent which requires capturing raw ASCII codes, cTrader exposes a native Chart.KeyDown event wrapper with strongly typed enums.
Additionally, because you are building this to handling enterprise-grade C#, we can leverage standard .NET libraries like System.Diagnostics.Stopwatch to achieve high-resolution execution profiling directly on the broker's API layer.
Re: cTrader Hotkey Profiles: Mapping Buy, Sell, Close-All and Reverse
Posted: Fri Oct 09, 2026 9:02 am
by FTtrader
Here is the complete C# cBot port of the MQL5 script, optimized for cTrader's TradeResult mechanics and native volume normalization:
Code: Select all
using System;
using System.Linq;
using System.Diagnostics;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;
namespace cAlgo.Robots
{
[Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
public class DynamicRiskHotkeyScalper : Robot
{
[Parameter("Risk Percent", DefaultValue = 1.0, MinValue = 0.1, Step = 0.1)]
public double RiskPercent { get; set; }
[Parameter("ATR Period", DefaultValue = 14, MinValue = 1)]
public int AtrPeriod { get; set; }
[Parameter("ATR Multiplier", DefaultValue = 1.0, MinValue = 0.1, Step = 0.1)]
public double AtrMultiplier { get; set; }
private AverageTrueRange _atr;
private const string Label = "HotkeyScalper";
protected override void OnStart()
{
_atr = Indicators.AverageTrueRange(AtrPeriod, MovingAverageType.Simple);
// Subscribe to chart keyboard events
Chart.KeyDown += Chart_KeyDown;
Print("cTrader Hotkeys Initialized (Diagnostics Enabled).");
Print("'B'=Buy, 'S'=Sell, 'C'=Close All, 'Space'=Scale 50%, 'Z'=BE+Spread.");
}
private void Chart_KeyDown(ChartKeyboardEventArgs obj)
{
// Ignore keystrokes if modifiers are held to avoid colliding with native platform shortcuts
if (obj.AltKey || obj.CtrlKey || obj.ShiftKey) return;
switch (obj.Key)
{
case Key.B:
ExecuteRiskTrade(TradeType.Buy);
break;
case Key.S:
ExecuteRiskTrade(TradeType.Sell);
break;
case Key.C:
CloseAllCurrentSymbol();
break;
case Key.Space:
ScaleOutCurrentSymbol();
break;
case Key.Z:
MoveToBreakEven();
break;
}
}
private void ExecuteRiskTrade(TradeType tradeType)
{
// Sample ATR from the last closed candle to prevent mid-tick sizing shifts
double currentAtr = _atr.Result.Last(1);
double slDistanceInPrice = currentAtr * AtrMultiplier;
double slDistanceInPips = slDistanceInPrice / Symbol.PipSize;
if (slDistanceInPips <= 0 || Symbol.PipValue <= 0) return;
// Calculate monetary risk and exact volume
double riskAmount = Account.Balance * (RiskPercent / 100.0);
double rawVolume = riskAmount / (slDistanceInPips * Symbol.PipValue);
// Normalize volume to strictly match broker requirements (min lot, step lot)
double normalizedVolume = Symbol.NormalizeVolumeInUnits(rawVolume, RoundingMode.Down);
if (normalizedVolume < Symbol.VolumeInUnitsMin)
{
Print("Calculated volume ({0}) is below broker minimum ({1}).", normalizedVolume, Symbol.VolumeInUnitsMin);
return;
}
double requestedPrice = tradeType == TradeType.Buy ? Symbol.Ask : Symbol.Bid;
Stopwatch sw = Stopwatch.StartNew();
TradeResult result = ExecuteMarketOrder(tradeType, SymbolName, normalizedVolume, Label, slDistanceInPips, null);
sw.Stop();
LogExecutionResult(tradeType.ToString(), requestedPrice, result, sw.Elapsed.TotalMilliseconds);
}
private void CloseAllCurrentSymbol()
{
var positions = Positions.FindAll(Label, SymbolName);
foreach (var pos in positions)
{
Stopwatch sw = Stopwatch.StartNew();
TradeResult result = ClosePosition(pos);
sw.Stop();
LogExecutionResult("CloseAll", 0, result, sw.Elapsed.TotalMilliseconds);
}
}
private void ScaleOutCurrentSymbol()
{
var positions = Positions.FindAll(Label, SymbolName);
foreach (var pos in positions)
{
// Calculate 50% of current volume, rounded down to nearest valid step size
double targetCloseVolume = Symbol.NormalizeVolumeInUnits(pos.VolumeInUnits / 2.0, RoundingMode.Down);
if (targetCloseVolume >= Symbol.VolumeInUnitsMin)
{
Stopwatch sw = Stopwatch.StartNew();
// In cTrader, closing a partial amount of a position modifies the original position naturally
TradeResult result = ClosePosition(pos, targetCloseVolume);
sw.Stop();
LogExecutionResult("ScaleOut", 0, result, sw.Elapsed.TotalMilliseconds);
}
}
}
private void MoveToBreakEven()
{
var positions = Positions.FindAll(Label, SymbolName);
foreach (var pos in positions)
{
double spread = Symbol.Spread;
double newSl = 0.0;
bool shouldModify = false;
if (pos.TradeType == TradeType.Buy)
{
newSl = pos.EntryPrice + spread;
// Check if current Bid is high enough to place SL at Break-Even
if (Symbol.Bid > newSl && (pos.StopLoss < newSl || pos.StopLoss == null))
shouldModify = true;
}
else if (pos.TradeType == TradeType.Sell)
{
newSl = pos.EntryPrice - spread;
if (Symbol.Ask < newSl && (pos.StopLoss > newSl || pos.StopLoss == null))
shouldModify = true;
}
if (shouldModify)
{
// Normalize the price to match broker digit requirements
newSl = Math.Round(newSl, Symbol.Digits);
Stopwatch sw = Stopwatch.StartNew();
TradeResult result = ModifyPosition(pos, newSl, pos.TakeProfit);
sw.Stop();
LogExecutionResult("BE+Spread", 0, result, sw.Elapsed.TotalMilliseconds);
}
}
}
private void LogExecutionResult(string action, double requestedPrice, TradeResult result, double latencyMs)
{
if (result.IsSuccessful)
{
if (action == "Buy" || action == "Sell")
{
double executedPrice = result.Position.EntryPrice;
double slippagePips = Math.Abs(requestedPrice - executedPrice) / Symbol.PipSize;
Print("[EXECUTION] {0} | Latency: {1:F2} ms | Req: {2:F5} | Exec: {3:F5} | Slippage: {4:F1} pips",
action, latencyMs, requestedPrice, executedPrice, slippagePips);
}
else
{
Print("[EXECUTION] {0} | Latency: {1:F2} ms | Ticket: {2}", action, latencyMs, result.Position?.Id);
}
}
else
{
Print("[REJECTION] {0} | Latency: {1:F2} ms | Error: {2}", action, latencyMs, result.Error);
}
}
}
}
Re: cTrader Hotkey Profiles: Mapping Buy, Sell, Close-All and Reverse
Posted: Fri Oct 09, 2026 9:03 am
by FTtrader
Key Architectural Shifts for C# cTrader
Symbol.NormalizeVolumeInUnits: cTrader handles the complex MathFloor and step-size logic natively. You pass it a raw calculated float of units, request RoundingMode.Down, and it returns a perfectly compliant volume for the broker API.
Partial Scale-Outs: Instead of manually querying tickets, cTrader allows you to pass a specific volume directly into the ClosePosition(position, targetVolume) overload. The broker API handles the ticket splitting in the background.
System.Diagnostics.Stopwatch: Because the Z1 workstation runs this natively within the CLR, Stopwatch.Elapsed.TotalMilliseconds provides highly accurate fractional millisecond tracking for network latency, entirely replacing MQL5's GetMicrosecondCount().
TradeResult Handling: cTrader returns a rich TradeResult object for every server interaction. Instead of calling GetLastError(), the rejection logic accesses result.Error directly, outputting exact enum flags (e.g., MarketClosed, InvalidVolume, Timeout).