From ec80bde82d86d9def3ee90d9221bc22f0a2e406b Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 14 Apr 2026 08:39:30 +1000 Subject: [PATCH] Feature: - added IC markets trade export file to trade analysis with account filter option --- .../view_trade_analysis.cpython-314.pyc | Bin 20285 -> 25633 bytes icmarkets_parser.py | 172 ++++++++++++++++++ view_trade_analysis.py | 139 +++++++++++--- 3 files changed, 289 insertions(+), 22 deletions(-) create mode 100644 icmarkets_parser.py diff --git a/__pycache__/view_trade_analysis.cpython-314.pyc b/__pycache__/view_trade_analysis.cpython-314.pyc index 1bd2e852904212420994e7fc42082a57131d269b..bbb39628bedefe583b705b19c019f8aa7952c66d 100644 GIT binary patch delta 9902 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 list[str]: + """Return sorted list of account numbers found in the file.""" + raw = _read_raw(file_bytes) + if raw is None: + return [] + accounts = raw["Account Number"].dropna().unique().tolist() + return sorted([str(a) for a in accounts if str(a).strip()]) + + +def parse_icmarkets_xlsx(file_bytes: bytes, account: str = None) -> pd.DataFrame: + """ + Parse IC Markets position history XLSX into a normalised trade DataFrame. + + Parameters + ---------- + file_bytes : bytes + Raw bytes of the .xlsx file. + account : str, optional + If provided, only trades for this account number are returned. + If None, all accounts are combined. + + Returns + ------- + pd.DataFrame with normalised columns ready for use in mt5_parser-compatible + analysis pages. + """ + raw = _read_raw(file_bytes) + if raw is None or raw.empty: + return pd.DataFrame() + + # Filter by account if requested + if account: + raw = raw[raw["Account Number"].astype(str) == str(account)].copy() + if raw.empty: + return pd.DataFrame() + + # ── Separate In (entry) and Out (exit) legs ──────────────────────────── + ins = raw[raw["Transaction Type"].str.contains("In", na=False)].copy() + outs = raw[raw["Transaction Type"].str.contains("Out", na=False)].copy() + + for df in (ins, outs): + df["Position"] = pd.to_numeric(df["Position"], errors="coerce") + + # ── Pair by Position ID ──────────────────────────────────────────────── + paired = ins.merge( + outs[["Position", "Date Time", "Open Price", "Profit", + "Transaction Type"]].rename(columns={ + "Date Time": "Date Time_close", + "Open Price": "Close Price", + "Profit": "Profit_close", + "Transaction Type": "TT_close", + }), + on="Position", + how="left", # keep open positions too (no Out leg yet) + ) + + # ── Build normalised columns ─────────────────────────────────────────── + out = pd.DataFrame() + out["position_id"] = paired["Position"] + out["symbol"] = paired["Symbol"].astype(str) + out["symbol_base"] = out["symbol"].str.split(".").str[0] + out["_account"] = paired["Account Number"].astype(str) + out["position_status"] = paired["Position Status"].astype(str) + + # Direction from the In leg transaction type + out["type"] = paired["Transaction Type"].apply( + lambda x: "buy" if "Buy" in str(x) else "sell" + ) + + out["open_time"] = pd.to_datetime(paired["Date Time"], errors="coerce") + out["close_time"] = pd.to_datetime(paired["Date Time_close"], errors="coerce") + out["open_price"] = pd.to_numeric(paired["Open Price"], errors="coerce") + out["close_price"] = pd.to_numeric(paired["Close Price"], errors="coerce") + out["volume"] = pd.to_numeric(paired["Trade Volume Lots"],errors="coerce") + out["net_profit"] = pd.to_numeric(paired["Profit_close"], errors="coerce").fillna(0) + out["commission"] = 0.0 # IC Markets file doesn't separate commission + out["swap"] = 0.0 + out["comment"] = "" + out["_strategy"] = out["symbol_base"] # use symbol as strategy label + + # ── Derived columns ──────────────────────────────────────────────────── + out["win"] = out["net_profit"] > 0 + out["open_date"] = out["open_time"].dt.date + out["close_date"] = out["close_time"].dt.date + out["day_of_week"] = out["open_time"].dt.day_name() + out["hour"] = out["open_time"].dt.hour + out["duration_min"] = ((out["close_time"] - out["open_time"]) + .dt.total_seconds() / 60).round(1) + + return out.reset_index(drop=True) + + +# ───────────────────────────────────────────────────────────────────────────── +# Internal helpers +# ───────────────────────────────────────────────────────────────────────────── + +def _read_raw(file_bytes: bytes) -> pd.DataFrame | None: + """Read the raw sheet and return a DataFrame with named columns.""" + try: + buf = io.BytesIO(file_bytes) + df = pd.read_excel(buf, sheet_name=0, header=None, dtype=str) + except Exception: + return None + + # Find the header row — it contains "Symbol" in column 0 + header_row = None + for i, row in df.iterrows(): + if str(row.iloc[0]).strip() == "Symbol": + header_row = i + break + + if header_row is None: + return None + + df.columns = df.iloc[header_row].tolist() + df = df.iloc[header_row + 1:].copy() + df.columns.name = None + + # Keep only real data rows (Position Status = Open or Closed) + if "Position Status" in df.columns: + df = df[df["Position Status"].isin(["Open", "Closed"])].copy() + + return df.reset_index(drop=True) \ No newline at end of file diff --git a/view_trade_analysis.py b/view_trade_analysis.py index 19eb112..a9b2f7a 100644 --- a/view_trade_analysis.py +++ b/view_trade_analysis.py @@ -11,37 +11,132 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from mt5_parser import detect_and_parse, calc_stats + +def _normalise_ic(df): + """Map IC Markets DataFrame columns to the schema expected by calc_stats.""" + import pandas as pd + out = df.copy() + # calc_stats / render helpers need: open_time, close_time, symbol, type, + # strategy, net_profit, win, volume, open_price, close_price, + # commission, swap, profit, duration_min, day_of_week, hour + if "symbol_base" in out.columns and "strategy" not in out.columns: + out["strategy"] = out["symbol_base"] + if "net_profit" in out.columns and "profit" not in out.columns: + out["profit"] = out["net_profit"] + if "commission" not in out.columns: + out["commission"] = 0.0 + if "swap" not in out.columns: + out["swap"] = 0.0 + if "sl" not in out.columns: + out["sl"] = None + if "tp" not in out.columns: + out["tp"] = None + # Ensure win column + if "win" not in out.columns and "net_profit" in out.columns: + out["win"] = out["net_profit"] > 0 + # Ensure day_of_week and hour + if "open_time" in out.columns: + out["open_time"] = pd.to_datetime(out["open_time"], errors="coerce") + if "day_of_week" not in out.columns: + out["day_of_week"] = out["open_time"].dt.day_name() + if "hour" not in out.columns: + out["hour"] = out["open_time"].dt.hour + if "close_time" in out.columns: + out["close_time"] = pd.to_datetime(out["close_time"], errors="coerce") + if "duration_min" not in out.columns and "open_time" in out.columns and "close_time" in out.columns: + out["duration_min"] = ((out["close_time"] - out["open_time"]) + .dt.total_seconds() / 60).round(1) + return out + + def render(): st.title("📊 Trade Analysis") # ── Session state ───────────────────────────────────────────────────────── - if 'ta_df' not in st.session_state: - st.session_state['ta_df'] = None - st.session_state['ta_format'] = None + for _k, _v in { + 'ta_df': None, 'ta_format': None, + 'ta_accounts': [], 'ta_ic_bytes': None, + }.items(): + if _k not in st.session_state: + st.session_state[_k] = _v - # ── File upload ─────────────────────────────────────────────────────────── - col1, col2 = st.columns([4, 1]) - with col1: - uploaded = st.file_uploader( - "Upload MT5 Report (HTM/HTML) or Quant Analyzer CSV", - type=['html', 'htm', 'csv'], - key='ta_upload' + # ── Source selector ────────────────────────────────────────────────────── + src_col1, src_col2 = st.columns([4, 1]) + with src_col1: + source = st.radio( + "File source", + ["MT5 / Quant Analyzer", "IC Markets XLSX"], + horizontal=True, key='ta_source', ) - with col2: + with src_col2: st.markdown("
", unsafe_allow_html=True) if st.button("🗑 Clear", key='ta_clear'): - st.session_state['ta_df'] = None - st.session_state['ta_format'] = None + st.session_state['ta_df'] = None + st.session_state['ta_format'] = None + st.session_state['ta_accounts'] = [] st.rerun() - if uploaded: - df, fmt = detect_and_parse(uploaded.read(), uploaded.name) - if df is not None: - st.session_state['ta_df'] = df - st.session_state['ta_format'] = fmt - st.success(f"✓ Loaded {len(df)} trades — {fmt}") - else: - st.error("Could not parse report — check file format") + # ── File upload ─────────────────────────────────────────────────────────── + if source == "MT5 / Quant Analyzer": + uploaded = st.file_uploader( + "Upload MT5 Report (HTM/HTML) or Quant Analyzer CSV", + type=None, key='ta_upload', + ) + if uploaded and uploaded.name.lower().endswith(('.htm','.html','.csv')): + df, fmt = detect_and_parse(uploaded.read(), uploaded.name) + if df is not None: + st.session_state['ta_df'] = df + st.session_state['ta_format'] = fmt + st.session_state['ta_accounts'] = [] + st.success(f"✓ Loaded {len(df)} trades — {fmt}") + else: + st.error("Could not parse report — check file format") + elif uploaded: + st.warning("Please upload a .htm, .html, or .csv file.") + + else: # IC Markets XLSX + uploaded = st.file_uploader( + "Upload IC Markets Position History (.xlsx)", + type=None, key='ta_upload', + ) + if uploaded and uploaded.name.lower().endswith(('.xlsx','.xls')): + try: + from icmarkets_parser import get_icmarkets_accounts, parse_icmarkets_xlsx + except ImportError as e: + st.error(f"icmarkets_parser.py not found — ensure it is in the MT5Tools folder. ({e})") + uploaded = None + if uploaded: + try: + file_bytes = uploaded.read() + accounts = get_icmarkets_accounts(file_bytes) + if not accounts: + st.error("No accounts found — check this is an IC Markets Position History export.") + else: + st.session_state['ta_ic_bytes'] = file_bytes + st.session_state['ta_accounts'] = accounts + st.session_state['ta_format'] = "IC Markets XLSX" + df_ic = parse_icmarkets_xlsx(file_bytes, account=accounts[0]) + df_ic = _normalise_ic(df_ic) + st.session_state['ta_df'] = df_ic + st.success(f"✓ Loaded {len(df_ic)} trades — {len(accounts)} account(s) found") + except Exception as e: + st.error(f"Error parsing file: {e}") + import traceback; st.code(traceback.format_exc()) + elif uploaded: + st.warning("Please upload an .xlsx file.") + + # IC Markets account selector (shown after upload) + if (st.session_state.get('ta_accounts') and + st.session_state.get('ta_source', source) == "IC Markets XLSX"): + accounts = st.session_state['ta_accounts'] + ac_opts = ["All accounts"] + accounts + sel_ac = st.selectbox("Account", ac_opts, key='ta_ic_account') + acct = None if sel_ac == "All accounts" else sel_ac + if st.session_state.get('ta_ic_bytes'): + from icmarkets_parser import parse_icmarkets_xlsx + df_ic = parse_icmarkets_xlsx(st.session_state['ta_ic_bytes'], account=acct) + df_ic = _normalise_ic(df_ic) + st.session_state['ta_df'] = df_ic df_all = st.session_state['ta_df'] fmt = st.session_state['ta_format'] @@ -330,4 +425,4 @@ def render(): data = df[show_cols].to_csv(index=False), file_name = f"mt5_trades_{date_from}_{date_to}.csv", mime = 'text/csv' - ) + ) \ No newline at end of file