feat(parse_tester_report): replace gap_days_to_end with idle_time (HH:MM:SS)

- Remove gap_days_to_end and last_trade_close from analyze_report output
- Add idle_time: total backtest duration minus all position holding times
  (includes flat time before first trade and after last trade)
- Add format_duration() helper for timedelta -> HH:MM:SS formatting
- Parse both start/end dates from period string for accurate calculation
- Update print_report() to display idle_time in Holding Times section
- main() now always computes analyze data for text report
- Update AGENTS.md: add scripts docs, jobs/resources dirs, verification method
This commit is contained in:
ZhijuCen
2026-06-29 12:20:16 +08:00
parent 4f9c569538
commit aa0909764f
2 changed files with 104 additions and 18 deletions
+64
View File
@@ -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 <report.html>
# JSON dump (raw parsed data)
python skills/mql5/scripts/parse_tester_report.py <report.html> --json
# JSON with trade analysis (--analyze includes idle_time, monthly breakdown, etc.)
python skills/mql5/scripts/parse_tester_report.py <report.html> --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-<topic>.py`
2. Import the changed functions, exercise them with known inputs
3. Run via `uv run python /tmp/hermes-verify-<topic>.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
+40 -18
View File
@@ -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__":