Initial commit: MT5 EA Optimizer v1.0
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
This commit is contained in:
+181
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
data/models.py
|
||||
Pydantic v2 data models for all entities in the optimizer.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal, Optional
|
||||
from pydantic import BaseModel, Field, computed_field, model_validator
|
||||
import uuid
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Trade
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class Trade(BaseModel):
|
||||
"""One closed trade, enriched with MAE/MFE from the logger CSV."""
|
||||
ticket: int
|
||||
open_time: datetime
|
||||
close_time: datetime
|
||||
direction: Literal["buy", "sell"]
|
||||
open_price: float
|
||||
close_price: float
|
||||
sl: float
|
||||
tp: float
|
||||
lot_size: float
|
||||
net_pips: float
|
||||
net_money: float
|
||||
duration_minutes: int
|
||||
commission: float = 0.0
|
||||
swap: float = 0.0
|
||||
|
||||
# From TradeLogger CSV (may be None if logger not available)
|
||||
mfe_pips: Optional[float] = None
|
||||
mae_pips: Optional[float] = None
|
||||
|
||||
# Derived — populated during ingest
|
||||
session: Optional[str] = None # London | NY | Asian | LondonNY | Off
|
||||
day_of_week: Optional[int] = None # 0=Mon … 4=Fri
|
||||
hour_broker: Optional[int] = None # broker local hour
|
||||
hour_utc: Optional[int] = None # UTC hour (after timezone normalisation)
|
||||
result_class: Optional[str] = None # win | loss | be | reversal
|
||||
|
||||
# Computed quality scores (None when MFE/MAE not available)
|
||||
mfe_capture_ratio: Optional[float] = None # net_money / mfe_value; 1.0 = captured all
|
||||
entry_quality: Optional[float] = None # 1 - (mae_pips / max(mfe_pips,1))
|
||||
exit_quality: Optional[float] = None # net_pips / max(mfe_pips, 1)
|
||||
|
||||
@property
|
||||
def won(self) -> bool:
|
||||
return self.net_money > 0
|
||||
|
||||
@property
|
||||
def lost(self) -> bool:
|
||||
return self.net_money < 0
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# RunMetrics
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class RunMetrics(BaseModel):
|
||||
"""Summary metrics for one backtest run."""
|
||||
run_id: str
|
||||
net_profit: float
|
||||
profit_factor: float
|
||||
max_drawdown_abs: float # in account currency
|
||||
max_drawdown_pct: float # as fraction (0.15 = 15%)
|
||||
calmar_ratio: float
|
||||
sharpe_ratio: float
|
||||
total_trades: int
|
||||
win_rate: float # fraction (0.55 = 55%)
|
||||
avg_win: float
|
||||
avg_loss: float
|
||||
recovery_factor: float
|
||||
largest_loss: float
|
||||
expected_payoff: float
|
||||
|
||||
# MAE/MFE derived (populated when logger CSV available)
|
||||
avg_mfe_capture: Optional[float] = None
|
||||
avg_mfe_pips: Optional[float] = None
|
||||
avg_mae_pips: Optional[float] = None
|
||||
reversal_rate: Optional[float] = None # reverted trades / total losers
|
||||
|
||||
# Filled by composite scorer
|
||||
composite_score: float = 0.0
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Run
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class Run(BaseModel):
|
||||
"""One complete backtest run record."""
|
||||
run_id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
|
||||
run_ts: datetime = Field(default_factory=datetime.utcnow)
|
||||
ea_name: str
|
||||
symbol: str
|
||||
timeframe: str
|
||||
period_start: str
|
||||
period_end: str
|
||||
params: dict[str, Any] # snapshot of all EA inputs used
|
||||
phase: Literal["baseline", "explore", "validate", "wfv", "oos"]
|
||||
hypothesis_id: Optional[str] = None
|
||||
tester_model: int = 0 # 0=Every Tick
|
||||
ini_snapshot: Optional[str] = None # full .ini content for reproducibility
|
||||
report_path: Optional[str] = None
|
||||
log_csv_path: Optional[str] = None
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Finding
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class Finding(BaseModel):
|
||||
"""One actionable observation from an analyzer module."""
|
||||
finding_id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
|
||||
run_id: str
|
||||
analyzer: str
|
||||
description: str
|
||||
severity: Literal["high", "medium", "low"]
|
||||
confidence: float # 0.0–1.0
|
||||
impact_estimate_pnl: float = 0.0 # estimated PnL recovery if addressed
|
||||
suggested_params: dict[str, Any] = {}
|
||||
evidence: dict[str, Any] = {} # raw supporting data (for reports)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Hypothesis
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class Hypothesis(BaseModel):
|
||||
"""A proposed parameter change, motivated by one or more findings."""
|
||||
hypothesis_id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
|
||||
parent_run_id: str
|
||||
finding_ids: list[str]
|
||||
description: str
|
||||
param_delta: dict[str, Any] # {param_name: proposed_value}
|
||||
strategy: Literal["targeted", "compound", "rollback", "explore"]
|
||||
kb_rule_id: Optional[str] = None # KB rule that generated this
|
||||
status: Literal["pending", "tested", "validated", "rejected"] = "pending"
|
||||
tested_run_id: Optional[str] = None
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Candidate
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class Candidate(BaseModel):
|
||||
"""A parameter set that has passed all validation gates."""
|
||||
candidate_id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
|
||||
run_id: str
|
||||
promoted_ts: datetime = Field(default_factory=datetime.utcnow)
|
||||
composite_score: float
|
||||
oos_score: Optional[float] = None
|
||||
params: dict[str, Any]
|
||||
lineage: list[str] = [] # run_ids from baseline to this candidate
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# RunResult (returned by MT5Runner)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class RunResult(BaseModel):
|
||||
"""Raw file paths returned after a tester run completes."""
|
||||
run_id: str
|
||||
report_xml: Optional[str] = None # path to MT5 XML report
|
||||
report_html: Optional[str] = None # path to MT5 HTML report
|
||||
trade_log_csv: Optional[str] = None # path to TradeLogger CSV
|
||||
success: bool = True
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GateResult (returned by ValidationGate)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class GateResult(BaseModel):
|
||||
passed: bool
|
||||
details: dict[str, bool | float | str] = {}
|
||||
reason: Optional[str] = None
|
||||
Reference in New Issue
Block a user