Changes: - Add FAL_KEY and GEMINI_API_KEY to .env.example - Update picture-it to use ~/.config/opencode/.env (unified creds) - Remove shodh-memory skill (no longer used) - Remove alphaear-* skills (deprecated) - Remove thai-frontend-dev skill (replaced by website-creator) - Remove theme-factory skill - Add mql-developer skill (MQL5 trading) - Add ecommerce-astro skill (Astro e-commerce) - Add website-creator skill (Next.js + Payload CMS) - Update install script for new skills
1143 lines
34 KiB
Markdown
1143 lines
34 KiB
Markdown
# Trading Operations Reference
|
|
|
|
## Table of Contents
|
|
|
|
- [Order Management - MQL4](#order-management---mql4)
|
|
- [Order Management - MQL5](#order-management---mql5)
|
|
- [Risk Management](#risk-management)
|
|
- [Trailing Stop Implementations](#trailing-stop-implementations)
|
|
- [Trade Return Codes (MQL5)](#trade-return-codes-mql5)
|
|
- [Runtime Persistence Across TF Changes (MT5)](#runtime-persistence-across-timeframe-changes-mt5)
|
|
- [Trailing Stop Implementations](#trailing-stop-implementations)
|
|
|
|
---
|
|
|
|
## Order Management - MQL4
|
|
|
|
### OrderSend with Error Handling and Retry
|
|
|
|
```mql4
|
|
int SendOrderReliable(string symbol, int cmd, double lots, double price,
|
|
int slippage, double sl, double tp,
|
|
string comment, int magic, int maxRetries = 5)
|
|
{
|
|
int ticket = -1;
|
|
int digits = (int)MarketInfo(symbol, MODE_DIGITS);
|
|
double point = MarketInfo(symbol, MODE_POINT);
|
|
double stopLevel = MarketInfo(symbol, MODE_STOPLEVEL) * point;
|
|
|
|
price = NormalizeDouble(price, digits);
|
|
sl = NormalizeDouble(sl, digits);
|
|
tp = NormalizeDouble(tp, digits);
|
|
|
|
// Validate stop level distances
|
|
if(sl != 0)
|
|
{
|
|
if(cmd == OP_BUY && (price - sl) < stopLevel)
|
|
{
|
|
Print("SL too close. Min distance: ", stopLevel / point, " points");
|
|
return(-1);
|
|
}
|
|
if(cmd == OP_SELL && (sl - price) < stopLevel)
|
|
{
|
|
Print("SL too close. Min distance: ", stopLevel / point, " points");
|
|
return(-1);
|
|
}
|
|
}
|
|
|
|
for(int attempt = 0; attempt < maxRetries; attempt++)
|
|
{
|
|
if(attempt > 0)
|
|
{
|
|
int sleepMs = 1000 * (int)MathPow(2, attempt - 1); // 1s, 2s, 4s, 8s
|
|
Sleep(sleepMs);
|
|
RefreshRates();
|
|
// Update price for market orders
|
|
if(cmd == OP_BUY) price = MarketInfo(symbol, MODE_ASK);
|
|
if(cmd == OP_SELL) price = MarketInfo(symbol, MODE_BID);
|
|
price = NormalizeDouble(price, digits);
|
|
}
|
|
|
|
ticket = OrderSend(symbol, cmd, lots, price, slippage, sl, tp, comment, magic, 0, clrNONE);
|
|
|
|
if(ticket >= 0)
|
|
return(ticket);
|
|
|
|
int err = GetLastError();
|
|
Print("OrderSend attempt ", attempt + 1, " failed. Error: ", err, " - ", ErrorDescription(err));
|
|
|
|
// Retryable errors
|
|
if(err == ERR_REQUOTE ||
|
|
err == ERR_PRICE_CHANGED ||
|
|
err == ERR_OFF_QUOTES ||
|
|
err == ERR_SERVER_BUSY ||
|
|
err == ERR_BROKER_BUSY ||
|
|
err == ERR_TRADE_CONTEXT_BUSY ||
|
|
err == ERR_TOO_MANY_REQUESTS ||
|
|
err == ERR_TRADE_TIMEOUT ||
|
|
err == ERR_NO_CONNECTION)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Non-retryable errors - exit immediately
|
|
break;
|
|
}
|
|
|
|
return(-1);
|
|
}
|
|
```
|
|
|
|
### Order Modification Pattern
|
|
|
|
```mql4
|
|
bool ModifyOrderReliable(int ticket, double sl, double tp)
|
|
{
|
|
if(!OrderSelect(ticket, SELECT_BY_TICKET))
|
|
{
|
|
Print("Order ", ticket, " not found");
|
|
return(false);
|
|
}
|
|
|
|
int digits = (int)MarketInfo(OrderSymbol(), MODE_DIGITS);
|
|
sl = NormalizeDouble(sl, digits);
|
|
tp = NormalizeDouble(tp, digits);
|
|
|
|
// Skip if nothing changed
|
|
if(MathAbs(OrderStopLoss() - sl) < Point / 2.0 &&
|
|
MathAbs(OrderTakeProfit() - tp) < Point / 2.0)
|
|
return(true);
|
|
|
|
bool result = OrderModify(ticket, OrderOpenPrice(), sl, tp, OrderExpiration(), clrNONE);
|
|
|
|
if(!result)
|
|
{
|
|
int err = GetLastError();
|
|
Print("OrderModify failed for ticket ", ticket, ". Error: ", err, " - ", ErrorDescription(err));
|
|
}
|
|
|
|
return(result);
|
|
}
|
|
```
|
|
|
|
### Order Loop Patterns
|
|
|
|
**CRITICAL: Always use reverse loops when closing/deleting orders.** Closing an order shifts indices, so forward loops skip orders.
|
|
|
|
#### Close All Orders for a Symbol
|
|
|
|
```mql4
|
|
void CloseAllOrders(string symbol, int magic)
|
|
{
|
|
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
|
|
if(OrderSymbol() != symbol) continue;
|
|
if(OrderMagicNumber() != magic) continue;
|
|
|
|
bool result = false;
|
|
if(OrderType() == OP_BUY)
|
|
result = OrderClose(OrderTicket(), OrderLots(), MarketInfo(symbol, MODE_BID), 3, clrNONE);
|
|
else if(OrderType() == OP_SELL)
|
|
result = OrderClose(OrderTicket(), OrderLots(), MarketInfo(symbol, MODE_ASK), 3, clrNONE);
|
|
else
|
|
result = OrderDelete(OrderTicket()); // Pending orders
|
|
|
|
if(!result)
|
|
Print("Failed to close order ", OrderTicket(), ". Error: ", GetLastError());
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Count Orders by Type and Magic Number
|
|
|
|
```mql4
|
|
int CountOrders(string symbol, int magic, int orderType = -1)
|
|
{
|
|
int count = 0;
|
|
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
|
|
if(OrderSymbol() != symbol) continue;
|
|
if(OrderMagicNumber() != magic) continue;
|
|
if(orderType >= 0 && OrderType() != orderType) continue;
|
|
count++;
|
|
}
|
|
return(count);
|
|
}
|
|
```
|
|
|
|
#### Find Most Recent Order
|
|
|
|
```mql4
|
|
int FindLatestOrder(string symbol, int magic)
|
|
{
|
|
int latestTicket = -1;
|
|
datetime latestTime = 0;
|
|
|
|
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
|
|
if(OrderSymbol() != symbol) continue;
|
|
if(OrderMagicNumber() != magic) continue;
|
|
|
|
if(OrderOpenTime() > latestTime)
|
|
{
|
|
latestTime = OrderOpenTime();
|
|
latestTicket = OrderTicket();
|
|
}
|
|
}
|
|
return(latestTicket);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Order Management - MQL5
|
|
|
|
### CTrade Setup
|
|
|
|
```mql5
|
|
#include <Trade/Trade.mqh>
|
|
|
|
CTrade trade;
|
|
|
|
int OnInit()
|
|
{
|
|
trade.SetExpertMagicNumber(MagicNumber);
|
|
trade.SetDeviationInPoints(Slippage);
|
|
SetFillingPolicy(trade, _Symbol);
|
|
return(INIT_SUCCEEDED);
|
|
}
|
|
```
|
|
|
|
### Filling Policy Detection
|
|
|
|
Brokers support different order filling modes. Setting the wrong filling type causes order rejection.
|
|
|
|
```mql5
|
|
void SetFillingPolicy(CTrade &trade, string symbol)
|
|
{
|
|
long filling = SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE);
|
|
|
|
if((filling & SYMBOL_FILLING_FOK) != 0)
|
|
trade.SetTypeFilling(ORDER_FILLING_FOK);
|
|
else if((filling & SYMBOL_FILLING_IOC) != 0)
|
|
trade.SetTypeFilling(ORDER_FILLING_IOC);
|
|
else
|
|
trade.SetTypeFilling(ORDER_FILLING_RETURN);
|
|
}
|
|
```
|
|
|
|
**Filling mode meanings:**
|
|
- **FOK (Fill or Kill):** Entire order must be filled at the requested price or better, otherwise it is cancelled entirely.
|
|
- **IOC (Immediate or Cancel):** Fill as much as possible immediately, cancel the remainder.
|
|
- **RETURN:** Partial fills allowed; unfilled portion remains as a live order. Common on exchange-traded instruments.
|
|
|
|
### Trade with Retry Logic (MQL5)
|
|
|
|
```mql5
|
|
class CTradeManager
|
|
{
|
|
private:
|
|
CTrade m_trade;
|
|
int m_maxRetries;
|
|
int m_magic;
|
|
|
|
bool IsRetryable(uint retcode)
|
|
{
|
|
switch(retcode)
|
|
{
|
|
case TRADE_RETCODE_REQUOTE: // 10004
|
|
case TRADE_RETCODE_PRICE_CHANGED: // 10020
|
|
case TRADE_RETCODE_PRICE_OFF: // 10021
|
|
case TRADE_RETCODE_CONNECTION: // 10031
|
|
case TRADE_RETCODE_TIMEOUT: // 10012
|
|
case TRADE_RETCODE_TOO_MANY_REQUESTS:// 10024
|
|
return(true);
|
|
default:
|
|
return(false);
|
|
}
|
|
}
|
|
|
|
public:
|
|
CTradeManager(int magic, int slippage, int maxRetries = 5)
|
|
{
|
|
m_magic = magic;
|
|
m_maxRetries = maxRetries;
|
|
m_trade.SetExpertMagicNumber(magic);
|
|
m_trade.SetDeviationInPoints(slippage);
|
|
}
|
|
|
|
bool Init(string symbol)
|
|
{
|
|
SetFillingPolicy(m_trade, symbol);
|
|
return(true);
|
|
}
|
|
|
|
bool Buy(string symbol, double lots, double sl = 0, double tp = 0, string comment = "")
|
|
{
|
|
for(int attempt = 0; attempt < m_maxRetries; attempt++)
|
|
{
|
|
if(attempt > 0)
|
|
Sleep(1000 * (int)MathPow(2, attempt - 1));
|
|
|
|
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
|
|
if(m_trade.Buy(lots, symbol, ask, sl, tp, comment))
|
|
{
|
|
uint retcode = m_trade.ResultRetcode();
|
|
if(retcode == TRADE_RETCODE_DONE || retcode == TRADE_RETCODE_PLACED)
|
|
return(true);
|
|
|
|
if(!IsRetryable(retcode))
|
|
{
|
|
Print("Buy failed (non-retryable): ", retcode, " - ", m_trade.ResultRetcodeDescription());
|
|
return(false);
|
|
}
|
|
Print("Buy attempt ", attempt + 1, " retryable error: ", retcode);
|
|
}
|
|
else
|
|
{
|
|
uint retcode = m_trade.ResultRetcode();
|
|
if(!IsRetryable(retcode))
|
|
{
|
|
Print("Buy failed (non-retryable): ", retcode, " - ", m_trade.ResultRetcodeDescription());
|
|
return(false);
|
|
}
|
|
Print("Buy attempt ", attempt + 1, " retryable error: ", retcode);
|
|
}
|
|
}
|
|
Print("Buy failed after ", m_maxRetries, " attempts");
|
|
return(false);
|
|
}
|
|
|
|
bool Sell(string symbol, double lots, double sl = 0, double tp = 0, string comment = "")
|
|
{
|
|
for(int attempt = 0; attempt < m_maxRetries; attempt++)
|
|
{
|
|
if(attempt > 0)
|
|
Sleep(1000 * (int)MathPow(2, attempt - 1));
|
|
|
|
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
|
|
if(m_trade.Sell(lots, symbol, bid, sl, tp, comment))
|
|
{
|
|
uint retcode = m_trade.ResultRetcode();
|
|
if(retcode == TRADE_RETCODE_DONE || retcode == TRADE_RETCODE_PLACED)
|
|
return(true);
|
|
|
|
if(!IsRetryable(retcode))
|
|
{
|
|
Print("Sell failed (non-retryable): ", retcode, " - ", m_trade.ResultRetcodeDescription());
|
|
return(false);
|
|
}
|
|
Print("Sell attempt ", attempt + 1, " retryable error: ", retcode);
|
|
}
|
|
else
|
|
{
|
|
uint retcode = m_trade.ResultRetcode();
|
|
if(!IsRetryable(retcode))
|
|
{
|
|
Print("Sell failed (non-retryable): ", retcode, " - ", m_trade.ResultRetcodeDescription());
|
|
return(false);
|
|
}
|
|
Print("Sell attempt ", attempt + 1, " retryable error: ", retcode);
|
|
}
|
|
}
|
|
Print("Sell failed after ", m_maxRetries, " attempts");
|
|
return(false);
|
|
}
|
|
|
|
CTrade* GetTrade() { return(&m_trade); }
|
|
};
|
|
```
|
|
|
|
### Position Loop Patterns (MQL5)
|
|
|
|
#### Iterate All Positions with Filtering
|
|
|
|
```mql5
|
|
void ProcessPositions(string symbol, int magic)
|
|
{
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
|
|
if(PositionGetString(POSITION_SYMBOL) != symbol) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != magic) continue;
|
|
|
|
double volume = PositionGetDouble(POSITION_VOLUME);
|
|
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
double profit = PositionGetDouble(POSITION_PROFIT);
|
|
long type = PositionGetInteger(POSITION_TYPE);
|
|
|
|
// Process position...
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Close All Positions for a Symbol
|
|
|
|
```mql5
|
|
void CloseAllPositions(string symbol, int magic)
|
|
{
|
|
CTrade trade;
|
|
trade.SetExpertMagicNumber(magic);
|
|
SetFillingPolicy(trade, symbol);
|
|
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != symbol) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != magic) continue;
|
|
|
|
if(!trade.PositionClose(ticket))
|
|
Print("Failed to close position ", ticket, ": ", trade.ResultRetcodeDescription());
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Count Positions by Type
|
|
|
|
```mql5
|
|
int CountPositions(string symbol, int magic, ENUM_POSITION_TYPE posType = -1)
|
|
{
|
|
int count = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != symbol) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != magic) continue;
|
|
if(posType >= 0 && (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) != posType) continue;
|
|
count++;
|
|
}
|
|
return(count);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Risk Management
|
|
|
|
### Position Sizing Formulas
|
|
|
|
#### Fixed Lot
|
|
|
|
```mql5
|
|
double FixedLot(double lotSize, string symbol)
|
|
{
|
|
double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
|
double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
|
|
double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
|
|
|
|
lotSize = MathFloor(lotSize / lotStep) * lotStep;
|
|
lotSize = MathMax(minLot, MathMin(maxLot, lotSize));
|
|
return(NormalizeDouble(lotSize, 2));
|
|
}
|
|
```
|
|
|
|
Simplest approach but ignores account growth and does not adapt to varying stop loss distances.
|
|
|
|
#### Percentage Risk (Recommended Default)
|
|
|
|
The core formula:
|
|
|
|
```
|
|
lots = (AccountBalance * RiskPercent / 100) / (SL_Points * TickValue / TickSize)
|
|
```
|
|
|
|
**MQL5 Implementation:**
|
|
|
|
```mql5
|
|
double CalcLotSize(string symbol, double riskPercent, double slPoints)
|
|
{
|
|
if(slPoints <= 0)
|
|
{
|
|
Print("CalcLotSize: SL distance must be > 0");
|
|
return(0);
|
|
}
|
|
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
double tickVal = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
|
|
double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
|
double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
|
|
double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
|
|
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
|
|
if(tickVal == 0 || tickSize == 0)
|
|
{
|
|
Print("CalcLotSize: Invalid tick info for ", symbol);
|
|
return(minLot);
|
|
}
|
|
|
|
double riskMoney = balance * riskPercent / 100.0;
|
|
double slValue = slPoints * point * (tickVal / tickSize);
|
|
double lots = riskMoney / slValue;
|
|
|
|
// Round down to lot step
|
|
lots = MathFloor(lots / lotStep) * lotStep;
|
|
|
|
// Clamp
|
|
lots = MathMax(minLot, MathMin(maxLot, lots));
|
|
|
|
// Verify margin is sufficient
|
|
double margin = 0;
|
|
if(!OrderCalcMargin(ORDER_TYPE_BUY, symbol, lots, SymbolInfoDouble(symbol, SYMBOL_ASK), margin))
|
|
{
|
|
Print("CalcLotSize: OrderCalcMargin failed");
|
|
return(minLot);
|
|
}
|
|
|
|
if(margin > AccountInfoDouble(ACCOUNT_MARGIN_FREE))
|
|
{
|
|
Print("CalcLotSize: Insufficient free margin. Required: ", margin);
|
|
// Reduce lots to fit available margin
|
|
double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE) * 0.95; // 5% safety buffer
|
|
lots = lots * (freeMargin / margin);
|
|
lots = MathFloor(lots / lotStep) * lotStep;
|
|
lots = MathMax(minLot, lots);
|
|
}
|
|
|
|
return(NormalizeDouble(lots, 2));
|
|
}
|
|
```
|
|
|
|
**MQL4 Implementation:**
|
|
|
|
```mql4
|
|
double CalcLotSize_MQL4(string symbol, double riskPercent, double slPoints)
|
|
{
|
|
if(slPoints <= 0)
|
|
{
|
|
Print("CalcLotSize: SL distance must be > 0");
|
|
return(0);
|
|
}
|
|
|
|
double balance = AccountBalance();
|
|
double tickVal = MarketInfo(symbol, MODE_TICKVALUE);
|
|
double tickSize = MarketInfo(symbol, MODE_TICKSIZE);
|
|
double minLot = MarketInfo(symbol, MODE_MINLOT);
|
|
double maxLot = MarketInfo(symbol, MODE_MAXLOT);
|
|
double lotStep = MarketInfo(symbol, MODE_LOTSTEP);
|
|
double point = MarketInfo(symbol, MODE_POINT);
|
|
|
|
if(tickVal == 0 || tickSize == 0)
|
|
{
|
|
Print("CalcLotSize: Invalid tick info for ", symbol);
|
|
return(minLot);
|
|
}
|
|
|
|
double riskMoney = balance * riskPercent / 100.0;
|
|
double slValue = slPoints * point * (tickVal / tickSize);
|
|
double lots = riskMoney / slValue;
|
|
|
|
lots = MathFloor(lots / lotStep) * lotStep;
|
|
lots = MathMax(minLot, MathMin(maxLot, lots));
|
|
|
|
// Verify margin
|
|
double marginRequired = AccountFreeMarginCheck(symbol, OP_BUY, lots);
|
|
if(marginRequired <= 0)
|
|
{
|
|
Print("CalcLotSize: Insufficient margin");
|
|
return(minLot);
|
|
}
|
|
|
|
return(NormalizeDouble(lots, 2));
|
|
}
|
|
```
|
|
|
|
#### Kelly Criterion
|
|
|
|
```
|
|
kelly_fraction = (winRate * avgWin - (1 - winRate) * avgLoss) / avgWin
|
|
```
|
|
|
|
```mql5
|
|
double KellyLotSize(string symbol, double winRate, double avgWin, double avgLoss,
|
|
double slPoints, double kellyFraction = 0.25)
|
|
{
|
|
// Full Kelly is too aggressive; use quarter-Kelly as maximum
|
|
double kelly = (winRate * avgWin - (1.0 - winRate) * avgLoss) / avgWin;
|
|
|
|
if(kelly <= 0)
|
|
{
|
|
Print("Kelly criterion negative - system has no edge");
|
|
return(0);
|
|
}
|
|
|
|
double riskPercent = kelly * kellyFraction * 100.0;
|
|
riskPercent = MathMin(riskPercent, 5.0); // Hard cap at 5%
|
|
|
|
return(CalcLotSize(symbol, riskPercent, slPoints));
|
|
}
|
|
```
|
|
|
|
**WARNING:** Full Kelly sizing leads to extreme drawdowns in practice. Always use quarter-Kelly (kellyFraction = 0.25) or less. Even quarter-Kelly can produce 40-50% drawdowns. Most professional traders use eighth-Kelly or fixed fractional risk of 1-2%.
|
|
|
|
### Multi-Market Lot Calculation
|
|
|
|
Different instruments have different contract sizes and tick values:
|
|
|
|
| Market | Typical Contract Size | Notes |
|
|
|---|---|---|
|
|
| Forex | 100,000 units (1 standard lot) | Tick value varies by pair denomination |
|
|
| Indices (CFD) | Variable (often 1 or 10 per point) | Contract size varies by broker and index |
|
|
| Gold (XAUUSD) | 100 troy ounces (1 lot) | Large tick value per lot |
|
|
| Oil (crude) | 1,000 barrels (1 lot) | Varies by broker |
|
|
| Crypto | Variable | Bitcoin often 1 BTC per lot |
|
|
|
|
**Always use SymbolInfo functions to get accurate values per instrument:**
|
|
|
|
```mql5
|
|
void PrintSymbolTradeInfo(string symbol)
|
|
{
|
|
double contractSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_CONTRACT_SIZE);
|
|
double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
|
|
double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
|
double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
|
|
double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
|
|
int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
|
|
|
|
PrintFormat("%s: Contract=%.2f TickVal=%.5f TickSize=%.5f Point=%.5f Digits=%d",
|
|
symbol, contractSize, tickValue, tickSize, point, digits);
|
|
PrintFormat(" MinLot=%.2f MaxLot=%.2f LotStep=%.2f", minLot, maxLot, lotStep);
|
|
}
|
|
```
|
|
|
|
**Key rule:** Never hardcode tick values or contract sizes. The `CalcLotSize` function above handles all markets correctly because it reads the values dynamically from the symbol properties.
|
|
|
|
### Drawdown Control
|
|
|
|
```mql5
|
|
class CDrawdownManager
|
|
{
|
|
private:
|
|
double m_startBalance;
|
|
double m_dailyStartBalance;
|
|
double m_maxDailyLossPercent; // e.g., 3.0
|
|
double m_maxTotalDDPercent; // e.g., 10.0
|
|
datetime m_currentDay;
|
|
bool m_dailyLocked;
|
|
bool m_totalLocked;
|
|
|
|
public:
|
|
CDrawdownManager(double maxDailyLoss = 3.0, double maxTotalDD = 10.0)
|
|
{
|
|
m_maxDailyLossPercent = maxDailyLoss;
|
|
m_maxTotalDDPercent = maxTotalDD;
|
|
m_startBalance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
m_dailyStartBalance = m_startBalance;
|
|
m_currentDay = iTime(_Symbol, PERIOD_D1, 0);
|
|
m_dailyLocked = false;
|
|
m_totalLocked = false;
|
|
}
|
|
|
|
void OnNewDay()
|
|
{
|
|
datetime today = iTime(_Symbol, PERIOD_D1, 0);
|
|
if(today != m_currentDay)
|
|
{
|
|
m_currentDay = today;
|
|
m_dailyStartBalance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
m_dailyLocked = false;
|
|
// Total lock is NOT reset on new day
|
|
}
|
|
}
|
|
|
|
bool CanTrade()
|
|
{
|
|
OnNewDay();
|
|
|
|
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
|
|
// Daily drawdown check
|
|
double dailyLoss = (m_dailyStartBalance - equity) / m_dailyStartBalance * 100.0;
|
|
if(dailyLoss >= m_maxDailyLossPercent)
|
|
{
|
|
if(!m_dailyLocked)
|
|
{
|
|
Print("Daily drawdown limit reached: ", DoubleToString(dailyLoss, 2), "%");
|
|
m_dailyLocked = true;
|
|
}
|
|
return(false);
|
|
}
|
|
|
|
// Total drawdown check
|
|
double totalDD = (m_startBalance - equity) / m_startBalance * 100.0;
|
|
if(totalDD >= m_maxTotalDDPercent)
|
|
{
|
|
if(!m_totalLocked)
|
|
{
|
|
Print("Total drawdown limit reached: ", DoubleToString(totalDD, 2), "%");
|
|
m_totalLocked = true;
|
|
}
|
|
return(false);
|
|
}
|
|
|
|
return(true);
|
|
}
|
|
|
|
void Reset()
|
|
{
|
|
m_startBalance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
m_dailyStartBalance = m_startBalance;
|
|
m_dailyLocked = false;
|
|
m_totalLocked = false;
|
|
}
|
|
|
|
double GetDailyDD()
|
|
{
|
|
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
return((m_dailyStartBalance - equity) / m_dailyStartBalance * 100.0);
|
|
}
|
|
|
|
double GetTotalDD()
|
|
{
|
|
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
return((m_startBalance - equity) / m_startBalance * 100.0);
|
|
}
|
|
};
|
|
```
|
|
|
|
### Margin Verification Before Trade
|
|
|
|
```mql5
|
|
bool HasSufficientMargin(string symbol, ENUM_ORDER_TYPE orderType, double lots, double safetyFactor = 0.9)
|
|
{
|
|
double price = (orderType == ORDER_TYPE_BUY)
|
|
? SymbolInfoDouble(symbol, SYMBOL_ASK)
|
|
: SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
|
|
double margin = 0;
|
|
if(!OrderCalcMargin(orderType, symbol, lots, price, margin))
|
|
{
|
|
Print("OrderCalcMargin failed: ", GetLastError());
|
|
return(false);
|
|
}
|
|
|
|
double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
|
|
|
|
if(margin > freeMargin * safetyFactor)
|
|
{
|
|
PrintFormat("Insufficient margin. Required: %.2f, Available: %.2f (safety: %.0f%%)",
|
|
margin, freeMargin, safetyFactor * 100);
|
|
return(false);
|
|
}
|
|
|
|
return(true);
|
|
}
|
|
```
|
|
|
|
**MQL4 equivalent:**
|
|
|
|
```mql4
|
|
bool HasSufficientMargin_MQL4(string symbol, int orderType, double lots)
|
|
{
|
|
double freeMargin = AccountFreeMarginCheck(symbol, orderType, lots);
|
|
|
|
if(freeMargin <= 0 || GetLastError() == ERR_NOT_ENOUGH_MONEY)
|
|
{
|
|
Print("Insufficient margin for ", lots, " lots on ", symbol);
|
|
return(false);
|
|
}
|
|
|
|
return(true);
|
|
}
|
|
```
|
|
|
|
**Common mistakes:**
|
|
- Setting `g_symbol` after `LoadRuntimeValues()` → GV keys become `SmartEntry____field` (empty symbol)
|
|
- No first-run check → defaults of 0 cause validation to fail or EA to not trade
|
|
- Using `FILE_WRITE | FILE_COMMON` without `FILE_REWRITE` → file locked on repeated saves
|
|
- Saving inside `OnDeinit` but `OnInit` runs before `OnDeinit` finishes (TF change race condition)
|
|
|
|
---
|
|
|
|
## Trailing Stop Implementations
|
|
|
|
### CTrailingManager (MQL5)
|
|
|
|
```mql5
|
|
#include <Trade/Trade.mqh>
|
|
#include <Trade/PositionInfo.mqh>
|
|
#include <Indicators/Trend.mqh>
|
|
|
|
class CTrailingManager
|
|
{
|
|
private:
|
|
CTrade m_trade;
|
|
int m_magic;
|
|
|
|
bool ModifySL(ulong ticket, double newSL)
|
|
{
|
|
if(!PositionSelectByTicket(ticket))
|
|
return(false);
|
|
|
|
double currentSL = PositionGetDouble(POSITION_SL);
|
|
double tp = PositionGetDouble(POSITION_TP);
|
|
string symbol = PositionGetString(POSITION_SYMBOL);
|
|
int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
|
|
|
|
newSL = NormalizeDouble(newSL, digits);
|
|
|
|
// Skip if SL hasn't changed meaningfully
|
|
if(MathAbs(currentSL - newSL) < SymbolInfoDouble(symbol, SYMBOL_POINT))
|
|
return(true);
|
|
|
|
return(m_trade.PositionModify(ticket, newSL, tp));
|
|
}
|
|
|
|
public:
|
|
CTrailingManager(int magic, int slippage = 10)
|
|
{
|
|
m_magic = magic;
|
|
m_trade.SetExpertMagicNumber(magic);
|
|
m_trade.SetDeviationInPoints(slippage);
|
|
}
|
|
|
|
//--- Fixed Trailing Stop ---
|
|
// trailPoints: distance in points to trail behind current price
|
|
// activationPoints: minimum profit in points before trailing activates (0 = immediate)
|
|
void TrailFixed(string symbol, double trailPoints, double activationPoints = 0)
|
|
{
|
|
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
double stopLevel = SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL) * point;
|
|
double trailDist = MathMax(trailPoints * point, stopLevel);
|
|
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != symbol) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != m_magic) continue;
|
|
|
|
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
double currentSL = PositionGetDouble(POSITION_SL);
|
|
long type = PositionGetInteger(POSITION_TYPE);
|
|
|
|
if(type == POSITION_TYPE_BUY)
|
|
{
|
|
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
double profitDist = bid - openPrice;
|
|
|
|
if(activationPoints > 0 && profitDist < activationPoints * point)
|
|
continue;
|
|
|
|
double newSL = bid - trailDist;
|
|
if(newSL > currentSL || currentSL == 0)
|
|
ModifySL(ticket, newSL);
|
|
}
|
|
else if(type == POSITION_TYPE_SELL)
|
|
{
|
|
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
double profitDist = openPrice - ask;
|
|
|
|
if(activationPoints > 0 && profitDist < activationPoints * point)
|
|
continue;
|
|
|
|
double newSL = ask + trailDist;
|
|
if(newSL < currentSL || currentSL == 0)
|
|
ModifySL(ticket, newSL);
|
|
}
|
|
}
|
|
}
|
|
|
|
//--- ATR-Based Trailing Stop ---
|
|
// atrHandle: handle from iATR()
|
|
// multiplier: ATR multiplier for trail distance (e.g., 2.0)
|
|
void TrailATR(string symbol, int atrHandle, double multiplier = 2.0)
|
|
{
|
|
double atrBuffer[];
|
|
ArraySetAsSeries(atrBuffer, true);
|
|
|
|
if(CopyBuffer(atrHandle, 0, 1, 1, atrBuffer) != 1)
|
|
{
|
|
Print("TrailATR: Failed to copy ATR buffer");
|
|
return;
|
|
}
|
|
|
|
double atrValue = atrBuffer[0];
|
|
double trailDist = atrValue * multiplier;
|
|
double stopLevel = SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL) *
|
|
SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
|
|
trailDist = MathMax(trailDist, stopLevel);
|
|
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != symbol) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != m_magic) continue;
|
|
|
|
double currentSL = PositionGetDouble(POSITION_SL);
|
|
long type = PositionGetInteger(POSITION_TYPE);
|
|
|
|
if(type == POSITION_TYPE_BUY)
|
|
{
|
|
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
double newSL = bid - trailDist;
|
|
|
|
if(newSL > currentSL || currentSL == 0)
|
|
ModifySL(ticket, newSL);
|
|
}
|
|
else if(type == POSITION_TYPE_SELL)
|
|
{
|
|
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
double newSL = ask + trailDist;
|
|
|
|
if(newSL < currentSL || currentSL == 0)
|
|
ModifySL(ticket, newSL);
|
|
}
|
|
}
|
|
}
|
|
|
|
//--- Parabolic SAR Trailing ---
|
|
// sarHandle: handle from iSAR()
|
|
// Uses bar[1] (last completed bar) for confirmed SAR value
|
|
void TrailParabolicSAR(string symbol, int sarHandle)
|
|
{
|
|
double sarBuffer[];
|
|
ArraySetAsSeries(sarBuffer, true);
|
|
|
|
// Use bar index 1 (last closed bar) for confirmed value
|
|
if(CopyBuffer(sarHandle, 0, 1, 1, sarBuffer) != 1)
|
|
{
|
|
Print("TrailSAR: Failed to copy SAR buffer");
|
|
return;
|
|
}
|
|
|
|
double sarValue = sarBuffer[0];
|
|
double stopLevel = SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL) *
|
|
SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != symbol) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != m_magic) continue;
|
|
|
|
double currentSL = PositionGetDouble(POSITION_SL);
|
|
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
long type = PositionGetInteger(POSITION_TYPE);
|
|
|
|
if(type == POSITION_TYPE_BUY)
|
|
{
|
|
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
|
|
// SAR must be below price for buy (confirming uptrend)
|
|
if(sarValue >= bid) continue;
|
|
|
|
// Ensure minimum stop level distance
|
|
if((bid - sarValue) < stopLevel) continue;
|
|
|
|
// Only move SL forward
|
|
if(sarValue > currentSL || currentSL == 0)
|
|
ModifySL(ticket, sarValue);
|
|
}
|
|
else if(type == POSITION_TYPE_SELL)
|
|
{
|
|
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
|
|
// SAR must be above price for sell (confirming downtrend)
|
|
if(sarValue <= ask) continue;
|
|
|
|
// Ensure minimum stop level distance
|
|
if((sarValue - ask) < stopLevel) continue;
|
|
|
|
// Only move SL closer to entry (lower for sells)
|
|
if(sarValue < currentSL || currentSL == 0)
|
|
ModifySL(ticket, sarValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
//--- Break-Even Logic ---
|
|
// activationPoints: profit in points before moving to break-even
|
|
// lockInPoints: points above entry to lock in (0 = exact entry, positive = small profit)
|
|
void ManageBreakEven(string symbol, double activationPoints, double lockInPoints = 0)
|
|
{
|
|
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != symbol) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != m_magic) continue;
|
|
|
|
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
double currentSL = PositionGetDouble(POSITION_SL);
|
|
long type = PositionGetInteger(POSITION_TYPE);
|
|
|
|
if(type == POSITION_TYPE_BUY)
|
|
{
|
|
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
double beSL = openPrice + lockInPoints * point;
|
|
|
|
// Check if profit threshold reached and SL not already at or above BE
|
|
if((bid - openPrice) >= activationPoints * point)
|
|
{
|
|
if(currentSL < beSL)
|
|
ModifySL(ticket, beSL);
|
|
}
|
|
}
|
|
else if(type == POSITION_TYPE_SELL)
|
|
{
|
|
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
double beSL = openPrice - lockInPoints * point;
|
|
|
|
if((openPrice - ask) >= activationPoints * point)
|
|
{
|
|
if(currentSL > beSL || currentSL == 0)
|
|
ModifySL(ticket, beSL);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
**Usage example:**
|
|
|
|
```mql5
|
|
// In global scope
|
|
CTrailingManager *trailMgr;
|
|
int atrHandle;
|
|
int sarHandle;
|
|
|
|
int OnInit()
|
|
{
|
|
trailMgr = new CTrailingManager(MagicNumber, Slippage);
|
|
atrHandle = iATR(_Symbol, PERIOD_CURRENT, 14);
|
|
sarHandle = iSAR(_Symbol, PERIOD_CURRENT, 0.02, 0.2);
|
|
return(INIT_SUCCEEDED);
|
|
}
|
|
|
|
void OnTick()
|
|
{
|
|
// Choose one trailing method:
|
|
// trailMgr.TrailFixed(_Symbol, 50, 30);
|
|
// trailMgr.TrailATR(_Symbol, atrHandle, 2.0);
|
|
// trailMgr.TrailParabolicSAR(_Symbol, sarHandle);
|
|
|
|
// Break-even can be combined with any trailing method
|
|
trailMgr.ManageBreakEven(_Symbol, 30, 5); // Activate at 30pts profit, lock 5pts
|
|
}
|
|
|
|
void OnDeinit(const int reason)
|
|
{
|
|
delete trailMgr;
|
|
IndicatorRelease(atrHandle);
|
|
IndicatorRelease(sarHandle);
|
|
}
|
|
```
|
|
|
|
### Fixed Trailing Stop (MQL4 Version)
|
|
|
|
```mql4
|
|
void TrailFixedMQL4(string symbol, int magic, double trailPoints)
|
|
{
|
|
double point = MarketInfo(symbol, MODE_POINT);
|
|
int digits = (int)MarketInfo(symbol, MODE_DIGITS);
|
|
double stopLevel = MarketInfo(symbol, MODE_STOPLEVEL) * point;
|
|
double trailDist = MathMax(trailPoints * point, stopLevel);
|
|
|
|
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
|
|
if(OrderSymbol() != symbol) continue;
|
|
if(OrderMagicNumber() != magic) continue;
|
|
|
|
if(OrderType() == OP_BUY)
|
|
{
|
|
double bid = MarketInfo(symbol, MODE_BID);
|
|
double newSL = NormalizeDouble(bid - trailDist, digits);
|
|
|
|
if(newSL > OrderStopLoss() || OrderStopLoss() == 0)
|
|
OrderModify(OrderTicket(), OrderOpenPrice(), newSL, OrderTakeProfit(), 0, clrNONE);
|
|
}
|
|
else if(OrderType() == OP_SELL)
|
|
{
|
|
double ask = MarketInfo(symbol, MODE_ASK);
|
|
double newSL = NormalizeDouble(ask + trailDist, digits);
|
|
|
|
if(newSL < OrderStopLoss() || OrderStopLoss() == 0)
|
|
OrderModify(OrderTicket(), OrderOpenPrice(), newSL, OrderTakeProfit(), 0, clrNONE);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Trade Return Codes (MQL5)
|
|
|
|
| Constant | Value | Meaning |
|
|
|---|---|---|
|
|
| `TRADE_RETCODE_REQUOTE` | 10004 | Requote - price changed during processing |
|
|
| `TRADE_RETCODE_REJECT` | 10006 | Request rejected by server |
|
|
| `TRADE_RETCODE_CANCEL` | 10007 | Request cancelled by trader |
|
|
| `TRADE_RETCODE_PLACED` | 10008 | Order placed (pending) |
|
|
| `TRADE_RETCODE_DONE` | 10009 | Request completed successfully |
|
|
| `TRADE_RETCODE_DONE_PARTIAL` | 10010 | Request partially completed |
|
|
| `TRADE_RETCODE_TIMEOUT` | 10012 | Request timed out |
|
|
| `TRADE_RETCODE_INVALID` | 10013 | Invalid request parameters |
|
|
| `TRADE_RETCODE_INVALID_VOLUME` | 10014 | Invalid volume (lot size) |
|
|
| `TRADE_RETCODE_INVALID_PRICE` | 10015 | Invalid price |
|
|
| `TRADE_RETCODE_INVALID_STOPS` | 10016 | Invalid SL/TP levels |
|
|
| `TRADE_RETCODE_TRADE_DISABLED` | 10017 | Trading disabled for this symbol |
|
|
| `TRADE_RETCODE_MARKET_CLOSED` | 10018 | Market is closed |
|
|
| `TRADE_RETCODE_NO_MONEY` | 10019 | Insufficient funds |
|
|
| `TRADE_RETCODE_PRICE_CHANGED` | 10020 | Price changed since request |
|
|
| `TRADE_RETCODE_PRICE_OFF` | 10021 | No quotes available |
|
|
| `TRADE_RETCODE_INVALID_EXPIRATION` | 10022 | Invalid order expiration |
|
|
| `TRADE_RETCODE_ORDER_CHANGED` | 10023 | Order state changed |
|
|
| `TRADE_RETCODE_TOO_MANY_REQUESTS` | 10024 | Too many requests |
|
|
| `TRADE_RETCODE_CONNECTION` | 10031 | No connection to trade server |
|
|
|
|
**Handling guidance:**
|
|
|
|
- **Success:** 10008 and 10009 indicate the trade was accepted. Always check for both.
|
|
- **Retryable:** 10004, 10012, 10020, 10021, 10024, 10031 can be retried with backoff.
|
|
- **Fix and retry:** 10014 (adjust volume), 10015 (refresh price), 10016 (adjust stops).
|
|
- **Fatal:** 10006, 10013, 10017, 10018, 10019 usually require user intervention or logic changes.
|
|
|
|
### Interpreting Trade Results
|
|
|
|
```mql5
|
|
void LogTradeResult(CTrade &trade)
|
|
{
|
|
uint retcode = trade.ResultRetcode();
|
|
|
|
switch(retcode)
|
|
{
|
|
case TRADE_RETCODE_DONE:
|
|
case TRADE_RETCODE_PLACED:
|
|
PrintFormat("Trade OK: Order #%d, Deal #%d, Volume=%.2f, Price=%.5f",
|
|
trade.ResultOrder(), trade.ResultDeal(),
|
|
trade.ResultVolume(), trade.ResultPrice());
|
|
break;
|
|
|
|
case TRADE_RETCODE_DONE_PARTIAL:
|
|
PrintFormat("Partial fill: Volume=%.2f of requested. Order #%d",
|
|
trade.ResultVolume(), trade.ResultOrder());
|
|
break;
|
|
|
|
default:
|
|
PrintFormat("Trade FAILED: retcode=%u (%s), Comment: %s",
|
|
retcode, trade.ResultRetcodeDescription(),
|
|
trade.ResultComment());
|
|
break;
|
|
}
|
|
}
|
|
```
|