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:
+79
-2
@@ -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 |
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user