Fixed mt5 parser for accounts with zero trades
This commit is contained in:
+69
-2
@@ -14,8 +14,6 @@ All normalise to a common DataFrame schema.
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from streamlit import text
|
|
||||||
|
|
||||||
|
|
||||||
# ── Common schema ─────────────────────────────────────────────────────────────
|
# ── Common schema ─────────────────────────────────────────────────────────────
|
||||||
# open_time, close_time, symbol, type, volume, open_price, close_price,
|
# 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
|
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('<b>Deals</b>')
|
||||||
|
working_start = text.find('<b>Working Orders</b>')
|
||||||
|
if deals_start == -1:
|
||||||
|
return None
|
||||||
|
|
||||||
|
section = text[deals_start:working_start] if working_start != -1 else text[deals_start:]
|
||||||
|
rows = re.findall(r'<tr[^>]*>(.*?)</tr>', section, re.DOTALL)
|
||||||
|
|
||||||
|
deals = []
|
||||||
|
for row in rows:
|
||||||
|
cells = re.findall(r'<t[dh][^>]*>(.*?)</t[dh]>', 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 ────────────────────────────────────────────────────────
|
# ── Auto-detect format ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def detect_and_parse(file_bytes, filename=''):
|
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)
|
df = parse_backtest_report(file_bytes, fallback_strategy=fallback)
|
||||||
return df, 'MT5 Backtest Report'
|
return df, 'MT5 Backtest Report'
|
||||||
|
|
||||||
|
# Prefer Deals format if present (newer MT5 Trade History Reports)
|
||||||
|
if '<b>Deals</b>' 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)
|
df = parse_mt5_report(file_bytes, fallback_strategy=fallback)
|
||||||
return df, 'MT5 Account History'
|
return df, 'MT5 Account History'
|
||||||
|
|
||||||
|
|||||||
+34
-2
@@ -111,17 +111,49 @@ def _extract_report_date(raw: bytes) -> str | None:
|
|||||||
|
|
||||||
def refresh_account(cfg: dict, account_folder: str, label: str = "") -> dict:
|
def refresh_account(cfg: dict, account_folder: str, label: str = "") -> dict:
|
||||||
"""Download, parse, cache one account. Returns {df, stats, error}."""
|
"""Download, parse, cache one account. Returns {df, stats, error}."""
|
||||||
|
import traceback
|
||||||
from mt5_parser import detect_and_parse, calc_stats
|
from mt5_parser import detect_and_parse, calc_stats
|
||||||
|
|
||||||
raw = ftp_download_report(cfg, account_folder)
|
raw = ftp_download_report(cfg, account_folder)
|
||||||
if raw is None:
|
if raw is None:
|
||||||
|
st.warning(f"[{account_folder}] ftp_download_report returned None")
|
||||||
return {"error": f"No report found for {account_folder}"}
|
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:
|
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}"}
|
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 {}
|
stats = calc_stats(df) if not df.empty else {}
|
||||||
|
|
||||||
from mt5_parser import parse_open_positions
|
from mt5_parser import parse_open_positions
|
||||||
df_open = parse_open_positions(raw)
|
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 = {
|
data = {
|
||||||
"account_folder": account_folder,
|
"account_folder": account_folder,
|
||||||
"label" : label or account_folder,
|
"label" : label or account_folder,
|
||||||
|
|||||||
Reference in New Issue
Block a user