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,