From 8482f7a8a1b0c1e0610a7202554125f1ebb02837 Mon Sep 17 00:00:00 2001 From: ZhijuCen Date: Thu, 25 Jun 2026 02:51:22 +0800 Subject: [PATCH] feat: sync SKILL.md and parse_tester_report.py with installed version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- skills/mql5/SKILL.md | 81 ++++++++++- skills/mql5/scripts/parse_tester_report.py | 152 ++++++++++++++++++++- 2 files changed, 230 insertions(+), 3 deletions(-) diff --git a/skills/mql5/SKILL.md b/skills/mql5/SKILL.md index 240da9f..bc3f44a 100644 --- a/skills/mql5/SKILL.md +++ b/skills/mql5/SKILL.md @@ -99,6 +99,12 @@ Multiple MT5 instances can run simultaneously for different accounts: - **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) ```mql5 @@ -191,8 +197,18 @@ 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.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 ```mql5 @@ -466,10 +482,15 @@ double tp = (orderType == ORDER_TYPE_BUY) ? price + tpDistance : price - tpDista 1. Never risk more than 1-2% per trade 2. Calculate SL price from risk% and lot size (Direction B), OR calculate lot size from SL price and risk% (Direction C) -3. Always verify with `OrderCalcProfit` or manual formula +3. **Always verify with `OrderCalcProfit`** — compute actual loss for the + lot you're about to open and confirm it doesn't exceed risk budget 4. Normalize SL with `NormalizeDouble(price, SYMBOL_DIGITS)` 5. Check SL distance ≥ `SYMBOL_TRADE_STOPS_LEVEL × Point` -6. Normalize lots to `SYMBOL_VOLUME_STEP`, clamp to `[VOLUME_MIN, VOLUME_MAX]` +6. Normalize lots to `SYMBOL_VOLUME_STEP`, clamp to `[VOLUME_MIN, VOLUME_MAX]`. + If `rawLots < minLot`, the clamp inflates risk — skip the trade + instead. Always verify with `OrderCalcProfit` before opening: compute + actual loss for `minLot` and confirm it doesn't exceed risk budget × 1.5. + If it does, skip the trade 7. When profit_currency ≠ account_currency, convert risk amount via FX rate ## 6. Backtesting and Optimization @@ -699,6 +720,62 @@ In the Deals table, check `Commission` and `Swap` columns: - 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. + +```mql5 +// 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: + +```mql5 +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. + +1. **Pair deals**: Iterate deals, pair each `direction=in` with the next + `direction=out` to form a complete trade (entry price, exit price, P&L, + close reason from comment). +2. **Risk check**: For each trade, compute `|net_loss| / deposit × 100` to + verify risk % is within budget. Flag any trade exceeding 2× target risk. +3. **SL distance analysis**: For SL hits, compute `|entry - exit| / point` + to get SL distance in points. Check if the EA is entering with SL too + close (oversized lots) or too far (oversized risk). +4. **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. +5. **Volume pattern**: Plot lot sizes across trades. Consistent 0.01 lots + regardless of SL distance = minLot clamp bug. +6. **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 | diff --git a/skills/mql5/scripts/parse_tester_report.py b/skills/mql5/scripts/parse_tester_report.py index 17b74b3..23ed526 100644 --- a/skills/mql5/scripts/parse_tester_report.py +++ b/skills/mql5/scripts/parse_tester_report.py @@ -518,12 +518,158 @@ def print_report(r: Report) -> None: print(f" ... ({len(r.deals) - 10} more)") + +# ── Trade Analysis ─────────────────────────────────────────────────── + +def pair_trades(deals: list) -> list: + """Pair entry/exit deals into complete trades.""" + trading = [d for d in deals if d.type != "balance"] + + trades = [] + i = 0 + while i < len(trading): + if trading[i].direction == "in": + entry = trading[i] + if i + 1 < len(trading) and trading[i + 1].direction == "out": + exit_d = trading[i + 1] + net = (exit_d.profit + entry.commission + exit_d.commission + + entry.swap + exit_d.swap) + sl_dist = 0.0 + if "sl" in exit_d.comment: + sl_dist = abs(entry.price - exit_d.price) + trades.append({ + "open_time": entry.time, + "close_time": exit_d.time, + "type": entry.type, + "volume": entry.volume, + "entry": entry.price, + "exit": exit_d.price, + "profit": exit_d.profit, + "commission": entry.commission + exit_d.commission, + "swap": entry.swap + exit_d.swap, + "net": net, + "comment": exit_d.comment, + "sl_distance": sl_dist, + }) + i += 2 + else: + i += 1 + else: + i += 1 + return trades + + +def analyze_report(report: Report) -> dict: + """Run full trade analysis on parsed report.""" + from datetime import datetime + + deposit = report.settings.initial_deposit + trades = pair_trades(report.deals) + + if not trades: + return {"error": "No trades found", "trades": []} + + # Per-trade risk check + for t in trades: + t["risk_pct"] = abs(t["net"]) / deposit * 100 if deposit > 0 else 0 + + # SL hit vs TP hit + sl_trades = [t for t in trades if "sl " in t["comment"]] + tp_trades = [t for t in trades if "tp " in t["comment"]] + other = [t for t in trades if t not in sl_trades and t not in tp_trades] + + avg_win = (sum(t["net"] for t in tp_trades) / len(tp_trades)) if tp_trades else 0 + avg_loss = (sum(t["net"] for t in sl_trades) / len(sl_trades)) if sl_trades else 0 + win_loss_ratio = abs(avg_win / avg_loss) if avg_loss != 0 else 0 + breakeven_wr = (abs(avg_loss) / (avg_win + abs(avg_loss)) + if (avg_win + abs(avg_loss)) > 0 else 0) + + # Consecutive loss analysis + streaks = [] + streak = 0 + for t in trades: + if t["net"] <= 0: + streak += 1 + else: + if streak > 0: + streaks.append(streak) + streak = 0 + if streak > 0: + streaks.append(streak) + + # Re-entry detection: SL hit followed by same direction with larger lot + reentries = [] + for i in range(len(trades) - 1): + t1, t2 = trades[i], trades[i + 1] + if "sl " in t1["comment"] and t1["type"] == t2["type"]: + if t2["volume"] > t1["volume"]: + reentries.append({ + "after_trade": i + 1, + "time": t2["open_time"], + "type": t2["type"], + "prev_lot": t1["volume"], + "new_lot": t2["volume"], + "multiplier": round(t2["volume"] / t1["volume"], 1), + }) + + # Monthly breakdown + monthly = {} + for t in trades: + month = t["open_time"][:7] + if month not in monthly: + monthly[month] = {"count": 0, "net": 0.0, "wins": 0, "losses": 0} + monthly[month]["count"] += 1 + monthly[month]["net"] += t["net"] + if t["net"] > 0: + monthly[month]["wins"] += 1 + else: + monthly[month]["losses"] += 1 + + for m in monthly: + d = monthly[m] + d["net"] = round(d["net"], 2) + d["win_rate"] = round(d["wins"] / d["count"] * 100, 1) if d["count"] else 0 + + # Volume pattern + lots = [t["volume"] for t in trades] + unique_lots = sorted(set(lots)) + + # Last trade gap relative to script execution time + last_close = trades[-1]["close_time"] + try: + last_dt = datetime.strptime(last_close, "%Y.%m.%d %H:%M:%S") + gap_days = (datetime.now() - last_dt).days + except Exception: + gap_days = -1 + + return { + "sl_hits": len(sl_trades), + "tp_hits": len(tp_trades), + "other_exits": len(other), + "win_loss_ratio": round(win_loss_ratio, 2), + "breakeven_win_rate": round(breakeven_wr * 100, 1), + "win_rate_gap_pct": round((len(tp_trades) / len(trades) - breakeven_wr) * 100, 1), + "consec_loss_streaks": streaks, + "reentries": reentries, + "monthly": monthly, + "lot_pattern": { + "unique_lots": unique_lots, + "uniform": len(unique_lots) == 1, + }, + "last_trade_close": last_close, + "gap_days_to_now": gap_days, + "trades": trades, + } + + # ── CLI ────────────────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser(description="Parse MT5 Strategy Tester HTML report") parser.add_argument("report", help="Path to HTML report file") parser.add_argument("--json", action="store_true", help="Output as JSON") + parser.add_argument("--analyze", action="store_true", + help="Run trade analysis (pair deals, risk check, monthly breakdown)") args = parser.parse_args() path = Path(args.report) @@ -533,7 +679,11 @@ def main(): report = parse_report(path) - if args.json: + if args.analyze: + report_dict = asdict(report) + report_dict["analyze"] = analyze_report(report) + print(json.dumps(report_dict, indent=2, ensure_ascii=False)) + elif args.json: print(json.dumps(asdict(report), indent=2, ensure_ascii=False)) else: print_report(report)