From fffaddb2bf5f77f3517778bf005e814971fa696f Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 13 Jun 2026 08:14:14 +1000 Subject: [PATCH] Fixed mt5 parser for accounts with zero trades --- mt5_parser.py | 71 ++++++++++++++++++++++++++++++++++++++++++-- view_live_mt5_eas.py | 36 ++++++++++++++++++++-- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/mt5_parser.py b/mt5_parser.py index f313b89..a947acd 100644 --- a/mt5_parser.py +++ b/mt5_parser.py @@ -14,8 +14,6 @@ All normalise to a common DataFrame schema. import pandas as pd import re -from streamlit import text - # ── Common schema ───────────────────────────────────────────────────────────── # open_time, close_time, symbol, type, volume, open_price, close_price, @@ -394,6 +392,70 @@ def parse_open_positions(file_bytes) -> 'pd.DataFrame | None': return df +# ── Format 4: Trade History Report (Deals format) ──────────────────────────── + +def parse_mt5_deals_report(file_bytes, fallback_strategy=None): + """Parse MT5 Trade History Report (Deals format — newer MT5 prop/live accounts). + This format has separate Deals section with individual entry/exit rows, + rather than the older Positions format with entry+exit on the same row. + """ + text = _decode(file_bytes) + + # Extract only the Deals section (stop at Working Orders) + deals_start = text.find('Deals') + working_start = text.find('Working Orders') + if deals_start == -1: + return None + + section = text[deals_start:working_start] if working_start != -1 else text[deals_start:] + rows = re.findall(r']*>(.*?)', section, re.DOTALL) + + deals = [] + for row in rows: + cells = re.findall(r']*>(.*?)', row, re.DOTALL) + cells = [re.sub(r'\s+', ' ', _strip(c)).strip() for c in cells] + # Remove hidden Cost cell (rendered but display:none — _strip removes tags + # but the empty cell is still present as empty string) + cells = [c for c in cells if c != ''] + + # Need at least: time, deal, symbol, type, direction, volume, price, + # order, commission, fee, swap, profit, balance, comment + if len(cells) < 12: + continue + if not re.match(r'\d{4}\.\d{2}\.\d{2}', cells[0]): + continue + + deal_type = cells[3].lower() + if deal_type in ('balance', 'credit'): + continue # skip deposit/withdrawal rows + + deals.append({ + 'open_time' : cells[0], + 'position' : cells[1], # deal ID + 'symbol' : cells[2], + 'type' : cells[3], + 'direction' : cells[4], + 'volume' : cells[5], + 'open_price' : cells[6], + 'close_time' : cells[0], # deals don't have separate close time + 'close_price': cells[6], + 'commission' : cells[8] if len(cells) > 8 else '0', + 'swap' : cells[10] if len(cells) > 10 else '0', + 'profit' : cells[11] if len(cells) > 11 else '0', + 'comment' : cells[13] if len(cells) > 13 else '', + 'sl' : '', + 'tp' : '', + }) + + if not deals: + # Valid case — new account with no closed trades yet + return pd.DataFrame() + + df = pd.DataFrame(deals) + df['source'] = 'real' + return _enrich(df, fallback_strategy=fallback_strategy) + + # ── Auto-detect format ──────────────────────────────────────────────────────── def detect_and_parse(file_bytes, filename=''): @@ -424,6 +486,11 @@ def detect_and_parse(file_bytes, filename=''): df = parse_backtest_report(file_bytes, fallback_strategy=fallback) return df, 'MT5 Backtest Report' + # Prefer Deals format if present (newer MT5 Trade History Reports) + if 'Deals' in text: + df = parse_mt5_deals_report(file_bytes, fallback_strategy=fallback) + return df, 'MT5 Account History' + df = parse_mt5_report(file_bytes, fallback_strategy=fallback) return df, 'MT5 Account History' diff --git a/view_live_mt5_eas.py b/view_live_mt5_eas.py index 60d4c21..ef075cb 100644 --- a/view_live_mt5_eas.py +++ b/view_live_mt5_eas.py @@ -111,17 +111,49 @@ def _extract_report_date(raw: bytes) -> str | None: def refresh_account(cfg: dict, account_folder: str, label: str = "") -> dict: """Download, parse, cache one account. Returns {df, stats, error}.""" + import traceback from mt5_parser import detect_and_parse, calc_stats + raw = ftp_download_report(cfg, account_folder) if raw is None: + st.warning(f"[{account_folder}] ftp_download_report returned None") return {"error": f"No report found for {account_folder}"} - df, fmt = detect_and_parse(raw, f"{account_folder}.htm") + + st.write(f"[{account_folder}] raw bytes: {len(raw)}, first 4 bytes: {raw[:4]}") + + try: + df, fmt = detect_and_parse(raw, f"{account_folder}.htm") + except Exception as e: + st.error(f"[{account_folder}] detect_and_parse raised: {e}") + st.code(traceback.format_exc()) + return {"error": str(e)} + + st.write(f"[{account_folder}] fmt={fmt}, df={'None' if df is None else f'shape {df.shape}'}") + if df is None: + st.warning(f"[{account_folder}] detect_and_parse returned None df") return {"error": f"Could not parse report for {account_folder}"} - # df.empty is valid for a new account with no closed trades yet + stats = calc_stats(df) if not df.empty else {} + from mt5_parser import parse_open_positions df_open = parse_open_positions(raw) + + data = { + "account_folder": account_folder, + "label" : label or account_folder, + "df" : df, + "stats" : stats, + "fmt" : fmt, + "df_open" : df_open, + "fetched_at" : datetime.now().isoformat(), + "report_date" : _extract_report_date(raw), + "error" : None, + } + CACHE_DIR.mkdir(exist_ok=True) + (CACHE_DIR / f"ftp_{account_folder}.pkl").write_bytes(pickle.dumps(data)) + return data + data = { "account_folder": account_folder, "label" : label or account_folder,