diff --git a/AGENTS.md b/AGENTS.md index e7ba09e..0df4fba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,8 @@ mql5-skills/ ├── LICENSE # MIT license ├── README.md # Public readme ├── pyproject.toml # uv project config +├── .python-version # Pins Python 3.14 +├── uv.lock # Locked dependencies ├── sitemaps/ # Source sitemaps from mql5.com │ ├── sitemap_book_en.xml # 581 URLs → programming book │ └── sitemap_docs_en.xml # 4135 URLs → API reference docs @@ -26,10 +28,14 @@ mql5-skills/ │ └── docs/ # 4135 .html files ├── scripts/ # Extraction scripts (Python) │ └── extract.py # Main extraction: XML → HTML → Markdown +├── resources/ # Static resources +│ └── random-user-agents.csv ├── skills/ │ └── mql5/ # The MQL5 development skill │ ├── SKILL.md # Skill definition (agentskills.io spec) │ ├── scripts/ +│ │ ├── mql5_helper.py # Compile/deploy/status via Wine +│ │ ├── parse_tester_report.py # Backtest report parser + analysis │ │ └── verify_sl_tp_formulas.py # SL/TP risk formula verification │ └── references/ │ ├── book/ # Programming book markdown (from sitemap_book_en.xml) @@ -47,6 +53,11 @@ mql5-skills/ │ └── symbol-spec/ # Broker symbol specifications (CSV) │ ├── specs-XAUUSD.csv │ └── specs-USDJPY.csv +├── jobs/ # Backtest job folders +│ ├── 250013-job.md # Job specification +│ └── ReportTester-250013/ +│ ├── ReportTester-600xxxxx.html # MT5 Strategy Tester HTML report +│ └── ReportTester-600xxxxx*.png # Screenshots (equity, holding, MFE/MAE) └── docs-dev/ # Development documentation ├── extraction.md # Extraction workflow and script design ├── naming.md # Folder/file naming conventions @@ -62,6 +73,40 @@ Per agentskills.io spec: - Optional dirs: `scripts/`, `references/`, `assets/` - Focus areas: positions, orders, indicators, ticks, bars +## Scripts + +### parse_tester_report.py + +Parses MT5 Strategy Tester HTML reports. Supports three output modes: + +``` +# Text report (default) +python skills/mql5/scripts/parse_tester_report.py + +# JSON dump (raw parsed data) +python skills/mql5/scripts/parse_tester_report.py --json + +# JSON with trade analysis (--analyze includes idle_time, monthly breakdown, etc.) +python skills/mql5/scripts/parse_tester_report.py --analyze +``` + +Key analysis fields: `idle_time` (HH:MM:SS flat duration across backtest period), +`win_loss_ratio`, `breakeven_win_rate`, `monthly`, `reentries`, `lot_pattern`. + +### mql5_helper.py + +MT5 development helper for compile/deploy/status via Wine: + +``` +python skills/mql5/scripts/mql5_helper.py compile FILE.mq5 +python skills/mql5/scripts/mql5_helper.py check FILE.mq5 # syntax only (/s flag) +python skills/mql5/scripts/mql5_helper.py deploy FILE.mq5 +python skills/mql5/scripts/mql5_helper.py status +python skills/mql5/scripts/mql5_helper.py list +``` + +MT5 paths resolved in order: `$MQL5_DIR` env → cwd walk-up → Program Files scan → Wine fallback. + ## Extraction Workflow Two-phase pipeline (network only needed for Phase 1): @@ -87,6 +132,25 @@ See `docs-dev/naming.md` for full specification. Key rules: - Max one level of subfolder under `book/` or `docs/` - Each chapter folder contains a `pics/` subfolder +## Ad-hoc Verification + +No formal test suite. Scripts are verified via temporary scripts under `/tmp` with +`hermes-verify-` filename prefix. Pattern: + +1. Write a focused verification script to `/tmp/hermes-verify-.py` +2. Import the changed functions, exercise them with known inputs +3. Run via `uv run python /tmp/hermes-verify-.py` (requires project venv for deps) +4. Clean up the temp file after passing + +Example (from `parse_tester_report.py` changes): +```bash +# Create /tmp/hermes-verify-parse-tester.py with test cases +# Run: +uv run python /tmp/hermes-verify-parse-tester.py +# Clean up: +rm /tmp/hermes-verify-parse-tester.py +``` + ## Git Workflow - Conventional commits diff --git a/skills/mql5/scripts/parse_tester_report.py b/skills/mql5/scripts/parse_tester_report.py index d8f490e..1619c81 100644 --- a/skills/mql5/scripts/parse_tester_report.py +++ b/skills/mql5/scripts/parse_tester_report.py @@ -429,7 +429,7 @@ def parse_report(html_path: Path) -> Report: # ── Pretty print ───────────────────────────────────────────────────── -def print_report(r: Report) -> None: +def print_report(r: Report, analyze_data: dict | None = None) -> None: s = r.settings res = r.results @@ -498,6 +498,8 @@ def print_report(r: Report) -> None: print(" Holding Times") print(f"{'─' * 72}") print(f" Min: {res.min_hold_time} Max: {res.max_hold_time} Avg: {res.avg_hold_time}") + if analyze_data: + print(f" Idle (no position): {analyze_data.get('idle_time', '')}") print(f"\n{'─' * 72}") print(f" Orders: {len(r.orders)} Deals: {len(r.deals)}") @@ -559,9 +561,19 @@ def pair_trades(deals: list) -> list: return trades +def format_duration(td) -> str: + """Format timedelta as HH:MM:SS.""" + total = int(td.total_seconds()) + sign = "-" if total < 0 else "" + total = abs(total) + h, rem = divmod(total, 3600) + m, s = divmod(rem, 60) + return f"{sign}{h:02d}:{m:02d}:{s:02d}" + + def analyze_report(report: Report) -> dict: """Run full trade analysis on parsed report.""" - from datetime import datetime + from datetime import datetime, timedelta deposit = report.settings.initial_deposit trades = pair_trades(report.deals) @@ -569,13 +581,18 @@ def analyze_report(report: Report) -> dict: if not trades: return {"error": "No trades found", "trades": []} - # Parse backtest end date from period string (e.g. "H4 (2024.01.01 - 2025.06.22)") + # Parse backtest start/end dates from period string + # e.g. "H4 (2024.01.01 - 2025.06.22)" + bt_start = None bt_end = None period = report.settings.period - m = re.search(r"(\d{4}\.\d{2}\.\d{2})\s*\)\s*$", period) - if m: + m_dates = re.search( + r"(\d{4}\.\d{2}\.\d{2})\s*-\s*(\d{4}\.\d{2}\.\d{2})\s*\)\s*$", period + ) + if m_dates: try: - bt_end = datetime.strptime(m.group(1), "%Y.%m.%d") + bt_start = datetime.strptime(m_dates.group(1), "%Y.%m.%d") + bt_end = datetime.strptime(m_dates.group(2), "%Y.%m.%d") except ValueError: pass @@ -644,14 +661,17 @@ def analyze_report(report: Report) -> dict: lots = [t["volume"] for t in trades] unique_lots = sorted(set(lots)) - # Last trade gap relative to backtest end date - last_close = trades[-1]["close_time"] - try: - last_dt = datetime.strptime(last_close, "%Y.%m.%d %H:%M:%S") - ref_date = bt_end if bt_end else datetime.now() - gap_days = (ref_date - last_dt).days - except Exception: - gap_days = -1 + # Idle time: total backtest duration minus time in positions + idle_str = "" + if bt_start and bt_end: + total_duration = bt_end - bt_start + position_time = timedelta() + for t in trades: + close_dt = datetime.strptime(t["close_time"], "%Y.%m.%d %H:%M:%S") + open_dt = datetime.strptime(t["open_time"], "%Y.%m.%d %H:%M:%S") + position_time += close_dt - open_dt + idle_td = total_duration - position_time + idle_str = format_duration(idle_td) return { "sl_hits": len(sl_trades), @@ -667,8 +687,7 @@ def analyze_report(report: Report) -> dict: "unique_lots": unique_lots, "uniform": len(unique_lots) == 1, }, - "last_trade_close": last_close, - "gap_days_to_end": gap_days, + "idle_time": idle_str, "trades": trades, } @@ -690,14 +709,17 @@ def main(): report = parse_report(path) + # Always compute analyze data (needed for idle_time in text report) + analyze_data = analyze_report(report) + if args.analyze: report_dict = asdict(report) - report_dict["analyze"] = analyze_report(report) + report_dict["analyze"] = analyze_data 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) + print_report(report, analyze_data) if __name__ == "__main__":