7a3e13a734
Full optimization system for LEGSTECH_EA_V2: - Flask + SocketIO live dashboard (dark premium UI) - MT5 process control (auto-kill, clean launch, retry) - HTML report parser (UTF-16 LE, 597 trades, metrics) - Pre-run validation and actionable error messages - Analysis engines: Reversal, TimePerfomance, EntryExit, EquityCurve - Composite scoring (Calmar-primary) - Mutation engine with knowledge_base.yaml - Validation gate: IS + Walk-Forward - Reports folder with HTML/CSV per run - Double-click launcher batch file
160 lines
6.4 KiB
Python
160 lines
6.4 KiB
Python
"""
|
|
mt5/log_reader.py
|
|
Reads the TradeLogger.mqh CSV and merges MAE/MFE data into parsed trades.
|
|
Also computes derived fields: session, day_of_week, result_class, quality scores.
|
|
"""
|
|
from __future__ import annotations
|
|
from datetime import datetime, timezone, timedelta
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import pandas as pd
|
|
from loguru import logger
|
|
|
|
from data.models import Trade
|
|
|
|
|
|
# Session definitions in UTC hours (inclusive start, exclusive end)
|
|
SESSIONS_UTC = {
|
|
"Asian": (0, 9),
|
|
"London": (7, 16),
|
|
"LondonNY": (13, 16),
|
|
"NY": (13, 22),
|
|
}
|
|
|
|
|
|
def classify_session(hour_utc: int) -> str:
|
|
"""Classify a UTC hour into its primary trading session."""
|
|
in_london = SESSIONS_UTC["London"][0] <= hour_utc < SESSIONS_UTC["London"][1]
|
|
in_ny = SESSIONS_UTC["NY"][0] <= hour_utc < SESSIONS_UTC["NY"][1]
|
|
|
|
if in_london and in_ny:
|
|
return "LondonNY"
|
|
elif in_london:
|
|
return "London"
|
|
elif in_ny:
|
|
return "NY"
|
|
elif SESSIONS_UTC["Asian"][0] <= hour_utc < SESSIONS_UTC["Asian"][1]:
|
|
return "Asian"
|
|
else:
|
|
return "Off"
|
|
|
|
|
|
# ── Main reader/merger ────────────────────────────────────────────────────────
|
|
|
|
class TradeLogReader:
|
|
"""
|
|
Reads the CSV produced by TradeLogger.mqh and merges into a list of Trade objects.
|
|
|
|
Strategy:
|
|
1. Load CSV, index by ticket
|
|
2. For each Trade, look up ticket in CSV
|
|
3. Fill mfe_pips, mae_pips, duration_minutes if found
|
|
4. Compute all derived fields for every trade (session, quality scores, etc.)
|
|
"""
|
|
|
|
def __init__(self, broker_tz_offset_hours: int = 2, pip_size: float = 0.1):
|
|
self.tz_offset = broker_tz_offset_hours # broker local = UTC + offset
|
|
self.pip_size = pip_size # XAUUSD: 0.1 per pip
|
|
|
|
# ── Public ────────────────────────────────────────────────────────────────
|
|
|
|
def merge(
|
|
self,
|
|
trades: list[Trade],
|
|
csv_path: Optional[str | Path],
|
|
reversal_mfe_threshold_pips: float = 15.0,
|
|
) -> list[Trade]:
|
|
"""
|
|
Merge TradeLogger CSV into trade list, compute all derived fields.
|
|
If csv_path is None or unreadable, derived fields are computed without MFE/MAE.
|
|
"""
|
|
log_df = self._load_csv(csv_path) if csv_path else None
|
|
|
|
enriched = []
|
|
for trade in trades:
|
|
# Fill MAE/MFE from logger if available
|
|
if log_df is not None and trade.ticket in log_df.index:
|
|
row = log_df.loc[trade.ticket]
|
|
trade.mfe_pips = float(row.get("mfe_pips", 0) or 0)
|
|
trade.mae_pips = float(row.get("mae_pips", 0) or 0)
|
|
# Override duration with logger value (tick-accurate)
|
|
if "duration_minutes" in row:
|
|
trade.duration_minutes = int(row["duration_minutes"] or trade.duration_minutes)
|
|
|
|
# Compute all derived fields
|
|
trade = self._enrich(trade, reversal_mfe_threshold_pips)
|
|
enriched.append(trade)
|
|
|
|
logger.info(
|
|
f"Enriched {len(enriched)} trades. "
|
|
f"MAE/MFE available: {sum(1 for t in enriched if t.mfe_pips is not None)}"
|
|
)
|
|
return enriched
|
|
|
|
# ── Internal ──────────────────────────────────────────────────────────────
|
|
|
|
def _load_csv(self, csv_path: str | Path) -> Optional[pd.DataFrame]:
|
|
path = Path(csv_path)
|
|
if not path.exists():
|
|
logger.warning(f"TradeLogger CSV not found: {path}")
|
|
return None
|
|
try:
|
|
df = pd.read_csv(path, dtype={"ticket": int})
|
|
if "ticket" not in df.columns:
|
|
logger.error("TradeLogger CSV missing 'ticket' column.")
|
|
return None
|
|
df = df.set_index("ticket")
|
|
logger.debug(f"Loaded {len(df)} rows from TradeLogger CSV.")
|
|
return df
|
|
except Exception as e:
|
|
logger.error(f"Failed to read TradeLogger CSV: {e}")
|
|
return None
|
|
|
|
def _enrich(self, trade: Trade, threshold_pips: float) -> Trade:
|
|
"""Compute all derived classification and quality fields."""
|
|
# --- Timezone normalisation ---
|
|
# Broker timestamps are in broker local time (UTC+offset).
|
|
# We compute UTC hour by subtracting the offset.
|
|
broker_hour = trade.open_time.hour
|
|
hour_utc = (broker_hour - self.tz_offset) % 24
|
|
trade.hour_broker = broker_hour
|
|
trade.hour_utc = hour_utc
|
|
trade.day_of_week = trade.open_time.weekday() # 0=Mon, 4=Fri
|
|
trade.session = classify_session(hour_utc)
|
|
|
|
# --- Result class ---
|
|
won = trade.net_money > 0
|
|
be = abs(trade.net_money) < 0.01 # effectively breakeven
|
|
|
|
if be:
|
|
trade.result_class = "be"
|
|
elif won:
|
|
trade.result_class = "win"
|
|
else:
|
|
# Check if it's a reversal: lost, but had positive MFE above threshold
|
|
if trade.mfe_pips is not None and trade.mfe_pips >= threshold_pips:
|
|
trade.result_class = "reversal"
|
|
else:
|
|
trade.result_class = "loss"
|
|
|
|
# --- Quality scores (only when MFE/MAE available) ---
|
|
if trade.mfe_pips is not None and trade.mae_pips is not None:
|
|
mfe = max(trade.mfe_pips, 0.01) # prevent division by zero
|
|
mae = max(trade.mae_pips, 0.0)
|
|
|
|
# Entry quality: how far against you before move in your favour
|
|
# High = entered well (little adverse move relative to favourable move)
|
|
trade.entry_quality = max(0.0, min(1.0, 1.0 - (mae / (mfe + mae + 0.01))))
|
|
|
|
# Exit quality: what fraction of MFE did we capture
|
|
mfe_value = mfe * self.pip_size * trade.lot_size * 100 # approx value in $
|
|
if mfe_value > 0:
|
|
trade.mfe_capture_ratio = max(0.0, trade.net_money / mfe_value)
|
|
trade.exit_quality = max(0.0, min(1.0, trade.net_pips / mfe))
|
|
else:
|
|
trade.mfe_capture_ratio = 0.0
|
|
trade.exit_quality = 0.0
|
|
|
|
return trade
|