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:
@@ -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
|
||||
|
||||
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user