SKILL.md: - §2: add "How to look up any trading function" guidance - §3: add ADX indicator example + "How to look up any indicator" guidance - §5: strengthen OrderCalcProfit verification (step 3), add minLot risk warning (step 6) - §8: add §11 Market Regime Filtering (ADX + time-based, generic) - §8: add §12 Deal-Level Debugging Methodology (pairs deals, risk check, re-entry detection, monthly breakdown) parse_tester_report.py: - Add pair_trades(): pair entry/exit deals into complete trades - Add analyze_report(): SL/TP hits, win/loss ratio, consecutive losses, re-entry detection, monthly breakdown, volume patterns - Add --analyze CLI flag - Use datetime.now() instead of hardcoded date for gap calculation All content is framework-agnostic (no hermes/openclaw/claude/codex refs).
37 KiB
name, description, version, license, compatibility, metadata
| name | description | version | license | compatibility | metadata | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| mql5 | MQL5 development skill for MetaTrader 5 Expert Advisors, Indicators, Scripts, and Services. Focus on positions, orders, indicators, ticks, bars, risk management, backtesting, and multi-instance MT5 operations. Includes programming book and API reference documentation. | 0.1 | MIT | Target: MetaTrader 5 platform. Language: MQL5 (C++-like syntax). File extensions: *.mq5 (source), *.mqh (headers). Run time: Windows native, Linux via Wine, macOS via Wine. |
|
MQL5 Development Skill
Expert development skill for MetaTrader 5. Covers EA, Indicator, Script, and Service creation with emphasis on trading operations, technical indicators, multi-timeframe analysis, risk management, and backtesting workflows.
1. MQL5 Fundamentals
Language and File Types
- MQL5 syntax is similar to C++ but with domain-specific additions
- Source files:
*.mq5(programs),*.mqh(headers) - Compiled output:
*.ex5(same name as source) - Compiler: built into MetaEditor IDE
Program Types
| Type | Purpose | Key Handler | Directory |
|---|---|---|---|
| Expert Advisor | Automated trading | OnTick() |
MQL5/Experts/ |
| Indicator | Technical analysis | OnCalculate() |
MQL5/Indicators/ |
| Script | One-shot execution | OnStart() |
MQL5/Scripts/ |
| Service | Background task | OnStart() + OnTimer() |
MQL5/Services/ |
MQL5 Directory Structure
Default locations per platform:
| Platform | Path |
|---|---|
| Windows 10+ | $env:USERPROFILE\AppData\Roaming\MetaQuotes\Terminal\$INSTANT_HEX\MQL5 |
| Linux (Wine) | ~/.wine/drive_c/Program Files/MetaTrader 5/MQL5/ |
| macOS | Unknown — verify per installation |
Key subdirectories:
MQL5/
├── Experts/ # EA source files (.mq5)
│ ├── Examples/ # Built-in example EAs
│ └── Free Robots/ # Downloaded EAs
├── Indicators/ # Indicator source files
├── Scripts/ # Script source files
├── Services/ # Service source files
├── Include/ # Header files (.mqh)
│ ├── Trade/ # Trading classes (Trade.mqh, PositionInfo.mqh, etc.)
│ ├── Indicators/ # Indicator helpers
│ ├── Expert/ # Expert base classes
│ └── Generic/ # Generic collections
├── Files/ # File I/O sandbox
├── Images/ # Image resources
├── Libraries/ # DLL/shared libraries
├── Profiles/ # Chart profiles
└── Logs/ # Log files
Multi-Instance MT5
Multiple MT5 instances can run simultaneously for different accounts:
- Install MT5 to separate target paths (e.g.
MT5_BrokerA/,MT5_BrokerB/) - Each instance has its own
MQL5/directory - To identify which account an instance is logged into:
AccountInfoInteger(ACCOUNT_LOGIN)— account numberAccountInfoString(ACCOUNT_NAME)— account nameAccountInfoString(ACCOUNT_SERVER)— broker server
- Each instance runs as a separate process — use
Magic Numberto distinguish EA trades across instances on the same symbol
2. Trading Operations
Core Concepts
- Order: instruction to buy/sell (Market or Pending)
- Deal: executed exchange (buy at Ask, sell at Bid)
- Position: current obligation (long or short)
How to look up any trading function: Full API docs are in
references/docs/19-trading/ (34 files). Filename pattern:
0801-trading-ordercalcprofit.md. Each file contains parameters, return
values, and usage notes. For functions not listed below, read the
corresponding doc file.
CTrade Class (Standard Library)
#include <Trade\Trade.mqh>
CTrade trade;
// Setup in OnInit()
trade.SetExpertMagicNumber(EA_MAGIC);
trade.SetMarginMode();
trade.SetTypeFillingBySymbol(Symbol());
trade.SetDeviationInPoints(Slippage);
Key methods:
| Method | Purpose |
|---|---|
PositionOpen(symbol, type, volume, price, sl, tp) |
Open a position |
PositionClose(symbol, deviation) |
Close a position |
PositionModify(symbol, sl, tp) |
Modify SL/TP |
PositionClosePartial(symbol, volume) |
Partial close |
Buy(volume, price, sl, tp, comment) |
Shortcut for buy |
Sell(volume, price, sl, tp, comment) |
Shortcut for sell |
BuyLimit/BuyStop/SellLimit/SellStop(...) |
Pending orders |
ResultRetcode() |
Check trade server return code |
ResultDeal() |
Get deal ticket after execution |
Position Queries
// Iterate open positions (Hedging account)
uint total = PositionsTotal();
for (uint i = 0; i < total; i++) {
string sym = PositionGetSymbol(i);
if (sym == _Symbol && PositionGetInteger(POSITION_MAGIC) == EA_MAGIC) {
double vol = PositionGetDouble(POSITION_VOLUME);
double sl = PositionGetDouble(POSITION_SL);
double tp = PositionGetDouble(POSITION_TP);
long type = PositionGetInteger(POSITION_TYPE);
}
}
// Netting account — simpler
if (PositionSelect(_Symbol)) {
// position is selected
}
Order Execution Pattern
// Calculate price
double price = (signal == ORDER_TYPE_BUY)
? SymbolInfoDouble(_Symbol, SYMBOL_ASK)
: SymbolInfoDouble(_Symbol, SYMBOL_BID);
// Open with SL/TP
trade.PositionOpen(_Symbol, signal, lotSize, price, sl, tp, "EA Signal");
// Always check result
if (trade.ResultRetcode() != TRADE_RETCODE_DONE) {
Print("Trade failed: ", trade.ResultRetcode());
}
Hedging vs Netting
bool IsHedging = ((ENUM_ACCOUNT_MARGIN_MODE)
AccountInfoInteger(ACCOUNT_MARGIN_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
- Hedging: multiple positions per symbol, must iterate and match Magic Number
- Netting: one position per symbol, use
PositionSelect()
3. Indicators and Multi-Timeframe
Built-in Indicator Handles
// Moving Average
int handle = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_SMA, PRICE_CLOSE);
// RSI
int handle = iRSI(_Symbol, PERIOD_H1, 14, PRICE_CLOSE);
// MACD
int handle = iMACD(_Symbol, PERIOD_H1, 12, 26, 9, PRICE_CLOSE);
// Bollinger Bands
int handle = iBands(_Symbol, PERIOD_H1, 20, 0, 2.0, PRICE_CLOSE);
// ADX (trend strength)
int handle = iADX(_Symbol, PERIOD_H4, 14);
How to look up any indicator: Full API docs are in
references/docs/26-indicators/ (41 files). Filename pattern:
0969-indicators-i<name>.md (e.g. iadx, iatr, ifractals).
Each file contains: function signature, parameters, return value,
buffer indices, and usage examples. For indicators not listed in §3,
read the corresponding doc file rather than guessing the API.
Reading Indicator Values
double buffer[];
ArraySetAsSeries(buffer, true);
if (CopyBuffer(handle, 0, 0, 3, buffer) != 3) {
Print("No indicator data");
return;
}
// buffer[0] = current bar value
// buffer[1] = previous bar value
Multi-Timeframe Analysis
// Higher timeframe trend
int h4_ma = iMA(_Symbol, PERIOD_H4, 50, 0, MODE_SMA, PRICE_CLOSE);
// Entry timeframe signal
int h1_rsi = iRSI(_Symbol, PERIOD_H1, 14, PRICE_CLOSE);
// In OnTick():
double h4_val[], h1_val[];
CopyBuffer(h4_ma, 0, 0, 1, h4_val);
CopyBuffer(h1_rsi, 0, 0, 1, h1_val);
bool bullish = (SymbolInfoDouble(_Symbol, SYMBOL_BID) > h4_val[0]);
bool oversold = (h1_val[0] < 30);
New Bar Detection
datetime lastBarTime = 0;
void OnTick() {
datetime currentBarTime = iTime(_Symbol, _Period, 0);
if (currentBarTime == lastBarTime) return; // not a new bar
lastBarTime = currentBarTime;
// New bar — run analysis here
}
4. Ticks and Bars
Timeseries Access
Index 0 = current (unfinished) bar. Array is reverse-ordered.
MqlRates rates[];
ArraySetAsSeries(rates, true);
CopyRates(_Symbol, _Period, 0, 100, rates);
// rates[0] = current bar
// rates[1] = previous bar
// rates[0].open, .high, .low, .close, .tick_volume, .time
Tick Data
MqlTick tick;
SymbolInfoTick(_Symbol, tick);
// tick.bid, tick.ask, tick.last, tick.volume, tick.time
Key Functions
| Function | Purpose |
|---|---|
CopyRates() |
Bulk OHLCV data |
CopyOpen/High/Low/Close() |
Individual price arrays |
CopyTime() |
Bar open times |
CopyBuffer() |
Indicator buffer values |
iBars() |
Bar count for symbol/period |
iBarShift() |
Bar index by time |
iTime() |
Bar open time by shift |
SymbolInfoTick() |
Current tick data |
5. Risk Management and Lot Sizing
Core Concept: PointValue
PointValue = profit/loss in profit-currency for a 1-point price move on 1 lot.
This is the foundation for all risk calculations.
double PointValue(string symbol) {
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
double contract = SymbolInfoDouble(symbol, SYMBOL_TRADE_CONTRACT_SIZE);
ENUM_SYMBOL_CALC_MODE mode =
(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol, SYMBOL_TRADE_CALC_MODE);
switch (mode) {
case SYMBOL_CALC_MODE_FOREX:
case SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE:
case SYMBOL_CALC_MODE_CFD:
case SYMBOL_CALC_MODE_CFDINDEX:
case SYMBOL_CALC_MODE_CFDLEVERAGE:
case SYMBOL_CALC_MODE_EXCH_STOCKS:
case SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX:
return point * contract;
case SYMBOL_CALC_MODE_FUTURES:
case SYMBOL_CALC_MODE_EXCH_FUTURES:
case SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS:
return point * SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE)
/ SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
}
return 0;
}
Key distinction:
SYMBOL_TRADE_TICK_VALUE= profit-currency per tick for 1 lot (broker-supplied)PointValue= profit-currency per 1 point for 1 lot (computed)loss = points × PointValue × Lots
Direction A: SL Distance Points → SL Price
Given a stop loss distance in points, compute the SL price level.
double CalcSLFromPoints(string symbol, double openPrice, int slPoints,
bool isBuy) {
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
double slDistPrice = slPoints * point;
if (isBuy)
return NormalizeDouble(openPrice - slDistPrice,
(int)SymbolInfoInteger(symbol, SYMBOL_DIGITS));
else
return NormalizeDouble(openPrice + slDistPrice,
(int)SymbolInfoInteger(symbol, SYMBOL_DIGITS));
}
Direction B: Risk % → SL Price (fixed lot size)
Given account balance, risk %, and lot size, compute where SL must be placed.
CRITICAL: When profit_currency ≠ account_currency, convert risk amount first.
double CalcSLFromRisk(string symbol, double balance, double riskPct,
double lots, double openPrice, bool isBuy) {
double pv = PointValue(symbol);
if (pv == 0 || lots == 0) return 0;
double riskAmount = balance * riskPct / 100.0;
// If profit currency differs from account currency, convert.
// Example: USDJPY → profit=JPY, account=USD → multiply by USDJPY bid
string profCy = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT);
string accCy = AccountInfoString(ACCOUNT_CURRENCY);
if (profCy != accCy) {
// Find exchange rate pair: look for a Forex symbol with
// base=accCy, profit=profCy (or reverse)
string rateSym = "";
int dir = FindFXRate(accCy, profCy, rateSym);
if (dir == 0) { Print("Cannot convert ", profCy, "→", accCy); return 0; }
MqlTick tick;
SymbolInfoTick(rateSym, tick);
double rate = (dir > 0) ? tick.bid : 1.0 / tick.ask;
riskAmount *= rate; // risk in profit currency
}
double points = riskAmount / (pv * lots);
double slPrice = points * SymbolInfoDouble(symbol, SYMBOL_POINT);
if (isBuy)
return NormalizeDouble(openPrice - slPrice,
(int)SymbolInfoInteger(symbol, SYMBOL_DIGITS));
else
return NormalizeDouble(openPrice + slPrice,
(int)SymbolInfoInteger(symbol, SYMBOL_DIGITS));
}
// Helper: find a Forex pair that converts from→to
// Returns +1 if pair is from/to, -1 if to/from, 0 if not found
int FindFXRate(string from, string to, string &result) {
for (int i = 0; i < SymbolsTotal(true); i++) {
string sym = SymbolName(i, true);
ENUM_SYMBOL_CALC_MODE m =
(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(sym, SYMBOL_TRADE_CALC_MODE);
if (m != SYMBOL_CALC_MODE_FOREX &&
m != SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE) continue;
string base = SymbolInfoString(sym, SYMBOL_CURRENCY_BASE);
string profit = SymbolInfoString(sym, SYMBOL_CURRENCY_PROFIT);
if (base == from && profit == to) { result = sym; return +1; }
if (base == to && profit == from) { result = sym; return -1; }
}
return 0;
}
Direction C: SL Price → Lot Size (risk-based sizing)
Given a fixed SL price, compute the lot size so loss matches the risk budget.
double CalcLotsFromSL(string symbol, double balance, double riskPct,
double openPrice, double slPrice) {
double pv = PointValue(symbol);
if (pv == 0) return 0;
double riskAmount = balance * riskPct / 100.0;
// Currency conversion (same as Direction B above)
string profCy = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT);
string accCy = AccountInfoString(ACCOUNT_CURRENCY);
if (profCy != accCy) {
string rateSym = "";
int dir = FindFXRate(accCy, profCy, rateSym);
if (dir == 0) return 0;
MqlTick tick;
SymbolInfoTick(rateSym, tick);
double rate = (dir > 0) ? tick.bid : 1.0 / tick.ask;
riskAmount *= rate;
}
double slDistPrice = MathAbs(openPrice - slPrice);
if (slDistPrice == 0) return 0;
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
double points = slDistPrice / point;
double rawLots = riskAmount / (pv * points);
// Normalize to broker constraints
double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
double lot = MathFloor(rawLots / lotStep) * lotStep;
lot = MathMax(lot, minLot);
lot = MathMin(lot, maxLot);
return NormalizeDouble(lot, 2);
}
Profit Verification
Use OrderCalcProfit (EA/scripts only) or manual formula to verify:
// Using OrderCalcProfit
double profit;
OrderCalcProfit(ORDER_TYPE_BUY, symbol, lots, openPrice, closePrice, profit);
// profit is in profit currency
// Manual formula (Forex/CFD)
double profit = (closePrice - openPrice) * ContractSize * Lots;
// Manual formula (Futures)
double profit = (closePrice - openPrice) * TickValue / TickSize * Lots;
Risk-to-Reward Ratio
// Minimum 1:2 RR
double slDistance = MathAbs(price - sl);
double tpDistance = slDistance * 2; // 1:2 minimum
double tp = (orderType == ORDER_TYPE_BUY) ? price + tpDistance : price - tpDistance;
Position Sizing Rules
- Never risk more than 1-2% per trade
- Calculate SL price from risk% and lot size (Direction B), OR calculate lot size from SL price and risk% (Direction C)
- Always verify with
OrderCalcProfit— compute actual loss for the lot you're about to open and confirm it doesn't exceed risk budget - Normalize SL with
NormalizeDouble(price, SYMBOL_DIGITS) - Check SL distance ≥
SYMBOL_TRADE_STOPS_LEVEL × Point - Normalize lots to
SYMBOL_VOLUME_STEP, clamp to[VOLUME_MIN, VOLUME_MAX]. IfrawLots < minLot, the clamp inflates risk — skip the trade instead. Always verify withOrderCalcProfitbefore opening: compute actual loss forminLotand confirm it doesn't exceed risk budget × 1.5. If it does, skip the trade - When profit_currency ≠ account_currency, convert risk amount via FX rate
6. Backtesting and Optimization
Strategy Tester
The Strategy Tester is built into MT5. Key concepts:
- Single Test: run EA once with fixed parameters
- Optimization: genetic algorithm searches parameter space
- Custom Criterion:
OnTester()returns optimization value
Important: The Strategy Tester is GUI-only. metatester64.exe only manages
remote testing agents (install/start/stop), not test execution itself.
terminal64.exe has no command-line parameters. Backtesting and optimization
must be performed through the MT5 Strategy Tester GUI.
CLI Automation — What Can and Cannot Be Automated
| Task | CLI Possible? | How |
|---|---|---|
| Syntax check | ✅ | wine MetaEditor64.exe /compile:"path" /log /s |
| Compile .mq5 → .ex5 | ✅ | wine MetaEditor64.exe /compile:"path" /log |
| Run backtest | ❌ | GUI only: Strategy Tester |
| Run optimization | ❌ | GUI only: Strategy Tester |
| Parse test report | ✅ | scripts/parse_tester_report.py |
MetaEditor CLI syntax (Linux/Wine, from MT5 base directory):
wine MetaEditor64.exe /compile:"MQL5/Experts/MyEA.mq5" /log # compile
wine MetaEditor64.exe /compile:"MQL5/Experts/MyEA.mq5" /log /s # syntax check only
Log file: same directory as source, same name with .log extension.
OnTester Handler
double OnTester() {
// Called after each test pass
// Return value used as "Custom max" optimization criterion
double profit = TesterStatistics(STAT_PROFIT);
double dd = TesterStatistics(STAT_BALANCE_DDREL_PERCENT);
double trades = TesterStatistics(STAT_TRADES);
double pf = TesterStatistics(STAT_PROFIT_FACTOR);
double sharpe = TesterStatistics(STAT_SHARPE_RATIO);
// Minimum trade count filter
if (trades < 50) return 0;
// Custom criterion: profit factor * (1 - max drawdown%)
return pf * (1.0 - dd / 100.0);
}
Key Statistics
| Stat | Description |
|---|---|
STAT_PROFIT |
Net profit/loss |
STAT_PROFIT_FACTOR |
Gross profit / gross loss |
STAT_BALANCE_DDREL_PERCENT |
Max balance drawdown % |
STAT_SHARPE_RATIO |
Sharpe ratio |
STAT_TRADES |
Number of trades |
STAT_PROFIT_TRADES |
Winning trades |
STAT_LOSS_TRADES |
Losing trades |
STAT_EXPECTED_PAYOFF |
Average profit per trade |
STAT_RECOVERY_FACTOR |
Profit / max drawdown |
Parameter Optimization
When running optimization in the GUI, define parameter ranges as
[start, stop, step] (stop inclusive). For example:
| Parameter | Start | Stop | Step |
|---|---|---|---|
| RiskPercent | 0.5 | 3.0 | 0.5 |
| Slippage | 5 | 20 | 5 |
| MagicNumber | 10000 | 10010 | 1 |
In MT5 Strategy Tester: set each input parameter to "Enable optimization",
then configure range/step in the optimization tab.
Backtesting Workflow
- Code the EA with
OnTick(),OnInit(),OnDeinit() - Add
OnTester()for custom optimization criterion - Compile and check syntax via CLI (see CLI Automation above)
- In MT5: Strategy Tester → select EA → set symbol/timeframe/period
- Choose "Open prices only" for speed, "Every tick" for accuracy
- Run single test → check results
- Run optimization → find best parameters
- Validate with out-of-sample data
EA Development Cycle
Code → Syntax Check (CLI) → Compile (CLI)
↓
GUI: Single Test → Check Results
↓
If promising → GUI: Optimize → Analyze Report
↓
If validated → GUI: Forward Test → Deploy
↓
Monitor → Collect Data → Refine → Repeat
Note: steps marked (CLI) can be automated via mql5_helper.py or direct
Wine commands. GUI steps require human interaction.
Report Analysis — Interpreting Tester Results
After each backtest, MT5 exports an HTML report. Use
scripts/parse_tester_report.py to extract structured data, or read the
HTML directly. Key areas to evaluate:
1. Data Quality Gate
Always check first. If history quality is poor, all metrics are suspect.
| Metric | Acceptable | Action if Failed |
|---|---|---|
| History Quality | ≥ 95% real ticks | Re-download tick data or use different broker |
| Bars | Enough for strategy (e.g. 1000+ for H4) | Extend test period |
| Modelling quality | Every tick or Every tick based on real ticks | Never trust "Open prices only" for final eval |
2. Profitability Metrics
| Metric | Good | Warning | Bad |
|---|---|---|---|
| Net Profit | > 0 | ≈ 0 | < 0 |
| Profit Factor | > 1.5 | 1.0–1.5 | < 1.0 |
| Expected Payoff | > 0 | ≈ 0 | < 0 |
| Recovery Factor | > 2.0 | 1.0–2.0 | < 1.0 |
Profit Factor < 1.0 = guaranteed loss. The EA loses more than it wins. No amount of parameter tuning will fix a fundamentally negative PF — the strategy logic itself needs rethinking.
3. Drawdown Analysis
Drawdown is the real killer. A 100% drawdown means account wiped.
| Metric | Safe | Risky | Dangerous |
|---|---|---|---|
| Max DD% | < 20% | 20–50% | > 50% |
| DD Absolute / Deposit | < 0.5x | 0.5–1x | > 1x (blown) |
Check both Balance DD and Equity DD. Equity DD captures floating losses that haven't realized yet — often much worse than balance DD.
If Balance DD Max% ≈ 100%, the account was wiped. Look at the balance
curve: did it recover or flatline at zero?
4. Trade Distribution
| Metric | Healthy | Concerning |
|---|---|---|
| Win Rate | 40–60% | < 30% or > 70% |
| Avg Win / Avg Loss | > 1.5 | < 1.0 |
| Profit Trades % | > 40% | < 30% |
| Largest Loss / Avg Loss | < 3x | > 5x (outlier risk) |
Low win rate is fine if avg win >> avg loss (trend following). High win rate is fine if avg loss << avg win (mean reversion). Red flag: low win rate AND small avg win = guaranteed bleed.
5. Consecutive Losses
| Metric | Tolerable | Stressed |
|---|---|---|
| Max Consecutive Losses | < 5 | > 8 |
| Max Consecutive Loss $ | < 2x deposit | > deposit |
More than 8 consecutive losses suggests the strategy has long anti-trend periods. With martingale or grid sizing, consecutive losses compound catastrophically.
6. Holding Time
| Pattern | Meaning | Risk |
|---|---|---|
| Very short avg (< 1 min) | Scalping / arbitrage | Spread/slippage sensitive |
| Very long avg (> 100 hrs) | Swing / position trading | Gap/overnight risk |
| Huge variance (min vs max) | Mixed strategy | Hard to predict behavior |
7. MFE/MAE Analysis
- MFE (Most Favorable Excursion): how far price went in your favor before exit. High MFE + low profit = premature exit (tight TP).
- MAE (Most Adverse Excursion): how far price went against you. High MAE + small loss = lucky exit (SL barely held).
- Correlation (Profits, MAE): high positive = losses come from large adverse moves (SL too loose or absent).
- Correlation (MFE, MAE): negative = when price moves far in one direction, it doesn't retrace (good for trend following).
8. Stop-Out Detection
Stop-outs (comment contains so) mean margin was insufficient — the
broker force-closed before SL was reached. This is always a critical bug:
Root causes:
1. SL too far from entry → floating loss exceeds available margin
2. Lot size too large for account balance
3. Risk per trade exceeds account capacity
4. Multiple concurrent positions drain margin
Fix: reduce lot size, tighten SL, or reduce concurrent positions.
9. Short vs Long Bias
Compare Short Trades (won%) vs Long Trades (won%):
- Heavily skewed (e.g. 91 long / 5 short) → EA only trades one direction
- Check if this is intentional (bullish filter) or a bug
- In trending markets, one-direction bias can mask poor signal quality
10. Commission & Swap Impact
In the Deals table, check Commission and Swap columns:
- Commission should be consistent per deal (proportional to volume)
- Swap accumulates on overnight positions — can turn winners into losers
Profit = Price P&L + Commission + Swap— verify this sums correctly
11. Market Regime Filtering
Trend-following strategies (including order-block / price-structure) degrade in choppy or sideways markets — order blocks get repeatedly broken, producing false signals and consecutive losses. Two simple filters can help:
ADX Trend Strength Filter: Only trade when ADX(14) on a higher timeframe (e.g. H4) exceeds a threshold (commonly 25). ADX below the threshold means no clear trend — the strategy's edge weakens.
// In entry logic, before trend check:
double adx[];
ArraySetAsSeries(adx, true);
if (CopyBuffer(g_h4adx, 0, 0, 1, adx) == 1) {
if (adx[0] < InpADX_Threshold) { // e.g. 25.0
Print("ADX ", adx[0], " < threshold, skipping");
return;
}
}
Time-Based Filter: Certain hours produce noise signals (session transitions, low liquidity). Identify the worst-performing hours from monthly breakdowns and skip them:
MqlDateTime dt;
TimeCurrent(dt);
// Parse InpBadHours = "4,16,18" and skip if match
12. Deal-Level Debugging Methodology
When summary metrics reveal problems, drill into individual trades.
Use scripts/parse_tester_report.py --analyze for automated analysis
(pairs deals, computes risk per trade, monthly breakdown, re-entry
detection, streak analysis). For raw data, use --json instead.
- Pair deals: Iterate deals, pair each
direction=inwith the nextdirection=outto form a complete trade (entry price, exit price, P&L, close reason from comment). - Risk check: For each trade, compute
|net_loss| / deposit × 100to verify risk % is within budget. Flag any trade exceeding 2× target risk. - SL distance analysis: For SL hits, compute
|entry - exit| / pointto get SL distance in points. Check if the EA is entering with SL too close (oversized lots) or too far (oversized risk). - Re-entry detection: Sort trades by entry time. If an SL hit is immediately followed by a trade at similar entry price with larger lot, the EA is doing implicit martingale on the same setup.
- Volume pattern: Plot lot sizes across trades. Consistent 0.01 lots regardless of SL distance = minLot clamp bug.
- Monthly breakdown: Group trades by month, compute win rate and net P&L per month. Identify worst months and correlate with market conditions.
7. Event Handlers Reference
| Handler | When Called | Use Case |
|---|---|---|
OnInit() |
EA/indicator starts | Initialize handles, variables |
OnDeinit() |
EA/indicator stops | Cleanup, release handles |
OnTick() |
New tick received | EA main logic |
OnTimer() |
Timer event | Periodic operations |
OnTrade() |
Trade event | React to trade changes |
OnTradeTransaction() |
Trade transaction | Detailed trade tracking |
OnChartEvent() |
Chart interaction | GUI buttons, objects |
OnCalculate() |
Indicator calculation | Indicator main logic |
OnTester() |
Test complete | Custom optimization criterion |
OnTesterInit() |
Optimization start | Setup for optimization |
OnTesterPass() |
Each optimization pass | Log intermediate results |
8. Common Pitfalls
General
- Always check
ResultRetcode()afterPositionOpen()— success != execution - Use
SetExpertMagicNumber()to distinguish your EA's trades - Normalize prices with
NormalizeDouble(price, SYMBOL_DIGITS) - Check
Bars() > Nbefore trading to ensure enough history - Use
ArraySetAsSeries(true)for timeseries arrays (index 0 = latest) - Release indicator handles in
OnDeinit()withIndicatorRelease() - Don't trade on
OnInit()— wait for firstOnTick() - Account type matters: Hedging requires iterating positions, Netting uses select
- Spread varies: use
SymbolInfoInteger(_Symbol, SYMBOL_SPREAD)for live spread - Timer in tester: use
EventSetTimer()inOnInit(), not hardcoded delays
SL/TP and Risk Calculation
- PointValue ≠ TICK_VALUE:
SYMBOL_TRADE_TICK_VALUEis per tick (broker-defined step),PointValue = point × ContractSizeis per point (smallest price unit). For most Forex: TickSize = Point, so they coincide; for futures/metals they may differ. - TickSize ≠ Point: Always use the correct formula for the symbol's
SYMBOL_TRADE_CALC_MODE. Forex/CFD:loss = delta_price × ContractSize × Lots. Futures:loss = delta_price × TickValue / TickSize × Lots. - Profit currency ≠ Account currency: USDJPY profit is JPY, not USD. Risk amount must be converted:
risk_JPY = risk_USD × USDJPY_bid. Failing this makes risk 100×+ too small. - NormalizeDouble introduces rounding: SL price rounded to
SYMBOL_DIGITScauses ~0.01-0.02% deviation from target loss. Acceptable; verify withOrderCalcProfit. - Lot step quantization:
MathFloor(rawLots / lotStep) * lotStepcan leave residual risk unmet. For large lot_step or small risk budgets, actual loss may differ from target by up to one lot_step worth of loss. - STOPS_LEVEL check: SL must be ≥
SYMBOL_TRADE_STOPS_LEVEL × Pointfrom current price. If stops_level ≤ 0, use a safety margin (e.g. 150 points).
9. Quick Reference — EA Skeleton
//+------------------------------------------------------------------+
//| MyExpertAdvisor.mq5 |
//+------------------------------------------------------------------+
#property copyright "Your Name"
#property link ""
#property version "1.00"
#include <Trade\Trade.mqh>
input double RiskPercent = 1.0; // Risk % per trade
input int Slippage = 10; // Max slippage in points
input int MagicNumber = 12345; // EA magic number
#define EA_MAGIC MagicNumber
CTrade trade;
bool IsHedging;
datetime lastBarTime = 0;
//+------------------------------------------------------------------+
//| PointValue: profit-currency per 1-point move for 1 lot |
//+------------------------------------------------------------------+
double PointValue(string symbol) {
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
double contract = SymbolInfoDouble(symbol, SYMBOL_TRADE_CONTRACT_SIZE);
ENUM_SYMBOL_CALC_MODE mode =
(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol, SYMBOL_TRADE_CALC_MODE);
if (mode == SYMBOL_CALC_MODE_FUTURES ||
mode == SYMBOL_CALC_MODE_EXCH_FUTURES ||
mode == SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS)
return point * SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE)
/ SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
return point * contract; // Forex, CFD, Stocks
}
//+------------------------------------------------------------------+
//| FindFXRate: locate a Forex pair for currency conversion |
//+------------------------------------------------------------------+
int FindFXRate(string from, string to, string &result) {
for (int i = 0; i < SymbolsTotal(true); i++) {
string sym = SymbolName(i, true);
ENUM_SYMBOL_CALC_MODE m =
(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(sym, SYMBOL_TRADE_CALC_MODE);
if (m != SYMBOL_CALC_MODE_FOREX &&
m != SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE) continue;
string base = SymbolInfoString(sym, SYMBOL_CURRENCY_BASE);
string profit = SymbolInfoString(sym, SYMBOL_CURRENCY_PROFIT);
if (base == from && profit == to) { result = sym; return +1; }
if (base == to && profit == from) { result = sym; return -1; }
}
return 0;
}
//+------------------------------------------------------------------+
//| CalcSLFromRisk: risk% + lots → SL price |
//+------------------------------------------------------------------+
double CalcSLFromRisk(string symbol, double balance, double riskPct,
double lots, double openPrice, bool isBuy) {
double pv = PointValue(symbol);
if (pv == 0 || lots == 0) return 0;
double riskAmount = balance * riskPct / 100.0;
// Currency conversion if needed
string profCy = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT);
string accCy = AccountInfoString(ACCOUNT_CURRENCY);
if (profCy != accCy) {
string rateSym = "";
int dir = FindFXRate(accCy, profCy, rateSym);
if (dir == 0) return 0;
MqlTick tick; SymbolInfoTick(rateSym, tick);
riskAmount *= (dir > 0) ? tick.bid : 1.0 / tick.ask;
}
double points = riskAmount / (pv * lots);
double slPrice = points * SymbolInfoDouble(symbol, SYMBOL_POINT);
int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
return isBuy ? NormalizeDouble(openPrice - slPrice, digits)
: NormalizeDouble(openPrice + slPrice, digits);
}
//+------------------------------------------------------------------+
//| CalcLotsFromSL: SL price + risk% → lot size |
//+------------------------------------------------------------------+
double CalcLotsFromSL(string symbol, double balance, double riskPct,
double openPrice, double slPrice) {
double pv = PointValue(symbol);
if (pv == 0) return 0;
double riskAmount = balance * riskPct / 100.0;
string profCy = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT);
string accCy = AccountInfoString(ACCOUNT_CURRENCY);
if (profCy != accCy) {
string rateSym = "";
int dir = FindFXRate(accCy, profCy, rateSym);
if (dir == 0) return 0;
MqlTick tick; SymbolInfoTick(rateSym, tick);
riskAmount *= (dir > 0) ? tick.bid : 1.0 / tick.ask;
}
double slDist = MathAbs(openPrice - slPrice);
if (slDist == 0) return 0;
double points = slDist / SymbolInfoDouble(symbol, SYMBOL_POINT);
double rawLots = riskAmount / (pv * points);
double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
double lot = MathFloor(rawLots / lotStep) * lotStep;
lot = MathMax(lot, minLot);
lot = MathMin(lot, maxLot);
return NormalizeDouble(lot, 2);
}
//+------------------------------------------------------------------+
int OnInit() {
IsHedging = ((ENUM_ACCOUNT_MARGIN_MODE)
AccountInfoInteger(ACCOUNT_MARGIN_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
trade.SetExpertMagicNumber(EA_MAGIC);
trade.SetMarginMode();
trade.SetTypeFillingBySymbol(Symbol());
trade.SetDeviationInPoints(Slippage);
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
// Cleanup
}
//+------------------------------------------------------------------+
void OnTick() {
// New bar check
datetime barTime = iTime(_Symbol, _Period, 0);
if (barTime == lastBarTime) return;
lastBarTime = barTime;
// Example: buy with 1% risk, SL at 500 points
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
int slPts = 500;
double sl = CalcSLFromPoints(_Symbol, bid, slPts, true);
// Or: double sl = CalcSLFromRisk(_Symbol,
// AccountInfoDouble(ACCOUNT_BALANCE), RiskPercent,
// 0.10, bid, true);
double lots = CalcLotsFromSL(_Symbol,
AccountInfoDouble(ACCOUNT_BALANCE), RiskPercent, bid, sl);
// Verify loss matches risk budget
double profit;
OrderCalcProfit(ORDER_TYPE_BUY, _Symbol, lots, bid, sl, profit);
PrintFormat("SL=%.5f lots=%.2f expected_loss=%.2f",
sl, lots, profit);
// trade.Buy(lots, _Symbol, 0, sl, 0, "EA Signal");
}
//+------------------------------------------------------------------+
double OnTester() {
double trades = TesterStatistics(STAT_TRADES);
if (trades < 30) return 0;
return TesterStatistics(STAT_PROFIT_FACTOR);
}
10. References
In this skill
references/book/— Programming book (learning path, 581 pages)00-intro/— Introduction and IDE01-basis/— Language fundamentals02-oop/— Object-oriented programming03-common/— Common functions (strings, files, math)04-applications/— Charts, indicators, objects, events05-automation/— Trading, symbols, tester06-advanced/— Resources, SQLite, Python, OpenCL
references/docs/— API reference (4135 pages)19-trading/— Trading functions (OrderSend, PositionGet, etc.)16-series/— Timeseries access (CopyRates, CopyBuffer, etc.)26-indicators/— Built-in indicators (iMA, iRSI, iMACD, etc.)24-customind/— Custom indicator creation13-event-handlers/— Event handlers (OnTick, OnTester, etc.)34-standardlibrary/— Standard library (CTrade, CPositionInfo, etc.)01-constants/— Enums and structures (MqlTradeRequest, ENUM_SYMBOL_CALC_MODE)
references/symbol-spec/— Symbol specification CSVs (broker-specific)specs-XAUUSD.csv— XAUUSD: CFD Leverage, ContractSize=100, Digits=2specs-USDJPY.csv— USDJPY: Forex, ContractSize=100000, Digits=3
scripts/verify_sl_tp_formulas.py— Python verification of SL/TP risk formulas