Feature:
- added IC markets trade export file to trade analysis with account filter option
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
icmarkets_parser.py
|
||||
===================
|
||||
Parser for IC Markets MT5 Position History XLSX exports.
|
||||
|
||||
The file contains one sheet ("MT5 Position history List") with:
|
||||
- Row 0: "Report"
|
||||
- Row 1: Name / Produced At metadata
|
||||
- Row 2: Column headers
|
||||
- Row 3+: Deal rows (one row per leg — In or Out)
|
||||
|
||||
Each closed trade has two legs sharing the same Position ID:
|
||||
- "Trade Buy In" / "Trade Sell In" → entry leg (Profit = 0)
|
||||
- "Trade Buy Out" / "Trade Sell Out" → exit leg (Profit = actual P/L)
|
||||
|
||||
Open positions have only an In leg (no Out yet).
|
||||
|
||||
Usage
|
||||
-----
|
||||
from icmarkets_parser import parse_icmarkets_xlsx, get_icmarkets_accounts
|
||||
|
||||
# Get list of accounts in the file
|
||||
accounts = get_icmarkets_accounts(file_bytes)
|
||||
# → ['11586098', '11586099']
|
||||
|
||||
# Parse all accounts (returns combined DataFrame)
|
||||
df = parse_icmarkets_xlsx(file_bytes)
|
||||
|
||||
# Parse a specific account
|
||||
df = parse_icmarkets_xlsx(file_bytes, account="11586098")
|
||||
|
||||
Output columns (normalised to match mt5_parser schema)
|
||||
-------------------------------------------------------
|
||||
symbol, type, open_time, close_time, open_price, close_price,
|
||||
volume, net_profit, win, commission, swap, comment,
|
||||
_account, _strategy, position_id, position_status,
|
||||
open_date, close_date, day_of_week, hour, duration_min,
|
||||
symbol_base
|
||||
"""
|
||||
|
||||
import io
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Public helpers
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def get_icmarkets_accounts(file_bytes: bytes) -> 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)
|
||||
+117
-22
@@ -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("<br>", 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'
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user