feat: sync SKILL.md and parse_tester_report.py with installed version

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).
This commit is contained in:
ZhijuCen
2026-06-25 02:51:22 +08:00
parent 63a8d67fb0
commit 8482f7a8a1
2 changed files with 230 additions and 3 deletions
+79 -2
View File
@@ -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<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
```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 |
+151 -1
View File
@@ -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)