Загрузка данных


//+------------------------------------------------------------------+
//|                                  ARBITRAGE PRO QUANT INSTITUTIONAL v1.0(ENG).mq5 |
//+------------------------------------------------------------------+
#property version   "1.0"
#property description "ARBITRAGE PRO QUANT INSTITUTIONAL v1.0(ENG)"

#include <Trade/Trade.mqh>
CTrade Trade;

enum ENUM_AP_MODE      { SIGNALS_ONLY=0, DEMO_TRADING=1, LIVE_TRADING=2 };
enum ENUM_AP_STRATEGY  { PAIR_STATISTICAL=0, MULTI_STATISTICAL=1, ARBITRAGE_SPREAD=2 };
enum ENUM_ON_RESTART   { RECOVER_CYCLES=0, CLOSE_ALL=1, KEEP_MANUAL=2 };

input group "=== GENERAL SETTINGS ==="
input ENUM_AP_MODE     InpTradingMode        = DEMO_TRADING;
input ENUM_AP_STRATEGY InpStrategy           = MULTI_STATISTICAL;
input ENUM_ON_RESTART  InpOnRestart          = RECOVER_CYCLES;
input long             InpMagic              = 123456789;
input int              InpTimerSeconds       = 1;
input int              InpScanIntervalSec    = 3;
input bool             InpAllowNewEntries    = true;
input bool             InpVerboseLog         = true;

input group "=== EMERGENCY PROTECTION PATCH ==="
input bool             InpUseEmergencyManager = true;
input double           InpEmergencyMaxLossMoney = 200.0;
input int              InpMaxCycleAgeMinutes = 1440;
input bool             InpAuditRejectedSignals = true;
input bool             InpUseSignalQualityAudit = true;
input double           InpMaxAllowedSpreadPoints = 50.0;
input int              InpMaxTradeHourUTC = 23;
input int              InpMinTradeHourUTC = 0;
input int              InpMaxSignalAgeMinutes = 60;

input group "=== SYMBOLS (PAIR STAT) ==="
input string InpSymbolA = "EURUSD";
input string InpSymbolB = "GBPUSD";

input group "=== MULTI-PAIR POOL (FX+METALS+INDICES+ENERGY) ==="
input string InpMultiPairs = "EURUSD,GBPUSD;AUDUSD,NZDUSD;USDJPY,CADJPY;EURJPY,GBPJPY;AUDUSD,AUDCAD;NZDUSD,NZDCAD;EURCHF,GBPCHF;XAUUSD,XAGUSD;.US500Cash,.US30Cash;BRENT,WTI";

input group "=== CORRELATION SCANNER (PAIR STAT MODE) ==="
input string InpScanPairs = "EURUSD,GBPUSD;AUDUSD,NZDUSD;USDJPY,CADJPY;EURJPY,GBPJPY;AUDUSD,AUDCAD;NZDUSD,NZDCAD;EURCHF,GBPCHF;GBPAUD,EURGBP;AUDNZD,NZDUSD;XAUUSD,XAGUSD;.US500Cash,.US30Cash;BRENT,WTI";

input group "=== ADVANCED STRATEGY ==="
input bool   InpUseClosedBarOnly     = true;
input bool   InpUseZCrossEntry       = true;
input bool   InpUseHardZStop         = true;
input double InpHardZStop            = 4.5;
input int    InpEntryCooldownSec     = 1800;

input group "=== PRO: KALMAN & REBALANCE ==="
input bool   InpUseKalmanBeta        = true;
input double InpKalmanQ              = 0.0001;
input double InpKalmanR              = 0.001;
input bool   InpUseRebalance         = true;
input double InpRebalanceBetaDrift   = 0.15;
input bool   InpUsePartialTP         = true;
input double InpPartialZScore        = 1.2;

input group "=== ADVANCED: COINTEGRATION & OU MODEL ==="
input bool   InpUseCointegrationTest  = false;
input double InpADF_pValueThreshold   = 0.05;
input bool   InpUseOUModel            = false;
input int    InpOU_lookback           = 200;
input int    InpADFMaxLag             = 2;

input group "=== ADVANCED: ADAPTIVE ENTRY & MULTI-TF ==="
input bool   InpUseAdaptiveZ          = false;
input double InpAdaptiveZ_base        = 2.2;
input double InpAdaptiveZ_k           = 1.5;
input int    InpAdaptiveZ_window      = 50;
input bool   InpUseMultiTimeframe     = false;
input ENUM_TIMEFRAMES InpConfirmTF    = PERIOD_M15;
input ENUM_TIMEFRAMES InpTrendTF      = PERIOD_H1;

input group "=== ADVANCED: PYRAMIDING & DYNAMIC EXIT ==="
input bool   InpUsePyramiding         = false;
input int    InpMaxPyramidLevels      = 3;
input double InpPyramidStepZ          = 0.8;
input bool   InpUseDynamicExit        = false;
input double InpExitSpeedMultiplier   = 1.5;

input group "=== ADVANCED: REGIME & LIQUIDITY ==="
input bool   InpUseRegimeDetection    = false;
input int    InpRegimeATRPeriod       = 14;
input int    InpRegimeADXPeriod       = 14;
input double InpRegimeTrendADX        = 25.0;
input double InpRegimeHighVolATR_Z    = 2.0;
input bool   InpUseLiquidityFilter    = false;
input double InpMinTickVolume         = 100.0;

input group "=== ADVANCED: EXECUTION & LATENCY ==="
input bool   InpUseLimitOrders        = false;
input double InpLimitOffsetPoints     = 1.0;
input int    InpLimitWaitMs           = 5000;
input bool   InpUseLatencyCheck       = false;
input double InpMaxLatencyMs          = 50.0;

input group "=== ADVANCED: MOMENTUM & STABILITY ==="
input bool   InpUseSpreadMomentum     = true;
input bool   InpUseBetaStability      = true;
input double InpMaxBetaCV             = 0.20;
input int    InpBetaStabilityBars     = 30;

input group "=== PROFIT MAXIMIZATION ==="
input bool   InpUseZTrailingExit       = true;
input double InpZTrailingStep          = 0.3;
input bool   InpUseDynamicSlippage     = true;
input double InpDynamicSlippageMult    = 2.0;
input bool   InpUseVolatilityFilter    = true;
input int    InpVolatilityATRPeriod    = 14;
input double InpMaxATRMultiplier       = 2.5;
input bool   InpUseTimeBasedScaling    = true;
input double InpTimeScaleThresholdPct  = 0.75;

input group "=== STATIONARITY FILTERS ==="
input bool   InpUseHalfLifeFilter    = true;
input double InpMaxHalfLifeBars      = 48.0;
input bool   InpUseHurstFilter       = true;
input double InpMaxHurst             = 0.45;
input double InpMinBeta              = 0.2;
input double InpMaxBeta              = 2.5;

input group "=== EXECUTION ==="
input double InpMaxSpreadPoints      = 10.0;
input double InpMaxSlippagePoints    = 10.0;
input double InpMaxLegDriftPoints    = 5.0;
input int    InpMaxLegDelayMs        = 300;
input int    InpMaxQuoteAgeMs        = 15000;
input int    InpOrderRetryCount      = 3;
input int    InpOrderRetryDelayMs    = 200;
input bool   InpReconcilePositions     = true;
input bool   InpRequireHedgingForLive  = true;
input bool   InpSelfDiagnostics        = true;
input bool   InpRecheckBeforeSecondLeg = true;
input double InpMaxSecondLegZDrift     = 0.40;
input double InpMaxLeggingLossMoney    = 2.50;
input double InpMaxHedgeImbalancePct   = 8.0;

input group "=== AUTO CALC ==="
input bool   InpAutoLotSize          = true;
input double InpRiskPercentPerTrade  = 0.5;
input double InpVolK                 = 3.0;
input double InpAutoSLPoints         = 30.0;
input bool   InpAutoSpreadFilter     = true;
input bool   InpAutoMarginCheck      = true;
input bool   InpUseDollarNeutral     = true;
input bool   InpUseSwapInCost        = true;
input bool   InpUseSpreadRegime      = true;
input double InpSpreadZMax           = 2.0;

input group "=== SESSION / TIME ==="
input bool   InpUseSessionFilter     = true;
input int    InpSessionStartHourGMT  = 6;
input int    InpSessionEndHourGMT    = 21;
input bool   InpCloseBeforeWeekend   = true;
input int    InpFridayCloseHourGMT   = 20;

input group "=== RISK MANAGEMENT ==="
input double InpMaxDailyLossPct      = 2.0;
input int    InpMaxOpenCycles        = 1;
input int    InpMaxPositions         = 4;
input int    InpMaxTradesPerDay      = 5;
input int    InpMaxLossesPerDay      = 3;
input int    InpMaxConsecLosses      = 3;
input double InpMaxDailyCommission   = 20.0;
input double InpMaxLots              = 0.05;
input double InpMinMarginLevel       = 300.0;
input bool   InpUsePeakDrawdown      = true;
input double InpMaxPeakDrawdownPct   = 5.0;

input group "=== DYNAMIC LOT (COMPOUND) ==="
input bool   InpUseDynamicLot        = true;
input double InpDynamicLotCap        = 3.0;

input group "=== COMMISSION & EXPOSURE ==="
input double InpCommissionPerLot     = 2.0;
input double InpMaxExposureLots      = 0.10;

input group "=== PAIR STATISTICAL ==="
input int    InpStatLookback         = 144;
input double InpStatEntryZScore      = 2.2;
input double InpStatExitZScore       = 0.5;
input double InpPairLotMultiplier    = 1.0;
input double InpMinPairCorrelation   = 0.70;
input double InpMinCostCoverage      = 2.0;

input group "=== ADAPTIVE PAIR INTELLIGENCE ==="
input bool   InpUsePairIntelligence       = true;
input double InpMinPairIntelligenceScore  = 55.0;
input double InpIntelligenceEdgeCap       = 5.0;
input double InpIntelligenceLotFloor      = 0.70;
input double InpIntelligenceLotCeiling    = 1.10;
input bool   InpUsePortfolioRisk          = true;
input double InpPortfolioCorrLimit        = 0.85;
input double InpPortfolioRiskPenalty      = 0.25;

input group "=== RESEARCH & OPTIMIZATION LEDGER ==="
input bool   InpUseResearchLedger         = true;
input bool   InpResearchWriteTester       = true;
input int    InpResearchMinSamples        = 20;
input bool   InpResearchTrackMFE_MAE      = true;
input bool   InpResearchTesterCriterion   = true;

input group "=== WALK-FORWARD VALIDATION ==="
input bool   InpUseWalkForwardGuard        = true;
input int    InpWalkForwardTrainDays       = 30;
input int    InpWalkForwardTestDays        = 7;
input int    InpWalkForwardMinTrainTrades  = 40;
input int    InpWalkForwardMinTestTrades   = 10;
input double InpWalkForwardMinTestAvgPnL   = 0.0;
input double InpWalkForwardMinTestCorr     = 0.0;
input bool   InpWalkForwardCalibrateScore  = true;

input group "=== AUTO EXIT & JOURNAL ==="
input bool   InpAutoExitEnabled      = true;
input int    InpPairMaxHoldSeconds   = 14400;
input double InpMaxHoldHLMultiple    = 2.0;
input double InpCycleTakeProfitMoney = 0.0;
input double InpCycleStopLossMoney   = 15.0;
input bool   InpUseJournal           = true;

input group "=== NEWS FILTER ==="
input bool   InpUseNewsFilter        = true;
input bool   InpNewsBlockModerate    = false;
input int    InpNewsMinutesBefore    = 30;
input int    InpNewsMinutesAfter     = 30;
input bool   InpCloseBeforeHighNews  = true;
input int    InpCloseMinBeforeNews   = 15;

input group "=== NOTIFICATIONS & TELEGRAM ==="
input bool   InpNotifyPush           = true;
input bool   InpNotifyEmail          = false;
input bool   InpNotifyTelegram       = false;
input string InpTelegramToken        = "";
input string InpTelegramChatId       = "";
input bool   InpTelegramCommands     = false;
input int    InpTelegramPollSec      = 5;
input bool   InpEnableDailyReport    = true;
input int    InpDailyReportHourGMT   = 21;
input bool   InpPersistRiskState       = true;

input group "=== TESTING & PROTECTION ==="
input bool   InpEnableExecutionAudit      = true;
input bool   InpTesterWriteAudit          = true;
input bool   InpUseExecutionCircuitBreaker= true;
input int    InpAbortWindowSec            = 300;
input int    InpMaxAbortsInWindow         = 4;
input int    InpCircuitPauseSec           = 900;
input bool   InpAutoResumeCircuit         = true;
input int    InpMaxReconcileFailuresDay   = 3;
input int    InpMaxRebalanceCount         = 5;
input bool   InpRequirePositiveNetEdge    = true;
input double InpMinNetEdgeMoney           = 0.10;
input int    InpMaxCycleQuoteAgeMs        = 10000;
input bool   InpFlattenOnReconcileFailure = true;
input bool   InpAuditEverySignalBlock     = true;
input bool   InpEnableTradeTelemetry      = true;
input bool   InpUseHistoryRealizedPnL      = true;
input int    InpHistoryRefreshSec         = 2;

input group "=== INSTITUTIONAL PORTFOLIO ENGINE ==="
input bool   InpUseInstitutionalEngine       = true;
input bool   InpUseRiskBudget                = true;
input double InpPortfolioRiskBudgetPct       = 1.00;
input double InpMaxPairRiskBudgetPct         = 0.50;
input double InpMaxClusterRiskBudgetPct      = 0.75;
input bool   InpUseVaR_CVaR                  = true;
input double InpVaRConfidence                = 0.95;
input double InpMaxVaRMoney                  = 100.0;
input double InpMaxCVaRMoney                 = 150.0;
input bool   InpUseStressTesting             = true;
input double InpStressSpreadMultiplier       = 2.5;
input double InpStressSlippageMultiplier     = 2.0;
input double InpStressVolatilityMultiplier   = 2.0;
input double InpMaxStressLossMoney           = 200.0;
input bool   InpUseCorrelationClusters       = true;
input double InpMaxClusterCorrelation        = 0.85;
input bool   InpUseRegimeStateMachine        = true;
input double InpCrisisDrawdownPct            = 3.0;
input double InpCrisisRiskMultiplier         = 0.25;
input double InpHighVolRiskMultiplier        = 0.50;
input double InpTrendRiskMultiplier          = 0.50;
input bool   InpUseDynamicRiskScaling        = true;
input double InpLossStreakRiskStep           = 0.15;
input double InpMinDynamicRiskMultiplier     = 0.25;
input bool   InpUseExecutionQualityEngine    = true;
input double InpMaxExecutionCostPct         = 0.35;
input bool   InpUseSignalDecayFilter         = true;
input int    InpSignalDecaySeconds           = 90;
input double InpSignalDecayMinRatio         = 0.65;
input bool   InpUseAdverseSelectionFilter     = true;
input double InpMaxAdverseZExpansion          = 0.20;
input bool   InpUseMonteCarloRobustness       = true;
input int    InpMonteCarloRuns                = 250;
input double InpMonteCarloMaxP5LossMoney      = 250.0;
input bool   InpUseParameterStabilityGuard   = true;
input double InpMinStabilityScore            = 0.60;
input bool   InpUseShadowMode                = false;
input bool   InpUseInstitutionalKillSwitch   = true;
input int    InpMaxInstitutionalBreachesDay = 3;
input bool   InpUseQualityWeightedSizing       = true;
input double InpMinQualitySizingMultiplier     = 0.40;
input double InpMaxResearchLiveScoreGap        = 25.0;
input bool   InpUseResearchLiveGuard           = false;
input int    InpPortfolioLookbackDays            = 30;
input int    InpPortfolioRefreshSec              = 3;
input double InpHealthMinScore                   = 55.0;
input bool   InpUsePairIntelligenceV2             = false;

input group "=== DEMO TRADING ==="
input double InpDemoInitialBalance   = 10000.0;

input group "=== DASHBOARD POSITION ==="
input int    InpPanelX = 0;
input int    InpPanelY = 0;

struct STradeLeg {
   string symbol;
   ENUM_ORDER_TYPE type;
   double volume, openPrice, commission, spread;
   ulong  ticket;
   datetime openTime;
};

struct SOUParams {
   double theta, mu, sigma, halfLife;
   bool   valid;
};

struct SRegimeState {
   string regime;
   double atr, adx, volatilityZ;
};

struct SInstitutionalState {
   string regime;
   double riskMultiplier;
   double portfolioRiskPct;
   double clusterRiskPct;
   double var95;
   double cvar95;
   double stressLoss;
   double executionCostPct;
   double signalDecay;
   double stabilityScore;
   double currentExposureLots;
   double executionQualityScore;
   double qualitySizingMultiplier;
   double capitalUtilizationPct;
   double researchLiveGap;
   int    clusterCount;
   int    breachesToday;
   bool   gatePass;
   bool   killSwitch;
   string gateReason;
};

struct SPairHistory {
   string pair;
   int    totalTrades, wins;
   double totalPnL, avgPnL, maxDrawdown;
   datetime lastTrade;
};

struct SArbitrageCycle {
   ulong    cycleId;
   datetime startTime;
   string   comment, symA, symB;
   STradeLeg legs[];
   double   totalCommission, realizedPnl, halfLifeAtEntry, entryZ, beta;
   int      strategy, rebalCount;
   bool     partialDone;
   datetime lastRebalTime;
   int      pyramidLevel;
   SOUParams ouParams;
   double   entrySpread;
   double   bestZ;
   double   dynamicExitZ;
   // Research snapshot captured at entry; used for walk-forward / optimization analysis.
   double   intelScore, intelLotFactor, intelEdgeCost, intelEdge, intelCost, intelNetEdge;
   double   intelZQuality, intelCorrQuality, intelHalfLifeQuality, intelHurstQuality;
   double   intelBetaQuality, intelExecutionQuality, intelRegimeQuality, intelPortfolioQuality;
   double   entryExpected, entryCost;
   double   entryCorr, entryHurst;
   double   maxFavorablePnl, maxAdversePnl, minAbsZ, maxAbsZ;
};

struct SPairStat {
   double z, beta, sigma, sigmaMoney, halfLife, hurst, corr, score;
   bool   valid;
   SOUParams ouParams;
};

struct SPairIntelligence {
   double score;
   double edgeCost;
   double edge;
   double cost;
   double zQuality;
   double corrQuality;
   double halfLifeQuality;
   double hurstQuality;
   double betaQuality;
   double executionQuality;
   double regimeQuality;
   double portfolioQuality;
   double lotFactor;
   bool   eligible;
   string reason;
};

struct SPairIntelligenceV2 {
   double score;
   double statisticalQuality;
   double economicQuality;
   double executionQuality;
   double portfolioQuality;
   double stabilityQuality;
   double conviction;
   bool   eligible;
   string reason;
};

struct SCloseCache {
   string symbol;
   ENUM_TIMEFRAMES tf;
   ulong last_update_ms;
   double data[];
};

string g_status="INIT", g_last_block="-";
datetime g_day_start=0;
double g_day_start_equity=0, g_demo_day_start_equity=0;
double g_base_equity=0, g_lot_scale=1.0, g_peak_equity=0;
bool   g_daily_stop=false, g_paused=false, g_daily_report_sent=false;
string g_stop_reason="";
ulong  g_cycle_id=0;
bool   g_confirm_live=false, g_confirm_strategy=false;
bool   g_show_strategy_panel=false, g_show_metrics_panel=false, g_show_portfolio_panel=false;
bool   g_show_quant_detail=false;
int    g_alpha_selected=-1;
int    g_pending_strategy=0;
string g_cycle_comment="";
double g_demo_balance=0;
ENUM_AP_MODE g_mode=SIGNALS_ONLY;
ENUM_AP_STRATEGY g_strategy=PAIR_STATISTICAL;
// Metrics page selector is display-only. It never changes the live trading strategy.
ENUM_AP_STRATEGY g_metrics_strategy=PAIR_STATISTICAL;
// Live METRICS preview: calculated from current market data for the selected
// Metrics tab only. It never changes g_strategy or any trading-engine state.
ulong g_metrics_preview_last_ms=0;
double g_metrics_live_z=0.0, g_metrics_live_corr=0.0, g_metrics_live_beta=0.0;
double g_metrics_live_hl=0.0, g_metrics_live_hurst=0.0, g_metrics_live_spread=0.0;
double g_metrics_live_score=0.0, g_metrics_live_lot_factor=1.0;
string g_metrics_live_pair="";
int g_metrics_top_count=0;
string g_metrics_top_a[8], g_metrics_top_b[8];
double g_metrics_top_z[8], g_metrics_top_corr[8], g_metrics_top_beta[8], g_metrics_top_hl[8], g_metrics_top_score[8];
datetime g_ui_last_refresh=0;
string GV_DAY_TIME, GV_DAY_EQUITY, GV_DAILY_STOP, GV_STRATEGY, GV_DEMO_BAL;
string GV_PEAK_EQUITY, GV_TOTAL_TRADES, GV_CLOSED_TRADES, GV_PROFITABLE_TRADES, GV_LOSSES, GV_CONSEC_LOSSES, GV_DAY_COMMISSION, GV_DAY_SWAP, GV_DAY_SPREAD, GV_DAY_SLIPPAGE;
string GV_ENTRY_ABORTS, GV_FAILED_HEDGES, GV_RECONCILE_FAILURES, GV_EXECUTION_FAILURES, GV_CIRCUIT_UNTIL;

int    g_total_cycles_today=0, g_closed_cycles_today=0, g_closed_profitable_today=0, g_losses_today=0, g_consec_losses=0;
double g_daily_commission=0, g_daily_swap=0, g_daily_spread=0, g_daily_slippage=0, g_daily_fees=0;
double g_open_swap=0.0;
datetime g_last_history_refresh=0;
ulong g_trade_tx_count=0, g_last_deal_ticket=0, g_last_trade_order=0;
uint g_last_trade_retcode=0;
string g_last_trade_symbol="", g_last_trade_action="";
double g_last_fill_price=0.0;
datetime g_last_trade_time=0;
int g_symbol_map_ok=0, g_symbol_map_warn=0;
int g_entry_aborts_today=0, g_failed_hedges_today=0, g_reconcile_failures_today=0, g_execution_failures_today=0;
datetime g_circuit_pause_until=0, g_abort_window_start=0;
int g_abort_window_count=0;
string g_last_audit_phase="", g_last_audit_reason="", g_last_audit_key="";
datetime g_last_audit_write=0;
int g_diag_errors=0, g_diag_warnings=0;
string g_diag_status="NOT RUN";
string g_diag_report[8];
int g_diag_report_count=0;

double g_live_signal=0;
string g_live_pair="";
double g_total_cost_money=0;

SArbitrageCycle g_cycles[];
string g_mp_a[], g_mp_b[];
int g_mp_count=0;
string g_scan_a[], g_scan_b[];
int g_scan_count=0;
string g_top_a[16], g_top_b[16];
double g_top_z[16], g_top_corr[16], g_top_sprA[16], g_top_sprB[16], g_top_beta[16], g_top_hl[16], g_top_score[16];
bool g_top_fresh[16];
double g_top_edge[16];
double g_alloc_risk_pct[16];
double g_alloc_weight[16];
double g_alloc_multiplier[16];
double g_alloc_quality[16];
string g_alloc_cluster[16];
bool g_alloc_ready=false;
datetime g_alloc_last_update=0;
string g_pz_a[], g_pz_b[];
double g_pz_v[];
string g_cd_a[], g_cd_b[];
datetime g_cd_t[];
string g_avg_sym[];
double g_avg_spread[], g_std_spread[];
string g_exp_cur[3];
double g_exp_lots[3];
int g_exp_count=0;
string g_sc_a[], g_sc_b[];
SPairStat g_sc_v[];
ulong g_sc_t[];

string g_intel_a[], g_intel_b[];
SPairIntelligence g_intel_v[];
ulong g_intel_t[];
double g_last_intelligence_score=0.0;
double g_last_intelligence_factor=1.0;
string g_last_intelligence_pair="";
string g_last_intelligence_reason="-";
double g_pi2_score=0.0;
double g_pi2_conviction=0.0;
string g_pi2_pair="";
string g_pi2_reason="OFF";

bool g_is_tester=false;
ulong g_last_scan_ms=0, g_last_manage_ms=0;
long  g_tg_offset=0;
ulong g_tg_last_poll=0;

SRegimeState g_regime;
SInstitutionalState g_inst;
int g_inst_breaches_today=0;
datetime g_inst_last_update=0;
datetime g_inst_last_breach_time=0;
string g_inst_last_breach_reason="";
datetime g_signal_birth_time=0;
double g_signal_birth_abs_z=0.0;
string g_signal_birth_pair="";
double g_inst_mc_p5=0.0;
double g_inst_mc_loss_probability=0.0;
SPairHistory g_pair_stats[];

// Research ledger session statistics. These are intentionally observational by default:
// they collect evidence for optimization without silently changing the trading model.
int    g_research_trades=0, g_research_wins=0;
double g_research_pnl=0.0, g_research_score_sum=0.0;
double g_research_score_pnl=0.0, g_research_score_sq=0.0, g_research_pnl_sq=0.0;
int    g_research_bucket_n[10];
double g_research_bucket_pnl[10];
int    g_research_bucket_wins[10];

int    g_wf_train_n=0, g_wf_test_n=0;
double g_wf_train_avg=0.0, g_wf_test_avg=0.0;
double g_wf_train_corr=0.0, g_wf_test_corr=0.0;
double g_wf_calibrated_threshold=0.0;
double g_wf_effective_threshold=0.0;
bool   g_wf_valid=false, g_wf_gate_pass=false;
datetime g_wf_test_start=0, g_wf_latest=0;
string g_wf_status="NOT READY";

double g_z_history[1000];
int    g_z_history_idx=0;
int    g_z_history_count=0;

double g_current_latency_ms=0;

// Portfolio Intelligence cache (rolling realized performance).
datetime g_pf_last_refresh=0;
int    g_pf_trades=0, g_pf_wins=0, g_pf_losses=0;
double g_pf_net=0.0, g_pf_gross_profit=0.0, g_pf_gross_loss=0.0;
double g_pf_profit_factor=0.0, g_pf_expectancy=0.0, g_pf_win_rate=0.0;
double g_pf_max_dd=0.0, g_pf_max_dd_pct=0.0, g_pf_sharpe=0.0, g_pf_sortino=0.0;
double g_pf_alpha_health=0.0, g_pf_risk_health=0.0, g_pf_exec_health=0.0, g_pf_health=0.0;
bool   g_high_latency=false;
bool   g_prev_high_latency=false;
bool   g_cached_vol_ok=true;
string g_cached_vol_symbol="";
double g_cached_atr_current=0;
double g_cached_atr_avg=0;
datetime g_cached_vol_time=0;
SCloseCache g_close_cache[];

// Added safety layer
datetime g_last_emergency_check=0;



const string STATE_FILE="AP_state.csv", JOURNAL_FILE="ArbitragePro_journal.csv", TCA_FILE="ArbitragePro_tca.csv", RESEARCH_FILE="ArbitragePro_research.csv";
const int STATE_VERSION=4;
const int STATE_MAX_LEGS=32;
string StateFileName(){ return "AP_state_"+IntegerToString((long)AccountInfoInteger(ACCOUNT_LOGIN))+"_"+IntegerToString(InpMagic)+".csv"; }

string ModeText(){ return (g_mode==SIGNALS_ONLY)?"SIGNALS_ONLY":(g_mode==DEMO_TRADING?"DEMO_TRADING":"LIVE_TRADING"); }
string StrategyText(){ if(g_strategy==PAIR_STATISTICAL) return "PAIR STAT"; if(g_strategy==ARBITRAGE_SPREAD) return "ARBITRAGE SPREAD"; return "MULTI STAT"; }
string StrategyFullName(){ if(g_strategy==PAIR_STATISTICAL) return "PAIR STATISTICAL"; if(g_strategy==ARBITRAGE_SPREAD) return "ARBITRAGE SPREAD"; return "MULTI STATISTICAL"; }
void SetStatus(const string t){ g_status=t; }
void ResetStat(SPairStat &st){
   st.valid=false; st.z=0; st.beta=1; st.sigma=0; st.sigmaMoney=0;
   st.halfLife=-1; st.hurst=1; st.corr=0; st.score=0; st.ouParams.valid=false;
}

bool GetCloseCached(const string symbol, const ENUM_TIMEFRAMES tf, const int count, double &arr[], bool asSeries=true) {
   int cacheIdx = -1;
   ulong now_ms = GetTickCount64();
   
   for(int i = 0; i < ArraySize(g_close_cache); i++) {
      if(g_close_cache[i].symbol == symbol && g_close_cache[i].tf == tf) {
         cacheIdx = i;
         break;
      }
   }
   
   int fetchCount = MathMax(count, 300);
   
   if(cacheIdx >= 0 && (now_ms - g_close_cache[cacheIdx].last_update_ms) < 500) {
      if(ArraySize(g_close_cache[cacheIdx].data) >= count) {
         ArrayResize(arr, count);
         if(asSeries) {
            ArraySetAsSeries(arr, true);
            for(int i = 0; i < count; i++) arr[i] = g_close_cache[cacheIdx].data[i];
         } else {
            ArraySetAsSeries(arr, false);
            int lastIdx = ArraySize(g_close_cache[cacheIdx].data) - 1;
            for(int i = 0; i < count; i++) arr[i] = g_close_cache[cacheIdx].data[lastIdx - i];
         }
         return true;
      }
   }
   
   double tmp[];
   ArraySetAsSeries(tmp, true);
   int copied = CopyClose(symbol, tf, 0, fetchCount, tmp);
   if(copied < count) return false;
   
   if(cacheIdx < 0) {
      cacheIdx = ArraySize(g_close_cache);
      ArrayResize(g_close_cache, cacheIdx + 1);
      g_close_cache[cacheIdx].symbol = symbol;
      g_close_cache[cacheIdx].tf = tf;
   }
   
   ArrayResize(g_close_cache[cacheIdx].data, copied);
   for(int i = 0; i < copied; i++) g_close_cache[cacheIdx].data[i] = tmp[i];
   g_close_cache[cacheIdx].last_update_ms = now_ms;
   
   ArrayResize(arr, count);
   if(asSeries) {
      ArraySetAsSeries(arr, true);
      for(int i = 0; i < count; i++) arr[i] = tmp[i];
   } else {
      ArraySetAsSeries(arr, false);
      for(int i = 0; i < count; i++) arr[i] = tmp[copied - 1 - i];
   }
   return true;
}

string TGUrlEncode(const string value)
{
   uchar bytes[];
   int n=StringToCharArray(value,bytes,0,-1,CP_UTF8);
   if(n>0 && bytes[n-1]==0) n--;

   string out="";
   for(int i=0;i<n;i++)
   {
      int c=(int)bytes[i];
      bool safe=((c>='a'&&c<='z')||(c>='A'&&c<='Z')||(c>='0'&&c<='9')||
                 c=='-'||c=='_'||c=='.'||c=='~');
      if(safe) out+=CharToString((uchar)c);
      else out+="%"+StringFormat("%02X",c);
   }
   return out;
}

bool TGApiOK(const int httpCode,const string body,const string action)
{
   if(httpCode<200 || httpCode>=300)
   {
      Print("Telegram ",action," failed: HTTP ",httpCode,
            " | response=",StringSubstr(body,0,500));
      return false;
   }
   if(StringFind(body,"\"ok\":true")<0)
   {
      Print("Telegram ",action," rejected by API | response=",StringSubstr(body,0,500));
      return false;
   }
   return true;
}

void SendTelegramMessage(const string msg)
{
   if(!InpNotifyTelegram||InpTelegramToken==""||InpTelegramChatId==""||g_is_tester) return;

   string url="https://api.telegram.org/bot"+InpTelegramToken+"/sendMessage";
   string post="chat_id="+TGUrlEncode(InpTelegramChatId)+
               "&text="+TGUrlEncode(msg)+
               "&parse_mode=Markdown";

   uchar data[],res[];
   StringToCharArray(post,data,0,StringLen(post),CP_UTF8);
   string hdr="Content-Type: application/x-www-form-urlencoded\r\n",rh="";
   ResetLastError();
   int httpCode=WebRequest("POST",url,hdr,5000,data,res,rh);
   if(httpCode<0)
   {
      int err=GetLastError();
      Print("Telegram send WebRequest failed err=",err,
            " | Ensure https://api.telegram.org is in Allowed URLs");
      return;
   }
   string body=CharArrayToString(res,0,WHOLE_ARRAY,CP_UTF8);
   TGApiOK(httpCode,body,"sendMessage");
}

void Notify(const string msg){
   if(g_is_tester) return;
   if(InpNotifyPush) SendNotification(msg);
   if(InpNotifyEmail) SendMail("ARBITRAGE PRO QUANT INSTITUTIONAL v1.0(ENG)",msg);
   SendTelegramMessage(msg);
}

void SendDailyReport(){
   if(!InpEnableDailyReport || g_is_tester) return;
   double winRate = (g_closed_cycles_today > 0) ? (double)g_closed_profitable_today / g_closed_cycles_today * 100.0 : 0.0;
   double currentEq = (g_mode == DEMO_TRADING) ? VirtualEquity() : AccountInfoDouble(ACCOUNT_EQUITY);
   double dailyPnL = (g_mode == DEMO_TRADING) ? (VirtualEquity() - g_demo_day_start_equity) : (AccountInfoDouble(ACCOUNT_EQUITY) - g_day_start_equity);
   string msg = " *ARBITRAGE PRO QUANT INSTITUTIONAL v1.0(ENG): Daily Report*\n"
                + " Date: " + TimeToString(TimeCurrent(), TIME_DATE) + "\n"
                + " Daily PnL: $" + DoubleToString(dailyPnL, 2) + "\n"
                + " Trades: " + IntegerToString(g_closed_cycles_today) + " | Win Rate: " + DoubleToString(winRate, 1) + "%\n"
                + " Commission: $" + DoubleToString(g_daily_commission, 2) + "\n"
                + " Slippage: " + DoubleToString(g_daily_slippage, 1) + " pts\n"
                + " Active Cycles: " + IntegerToString(OpenCyclesCount()) + "\n"
                + " Current Equity: $" + DoubleToString(currentEq, 2);
   Notify(msg);
}

long TGLastUpdateId(const string res){
   long last=0; int pos=0;
   while(true){
      int f=StringFind(res,"\"update_id\":",pos);
      if(f<0) break;
      f+=12;
      string num="";
      while(f<StringLen(res)){
         ushort ch=StringGetCharacter(res,f);
         if(ch>='0'&&ch<='9') num+=CharToString((uchar)ch); else break;
         f++;
      }
      if(num!="") last=(long)StringToInteger(num);
      pos=f;
   }
   return last;
}

string TGFirstText(const string res){
   int f=StringFind(res,"\"text\":\"");
   if(f<0) return "";
   f+=8;
   string out="";
   bool escaped=false;
   while(f<StringLen(res)){
      ushort ch=StringGetCharacter(res,f);
      if(escaped){
         if(ch=='n') out+="\n";
         else if(ch=='r') out+="\r";
         else if(ch=='t') out+="\t";
         else if(ch=='"' || ch=='\\' || ch=='/') out+=CharToString((uchar)ch);
         else out+=CharToString((uchar)ch);
         escaped=false;
      } else {
         if(ch=='\\') escaped=true;
         else if(ch=='"') break;
         else out+=CharToString((uchar)ch);
      }
      f++;
   }
   return out;
}

long TGFirstChatId(const string res)
{
   int chatPos=StringFind(res,"\"chat\"");
   if(chatPos<0) return 0;
   int idPos=StringFind(res,"\"id\"",chatPos+6);
   if(idPos<0) return 0;
   int colon=StringFind(res,":",idPos+4);
   if(colon<0) return 0;
   int p=colon+1;
   while(p<StringLen(res)){
      ushort ch=StringGetCharacter(res,p);
      if(ch==' ' || ch=='\t' || ch=='\r' || ch=='\n') p++; else break;
   }
   bool negative=false;
   if(p<StringLen(res) && StringGetCharacter(res,p)=='-'){ negative=true; p++; }
   string num="";
   while(p<StringLen(res)){
      ushort ch=StringGetCharacter(res,p);
      if(ch>='0'&&ch<='9'){ num+=CharToString((uchar)ch); p++; } else break;
   }
   if(num=="") return 0;
   long id=(long)StringToInteger(num);
   return negative ? -id : id;
}

string TGCommand(const string text)
{
   string cmd=text;
   StringTrimLeft(cmd);
   StringTrimRight(cmd);
   int sp=StringFind(cmd," ");
   if(sp>=0) cmd=StringSubstr(cmd,0,sp);
   int at=StringFind(cmd,"@");
   if(at>0) cmd=StringSubstr(cmd,0,at);
   return cmd;
}

string StatusSummary(){
   double eq=(g_mode==DEMO_TRADING)?VirtualEquity():AccountInfoDouble(ACCOUNT_EQUITY);
   return "*AP Status*\n"
          + "Mode: " + ModeText() + "\n"
          + "Strategy: " + StrategyText() + "\n"
          + "Cycles: " + IntegerToString(OpenCyclesCount()) + "\n"
          + "uPnL: $" + DoubleToString(TotalUnrealizedPnL(),2) + "\n"
          + "Equity: $" + DoubleToString(eq,2) + "\n"
          + "Today: " + IntegerToString(g_closed_cycles_today) + " trades (" + IntegerToString(g_closed_profitable_today) + " wins)"
          + (g_paused?"\n PAUSED":"")+(g_daily_stop?"\n STOP:"+g_stop_reason:"")
          + (ExecutionCircuitActive()?"\n CIRCUIT: "+IntegerToString((int)MathMax(0,g_circuit_pause_until-TimeCurrent()))+"s":"");
}

void PollTelegramCommands(){
   if(!InpNotifyTelegram||!InpTelegramCommands||InpTelegramToken==""||InpTelegramChatId==""||g_is_tester) return;

   ulong now=GetTickCount64();
   if(now-g_tg_last_poll<(ulong)MathMax(1,InpTelegramPollSec)*1000) return;
   g_tg_last_poll=now;

   string url="https://api.telegram.org/bot"+InpTelegramToken+
              "/getUpdates?offset="+IntegerToString(g_tg_offset)+"&timeout=1";
   uchar req[],res[]; string hdr="",rh="";
   ResetLastError();
   int httpCode=WebRequest("GET",url,hdr,3000,req,res,rh);
   if(httpCode<0)
   {
      int err=GetLastError();
      Print("Telegram polling failed err=",err,
            " | Ensure https://api.telegram.org is in Allowed URLs");
      return;
   }

   string body=CharArrayToString(res,0,WHOLE_ARRAY,CP_UTF8);
   if(!TGApiOK(httpCode,body,"getUpdates")) return;

   long lastId=TGLastUpdateId(body);
   if(lastId>0) g_tg_offset=lastId+1;

   string txt=TGFirstText(body);
   if(txt=="") return;

   long incomingChatId=TGFirstChatId(body);
   long allowedChatId=(long)StringToInteger(InpTelegramChatId);
   if(incomingChatId==0 || allowedChatId==0 || incomingChatId!=allowedChatId)
   {
      Print("Telegram command ignored: unauthorized chat id=",incomingChatId);
      return;
   }

   string cmd=TGCommand(txt);
   if(cmd=="/status") SendTelegramMessage(StatusSummary());
   else if(cmd=="/pause"){
      g_paused=true;
      SendTelegramMessage(" *PAUSED* via Telegram");
   }
   else if(cmd=="/resume"){
      g_paused=false;
      SendTelegramMessage(" *RESUMED* via Telegram");
   }
   else if(cmd=="/close"){
      CloseAllCycles("TG-CLOSE");
      SendTelegramMessage(" *CLOSE ALL* executed via Telegram");
   }
   else if(cmd=="/report") SendDailyReport();
}
datetime EngineNow(){
   datetime ts=TimeTradeServer();
   if(ts>0) return ts;
   return TimeCurrent();
}
datetime EngineDayStart(){
   return StringToTime(TimeToString(EngineNow(),TIME_DATE));
}

void BuildGlobalNames(){
   // Magic remains the order/statistics namespace, but session risk state must not reset when Magic changes.
   string p="AP_"+IntegerToString((long)AccountInfoInteger(ACCOUNT_LOGIN))+"_"+IntegerToString(InpMagic)+"_M"+IntegerToString((int)g_mode);
   string r="AP_RISK_"+IntegerToString((long)AccountInfoInteger(ACCOUNT_LOGIN))+"_M"+IntegerToString((int)g_mode);
   GV_DAY_TIME=r+"_DAY_TIME"; GV_DAY_EQUITY=r+"_DAY_EQUITY"; GV_DAILY_STOP=r+"_DAILY_STOP";
   GV_STRATEGY=p+"_STRATEGY"; GV_DEMO_BAL=r+"_DEMO_BAL"; GV_PEAK_EQUITY=r+"_PEAK_EQUITY";
   GV_TOTAL_TRADES=r+"_TOTAL_TRADES"; GV_CLOSED_TRADES=r+"_CLOSED_TRADES"; GV_PROFITABLE_TRADES=r+"_PROFITABLE_TRADES";
   GV_LOSSES=r+"_LOSSES"; GV_CONSEC_LOSSES=r+"_CONSEC_LOSSES"; GV_DAY_COMMISSION=r+"_DAY_COMMISSION";
   GV_DAY_SWAP=r+"_DAY_SWAP"; GV_DAY_SPREAD=r+"_DAY_SPREAD"; GV_DAY_SLIPPAGE=r+"_DAY_SLIPPAGE";
   GV_ENTRY_ABORTS=r+"_ENTRY_ABORTS"; GV_FAILED_HEDGES=r+"_FAILED_HEDGES"; GV_RECONCILE_FAILURES=r+"_RECONCILE_FAILURES"; GV_EXECUTION_FAILURES=r+"_EXECUTION_FAILURES"; GV_CIRCUIT_UNTIL=r+"_CIRCUIT_UNTIL";
}
void SaveDailyState(){
   if(g_is_tester || !InpPersistRiskState) return;
   GlobalVariableSet(GV_DAY_TIME,(double)g_day_start);
   GlobalVariableSet(GV_DAY_EQUITY,g_day_start_equity);
   GlobalVariableSet(GV_DAILY_STOP,g_daily_stop?1:0);
   GlobalVariableSet(GV_PEAK_EQUITY,g_peak_equity);
   GlobalVariableSet(GV_TOTAL_TRADES,(double)g_total_cycles_today);
   GlobalVariableSet(GV_CLOSED_TRADES,(double)g_closed_cycles_today);
   GlobalVariableSet(GV_PROFITABLE_TRADES,(double)g_closed_profitable_today);
   GlobalVariableSet(GV_LOSSES,(double)g_losses_today);
   GlobalVariableSet(GV_CONSEC_LOSSES,(double)g_consec_losses);
   GlobalVariableSet(GV_DAY_COMMISSION,g_daily_commission);
   GlobalVariableSet(GV_DAY_SWAP,g_daily_swap);
   GlobalVariableSet(GV_DAY_SPREAD,g_daily_spread);
   GlobalVariableSet(GV_DAY_SLIPPAGE,g_daily_slippage);
   GlobalVariableSet(GV_ENTRY_ABORTS,(double)g_entry_aborts_today);
   GlobalVariableSet(GV_FAILED_HEDGES,(double)g_failed_hedges_today);
   GlobalVariableSet(GV_RECONCILE_FAILURES,(double)g_reconcile_failures_today);
   GlobalVariableSet(GV_EXECUTION_FAILURES,(double)g_execution_failures_today);
   GlobalVariableSet(GV_CIRCUIT_UNTIL,(double)g_circuit_pause_until);
}
void SaveDemoBalance(){
   if(g_is_tester) return;
   GlobalVariableSet(GV_DEMO_BAL,g_demo_balance);
}
void LoadDailyState(){
   if(!g_is_tester && InpPersistRiskState && GlobalVariableCheck(GV_DAY_TIME) && GlobalVariableCheck(GV_DAY_EQUITY)){
      g_day_start=(datetime)GlobalVariableGet(GV_DAY_TIME);
      g_day_start_equity=GlobalVariableGet(GV_DAY_EQUITY);
      g_daily_stop=(g_mode==DEMO_TRADING)?false:(GlobalVariableCheck(GV_DAILY_STOP)&&GlobalVariableGet(GV_DAILY_STOP)>0.5);
      g_peak_equity=GlobalVariableCheck(GV_PEAK_EQUITY)?GlobalVariableGet(GV_PEAK_EQUITY):g_day_start_equity;
      g_total_cycles_today=(int)(GlobalVariableCheck(GV_TOTAL_TRADES)?GlobalVariableGet(GV_TOTAL_TRADES):0);
      g_closed_cycles_today=(int)(GlobalVariableCheck(GV_CLOSED_TRADES)?GlobalVariableGet(GV_CLOSED_TRADES):0);
      g_closed_profitable_today=(int)(GlobalVariableCheck(GV_PROFITABLE_TRADES)?GlobalVariableGet(GV_PROFITABLE_TRADES):0);
      g_losses_today=(int)(GlobalVariableCheck(GV_LOSSES)?GlobalVariableGet(GV_LOSSES):0);
      g_consec_losses=(int)(GlobalVariableCheck(GV_CONSEC_LOSSES)?GlobalVariableGet(GV_CONSEC_LOSSES):0);
      g_daily_commission=GlobalVariableCheck(GV_DAY_COMMISSION)?GlobalVariableGet(GV_DAY_COMMISSION):0;
      g_daily_swap=GlobalVariableCheck(GV_DAY_SWAP)?GlobalVariableGet(GV_DAY_SWAP):0;
      g_daily_spread=GlobalVariableCheck(GV_DAY_SPREAD)?GlobalVariableGet(GV_DAY_SPREAD):0;
      g_daily_slippage=GlobalVariableCheck(GV_DAY_SLIPPAGE)?GlobalVariableGet(GV_DAY_SLIPPAGE):0;
      g_entry_aborts_today=(int)(GlobalVariableCheck(GV_ENTRY_ABORTS)?GlobalVariableGet(GV_ENTRY_ABORTS):0);
      g_failed_hedges_today=(int)(GlobalVariableCheck(GV_FAILED_HEDGES)?GlobalVariableGet(GV_FAILED_HEDGES):0);
      g_reconcile_failures_today=(int)(GlobalVariableCheck(GV_RECONCILE_FAILURES)?GlobalVariableGet(GV_RECONCILE_FAILURES):0);
      g_execution_failures_today=(int)(GlobalVariableCheck(GV_EXECUTION_FAILURES)?GlobalVariableGet(GV_EXECUTION_FAILURES):0);
      g_circuit_pause_until=(datetime)(GlobalVariableCheck(GV_CIRCUIT_UNTIL)?GlobalVariableGet(GV_CIRCUIT_UNTIL):0);
      // A persisted DAILY STOP is valid only for its original trading day.
      // This also protects DEMO mode when the terminal was offline for several days.
      datetime persistedDay=g_day_start;
      datetime currentDay=EngineDayStart();
      if(persistedDay>0 && persistedDay!=currentDay){
         g_day_start=currentDay;
         g_day_start_equity=(g_mode==DEMO_TRADING)?g_demo_balance:AccountInfoDouble(ACCOUNT_EQUITY);
         g_demo_day_start_equity=g_day_start_equity;
         ResetSessionStopState(); g_peak_equity=g_day_start_equity;
         g_total_cycles_today=0; g_closed_cycles_today=0; g_closed_profitable_today=0;
         g_losses_today=0; g_consec_losses=0; g_daily_commission=0; g_daily_swap=0; g_daily_spread=0; g_daily_slippage=0; g_daily_fees=0;
         g_entry_aborts_today=0; g_failed_hedges_today=0; g_reconcile_failures_today=0; g_execution_failures_today=0; g_circuit_pause_until=0;
         SaveDailyState();
      }
      return;
   }
   g_day_start=EngineDayStart();
   g_day_start_equity=(g_mode==DEMO_TRADING)?g_demo_balance:AccountInfoDouble(ACCOUNT_EQUITY);
   g_demo_day_start_equity=g_day_start_equity;
   g_daily_stop=false; g_stop_reason="";
   g_peak_equity=g_day_start_equity;
   g_total_cycles_today=0; g_closed_cycles_today=0; g_closed_profitable_today=0; g_losses_today=0; g_consec_losses=0;
   g_daily_commission=0; g_daily_swap=0; g_daily_spread=0; g_daily_slippage=0; g_daily_fees=0; g_last_history_refresh=0;
   g_entry_aborts_today=0; g_failed_hedges_today=0; g_reconcile_failures_today=0; g_execution_failures_today=0; g_circuit_pause_until=0; g_abort_window_start=0; g_abort_window_count=0;
   SaveDailyState();
}
void ResetSessionStopState(){
   // DAILY STOP is a daily/session protection state.
   // On a new trading day it must disappear in both LIVE and DEMO modes.
   g_daily_stop=false;
   g_stop_reason="";
   g_status="";
   g_inst_breaches_today=0;
   g_inst_last_breach_time=0;
   g_inst_last_breach_reason="";
   g_circuit_pause_until=0;
}

void UpdateTradingDay(){
   datetime today=EngineDayStart();
   if(today!=g_day_start){
      Print("NEW TRADING DAY DETECTED: Resetting daily state");
      g_day_start=today;
      g_day_start_equity=(g_mode==DEMO_TRADING)?VirtualEquity():AccountInfoDouble(ACCOUNT_EQUITY);
      g_demo_day_start_equity=g_day_start_equity;
      ResetSessionStopState(); g_peak_equity=g_day_start_equity;
      g_total_cycles_today=0; g_closed_cycles_today=0; g_closed_profitable_today=0;
      g_losses_today=0; g_consec_losses=0; g_daily_commission=0; g_daily_swap=0; g_daily_spread=0; g_daily_slippage=0; g_daily_fees=0; g_last_history_refresh=0; g_entry_aborts_today=0; g_failed_hedges_today=0; g_reconcile_failures_today=0; g_execution_failures_today=0; g_circuit_pause_until=0; g_abort_window_start=0; g_abort_window_count=0;
      g_daily_report_sent=false;
      SaveDailyState();
   }
}
double VirtualEquity(){ return g_demo_balance+TotalUnrealizedPnL(); }
void UpdateLotScale(){
   if(!InpUseDynamicLot){ g_lot_scale=1.0; return; }
   if(g_base_equity<=0){ g_lot_scale=1.0; return; }
   double eq=(g_mode==DEMO_TRADING)?VirtualEquity():AccountInfoDouble(ACCOUNT_EQUITY);
   g_lot_scale=MathMax(0.25,MathMin(eq/g_base_equity,InpDynamicLotCap));
}
double BaseLots(){ return InpMaxLots*g_lot_scale; }

bool EnsureSymbolSelected(const string requested,string &resolved,int &errCode);
bool InstPortfolioGate(const string pair,const double proposedRiskPct,string &reason);
void InstRefresh();
void DrawInstitutionalMetrics(const int panelY,const int x,const int w);
void RefreshPortfolioIntelligence();
void DrawPortfolioIntelligencePage();
void DrawStrategyPage();
void DrawMetricsPage();
void DrawNewsPage();
void DrawSymbolMapPage();
void DrawAlphaPage();
void DrawQuantDetailPage();
void CalculateCapitalAllocation();
bool SelectSymbolSafe(const string s){
   string resolved=""; int err=0;
   if(!EnsureSymbolSelected(s,resolved,err)) return false;
   return SymbolInfoInteger(resolved,SYMBOL_TRADE_MODE)!=SYMBOL_TRADE_MODE_DISABLED;
}
bool GetTickSafe(const string s,MqlTick &t){
   ZeroMemory(t);
   if(!SymbolInfoTick(s,t)) return false;
   return (t.bid>0&&t.ask>0&&t.ask>=t.bid);
}
double PointOf(const string s){ return SymbolInfoDouble(s,SYMBOL_POINT); }
double SpreadPoints(const string s){
   MqlTick t; double p=PointOf(s);
   if(p<=0||!GetTickSafe(s,t)) return 1e100;
   return (t.ask-t.bid)/p;
}
void UpdateAvgSpread(){
   string pool[]; int n=0;
   int maxPool = 2 + g_mp_count*2 + g_scan_count*2;
   ArrayResize(pool, maxPool);
   pool[n++]=InpSymbolA; pool[n++]=InpSymbolB;
   for(int i=0;i<g_mp_count;i++){ pool[n++]=g_mp_a[i]; pool[n++]=g_mp_b[i]; }
   for(int i=0;i<g_scan_count;i++){ pool[n++]=g_scan_a[i]; pool[n++]=g_scan_b[i]; }
   
   for(int i=0;i<n;i++){
      int idx=-1;
      for(int k=0;k<ArraySize(g_avg_sym);k++) if(g_avg_sym[k]==pool[i]) idx=k;
      double cur=SpreadPoints(pool[i]);
      if(cur>1e90) continue;
      if(idx<0){
         idx=ArraySize(g_avg_sym);
         ArrayResize(g_avg_sym,idx+1); ArrayResize(g_avg_spread,idx+1); ArrayResize(g_std_spread,idx+1);
         g_avg_sym[idx]=pool[i]; g_avg_spread[idx]=cur; g_std_spread[idx]=1.0;
      }else{
         double dev=cur-g_avg_spread[idx];
         g_avg_spread[idx]=0.8*g_avg_spread[idx]+0.2*cur;
         g_std_spread[idx]=MathSqrt(MathMax(0.8*g_std_spread[idx]*g_std_spread[idx]+0.2*dev*dev,1e-9));
      }
   }
}
double AvgSpread(const string s){
   for(int k=0;k<ArraySize(g_avg_sym);k++) if(g_avg_sym[k]==s) return g_avg_spread[k];
   return SpreadPoints(s);
}
bool SpreadRegimeOK(const string s){
   if(!InpUseSpreadRegime) return true;
   for(int k=0;k<ArraySize(g_avg_sym);k++)
      if(g_avg_sym[k]==s) return SpreadPoints(s)<=g_avg_spread[k]+InpSpreadZMax*g_std_spread[k];
   return true;
}
double StrategySpreadMult(){ return (g_strategy==PAIR_STATISTICAL)?1.2:1.3; }
bool AutoSpreadOK(const string s){
   if(!InpAutoSpreadFilter) return SpreadPoints(s)<=InpMaxSpreadPoints;
   double allow=MathMin(MathMax(AvgSpread(s)*StrategySpreadMult(),1.0),InpMaxSpreadPoints);
   return SpreadPoints(s)<=allow;
}
long QuoteAgeMs(const MqlTick &t){
   if(t.time_msc<=0) return 9223372036854775807;
   long age=(long)TimeCurrent()*1000-(long)t.time_msc;
   if(age<0) age=0;
   return age;
}
double CurrentDynamicSlippagePoints(const double currentZ){
   double value=InpMaxSlippagePoints;
   if(InpUseDynamicSlippage) value+=MathAbs(currentZ)*InpDynamicSlippageMult;
   return MathMax(0.0,value);
}
bool QuoteFresh(const MqlTick &t){
   if(t.time_msc<=0) return false;
   return QuoteAgeMs(t)<=(long)InpMaxQuoteAgeMs;
}
bool SymbolsReady(){
   if(g_strategy==MULTI_STATISTICAL) return true;
   if(!SelectSymbolSafe(InpSymbolA)||!SelectSymbolSafe(InpSymbolB)) return false;
   MqlTick a,b;
   if(!GetTickSafe(InpSymbolA,a)||!GetTickSafe(InpSymbolB,b)) return false;
   return QuoteFresh(a)&&QuoteFresh(b);
}
double NormalizeVolumeSafe(const string s,double vol,const double cap){
   double mn=SymbolInfoDouble(s,SYMBOL_VOLUME_MIN),mx=SymbolInfoDouble(s,SYMBOL_VOLUME_MAX),st=SymbolInfoDouble(s,SYMBOL_VOLUME_STEP);
   if(mn<=0||mx<=0||st<=0) return 0;
   vol=MathMin(vol,MathMin(mx,cap));
   if(vol<mn) return 0;
   int dg=(st>=1)?0:(st>=0.1)?1:(st>=0.01)?2:(st>=0.001)?3:4;
   vol=MathFloor(vol/st+1e-9)*st;
   if(vol<mn) return 0;
   return NormalizeDouble(vol,dg);
}

int OpenCyclesCount(){ return ArraySize(g_cycles); }
bool HasOurSymbol(const string s){
   for(int i=0;i<ArraySize(g_cycles);i++)
      for(int k=0;k<ArraySize(g_cycles[i].legs);k++)
         if(g_cycles[i].legs[k].symbol==s) return true;
   return false;
}
void RemoveCycle(int idx){
   if(idx<0||idx>=ArraySize(g_cycles)) return;
   ArrayRemove(g_cycles,idx,1);
   SaveCyclesState();
}
bool SelectOurPosition(int i,ulong &ticket){
   ticket=PositionGetTicket(i);
   if(ticket==0) return false;
   if(!PositionSelectByTicket(ticket)) return false;
   return PositionGetInteger(POSITION_MAGIC)==InpMagic;
}
int OurPositions(){
   int c=0; ulong t;
   for(int i=PositionsTotal()-1;i>=0;i--) if(SelectOurPosition(i,t)) c++;
   return c;
}

bool FindPositionForLeg(const string symbol,const string comment,ulong &ticket,double &volume,double &openPrice){
   ticket=0; volume=0; openPrice=0;
   for(int i=PositionsTotal()-1;i>=0;i--){
      ulong t=0; if(!SelectOurPosition(i,t)) continue;
      string ps=PositionGetString(POSITION_SYMBOL);
      string pc=PositionGetString(POSITION_COMMENT);
      if(ps!=symbol) continue;
      if(comment!="" && StringFind(pc,comment)<0) continue;
      ticket=t; volume=PositionGetDouble(POSITION_VOLUME); openPrice=PositionGetDouble(POSITION_PRICE_OPEN); return true;
   }
   return false;
}

double CurrencyToDepositRate(const string cur){
   string dep=AccountInfoString(ACCOUNT_CURRENCY);
   if(cur==""||dep=="") return 0;
   if(cur==dep) return 1.0;
   string direct=cur+dep, inverse=dep+cur;
   if(SymbolSelect(direct,true)){ double b=SymbolInfoDouble(direct,SYMBOL_BID); if(b>0) return b; }
   if(SymbolSelect(inverse,true)){ double b=SymbolInfoDouble(inverse,SYMBOL_BID); if(b>0) return 1.0/b; }
   int total=SymbolsTotal(true);
   for(int i=0;i<total;i++){
      string s=SymbolName(i,true); if(s=="") continue;
      string base=SymbolInfoString(s,SYMBOL_CURRENCY_BASE), quote=SymbolInfoString(s,SYMBOL_CURRENCY_PROFIT);
      double b=SymbolInfoDouble(s,SYMBOL_BID); if(b<=0) continue;
      if(base==cur&&quote==dep) return b;
      if(base==dep&&quote==cur) return 1.0/b;
   }
   return 0;
}
void AddUnits(string &names[],double &amts[],int &n,const string cur,const double amt){
   int idx=-1;
   for(int j=0;j<n;j++) if(names[j]==cur) idx=j;
   if(idx<0&&n<16){ idx=n; names[n]=cur; n++; }
   if(idx>=0) amts[idx]+=amt;
}
bool ExposureOKWith(const string sA,const double signA,const double volA,const string sB,const double signB,const double volB){
   string names[16]; double amts[16]; int n=0;
   for(int i=0;i<ArraySize(g_cycles);i++)
      for(int k=0;k<ArraySize(g_cycles[i].legs);k++){
         STradeLeg L=g_cycles[i].legs[k];
         string base=SymbolInfoString(L.symbol,SYMBOL_CURRENCY_BASE);
         string quote=SymbolInfoString(L.symbol,SYMBOL_CURRENCY_PROFIT);
         double contract=SymbolInfoDouble(L.symbol,SYMBOL_TRADE_CONTRACT_SIZE);
         double sign=(L.type==ORDER_TYPE_BUY)?1:-1;
         double units=L.volume*contract*sign;
         AddUnits(names,amts,n,base,units);
         AddUnits(names,amts,n,quote,-units);
      }
   string cs[2]; double cu[2];
   cs[0]=sA; cu[0]=signA*volA*SymbolInfoDouble(sA,SYMBOL_TRADE_CONTRACT_SIZE);
   cs[1]=sB; cu[1]=signB*volB*SymbolInfoDouble(sB,SYMBOL_TRADE_CONTRACT_SIZE);
   for(int q=0;q<2;q++){
      string base=SymbolInfoString(cs[q],SYMBOL_CURRENCY_BASE);
      string quote=SymbolInfoString(cs[q],SYMBOL_CURRENCY_PROFIT);
      AddUnits(names,amts,n,base,cu[q]);
      AddUnits(names,amts,n,quote,-cu[q]);
   }
   double limit=InpMaxExposureLots*g_lot_scale;
   for(int k=0;k<n;k++){
      double rate=CurrencyToDepositRate(names[k]);
      if(rate<=0) {
         if(InpVerboseLog) Print("WARNING: Cannot find exchange rate for ", names[k], " - BLOCKING TRADE for safety");
         return false; 
      }
      double exposureInDeposit = amts[k] * rate;
      double lotsEquivalent = MathAbs(exposureInDeposit) / 100000.0;
      if(lotsEquivalent > limit) return false;
   }
   return true;
}
void UpdateExposureView(){
   g_exp_count=0;
   string names[16]; double amts[16]; int n=0;
   for(int i=0;i<ArraySize(g_cycles);i++)
      for(int k=0;k<ArraySize(g_cycles[i].legs);k++){
         STradeLeg L=g_cycles[i].legs[k];
         string base=SymbolInfoString(L.symbol,SYMBOL_CURRENCY_BASE);
         string quote=SymbolInfoString(L.symbol,SYMBOL_CURRENCY_PROFIT);
         double contract=SymbolInfoDouble(L.symbol,SYMBOL_TRADE_CONTRACT_SIZE);
         double sign=(L.type==ORDER_TYPE_BUY)?1:-1;
         double units=L.volume*contract*sign;
         AddUnits(names,amts,n,base,units);
         AddUnits(names,amts,n,quote,-units);
      }
   string tc[16]; double tl[16]; int m=0;
   for(int k=0;k<n;k++){
      double r=CurrencyToDepositRate(names[k]);
      if(r<=0) continue;
      double exposureInDeposit = amts[k] * r;
      double lots = exposureInDeposit / 100000.0;
      if(MathAbs(lots)<1e-4) continue;
      tc[m]=names[k]; tl[m]=lots; m++;
   }
   for(int a=0;a<m;a++)
      for(int b=a+1;b<m;b++)
         if(MathAbs(tl[b])>MathAbs(tl[a])){
            double t=tl[a]; tl[a]=tl[b]; tl[b]=t;
            string s=tc[a]; tc[a]=tc[b]; tc[b]=s;
         }
   g_exp_count=MathMin(3,m);
   for(int k=0;k<g_exp_count;k++){ g_exp_cur[k]=tc[k]; g_exp_lots[k]=tl[k]; }
}

double MarginLevel(){
   double m=AccountInfoDouble(ACCOUNT_MARGIN);
   return (m<=0)?1e100:AccountInfoDouble(ACCOUNT_EQUITY)/m*100.0;
}

bool CheckDailyLoss(){
   if(g_daily_stop) return true;
   double base=(g_mode==DEMO_TRADING)?g_demo_day_start_equity:g_day_start_equity;
   double eq=(g_mode==DEMO_TRADING)?VirtualEquity():AccountInfoDouble(ACCOUNT_EQUITY);
   if(base<=0) return false;
   double lossPct=(base-eq)/base*100.0;
   return (lossPct>=InpMaxDailyLossPct);
}

bool DailyLossHit(){
   if(g_daily_stop) return true;
   double base=(g_mode==DEMO_TRADING)?g_demo_day_start_equity:g_day_start_equity;
   double eq=(g_mode==DEMO_TRADING)?VirtualEquity():AccountInfoDouble(ACCOUNT_EQUITY);
   if(base<=0) return false;
   double lossPct=(base-eq)/base*100.0;
   if(lossPct>=InpMaxDailyLossPct){
      g_daily_stop=true; 
      if(g_stop_reason=="") g_stop_reason="DAILY LOSS";
      SaveDailyState(); 
      Notify(" *DAILY STOP HIT* Loss: "+DoubleToString(lossPct,2)+"%"); 
      return true;
   }
   return false;
}
bool CheckPeakDrawdown(){
   if(!InpUsePeakDrawdown||g_mode==SIGNALS_ONLY) return false;
   double eq=(g_mode==DEMO_TRADING)?VirtualEquity():AccountInfoDouble(ACCOUNT_EQUITY);
   if(eq>g_peak_equity){ g_peak_equity=eq; if(!g_is_tester&&InpPersistRiskState) GlobalVariableSet(GV_PEAK_EQUITY,g_peak_equity); }
   if(g_peak_equity<=0) return false;
   double ddPct=(g_peak_equity-eq)/g_peak_equity*100.0;
   return (ddPct>=InpMaxPeakDrawdownPct);
}
bool PeakDrawdownHit(){
   if(!InpUsePeakDrawdown||g_mode==SIGNALS_ONLY) return false;
   double eq=(g_mode==DEMO_TRADING)?VirtualEquity():AccountInfoDouble(ACCOUNT_EQUITY);
   if(eq>g_peak_equity){ g_peak_equity=eq; if(!g_is_tester&&InpPersistRiskState) GlobalVariableSet(GV_PEAK_EQUITY,g_peak_equity); }
   if(g_peak_equity<=0) return false;
   double ddPct=(g_peak_equity-eq)/g_peak_equity*100.0;
   if(ddPct>=InpMaxPeakDrawdownPct){
      g_daily_stop=true; if(g_stop_reason=="") g_stop_reason="PEAK DRAWDOWN";
      SaveDailyState(); Notify("PEAK DRAWDOWN STOP: "+DoubleToString(ddPct,2)+"%"); return true;
   }
   return false;
}

bool SessionOK(){
   if(!InpUseSessionFilter) return true;
   MqlDateTime dt; TimeToStruct(TimeGMT(),dt);
   if(InpSessionStartHourGMT<InpSessionEndHourGMT) return dt.hour>=InpSessionStartHourGMT&&dt.hour<InpSessionEndHourGMT;
   return dt.hour>=InpSessionStartHourGMT||dt.hour<InpSessionEndHourGMT;
}

// Dashboard health indicator.  This is deliberately independent from SIGNALS/DEMO/LIVE.
// The dot is green only when the EA is healthy AND the market/session is actually open
// AND fresh quotes are arriving.  A weekend/holiday/no-quotes condition OR a critical
// diagnostic/execution/reconciliation error makes it red.
bool DashboardSymbolSessionOpen(const string symbol,const datetime now){
   if(StringLen(symbol)<1) return false;
   MqlDateTime nd; TimeToStruct(now,nd);
   if(nd.day_of_week==0 || nd.day_of_week==6) return false;

   bool gotSession=false;
   for(uint idx=0; idx<32; idx++){
      datetime from=0,to=0;
      if(!SymbolInfoSessionTrade(symbol,(ENUM_DAY_OF_WEEK)nd.day_of_week,idx,from,to)) break;
      gotSession=true;
      MqlDateTime fd,td; TimeToStruct(from,fd); TimeToStruct(to,td);
      int curSec=nd.hour*3600+nd.min*60+nd.sec;
      int fromSec=fd.hour*3600+fd.min*60+fd.sec;
      int toSec=td.hour*3600+td.min*60+td.sec;
      if(fromSec==toSec) return true; // full-day session
      if(fromSec<toSec){
         if(curSec>=fromSec && curSec<toSec) return true;
      }else{
         if(curSec>=fromSec || curSec<toSec) return true;
      }
   }
   // Some brokers do not expose session metadata. In that case do not falsely mark
   // a weekday as closed; fresh quotes will still be required below.
   return !gotSession;
}

bool DashboardMarketDataLive(){
   if(g_is_tester) return true;
   if(!TerminalInfoInteger(TERMINAL_CONNECTED)) return false;

   datetime now=TimeTradeServer();
   if(now<=0) now=TimeCurrent();
   MqlDateTime dt; TimeToStruct(now,dt);
   if(dt.day_of_week==0 || dt.day_of_week==6) return false;

   string a[20],b[20]; int n=0;
   a[n]=InpSymbolA; b[n]=InpSymbolB; n++;
   for(int i=0;i<g_mp_count && n<20;i++){ a[n]=g_mp_a[i]; b[n]=g_mp_b[i]; n++; }
   for(int i=0;i<g_scan_count && n<20;i++){ a[n]=g_scan_a[i]; b[n]=g_scan_b[i]; n++; }

   for(int i=0;i<n;i++){
      if(!DashboardSymbolSessionOpen(a[i],now) || !DashboardSymbolSessionOpen(b[i],now)) continue;
      MqlTick ta,tb;
      if(GetTickSafe(a[i],ta) && GetTickSafe(b[i],tb) && QuoteFresh(ta) && QuoteFresh(tb))
         return true;
   }
   return false;
}

// A red dashboard dot must also catch real operational failures, not only market closure.
// These counters/statuses are already maintained by the EA's risk/diagnostic pipeline,
// so this function does not change trading decisions; it only reports health.
bool DashboardSystemHealthy(){
   if(g_diag_errors>0) return false;
   if(g_execution_failures_today>0) return false;
   if(g_reconcile_failures_today>0) return false;
   if(ExecutionCircuitActive()) return false;
   if(g_daily_stop) return false;
   if(g_inst.killSwitch) return false;
   if(!TerminalInfoInteger(TERMINAL_CONNECTED)) return false;
   return true;
}
bool WeekendBlock(){
   if(!InpCloseBeforeWeekend) return false;
   MqlDateTime dt; TimeToStruct(TimeGMT(),dt);
   return (dt.day_of_week==5&&dt.hour>=InpFridayCloseHourGMT)||dt.day_of_week==6;
}

void UpdateVolatilityCache(const string symbol){
   if(!InpUseVolatilityFilter){ g_cached_vol_ok=true; return; }
   if(TimeCurrent()-g_cached_vol_time<5 && g_cached_vol_symbol==symbol) return;
   g_cached_vol_time=TimeCurrent();
   g_cached_vol_symbol=symbol;
   
   int h_cur = iATR(symbol, PERIOD_CURRENT, InpVolatilityATRPeriod);
   int h_avg = iATR(symbol, PERIOD_CURRENT, 50);
   
   if(h_cur == INVALID_HANDLE || h_avg == INVALID_HANDLE) {
      g_cached_vol_ok=true; 
      if(h_cur != INVALID_HANDLE) IndicatorRelease(h_cur);
      if(h_avg != INVALID_HANDLE) IndicatorRelease(h_avg);
      return;
   }
   
   double currentATR[], avgATR[];
   ArraySetAsSeries(currentATR, true);
   ArraySetAsSeries(avgATR, true);
   
   bool ok = (CopyBuffer(h_cur, 0, 0, 1, currentATR) >= 1) && 
             (CopyBuffer(h_avg, 0, 0, 1, avgATR) >= 1);
   
   IndicatorRelease(h_cur);
   IndicatorRelease(h_avg);
   
   if(!ok) { g_cached_vol_ok=true; return; }
   
   g_cached_atr_current=currentATR[0];
   g_cached_atr_avg=avgATR[0];
   
   if(g_cached_atr_avg > 0 && g_cached_atr_current > (g_cached_atr_avg * InpMaxATRMultiplier)){
      g_cached_vol_ok=false;
   } else {
      g_cached_vol_ok=true;
   }
}

bool IsVolatilityOK(const string symbol=""){
   if(!InpUseVolatilityFilter) return true;
   UpdateVolatilityCache(symbol);
   return g_cached_vol_ok;
}
bool RiskOK(const string symbol=""){
   if(g_daily_stop) return false;
   if(ExecutionCircuitActive()) return false;
   if(DailyLossHit()||PeakDrawdownHit()) return false;
   if(g_mode!=DEMO_TRADING && MarginLevel()<InpMinMarginLevel) return false;
   if(g_mode!=DEMO_TRADING && OurPositions()>=InpMaxPositions) return false;
   if(g_losses_today>=InpMaxLossesPerDay || g_consec_losses>=InpMaxConsecLosses) return false;
   if(g_total_cycles_today>=InpMaxTradesPerDay) return false;
   if(g_daily_commission>=InpMaxDailyCommission) return false;
   if(symbol!="" && !IsVolatilityOK(symbol)) return false;
   return true;
}
bool MarginOKFor(const string s,const double lot,const ENUM_ORDER_TYPE type){
   if(!InpAutoMarginCheck||g_mode==DEMO_TRADING) return true;
   double price=(type==ORDER_TYPE_BUY)?SymbolInfoDouble(s,SYMBOL_ASK):SymbolInfoDouble(s,SYMBOL_BID);
   double need=0;
   if(!OrderCalcMargin(type,s,lot,price,need)) return false;
   return AccountInfoDouble(ACCOUNT_MARGIN_FREE)>=need*((g_strategy==PAIR_STATISTICAL)?1.3:1.5);
}

bool CurrencyHasHighNews(const string cur){
   if(!InpUseNewsFilter||g_is_tester) return false;
   MqlCalendarEvent ev[];
   int total=CalendarEventByCurrency(cur,ev);
   if(total<0) return true;
   if(total==0) return false;
   datetime from=TimeCurrent()-InpNewsMinutesBefore*60, to=TimeCurrent()+InpNewsMinutesAfter*60;
   for(int i=0;i<total;i++){
      bool hi=(ev[i].importance==CALENDAR_IMPORTANCE_HIGH);
      bool md=(ev[i].importance==CALENDAR_IMPORTANCE_MODERATE);
      if(!hi&&!(InpNewsBlockModerate&&md)) continue;
      MqlCalendarValue vals[];
      if(CalendarValueHistoryByEvent(ev[i].id,vals,from,to)>0) return true;
   }
   return false;
}
bool CurrencyNewsWithin(const string cur,const int minutes){
   if(!InpUseNewsFilter||g_is_tester) return false;
   MqlCalendarEvent ev[];
   int total=CalendarEventByCurrency(cur,ev);
   if(total<=0) return false;
   datetime from=TimeCurrent(), to=TimeCurrent()+minutes*60;
   for(int i=0;i<total;i++){
      if(ev[i].importance!=CALENDAR_IMPORTANCE_HIGH) continue;
      MqlCalendarValue vals[];
      if(CalendarValueHistoryByEvent(ev[i].id,vals,from,to)>0) return true;
   }
   return false;
}
void PairCurrencies(const string sA,const string sB,string &c1,string &c2,string &c3,string &c4){
   c1=SymbolInfoString(sA,SYMBOL_CURRENCY_BASE); c2=SymbolInfoString(sA,SYMBOL_CURRENCY_PROFIT);
   c3=SymbolInfoString(sB,SYMBOL_CURRENCY_BASE); c4=SymbolInfoString(sB,SYMBOL_CURRENCY_PROFIT);
}
bool PairNewsBlocked(const string sA,const string sB){
   if(!InpUseNewsFilter||g_is_tester) return false;
   string c[4]; PairCurrencies(sA,sB,c[0],c[1],c[2],c[3]);
   for(int i=0;i<4;i++){
      bool dup=false; for(int j=0;j<i;j++) if(c[j]==c[i]) dup=true;
      if(dup) continue;
      if(CurrencyHasHighNews(c[i])) return true;
   }
   return false;
}
bool PairNewsSoon(const string sA,const string sB){
   if(!InpCloseBeforeHighNews||g_is_tester) return false;
   string c[4]; PairCurrencies(sA,sB,c[0],c[1],c[2],c[3]);
   for(int i=0;i<4;i++){
      bool dup=false; for(int j=0;j<i;j++) if(c[j]==c[i]) dup=true;
      if(dup) continue;
      if(CurrencyNewsWithin(c[i],InpCloseMinBeforeNews)) return true;
   }
   return false;
}
bool GlobalNewsBlocked(){
   if(g_strategy==PAIR_STATISTICAL) return PairNewsBlocked(InpSymbolA,InpSymbolB);
   return false;
}

double KalmanBetaCalc(const double &a[],const double &b[],const int n){
   double beta=1.0,P=1e-4;
   for(int i=n-1;i>=0;i--){
      P+=InpKalmanQ;
      double x=b[i];
      double denom=P*x*x+InpKalmanR;
      if(denom<=0) continue;
      double K=P*x/denom;
      double e=a[i]-beta*x;
      beta+=K*e; P=(1-K*x)*P;
   }
   return beta;
}
double CalcReturnCorrelation(const string s1,const string s2,const int bars){
   if(bars<2) return 0.0;
   double c1[],c2[];
   ArraySetAsSeries(c1,true); ArraySetAsSeries(c2,true);
   if(!GetCloseCached(s1, PERIOD_CURRENT, bars+1, c1)) return 0;
   if(!GetCloseCached(s2, PERIOD_CURRENT, bars+1, c2)) return 0;
   double r1[],r2[];
   ArrayResize(r1,bars); ArrayResize(r2,bars);
   for(int i=0;i<bars;i++){ r1[i]=MathLog(c1[i]/c1[i+1]); r2[i]=MathLog(c2[i]/c2[i+1]); }
   double m1=0,m2=0;
   for(int i=0;i<bars;i++){ m1+=r1[i]; m2+=r2[i]; }
   m1/=bars; m2/=bars;
   double num=0,d1=0,d2=0;
   for(int i=0;i<bars;i++){ double a=r1[i]-m1,b=r2[i]-m2; num+=a*b; d1+=a*a; d2+=b*b; }
   double den=MathSqrt(d1*d2);
   return (den>0)?num/den:0;
}
bool FitOUModel(const double &prices[], const int n, SOUParams &params){
   params.valid=false;
   if(n<30) return false;
   // prices[] are series: [0] newest. Build chronological x_t -> x_{t+1}.
   double x[], dx[]; ArrayResize(x,n-1); ArrayResize(dx,n-1);
   for(int j=0;j<n-1;j++){ int i=n-1-j; x[j]=prices[i]; dx[j]=prices[i-1]-prices[i]; }
   int N=n-1; double sx=0,sd=0,sxx=0,sxd=0;
   for(int i=0;i<N;i++){ sx+=x[i]; sd+=dx[i]; sxx+=x[i]*x[i]; sxd+=x[i]*dx[i]; }
   double mx=sx/N, md=sd/N;
   double cov=(sxd-N*mx*md)/(N-1), var=(sxx-N*mx*mx)/(N-1);
   if(var<=0) return false;
   double b=cov/var, a=md-b*mx;
   if(b>=0 || MathAbs(b)<1e-12) return false;
   params.theta=-b; params.mu=-a/b; params.halfLife=MathLog(2.0)/params.theta;
   double rss=0; for(int i=0;i<N;i++){ double e=dx[i]-(a+b*x[i]); rss+=e*e; }
   params.sigma=MathSqrt(rss/MathMax(1,N-2));
   params.valid=(MathIsValidNumber(params.theta)&&MathIsValidNumber(params.mu)&&MathIsValidNumber(params.sigma)&&params.sigma>0);
   return params.valid;
}
bool ADFTestResiduals(const double &residuals[],const int n,double &pvalue){
   pvalue=1.0;
   int maxLag=MathMax(0,MathMin(InpADFMaxLag,5));
   if(n<40+maxLag) return false;
   int N=n-1-maxLag, cols=2+maxLag;
   if(N<=cols+5) return false;
   double xtx[8][8]; double xty[8]; double aug[8][17];
   ArrayInitialize(xty,0.0); ArrayInitialize(xtx,0.0); ArrayInitialize(aug,0.0);
   for(int r=0;r<N;r++){
      int t=maxLag+1+r; double x[8]; ArrayInitialize(x,0.0); x[0]=1.0;
      int idxPrev=n-1-(t-1); x[1]=residuals[idxPrev];
      for(int l=1;l<=maxLag;l++){
         int chrono=t-l; int idx=n-1-chrono; int idx0=n-1-(chrono-1); x[1+l]=residuals[idx]-residuals[idx0];
      }
      int idxT=n-1-t; double dy=residuals[idxT]-residuals[idxPrev];
      for(int a=0;a<cols;a++){ xty[a]+=x[a]*dy; for(int b=0;b<cols;b++) xtx[a][b]+=x[a]*x[b]; }
   }
   // [X'X | I | X'y] -> Gauss-Jordan inverse and OLS coefficients.
   for(int r=0;r<cols;r++) for(int c=0;c<cols;c++) aug[r][c]=xtx[r][c];
   for(int r=0;r<cols;r++) for(int c=0;c<cols;c++) aug[r][cols+c]=(r==c)?1.0:0.0;
   for(int r=0;r<cols;r++) aug[r][2*cols]=xty[r];
   for(int c=0;c<cols;c++){
      int piv=c; double best=MathAbs(aug[c][c]);
      for(int r=c+1;r<cols;r++) if(MathAbs(aug[r][c])>best){best=MathAbs(aug[r][c]);piv=r;}
      if(best<1e-12) return false;
      if(piv!=c) for(int j=0;j<=2*cols;j++){double tmp=aug[c][j];aug[c][j]=aug[piv][j];aug[piv][j]=tmp;}
      double d=aug[c][c]; for(int j=0;j<=2*cols;j++) aug[c][j]/=d;
      for(int r=0;r<cols;r++) if(r!=c){ double f=aug[r][c]; if(MathAbs(f)>1e-15) for(int j=0;j<=2*cols;j++) aug[r][j]-=f*aug[c][j]; }
   }
   double beta=aug[1][2*cols];
   double rss=0;
   for(int r=0;r<N;r++){
      int t=maxLag+1+r; int idxT=n-1-t, idxPrev=n-1-(t-1); double dy=residuals[idxT]-residuals[idxPrev];
      double pred=aug[0][2*cols]+aug[1][2*cols]*residuals[idxPrev];
      for(int l=1;l<=maxLag;l++){ int chrono=t-l; int idx=n-1-chrono, idx0=n-1-(chrono-1); pred+=aug[1+l][2*cols]*(residuals[idx]-residuals[idx0]); }
      double e=dy-pred; rss+=e*e;
   }
   int df=N-cols; if(df<=0) return false;
   double mse=rss/df, varBeta=mse*aug[1][cols+1]; if(varBeta<=0) return false;
   double tstat=beta/MathSqrt(varBeta);
   if(tstat<=-3.43) pvalue=0.01; else if(tstat<=-2.86) pvalue=0.05; else if(tstat<=-2.57) pvalue=0.10; else pvalue=0.50;
   return MathIsValidNumber(tstat);
}

bool TestCointegration(const string sA, const string sB, double &pvalue, double &beta){
   pvalue=1.0; beta=1.0;
   int lookback=InpOU_lookback;
   int shift=InpUseClosedBarOnly?1:0;
   if(lookback<50) return false;
   double a_full[],b_full[]; ArraySetAsSeries(a_full,true); ArraySetAsSeries(b_full,true);
   if(!GetCloseCached(sA, PERIOD_CURRENT, lookback+shift, a_full)) return false;
   if(!GetCloseCached(sB, PERIOD_CURRENT, lookback+shift, b_full)) return false;
   double a[],b[]; ArrayResize(a,lookback); ArrayResize(b,lookback); ArraySetAsSeries(a,true); ArraySetAsSeries(b,true);
   for(int i=0;i<lookback;i++){
      a[i]=a_full[i+shift]; b[i]=b_full[i+shift];
      if(a[i]<=0||b[i]<=0) return false;
      a[i]=MathLog(a[i]); b[i]=MathLog(b[i]);
   }
   double sx=0,sy=0,sxx=0,sxy=0; for(int i=0;i<lookback;i++){sx+=b[i];sy+=a[i];sxx+=b[i]*b[i];sxy+=a[i]*b[i];}
   double mb=sx/lookback, ma=sy/lookback;
   double cov=(sxy-lookback*mb*ma)/(lookback-1), var=(sxx-lookback*mb*mb)/(lookback-1); if(var<=0) return false;
   beta=cov/var; double alpha=ma-beta*mb;
   double r[]; ArrayResize(r,lookback); for(int i=0;i<lookback;i++) r[i]=a[i]-(alpha+beta*b[i]);
   return ADFTestResiduals(r,lookback,pvalue);
}

double CalculateOUZScore(const string sA, const string sB, const SOUParams &ou, const double currentSpread){
   if(!ou.valid) return 0;
   if(ou.sigma<=0) return 0;
   return (currentSpread-ou.mu)/ou.sigma;
}

double GetAdaptiveZThreshold(){
   if(!InpUseAdaptiveZ || g_z_history_count<InpAdaptiveZ_window) return InpStatEntryZScore;
   double sum=0,sum2=0;
   int n=MathMin(g_z_history_count, InpAdaptiveZ_window);
   for(int i=0;i<n;i++){ 
      int idx = (g_z_history_idx - 1 - i + 1000) % 1000;
      double val = g_z_history[idx];
      sum+=val; sum2+=val*val; 
   }
   double mean=sum/n;
   if(n<2) return InpStatEntryZScore;
   double var=(sum2-n*mean*mean)/(n-1);
   double std=MathSqrt(MathMax(var, 0.01));
   return InpAdaptiveZ_base+InpAdaptiveZ_k*std;
}

void DetectMarketRegime(const string symbol){
   g_regime.regime="UNKNOWN"; g_regime.atr=0; g_regime.adx=0; g_regime.volatilityZ=0;
   if(!InpUseRegimeDetection) return;
   double atr[]; ArraySetAsSeries(atr, true);
   int hATR=iATR(symbol, PERIOD_CURRENT, InpRegimeATRPeriod);
   if(hATR==INVALID_HANDLE) return;
   if(CopyBuffer(hATR, 0, 0, 2, atr)<2) return;
   IndicatorRelease(hATR);
   g_regime.atr=atr[0];
   double adx[]; ArraySetAsSeries(adx, true);
   int hADX=iADX(symbol, PERIOD_CURRENT, InpRegimeADXPeriod);
   if(hADX==INVALID_HANDLE) return;
   if(CopyBuffer(hADX, 0, 0, 2, adx)<2) return;
   IndicatorRelease(hADX);
   g_regime.adx=adx[0];
   double atrHist[]; ArraySetAsSeries(atrHist, true);
   int hATR2=iATR(symbol, PERIOD_CURRENT, InpRegimeATRPeriod);
   if(hATR2==INVALID_HANDLE) return;
   if(CopyBuffer(hATR2, 0, 0, 100, atrHist)<100) return;
   IndicatorRelease(hATR2);
   double sumATR=0,sumATR2=0;
   for(int i=0;i<100;i++){ sumATR+=atrHist[i]; sumATR2+=atrHist[i]*atrHist[i]; }
   double meanATR=sumATR/100;
   double varATR=(sumATR2-100*meanATR*meanATR)/99;
   double stdATR=MathSqrt(MathMax(varATR, 0.0001));
   g_regime.volatilityZ=(atr[0]-meanATR)/stdATR;
   if(g_regime.adx>=InpRegimeTrendADX) g_regime.regime="TRENDING";
   else if(g_regime.volatilityZ>=InpRegimeHighVolATR_Z) g_regime.regime="HIGH_VOL";
   else g_regime.regime="RANGING";
}

bool MultiTimeframeConfirmation(const string sA, const string sB, const double currentZ, const double beta){
   if(!InpUseMultiTimeframe) return true;
   if(beta<=0) return true;
   double a15[], b15[];
   ArraySetAsSeries(a15, true); ArraySetAsSeries(b15, true);
   if(!GetCloseCached(sA, InpConfirmTF, 50, a15)) return false;
   if(!GetCloseCached(sB, InpConfirmTF, 50, b15)) return false;
   double spread15[]; ArrayResize(spread15, 50);
   for(int i=0;i<50;i++){
      if(a15[i]<=0||b15[i]<=0) return false;
      spread15[i]=MathLog(a15[i])-beta*MathLog(b15[i]);
   }
   double sum15=0;
   for(int i=0;i<50;i++) sum15+=spread15[i];
   double mean15=sum15/50;
   double var15=0;
   for(int i=0;i<50;i++) var15+=(spread15[i]-mean15)*(spread15[i]-mean15);
   var15/=49;
   double std15=MathSqrt(MathMax(var15, 0.0001));
   double z15=(spread15[0]-mean15)/std15;
   double aH1[], bH1[];
   ArraySetAsSeries(aH1, true); ArraySetAsSeries(bH1, true);
   if(!GetCloseCached(sA, InpTrendTF, 30, aH1)) return false;
   if(!GetCloseCached(sB, InpTrendTF, 30, bH1)) return false;
   double spreadH1[]; ArrayResize(spreadH1, 30);
   for(int i=0;i<30;i++){
      if(aH1[i]<=0||bH1[i]<=0) return false;
      spreadH1[i]=MathLog(aH1[i])-beta*MathLog(bH1[i]);
   }
   double sumH1=0;
   for(int i=0;i<30;i++) sumH1+=spreadH1[i];
   double meanH1=sumH1/30;
   double varH1=0;
   for(int i=0;i<30;i++) varH1+=(spreadH1[i]-meanH1)*(spreadH1[i]-meanH1);
   varH1/=29;
   double stdH1=MathSqrt(MathMax(varH1, 0.0001));
   double zH1=(spreadH1[0]-meanH1)/stdH1;
   int signCurrent=(currentZ>0)?1:-1;
   int sign15=(z15>0)?1:-1;
   int signH1=(zH1>0)?1:-1;
   return (signCurrent==sign15 && signCurrent==signH1);
}

bool CheckLiquidity(const string symbol){
   if(!InpUseLiquidityFilter) return true;
   long tickVolume[]; ArraySetAsSeries(tickVolume, true);
   if(CopyTickVolume(symbol, PERIOD_CURRENT, 0, 1, tickVolume)<1) return false;
   return (tickVolume[0]>=InpMinTickVolume);
}

double MeasureLatency(const string symbol){
   ulong t1=GetMicrosecondCount(); MqlTick tick; bool ok=SymbolInfoTick(symbol,tick); ulong t2=GetMicrosecondCount();
   if(!ok) return 1e9;
   return (double)(t2-t1)/1000.0;
}

double PredictConvergeMinutes(const SOUParams &ou, const double currentSpread){
   if(!ou.valid || ou.theta<=0) return -1;
   double distance=MathAbs(currentSpread-ou.mu);
   if(distance<0.5*ou.sigma) return 0;
   double targetDistance=0.5*ou.sigma;
   double timeToMean=-MathLog(targetDistance/distance)/ou.theta;
   return timeToMean/60.0;
}
bool ShouldExitDynamically(const int cycleIdx, const SOUParams &ou, const double currentSpread){
   if(!InpUseDynamicExit || !ou.valid || ou.theta<=0 || ou.sigma<=0) return false;
   double distance=MathAbs(currentSpread-ou.mu);
   if(distance<0.5*ou.sigma) return true;
   double targetDistance=0.5*ou.sigma;
   double timeToMean=-MathLog(targetDistance/distance)/ou.theta;
   double barSec=(double)MathMax(1,PeriodSeconds(PERIOD_CURRENT));
   double barsToMean=timeToMean/barSec;
   int held=(int)(TimeCurrent()-g_cycles[cycleIdx].startTime);
   double heldBars=held/barSec;
   return (heldBars>barsToMean*InpExitSpeedMultiplier);
}

double CalcHurstExponent(const double &series[],const int n){
   if(n<32) return 0.5;
   double lx[8],ly[8]; int m=0;
   int block=8;
   while(block<=n/2 && m<8){
      int blocks=n/block; double rsSum=0; int used=0;
      for(int b=0;b<blocks;b++){
         double mean=0; for(int j=0;j<block;j++) mean+=series[b*block+j]; mean/=block;
         double sd2=0,cum=0; for(int j=0;j<block;j++){ double d=series[b*block+j]-mean; sd2+=d*d; }
         double sd=MathSqrt(sd2/MathMax(1,block-1));
         cum=0; double cmin=0,cmax=0; for(int j=0;j<block;j++){ cum+=series[b*block+j]-mean; if(cum<cmin)cmin=cum; if(cum>cmax)cmax=cum; }
         double R=cmax-cmin; if(sd>0&&R>0){ rsSum+=R/sd; used++; }
      }
      if(used>0&&rsSum>0){ double rs=rsSum/used; lx[m]=MathLog((double)block); ly[m]=MathLog(rs); m++; }
      block*=2;
   }
   if(m<2) return 0.5;
   double sx=0,sy=0,sxx=0,sxy=0; for(int i=0;i<m;i++){sx+=lx[i];sy+=ly[i];sxx+=lx[i]*lx[i];sxy+=lx[i]*ly[i];}
   double den=m*sxx-sx*sx; if(MathAbs(den)<1e-12) return 0.5;
   double h=(m*sxy-sx*sy)/den; if(!MathIsValidNumber(h)) return 0.5;
   return MathMax(0.0,MathMin(1.0,h));
}

bool CalcSpreadStats(const string sA, const string sB, SPairStat &st){
   ResetStat(st);
   if(InpStatLookback < 3) return false;
   
   if(!SeriesInfoInteger(sA, PERIOD_CURRENT, SERIES_SYNCHRONIZED) || 
      !SeriesInfoInteger(sB, PERIOD_CURRENT, SERIES_SYNCHRONIZED)) {
      return false;
   }
   
   int shift=InpUseClosedBarOnly?1:0;
   int reqCount = InpStatLookback + shift;
   double a_full[], b_full[];
   if(!GetCloseCached(sA, PERIOD_CURRENT, reqCount, a_full)) return false;
   if(!GetCloseCached(sB, PERIOD_CURRENT, reqCount, b_full)) return false;
   
   double a[], b[];
   ArrayResize(a, InpStatLookback); ArraySetAsSeries(a, true);
   ArrayResize(b, InpStatLookback); ArraySetAsSeries(b, true);
   for(int i = 0; i < InpStatLookback; i++) {
      a[i] = a_full[i + shift];
      b[i] = b_full[i + shift];
   }
   
   if(InpUseCointegrationTest){
      double pvalue=1.0, coint_beta=1.0;
      if(!TestCointegration(sA, sB, pvalue, coint_beta)) return false;
      if(pvalue>InpADF_pValueThreshold) return false;
   }
   double sp[]; ArrayResize(sp, InpStatLookback);
   double sumA=0,sumB=0;
   for(int i=0;i<InpStatLookback;i++){
      if(a[i]<=0||b[i]<=0) return false;
      a[i]=MathLog(a[i]); b[i]=MathLog(b[i]);
      sumA+=a[i]; sumB+=b[i];
   }
   double mA=sumA/InpStatLookback, mB=sumB/InpStatLookback;
   double cov=0,varB=0;
   for(int i=0;i<InpStatLookback;i++){ cov+=(a[i]-mA)*(b[i]-mB); varB+=(b[i]-mB)*(b[i]-mB); }
   double olsBeta=(varB>0)?cov/varB:1.0;
   st.beta=InpUseKalmanBeta?KalmanBetaCalc(a, b, InpStatLookback):olsBeta;
   double sum=0;
   for(int i=0;i<InpStatLookback;i++){ sp[i]=a[i]-st.beta*b[i]; sum+=sp[i]; }
   double mean=sum/InpStatLookback;
   double var=0;
   for(int i=0;i<InpStatLookback;i++) var+=(sp[i]-mean)*(sp[i]-mean);
   var/=(InpStatLookback-1);
   double dev=MathSqrt(var);
   if(dev<=0) return false;
   if(InpUseOUModel){
      SOUParams ou;
      if(FitOUModel(sp, InpStatLookback, ou)){
         st.halfLife=ou.halfLife; st.sigma=ou.sigma;
         st.z=CalculateOUZScore(sA, sB, ou, sp[0]);
         st.ouParams=ou;
      } else { st.z=(sp[0]-mean)/dev; st.sigma=dev; }
   } else {
      st.z=(sp[0]-mean)/dev; st.sigma=dev;
      int n=InpStatLookback-1;
      double sx=0,sy=0,sxy=0,sxx=0;
      for(int i=0;i<n;i++){ double x=sp[i+1], y=sp[i]-sp[i+1]; sx+=x; sy+=y; sxy+=x*y; sxx+=x*x; }
      double mx=sx/n, my=sy/n;
      double covh=sxy-n*mx*my, varh=sxx-n*mx*mx;
      if(varh>0){ double bh=covh/varh; if(bh<0) st.halfLife=-MathLog(2)/bh; }
   }
   st.hurst=CalcHurstExponent(sp,InpStatLookback);
   double tvA=SymbolInfoDouble(sA, SYMBOL_TRADE_TICK_VALUE);
   double tsA=SymbolInfoDouble(sA, SYMBOL_TRADE_TICK_SIZE);
   double tvB=SymbolInfoDouble(sB, SYMBOL_TRADE_TICK_VALUE);
   double tsB=SymbolInfoDouble(sB, SYMBOL_TRADE_TICK_SIZE);
   if(tsA<=0) tsA=PointOf(sA); if(tsB<=0) tsB=PointOf(sB);
   if(tvA<=0) tvA=PointOf(sA)*SymbolInfoDouble(sA, SYMBOL_TRADE_CONTRACT_SIZE);
   if(tvB<=0) tvB=PointOf(sB)*SymbolInfoDouble(sB, SYMBOL_TRADE_CONTRACT_SIZE);
   double pA=SymbolInfoDouble(sA, SYMBOL_BID);
   double pB=SymbolInfoDouble(sB, SYMBOL_BID);
   st.sigmaMoney=st.sigma*(pA*tvA/tsA+MathAbs(st.beta)*pB*tvB/tsB);
   st.corr=CalcReturnCorrelation(sA, sB, 100);
   st.valid=true;
   return true;
}

bool GetStat(const string a, const string b, SPairStat &st){
   ulong now=GetTickCount64();
   int idx=-1;
   for(int i=0;i<ArraySize(g_sc_a);i++) if(g_sc_a[i]==a && g_sc_b[i]==b){ idx=i; break; }
   if(idx>=0 && (now-g_sc_t[idx]<1000)){ st=g_sc_v[idx]; return st.valid; }
   bool ok=CalcSpreadStats(a, b, st);
   if(idx<0){
      idx=ArraySize(g_sc_a);
      ArrayResize(g_sc_a, idx+1); ArrayResize(g_sc_b, idx+1);
      ArrayResize(g_sc_v, idx+1); ArrayResize(g_sc_t, idx+1);
      g_sc_a[idx]=a; g_sc_b[idx]=b;
   }
   g_sc_v[idx]=st; g_sc_t[idx]=now;
   return ok && st.valid;
}

double Clamp01(const double v){ return MathMax(0.0,MathMin(1.0,v)); }
double Clamp100(const double v){ return MathMax(0.0,MathMin(100.0,v)); }
double ScoreBand(const double value,const double goodLow,const double goodHigh,const double hardLow,const double hardHigh){
   if(value>=goodLow && value<=goodHigh) return 100.0;
   if(value<hardLow || value>hardHigh) return 0.0;
   if(value<goodLow) return Clamp100((value-hardLow)/MathMax(goodLow-hardLow,1e-9)*100.0);
   return Clamp100((hardHigh-value)/MathMax(hardHigh-goodHigh,1e-9)*100.0);
}
double PairScore(const SPairStat &st, const double expected, const double cost){
   if(!st.valid) return 0;
   double edge=(cost>0)?expected/cost:0;
   double stat=0;
   if(st.halfLife>0 && st.halfLife<=InpMaxHalfLifeBars) stat=1.0-0.5*st.halfLife/InpMaxHalfLifeBars;
   else if(!InpUseHalfLifeFilter) stat=0.5;
   double h=(InpUseHurstFilter && st.hurst>=InpMaxHurst)?0:1;
   return edge*MathMax(stat, 0.05)*h;
}

bool PortfolioPairCompatible(const string sA,const string sB,double &quality,string &reason){
   quality=100.0; reason="";
   if(!InpUsePortfolioRisk || ArraySize(g_cycles)==0) return true;
   double worst=0.0;
   for(int i=0;i<ArraySize(g_cycles);i++){
      double c1=MathAbs(CalcReturnCorrelation(sA,g_cycles[i].symA,60));
      double c2=MathAbs(CalcReturnCorrelation(sA,g_cycles[i].symB,60));
      double c3=MathAbs(CalcReturnCorrelation(sB,g_cycles[i].symA,60));
      double c4=MathAbs(CalcReturnCorrelation(sB,g_cycles[i].symB,60));
      worst=MathMax(worst,MathMax(MathMax(c1,c2),MathMax(c3,c4)));
   }
   if(worst<=InpPortfolioCorrLimit) return true;
   double excess=worst-InpPortfolioCorrLimit;
   double span=MathMax(0.15,1.0-InpPortfolioCorrLimit);
   quality=Clamp100(100.0-excess/span*100.0);
   reason="portfolio correlation "+DoubleToString(worst,2);
   return quality>=25.0;
}

double EffectivePairIntelligenceThreshold(){
   double base=Clamp100(InpMinPairIntelligenceScore);
   if(!InpUseWalkForwardGuard || !InpWalkForwardCalibrateScore) return base;
   if(!g_wf_valid || !g_wf_gate_pass) return base;
   return MathMax(base,Clamp100(g_wf_calibrated_threshold));
}
double PairIntelligenceLotFactor(const double score){
   if(!InpUsePairIntelligence) return 1.0;
   double lo=MathMin(InpIntelligenceLotFloor,InpIntelligenceLotCeiling);
   double hi=MathMax(InpIntelligenceLotFloor,InpIntelligenceLotCeiling);
   if(hi<=0) return 1.0;
   double threshold=EffectivePairIntelligenceThreshold();
   double x=Clamp01((score-threshold)/MathMax(100.0-threshold,1e-9));
   return MathMax(0.10,lo+(hi-lo)*x);
}

double PairHistoryQuality(const string sA,const string sB){
   string key=sA+"/"+sB;
   for(int i=0;i<ArraySize(g_pair_stats);i++) if(g_pair_stats[i].pair==key){
      if(g_pair_stats[i].totalTrades<3) return 50.0;
      double wr=(double)g_pair_stats[i].wins/g_pair_stats[i].totalTrades;
      double avg=g_pair_stats[i].avgPnL;
      double q=50.0+(wr-0.5)*100.0;
      if(avg>0) q+=10.0; else if(avg<0) q-=10.0;
      return Clamp100(q);
   }
   return 50.0;
}
void UpdatePairHistory(const string sA,const string sB,const double pnl){
   string key=sA+"/"+sB; int idx=-1;
   for(int i=0;i<ArraySize(g_pair_stats);i++) if(g_pair_stats[i].pair==key){idx=i;break;}
   if(idx<0){ idx=ArraySize(g_pair_stats); ArrayResize(g_pair_stats,idx+1); g_pair_stats[idx].pair=key; g_pair_stats[idx].totalTrades=0; g_pair_stats[idx].wins=0; g_pair_stats[idx].totalPnL=0; g_pair_stats[idx].avgPnL=0; g_pair_stats[idx].maxDrawdown=0; }
   g_pair_stats[idx].totalTrades++; if(pnl>0) g_pair_stats[idx].wins++; g_pair_stats[idx].totalPnL+=pnl;
   g_pair_stats[idx].avgPnL=g_pair_stats[idx].totalPnL/g_pair_stats[idx].totalTrades; g_pair_stats[idx].lastTrade=TimeCurrent();
}

bool CalculatePairIntelligence(const string sA,const string sB,const SPairStat &st,const double expected,const double cost,SPairIntelligence &pi){
   ZeroMemory(pi);
   if(!st.valid){ pi.reason="invalid statistics"; return false; }
   pi.edge=expected; pi.cost=MathMax(cost,0.0001); pi.edgeCost=expected/pi.cost;
   double zabs=MathAbs(st.z), entryZ=MathMax(InpStatEntryZScore,0.1), hardZ=MathMax(InpHardZStop,entryZ+0.1);
   double sweetHigh=entryZ+(hardZ-entryZ)*0.55;
   pi.zQuality=ScoreBand(zabs,entryZ,sweetHigh,entryZ*0.85,hardZ);
   double minCorr=MathMax(0.0,MathMin(0.99,InpMinPairCorrelation));
   pi.corrQuality=Clamp100((MathAbs(st.corr)-minCorr)/MathMax(1.0-minCorr,0.01)*100.0);
   if(st.halfLife>0){
      if(st.halfLife<=InpMaxHalfLifeBars*0.50) pi.halfLifeQuality=100.0;
      else pi.halfLifeQuality=Clamp100((InpMaxHalfLifeBars-st.halfLife)/MathMax(InpMaxHalfLifeBars*0.50,1.0)*100.0);
   } else pi.halfLifeQuality=InpUseHalfLifeFilter?0.0:60.0;
   pi.hurstQuality=InpUseHurstFilter?Clamp100((InpMaxHurst-st.hurst)/MathMax(InpMaxHurst,0.05)*100.0):60.0;
   double betaMid=(InpMinBeta+InpMaxBeta)*0.5, betaHalf=MathMax((InpMaxBeta-InpMinBeta)*0.5,0.05);
   pi.betaQuality=Clamp100(100.0-MathAbs(st.beta-betaMid)/betaHalf*45.0);
   if(st.beta<InpMinBeta || st.beta>InpMaxBeta) pi.betaQuality=0.0;
   MqlTick ta,tb; bool quotes=GetTickSafe(sA,ta)&&GetTickSafe(sB,tb)&&QuoteFresh(ta)&&QuoteFresh(tb);
   if(!quotes) pi.executionQuality=0.0;
   else{
      double pa=SymbolInfoDouble(sA,SYMBOL_POINT), pb=SymbolInfoDouble(sB,SYMBOL_POINT);
      double spa=(pa>0)?(ta.ask-ta.bid)/pa:9999.0, spb=(pb>0)?(tb.ask-tb.bid)/pb:9999.0;
      double qa=InpMaxSpreadPoints>0?Clamp100((1.0-spa/InpMaxSpreadPoints)*100.0):100.0;
      double qb=InpMaxSpreadPoints>0?Clamp100((1.0-spb/InpMaxSpreadPoints)*100.0):100.0;
      pi.executionQuality=(qa+qb)*0.5;
   }
   if(InpUseLatencyCheck && g_high_latency) pi.executionQuality*=0.5;
   pi.regimeQuality=100.0;
   if(InpUseRegimeDetection){
      DetectMarketRegime(sA);
      if(g_regime.regime=="TRENDING") pi.regimeQuality=20.0;
      else if(g_regime.regime=="HIGH_VOL") pi.regimeQuality=35.0;
      else if(g_regime.regime=="LOW_VOL") pi.regimeQuality=80.0;
   }
   pi.portfolioQuality=100.0; string portfolioReason="";
   bool portfolioOK=PortfolioPairCompatible(sA,sB,pi.portfolioQuality,portfolioReason);
   if(!portfolioOK) pi.reason=portfolioReason;
   double edgeQuality=Clamp100(pi.edgeCost/MathMax(InpIntelligenceEdgeCap,1.0)*100.0);
   double historyQuality=PairHistoryQuality(sA,sB);
   pi.score=0.22*edgeQuality+0.17*pi.zQuality+0.14*pi.corrQuality+0.11*pi.halfLifeQuality+0.07*pi.hurstQuality+0.07*pi.betaQuality+0.08*pi.executionQuality+0.04*pi.regimeQuality+0.03*pi.portfolioQuality+0.07*historyQuality;
   if(InpUsePortfolioRisk && pi.portfolioQuality<100.0) pi.score*=MathMax(0.25,1.0-InpPortfolioRiskPenalty*(1.0-pi.portfolioQuality/100.0));
   pi.score=Clamp100(pi.score); pi.lotFactor=PairIntelligenceLotFactor(pi.score);
   double minScore=EffectivePairIntelligenceThreshold();
   pi.eligible=(pi.score>=minScore && portfolioOK);
   if(!pi.eligible && pi.reason=="") pi.reason="score "+DoubleToString(pi.score,0)+" < "+DoubleToString(minScore,0);
   return true;
}


// -----------------------------------------------------------------------------
// PAIR INTELLIGENCE — SHADOW QUALITY ENGINE
// Conservative upgrade: this layer is observational by default and does not
// alter entry, sizing or exit decisions unless explicitly integrated later.
// It reuses the existing Intelligence components and adds a clearer separation
// between statistical, economic, execution, portfolio and stability quality.
// -----------------------------------------------------------------------------
bool CalculatePairIntelligenceV2(const string sA,const string sB,const SPairStat &st,
                                 const double expected,const double cost,
                                 SPairIntelligenceV2 &v2){
   ZeroMemory(v2);
   if(!st.valid){ v2.reason="invalid statistics"; return false; }

   SPairIntelligence base;
   if(!CalculatePairIntelligence(sA,sB,st,expected,cost,base)){
      v2.reason="base intelligence unavailable";
      return false;
   }

   v2.statisticalQuality=Clamp100(
      0.32*base.zQuality+
      0.24*base.corrQuality+
      0.18*base.halfLifeQuality+
      0.12*base.hurstQuality+
      0.14*base.betaQuality);

   double edgeCost=(cost>0.0)?expected/cost:0.0;
   double edgeQ=Clamp100(edgeCost/MathMax(InpIntelligenceEdgeCap,1.0)*100.0);
   double netEdge=expected-cost;
   double netQ=(netEdge>0.0)?Clamp100(50.0+netEdge/MathMax(MathAbs(cost),0.01)*50.0):0.0;
   v2.economicQuality=Clamp100(0.65*edgeQ+0.35*netQ);

   v2.executionQuality=base.executionQuality;
   v2.portfolioQuality=base.portfolioQuality;

   double historyQ=PairHistoryQuality(sA,sB);
   double betaStability=base.betaQuality;
   double hlStability=base.halfLifeQuality;
   v2.stabilityQuality=Clamp100(0.45*historyQ+0.35*betaStability+0.20*hlStability);

   v2.score=Clamp100(
      0.32*v2.statisticalQuality+
      0.23*v2.economicQuality+
      0.16*v2.executionQuality+
      0.13*v2.portfolioQuality+
      0.16*v2.stabilityQuality);

   v2.conviction=Clamp100(
      0.55*v2.score+
      0.25*v2.statisticalQuality+
      0.20*v2.economicQuality);

   double threshold=EffectivePairIntelligenceThreshold();
   v2.eligible=(v2.score>=threshold && base.eligible);
   if(v2.eligible) v2.reason="PASS";
   else if(!base.eligible) v2.reason="BASE "+base.reason;
   else v2.reason="Score "+DoubleToString(v2.score,0)+" < "+DoubleToString(threshold,0);
   return true;
}

bool GetPairIntelligence(const string sA,const string sB,SPairIntelligence &pi,const bool force=false){
   ulong now=GetTickCount64();
   for(int i=0;i<ArraySize(g_intel_a);i++) if(g_intel_a[i]==sA&&g_intel_b[i]==sB&&!force&&now-g_intel_t[i]<1500){ pi=g_intel_v[i]; return true; }
   SPairStat st; ResetStat(st); if(!GetStat(sA,sB,st)) return false;
   double va=0,vb=0,cost=0,expected=0,netEdge=0; if(!EstimatePairEconomics(sA,sB,st,cost,expected,netEdge,va,vb)) return false;
   if(!CalculatePairIntelligence(sA,sB,st,expected,cost,pi)) return false;
   int idx=-1; for(int i=0;i<ArraySize(g_intel_a);i++) if(g_intel_a[i]==sA&&g_intel_b[i]==sB){idx=i;break;}
   if(idx<0){ idx=ArraySize(g_intel_a); ArrayResize(g_intel_a,idx+1); ArrayResize(g_intel_b,idx+1); ArrayResize(g_intel_v,idx+1); ArrayResize(g_intel_t,idx+1); g_intel_a[idx]=sA; g_intel_b[idx]=sB; }
   g_intel_v[idx]=pi; g_intel_t[idx]=now; return true;
}

double GetPrevZ(const string a, const string b){
   for(int i=0;i<ArraySize(g_pz_a);i++) if(g_pz_a[i]==a && g_pz_b[i]==b) return g_pz_v[i];
   return 0;
}
void SetPrevZ(const string a, const string b, const double z){
   if(ArraySize(g_pz_a) > 200) {
      ArrayRemove(g_pz_a, 0, 50);
      ArrayRemove(g_pz_b, 0, 50);
      ArrayRemove(g_pz_v, 0, 50);
   }
   for(int i=0;i<ArraySize(g_pz_a);i++) if(g_pz_a[i]==a && g_pz_b[i]==b){ g_pz_v[i]=z; return; }
   int n=ArraySize(g_pz_a);
   ArrayResize(g_pz_a, n+1); ArrayResize(g_pz_b, n+1); ArrayResize(g_pz_v, n+1);
   g_pz_a[n]=a; g_pz_b[n]=b; g_pz_v[n]=z;
}
bool CooldownOK(const string a, const string b){
   if(InpEntryCooldownSec<=0) return true;
   for(int i=0;i<ArraySize(g_cd_a);i++)
      if(g_cd_a[i]==a && g_cd_b[i]==b) return (TimeCurrent()-g_cd_t[i]>=InpEntryCooldownSec);
   return true;
}
void SetCooldown(const string a, const string b){
   datetime now = TimeCurrent();
   int writeIdx = 0;
   for(int i=0; i<ArraySize(g_cd_a); i++){
      if(now - g_cd_t[i] < 86400){
         if(writeIdx != i){
            g_cd_a[writeIdx] = g_cd_a[i];
            g_cd_b[writeIdx] = g_cd_b[i];
            g_cd_t[writeIdx] = g_cd_t[i];
         }
         writeIdx++;
      }
   }
   if(writeIdx < ArraySize(g_cd_a)){
      ArrayResize(g_cd_a, writeIdx);
      ArrayResize(g_cd_b, writeIdx);
      ArrayResize(g_cd_t, writeIdx);
   }
   
   for(int i=0;i<ArraySize(g_cd_a);i++) if(g_cd_a[i]==a && g_cd_b[i]==b){ g_cd_t[i]=now; SaveCooldownState(); return; }
   int n=ArraySize(g_cd_a);
   ArrayResize(g_cd_a, n+1); ArrayResize(g_cd_b, n+1); ArrayResize(g_cd_t, n+1);
   g_cd_a[n]=a; g_cd_b[n]=b; g_cd_t[n]=now; SaveCooldownState();
}
string CooldownFileName(){ return "AP_cooldowns_"+IntegerToString((long)AccountInfoInteger(ACCOUNT_LOGIN))+"_"+IntegerToString(InpMagic)+"_M"+IntegerToString((int)g_mode)+".csv"; }
void SaveCooldownState(){
   if(g_is_tester) return;
   int h=FileOpen(CooldownFileName(),FILE_WRITE|FILE_CSV|FILE_COMMON,','); if(h==INVALID_HANDLE) return;
   FileWrite(h,"A","B","TIME");
   datetime now=TimeCurrent();
   for(int i=0;i<ArraySize(g_cd_a);i++) if(now-g_cd_t[i]<86400) FileWrite(h,g_cd_a[i],g_cd_b[i],(long)g_cd_t[i]);
   FileClose(h);
}
void LoadCooldownState(){
   ArrayResize(g_cd_a,0); ArrayResize(g_cd_b,0); ArrayResize(g_cd_t,0);
   if(g_is_tester) return;
   int h=FileOpen(CooldownFileName(),FILE_READ|FILE_CSV|FILE_COMMON,','); if(h==INVALID_HANDLE) return;
   if(!FileIsEnding(h)){ FileReadString(h); FileReadString(h); FileReadString(h); }
   datetime now=TimeCurrent();
   while(!FileIsEnding(h)){
      string a=FileReadString(h); if(a==""&&FileIsEnding(h)) break; string b=FileReadString(h); datetime t=(datetime)StringToInteger(FileReadString(h));
      if(a==""||b==""||t<=0||now-t>=86400) continue;
      int n=ArraySize(g_cd_a); ArrayResize(g_cd_a,n+1); ArrayResize(g_cd_b,n+1); ArrayResize(g_cd_t,n+1); g_cd_a[n]=a; g_cd_b[n]=b; g_cd_t[n]=t;
   }
   FileClose(h);
}

bool ZCrossOK(const string a, const string b, const double z){
   if(!InpUseZCrossEntry) return MathAbs(z)>=InpStatEntryZScore;
   double pz=GetPrevZ(a, b);
   return (MathAbs(z)>=InpStatEntryZScore && MathAbs(pz)<InpStatEntryZScore);
}

double CalculateTotalCommission(const string s, const double vol){ return InpCommissionPerLot*vol; }
double GetSpreadCost(const string s,const double vol){
   MqlTick t; if(!SymbolInfoTick(s,t)) return 0;
   double tv=SymbolInfoDouble(s,SYMBOL_TRADE_TICK_VALUE_PROFIT); if(tv<=0) tv=SymbolInfoDouble(s,SYMBOL_TRADE_TICK_VALUE);
   double ts=SymbolInfoDouble(s,SYMBOL_TRADE_TICK_SIZE); if(ts<=0) ts=PointOf(s);
   if(tv<=0||ts<=0) return 0;
   double money=(t.ask-t.bid)/ts*tv*vol;
   string pc=SymbolInfoString(s,SYMBOL_CURRENCY_PROFIT); double rate=CurrencyToDepositRate(pc);
   return (rate>0)?money*rate:money;
}
double GetSwapPerLot(const string s, const ENUM_POSITION_TYPE t){
   return (t==POSITION_TYPE_BUY)?SymbolInfoDouble(s, SYMBOL_SWAP_LONG):SymbolInfoDouble(s, SYMBOL_SWAP_SHORT);
}
double AllocationSizingMultiplier(const string sA,const string sB){
   if(!g_alloc_ready) return 1.0;
   for(int i=0;i<ArraySize(g_top_a);i++){
      if(g_top_a[i]==sA && g_top_b[i]==sB){
         if(g_alloc_risk_pct[i]<=0.0) return 0.0;
         return MathMax(0.25,MathMin(1.0,g_alloc_multiplier[i]));
      }
   }
   return 1.0;
}
bool AllocationGateForPair(const string sA,const string sB,string &reason){
   reason="";
   if(!g_alloc_ready) return true;
   for(int i=0;i<ArraySize(g_top_a);i++){
      if(g_top_a[i]==sA && g_top_b[i]==sB){
         if(g_alloc_risk_pct[i]<=0.0){ reason="allocation FILTER"; return false; }
         return true;
      }
   }
   return true;
}

double AutoLotFor(const string sA, const string sB, const double sigmaMoney){
   double base=InpMaxLots*g_lot_scale*((g_strategy==PAIR_STATISTICAL)?1.0:0.7);
   if(!InpAutoLotSize) return NormalizeVolumeSafe(sA, base, base);
   double equity=(g_mode==DEMO_TRADING)?VirtualEquity():AccountInfoDouble(ACCOUNT_EQUITY);
   double instMult=1.0;
   if(InpUseInstitutionalEngine){
      instMult=MathMax(InpMinDynamicRiskMultiplier,MathMin(1.0,g_inst.riskMultiplier));
      if(InpUseQualityWeightedSizing) instMult*=MathMax(InpMinQualitySizingMultiplier,MathMin(1.0,g_inst.qualitySizingMultiplier));
      double allocMult=AllocationSizingMultiplier(sA,sB);
      if(g_alloc_ready && allocMult>0.0) instMult*=allocMult;
   }
   double risk=equity*InpRiskPercentPerTrade*instMult/100.0;
   double lot=base*instMult;
   if(sigmaMoney>0 && InpVolK>0) lot=risk/(InpVolK*sigmaMoney);
   else{
      double tv=SymbolInfoDouble(sA, SYMBOL_TRADE_TICK_VALUE), ts=SymbolInfoDouble(sA, SYMBOL_TRADE_TICK_SIZE), pt=PointOf(sA);
      if(tv>0 && ts>0 && pt>0 && InpAutoSLPoints>0){
         double slm=InpAutoSLPoints*tv*(pt/ts);
         if(slm>0) lot=risk/slm;
      }
   }
   if(lot>base) lot=base;
   return NormalizeVolumeSafe(sA, lot, base);
}
double NotionalPerLotDeposit(const string s){
   MqlTick t; if(!GetTickSafe(s,t)) return 0;
   double px=(t.bid+t.ask)*0.5; if(px<=0) px=t.bid;
   double contract=SymbolInfoDouble(s,SYMBOL_TRADE_CONTRACT_SIZE); if(contract<=0||px<=0) return 0;
   string profitCur=SymbolInfoString(s,SYMBOL_CURRENCY_PROFIT);
   double rate=CurrencyToDepositRate(profitCur); if(rate<=0) rate=1.0;
   return px*contract*rate;
}
double DollarNeutralVolume(const string sA, const string sB, const double lotA, const double beta){
   double nA=NotionalPerLotDeposit(sA), nB=NotionalPerLotDeposit(sB);
   if(nA<=0||nB<=0) return lotA*InpPairLotMultiplier;
   return lotA*(nA/nB)*MathAbs(beta);
}
double HedgeImbalancePct(const string sA,const double volA,const string sB,const double volB,const double beta){
   double nA=NotionalPerLotDeposit(sA)*volA; double nB=NotionalPerLotDeposit(sB)*volB;
   double targetB=nA*MathAbs(beta); if(targetB<=0||nB<=0) return 100.0;
   return MathAbs(nB-targetB)/MathMax(targetB,nB)*100.0;
}

bool EstimatePairEconomics(const string sA,const string sB,const SPairStat &st,
                            double &cost,double &expected,double &netEdge,double &va,double &vb){
   cost=0; expected=0; netEdge=0; va=0; vb=0;
   if(!st.valid) return false;
   va=AutoLotFor(sA,sB,st.sigmaMoney);
   double vbReq=InpUseDollarNeutral?DollarNeutralVolume(sA,sB,va,st.beta):va*InpPairLotMultiplier;
   double vbBase=AutoLotFor(sB,sA,st.sigmaMoney);
   vb=NormalizeVolumeSafe(sB,MathMin(vbBase,vbReq),vbReq);
   va=NormalizeVolumeSafe(sA,va,va);
   if(va<=0||vb<=0) return false;
   cost=2.0*(CalculateTotalCommission(sA,va)+CalculateTotalCommission(sB,vb))
       +2.0*(GetSpreadCost(sA,va)+GetSpreadCost(sB,vb));
   if(InpUseSwapInCost){
      double days=MathMax(st.halfLife,1.0)*PeriodSeconds(PERIOD_CURRENT)/86400.0;
      ENUM_POSITION_TYPE dA=(st.z>0)?POSITION_TYPE_SELL:POSITION_TYPE_BUY;
      ENUM_POSITION_TYPE dB=(st.z>0)?POSITION_TYPE_BUY:POSITION_TYPE_SELL;
      cost+=MathAbs(GetSwapPerLot(sA,dA))*va*days+MathAbs(GetSwapPerLot(sB,dB))*vb*days;
   }
   expected=(MathAbs(st.z)-InpStatExitZScore)*st.sigmaMoney*MathMin(va,MathMax(vb,1e-12));
   netEdge=expected-cost;
   return true;
}

bool FindOurPos(const string s, ulong &ticket, double &vol){
   for(int i=PositionsTotal()-1;i>=0;i--){
      ulong t=0;
      if(!SelectOurPosition(i, t)) continue;
      if(PositionGetString(POSITION_SYMBOL)==s){ ticket=t; vol=PositionGetDouble(POSITION_VOLUME); return true; }
   }
   return false;
}
bool SendOrderWithRetry(const string s,const ENUM_ORDER_TYPE type,const double vol,ulong &ticket,double &fill,double &actualVol){
   ticket=0; fill=0; actualVol=0;
   for(int r=0;r<MathMax(1,InpOrderRetryCount);r++){
      if(!MarginOKFor(s,vol,type)) return false;
      bool sent=(type==ORDER_TYPE_BUY)?Trade.Buy(vol,s,0,0,0,g_cycle_comment):Trade.Sell(vol,s,0,0,0,g_cycle_comment);
      uint rc=Trade.ResultRetcode();
      if(sent && (rc==TRADE_RETCODE_DONE || rc==TRADE_RETCODE_DONE_PARTIAL || rc==TRADE_RETCODE_PLACED)){
         for(int w=0;w<20;w++){
            if(FindPositionForLeg(s,g_cycle_comment,ticket,actualVol,fill)) break;
            Sleep(25);
         }
         if(ticket==0){ Print("ORDER OK but position not found: ",s," rc=",rc); return false; }
         if(actualVol+1e-12<vol){
            Print("PARTIAL FILL: ",s," req=",vol," got=",actualVol," - flattening leg");
            Trade.PositionClose(ticket); return false;
         }
         return true;
      }
      if(InpVerboseLog) Print("Order retry ",r," ",s," rc=",rc," ",Trade.ResultRetcodeDescription());
      if(r+1<InpOrderRetryCount) Sleep(MathMax(10,InpOrderRetryDelayMs));
   }
   return false;
}
bool SendLimitOrderWithTimeout(const string s,const ENUM_ORDER_TYPE type,const double vol,ulong &ticket,double &fill,double &actualVol){
   ticket=0; fill=0; actualVol=0;
   MqlTick t; if(!GetTickSafe(s,t)) return false;
   double price=(type==ORDER_TYPE_BUY)?t.ask-InpLimitOffsetPoints*PointOf(s):t.bid+InpLimitOffsetPoints*PointOf(s);
   bool sent=(type==ORDER_TYPE_BUY)?Trade.BuyLimit(vol,price,s,0,0,ORDER_TIME_GTC,0,g_cycle_comment):Trade.SellLimit(vol,price,s,0,0,ORDER_TIME_GTC,0,g_cycle_comment);
   if(!sent){ Print("Limit order failed ",s," rc=",Trade.ResultRetcode()); return false; }
   ulong orderTicket=Trade.ResultOrder(); ulong startMs=GetTickCount64(); ulong timeoutMs=(ulong)MathMax(1,InpLimitWaitMs);
   while(GetTickCount64()-startMs<timeoutMs){
      if(FindPositionForLeg(s,g_cycle_comment,ticket,actualVol,fill)){
         if(actualVol+1e-12<vol){ Trade.PositionClose(ticket); return false; }
         return true;
      }
      if(OrderSelect(orderTicket)){
         ENUM_ORDER_STATE state=(ENUM_ORDER_STATE)OrderGetInteger(ORDER_STATE);
         if(state==ORDER_STATE_CANCELED||state==ORDER_STATE_REJECTED||state==ORDER_STATE_EXPIRED) return false;
      }
      Sleep(50);
   }
   if(orderTicket>0) Trade.OrderDelete(orderTicket);
   return false;
}
bool SmartExecuteLeg(const string s,const ENUM_ORDER_TYPE type,const double vol,STradeLeg &leg,const double expectPrice,double currentZ){
   ZeroMemory(leg); leg.symbol=s; leg.type=type; leg.volume=vol; leg.openTime=TimeCurrent();
   AuditEvent("LEG_START",s,((type==ORDER_TYPE_BUY)?"BUY":"SELL"),currentZ,vol);
   MqlTick t; if(!GetTickSafe(s,t)){ RegisterExecutionFailure(s,"no tick"); return false; }
   leg.openPrice=(type==ORDER_TYPE_BUY)?t.ask:t.bid; leg.spread=GetSpreadCost(s,vol); leg.commission=CalculateTotalCommission(s,vol);
   if(g_mode==DEMO_TRADING){ leg.ticket=1; return true; }
   double currentSlippage=CurrentDynamicSlippagePoints(currentZ);
   Trade.SetDeviationInPoints((int)MathMax(0.0,currentSlippage));
   double fill=0,av=0; ulong tk=0; bool success=false;
   if(InpUseLimitOrders){ success=SendLimitOrderWithTimeout(s,type,vol,tk,fill,av); if(!success) success=SendOrderWithRetry(s,type,vol,tk,fill,av); }
   else success=SendOrderWithRetry(s,type,vol,tk,fill,av);
   Trade.SetDeviationInPoints((int)InpMaxSlippagePoints);
   if(!success){ RegisterExecutionFailure(s,"order execution failed"); return false; }
   leg.ticket=tk; leg.volume=av;
   AuditEvent("LEG_FILLED",s,"ticket="+IntegerToString((long)tk),currentZ,av);
   if(fill>0){
      double sl=MathAbs(fill-leg.openPrice)/MathMax(PointOf(s),1e-12); leg.openPrice=fill; g_daily_slippage+=sl;
      if(MathAbs(fill-expectPrice)/MathMax(PointOf(s),1e-12)>InpMaxLegDriftPoints){
         Print("LEG DRIFT exceeded ",s); AuditEvent("LEG_ABORT",s,"leg drift",currentZ,sl); Trade.PositionClose(tk); RegisterExecutionFailure(s,"leg drift"); return false;
      }
   }
   return true;
}
bool TryPyramid(const int cycleIdx,const double currentZ){
   if(!InpUsePyramiding || cycleIdx<0 || cycleIdx>=ArraySize(g_cycles)) return false;
   if(g_cycles[cycleIdx].pyramidLevel>=InpMaxPyramidLevels || ArraySize(g_cycles[cycleIdx].legs)<2) return false;
   double threshold=MathAbs(g_cycles[cycleIdx].entryZ)+(g_cycles[cycleIdx].pyramidLevel+1)*InpPyramidStepZ;
   if(MathAbs(currentZ)<threshold) return false;
   string sA=g_cycles[cycleIdx].symA,sB=g_cycles[cycleIdx].symB;
   double volA=g_cycles[cycleIdx].legs[0].volume*0.5, volB=g_cycles[cycleIdx].legs[1].volume*0.5;
   volA=NormalizeVolumeSafe(sA,volA,volA); volB=NormalizeVolumeSafe(sB,volB,volB); if(volA<=0||volB<=0) return false;
   STradeLeg legA,legB; bool ok1=false,ok2=false; string prev=g_cycle_comment; g_cycle_comment=g_cycles[cycleIdx].comment;
   string why="";
   if(currentZ>0){
      ok1=SmartExecuteLeg(sA,ORDER_TYPE_SELL,volA,legA,SymbolInfoDouble(sA,SYMBOL_BID),currentZ);
      if(ok1 && !FirstLegRiskOK(legA)) why="pyramid legging loss";
      if(ok1 && why=="" && !SecondLegEntryStillValid(sA,sB,currentZ,why)){}
      if(ok1 && why=="") ok2=SmartExecuteLeg(sB,ORDER_TYPE_BUY,volB,legB,SymbolInfoDouble(sB,SYMBOL_ASK),currentZ);
   } else {
      ok1=SmartExecuteLeg(sA,ORDER_TYPE_BUY,volA,legA,SymbolInfoDouble(sA,SYMBOL_ASK),currentZ);
      if(ok1 && !FirstLegRiskOK(legA)) why="pyramid legging loss";
      if(ok1 && why=="" && !SecondLegEntryStillValid(sA,sB,currentZ,why)){}
      if(ok1 && why=="") ok2=SmartExecuteLeg(sB,ORDER_TYPE_SELL,volB,legB,SymbolInfoDouble(sB,SYMBOL_BID),currentZ);
   }
   g_cycle_comment=prev;
   if(ok1 && ok2 && InpMaxHedgeImbalancePct>0){
      double imb=HedgeImbalancePct(sA,legA.volume,sB,legB.volume,g_cycles[cycleIdx].beta);
      if(imb>InpMaxHedgeImbalancePct){ why="pyramid hedge imbalance "+DoubleToString(imb,1)+"%"; ok2=false; }
   }
   if(!(ok1&&ok2)){
      if(why=="") why="pyramid second leg execution failed";
      AuditEvent("PYRAMID_ABORT",sA+"/"+sB,why,currentZ);
      if(g_mode==LIVE_TRADING){ if(legA.ticket>0) Trade.PositionClose(legA.ticket); if(legB.ticket>0) Trade.PositionClose(legB.ticket); }
      return false;
   }
   int nLegs=ArraySize(g_cycles[cycleIdx].legs); if(nLegs+2>STATE_MAX_LEGS){ if(g_mode==LIVE_TRADING){ if(legA.ticket>0) Trade.PositionClose(legA.ticket); if(legB.ticket>0) Trade.PositionClose(legB.ticket); } return false; }
   ArrayResize(g_cycles[cycleIdx].legs,nLegs+2); g_cycles[cycleIdx].legs[nLegs]=legA; g_cycles[cycleIdx].legs[nLegs+1]=legB;
   g_cycles[cycleIdx].pyramidLevel++; g_cycles[cycleIdx].totalCommission+=legA.commission+legB.commission; if(g_mode==DEMO_TRADING) g_daily_commission+=legA.commission+legB.commission;
   SaveDailyState(); SaveCyclesState();
   Notify("PYRAMID LEVEL "+IntegerToString(g_cycles[cycleIdx].pyramidLevel)+" | "+sA+"/"+sB+" | Z="+DoubleToString(currentZ,2));
   return true;
}

bool CloseAllOurPositions(){
   bool ok=true; ulong t;
   for(int i=PositionsTotal()-1;i>=0;i--){
      if(!SelectOurPosition(i, t)) continue;
      if(!Trade.PositionClose(t)) ok=false;
   }
   return ok;
}
bool CloseByComment(const string comment){
   bool ok=true; ulong t;
   for(int i=PositionsTotal()-1;i>=0;i--){
      if(!SelectOurPosition(i, t)) continue;
      if(StringFind(PositionGetString(POSITION_COMMENT), comment)<0) continue;
      if(!Trade.PositionClose(t)){ ok=false; RegisterExecutionFailure(PositionGetString(POSITION_SYMBOL),"close-by-comment failed"); }
   }
   return ok;
}
bool FlattenCycleLegs(const SArbitrageCycle &cy){
   bool ok=true;
   if(g_mode!=LIVE_TRADING) return true;
   for(int k=ArraySize(cy.legs)-1;k>=0;k--){
      if(cy.legs[k].ticket>0 && PositionSelectByTicket(cy.legs[k].ticket)){
         if(!Trade.PositionClose(cy.legs[k].ticket)){ ok=false; RegisterExecutionFailure(cy.legs[k].symbol,"flatten failed"); }
      }
   }
   if(!ok) Print("WARNING: failed to flatten one or more cycle legs: ",cy.comment);
   return ok;
}

bool PartialCloseLeg(const int cycleIdx,const int legIdx,const double delta){
   if(cycleIdx<0||cycleIdx>=ArraySize(g_cycles)||legIdx<0||legIdx>=ArraySize(g_cycles[cycleIdx].legs)||delta<=0) return false;
   string legSymbol=g_cycles[cycleIdx].legs[legIdx].symbol;
   ulong legTicket=g_cycles[cycleIdx].legs[legIdx].ticket;
   double legVolume=g_cycles[cycleIdx].legs[legIdx].volume;
   double step=SymbolInfoDouble(legSymbol,SYMBOL_VOLUME_STEP), mn=SymbolInfoDouble(legSymbol,SYMBOL_VOLUME_MIN); if(step<=0||mn<=0) return false;
   double normDelta=MathFloor(delta/step+1e-9)*step; int dg=(step>=1)?0:(step>=0.1)?1:(step>=0.01)?2:(step>=0.001)?3:4; normDelta=NormalizeDouble(normDelta,dg);
   double remain=NormalizeDouble(legVolume-normDelta,dg); if(normDelta<step||remain<mn) return false;
   double realized=0;
   if(g_mode==DEMO_TRADING){
      STradeLeg part=g_cycles[cycleIdx].legs[legIdx]; part.volume=normDelta; realized=DemoLegPnL(part); g_cycles[cycleIdx].legs[legIdx].volume=remain; g_cycles[cycleIdx].realizedPnl+=realized; return true;
   }
   if(!PositionSelectByTicket(legTicket)) return false;
   double before=PositionGetDouble(POSITION_PROFIT)+PositionGetDouble(POSITION_SWAP);
   if(!Trade.PositionClosePartial(legTicket,normDelta)) return false;
   g_cycles[cycleIdx].legs[legIdx].volume=remain;
   g_cycles[cycleIdx].realizedPnl+=before*(normDelta/MathMax(legVolume,1e-12));
   return true;
}

double QuoteToDeposit(const string s, const double amount){
   return amount*CurrencyToDepositRate(SymbolInfoString(s, SYMBOL_CURRENCY_PROFIT));
}
double DemoLegPnL(const STradeLeg &leg){
   MqlTick t;
   if(!GetTickSafe(leg.symbol, t)) return 0;
   double contract=SymbolInfoDouble(leg.symbol, SYMBOL_TRADE_CONTRACT_SIZE);
   double diff=(leg.type==ORDER_TYPE_BUY)?t.bid-leg.openPrice:leg.openPrice-t.ask;
   return QuoteToDeposit(leg.symbol, diff*contract*leg.volume);
}
double CyclePnL(const int idx){
   if(idx<0||idx>=ArraySize(g_cycles)) return 0;
   double total=g_cycles[idx].realizedPnl;
   for(int k=0;k<ArraySize(g_cycles[idx].legs);k++){
      if(g_mode==DEMO_TRADING) total+=DemoLegPnL(g_cycles[idx].legs[k]);
      else if(g_cycles[idx].legs[k].ticket>0 && PositionSelectByTicket(g_cycles[idx].legs[k].ticket)) total+=PositionGetDouble(POSITION_PROFIT)+PositionGetDouble(POSITION_SWAP);
   }
   return total-g_cycles[idx].totalCommission;
}
void RefreshTodayDealCosts(){
   if(g_is_tester||g_mode!=LIVE_TRADING) return;
   if(g_day_start<=0) return;
   datetime now=TimeCurrent();
   if(InpHistoryRefreshSec>0 && g_last_history_refresh>0 && (now-g_last_history_refresh)<InpHistoryRefreshSec) return;
   if(!HistorySelect(g_day_start,now+60)) return;
   double comm=0,swap=0,fees=0;
   int total=HistoryDealsTotal();
   for(int i=0;i<total;i++){
      ulong deal=HistoryDealGetTicket(i); if(deal==0) continue;
      if((long)HistoryDealGetInteger(deal,DEAL_MAGIC)!=InpMagic) continue;
      comm+=MathAbs(HistoryDealGetDouble(deal,DEAL_COMMISSION));
      swap+=HistoryDealGetDouble(deal,DEAL_SWAP);
      fees+=MathAbs(HistoryDealGetDouble(deal,DEAL_FEE));
   }
   g_daily_commission=comm; g_daily_swap=swap; g_daily_fees=fees; g_last_history_refresh=now;
}

double HistoryCycleNetPnL(const SArbitrageCycle &cy,bool &found){
   found=false;
   if(g_is_tester||g_mode!=LIVE_TRADING||!InpUseHistoryRealizedPnL) return 0.0;
   datetime from=cy.startTime-10;
   if(from<0) from=0;
   if(!HistorySelect(from,TimeCurrent()+60)) return 0.0;
   double net=0.0;
   bool hasExit=false;
   int total=HistoryDealsTotal();
   for(int i=0;i<total;i++){
      ulong deal=HistoryDealGetTicket(i); if(deal==0) continue;
      if((long)HistoryDealGetInteger(deal,DEAL_MAGIC)!=InpMagic) continue;
      string sym=HistoryDealGetString(deal,DEAL_SYMBOL);
      if(sym!=cy.symA && sym!=cy.symB) continue;
      datetime dt=(datetime)HistoryDealGetInteger(deal,DEAL_TIME);
      if(dt<cy.startTime-10) continue;
      string comment=HistoryDealGetString(deal,DEAL_COMMENT);
      if(StringFind(comment,cy.comment)<0) continue;
      ENUM_DEAL_ENTRY entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal,DEAL_ENTRY);
      if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_OUT_BY || entry==DEAL_ENTRY_INOUT) hasExit=true;
      net+=HistoryDealGetDouble(deal,DEAL_PROFIT);
      net+=HistoryDealGetDouble(deal,DEAL_SWAP);
      net+=HistoryDealGetDouble(deal,DEAL_COMMISSION);
      net+=HistoryDealGetDouble(deal,DEAL_FEE);
   }
   found=hasExit;
   return found?net:0.0;
}

double TotalUnrealizedPnL(){
   double t=0;
   for(int i=0;i<ArraySize(g_cycles);i++) t+=CyclePnL(i);
   return t;
}

string AuditFileName(){
   return "AP_audit_"+IntegerToString((long)AccountInfoInteger(ACCOUNT_LOGIN))+"_"+IntegerToString(InpMagic)+".csv";
}
void AuditEvent(const string phase,const string pair,const string reason,const double z=0.0,const double value=0.0){
   g_last_audit_phase=phase;
   g_last_audit_reason=reason;
   if(!InpEnableExecutionAudit) return;
   if(g_is_tester && !InpTesterWriteAudit) return;
   int h=FileOpen(AuditFileName(),FILE_READ|FILE_WRITE|FILE_CSV|FILE_COMMON,',');
   if(h==INVALID_HANDLE) return;
   if(FileSize(h)==0)
      FileWrite(h,"Time","Phase","Mode","Strategy","Pair","Reason","Z","Value","OpenCycles","Positions","Status");
   FileSeek(h,0,SEEK_END);
   FileWrite(h,TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS),phase,ModeText(),StrategyText(),pair,reason,
             DoubleToString(z,4),DoubleToString(value,4),IntegerToString(OpenCyclesCount()),
             IntegerToString(OurPositions()),g_status);
   FileClose(h);
}
void AuditSignalBlock(const string pair,const string reason,const double z=0.0){
   g_last_block=reason;
   if(!InpAuditEverySignalBlock) return;
   string key=pair+"|"+reason;
   datetime now=TimeCurrent();
   if(key==g_last_audit_key && (now-g_last_audit_write)<10) return;
   g_last_audit_key=key; g_last_audit_write=now;
   AuditEvent("SIGNAL_BLOCK",pair,reason,z,0.0);
}
bool ExecutionCircuitActive(){
   if(!InpUseExecutionCircuitBreaker) return false;
   if(g_circuit_pause_until<=0) return false;
   if(TimeCurrent()>=g_circuit_pause_until){
      if(InpAutoResumeCircuit){
         g_circuit_pause_until=0;
         g_abort_window_count=0;
         g_abort_window_start=0;
         SaveDailyState();
         AuditEvent("CIRCUIT_RESUME","*","execution circuit auto-resumed");
      }
      return false;
   }
   return true;
}
void RegisterExecutionFailure(const string pair,const string reason){
   g_execution_failures_today++;
   SaveDailyState();
   AuditEvent("EXECUTION_FAIL",pair,reason);
}
void RegisterReconcileFailure(const string pair,const string reason){
   g_reconcile_failures_today++;
   AuditEvent("RECONCILE_FAIL",pair,reason);
   if(g_reconcile_failures_today>=InpMaxReconcileFailuresDay && InpMaxReconcileFailuresDay>0){
      g_daily_stop=true;
      if(g_stop_reason=="") g_stop_reason="RECONCILE FAILURES";
      Notify("RECONCILE FAILURE STOP | "+pair);
   }
   SaveDailyState();
}
void UpdateAbortCircuit(const string pair,const string reason){
   if(!InpUseExecutionCircuitBreaker || InpMaxAbortsInWindow<=0) return;
   datetime now=TimeCurrent();
   if(g_abort_window_start<=0 || (now-g_abort_window_start)>InpAbortWindowSec){
      g_abort_window_start=now;
      g_abort_window_count=0;
   }
   g_abort_window_count++;
   if(g_abort_window_count>=InpMaxAbortsInWindow){
      g_circuit_pause_until=now+MathMax(1,InpCircuitPauseSec);
      AuditEvent("CIRCUIT_TRIP",pair,reason,(double)g_abort_window_count,(double)InpCircuitPauseSec);
      Notify("EXECUTION CIRCUIT BREAKER | "+pair+" | "+reason+" | pause "+IntegerToString(InpCircuitPauseSec)+"s");
      SaveDailyState();
   }
}

int ResearchScoreBucket(const double score){
   int b=(int)MathFloor(Clamp100(score)/10.0);
   if(b<0) b=0; if(b>9) b=9;
   return b;
}
string ResearchFileName(){
   string tag=(g_strategy==MULTI_STATISTICAL?"MULTI":"PAIR");
   string mode=(g_is_tester?"TEST":"RUN");
   return "AP_research_"+IntegerToString((long)AccountInfoInteger(ACCOUNT_LOGIN))+"_"+IntegerToString(InpMagic)+"_"+IntegerToString((int)Period())+"_"+tag+"_"+mode+".csv";
}
bool ResearchFileAllowed(){
   if(!InpUseResearchLedger) return false;
   if(g_is_tester && !InpResearchWriteTester) return false;
   return true;
}
void ResearchWriteRow(const string event,const SArbitrageCycle &cy,const double pnl,const string reason){
   if(!ResearchFileAllowed()) return;
   int h=FileOpen(ResearchFileName(),FILE_READ|FILE_WRITE|FILE_CSV|FILE_COMMON,',');
   if(h==INVALID_HANDLE) return;
   if(FileSize(h)==0)
      FileWrite(h,"Time","Event","Cycle","Strategy","Mode","Pair","Score","LotFactor","EdgeCost","Edge","Cost","NetEdge",
                "EntryZ","Corr","HalfLife","Hurst","Beta","ZQ","CorrQ","HLQ","HurstQ","BetaQ","ExecQ","RegimeQ","PortfolioQ","EntryExpected","EntryCost",
                "PnL","MFE","MAE","MinAbsZ","MaxAbsZ","Reason","Tester");
   FileSeek(h,0,SEEK_END);
   FileWrite(h,TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS),event,IntegerToString((long)cy.cycleId),StrategyText(),ModeText(),
             cy.symA+"/"+cy.symB,DoubleToString(cy.intelScore,4),DoubleToString(cy.intelLotFactor,4),DoubleToString(cy.intelEdgeCost,4),
             DoubleToString(cy.intelEdge,2),DoubleToString(cy.intelCost,2),DoubleToString(cy.intelNetEdge,2),DoubleToString(cy.entryZ,4),
             DoubleToString(cy.entryCorr,4),DoubleToString(cy.halfLifeAtEntry,2),DoubleToString(cy.entryHurst,4),DoubleToString(cy.beta,4),
             DoubleToString(cy.intelZQuality,2),DoubleToString(cy.intelCorrQuality,2),DoubleToString(cy.intelHalfLifeQuality,2),
             DoubleToString(cy.intelHurstQuality,2),DoubleToString(cy.intelBetaQuality,2),DoubleToString(cy.intelExecutionQuality,2),
             DoubleToString(cy.intelRegimeQuality,2),DoubleToString(cy.intelPortfolioQuality,2),
             DoubleToString(cy.entryExpected,2),DoubleToString(cy.entryCost,2),DoubleToString(pnl,2),DoubleToString(cy.maxFavorablePnl,2),
             DoubleToString(cy.maxAdversePnl,2),DoubleToString(cy.minAbsZ,4),DoubleToString(cy.maxAbsZ,4),reason,g_is_tester?"1":"0");
   FileClose(h);
}
void ResearchRegisterClosed(const SArbitrageCycle &cy,const double pnl){
   if(!InpUseResearchLedger) return;
   g_research_trades++;
   if(pnl>0) g_research_wins++;
   g_research_pnl+=pnl;
   g_research_score_sum+=cy.intelScore;
   g_research_score_pnl+=cy.intelScore*pnl;
   g_research_score_sq+=cy.intelScore*cy.intelScore;
   g_research_pnl_sq+=pnl*pnl;
   int b=ResearchScoreBucket(cy.intelScore);
   g_research_bucket_n[b]++;
   g_research_bucket_pnl[b]+=pnl;
   if(pnl>0) g_research_bucket_wins[b]++;
}
void WriteResearchEntry(const SArbitrageCycle &cy){ ResearchWriteRow("ENTRY",cy,0.0,"accepted"); }
void WriteResearchExit(const SArbitrageCycle &cy,const double pnl,const string reason){ ResearchWriteRow("EXIT",cy,pnl,reason); ResearchRegisterClosed(cy,pnl); }
double ResearchScoreOutcomeCorrelation(){
   if(g_research_trades<2) return 0.0;
   double n=g_research_trades;
   double cov=n*g_research_score_pnl-g_research_score_sum*g_research_pnl;
   double vs=n*g_research_score_sq-g_research_score_sum*g_research_score_sum;
   double vp=n*g_research_pnl_sq-g_research_pnl*g_research_pnl;
   if(vs<=1e-9 || vp<=1e-9) return 0.0;
   return cov/MathSqrt(vs*vp);
}
void ResearchSummary(string &text){
   if(!InpUseResearchLedger || g_research_trades<=0){ text="RESEARCH: waiting for closed cycles"; return; }
   double wr=(double)g_research_wins/g_research_trades*100.0;
   double avg=g_research_pnl/g_research_trades;
   double avgScore=g_research_score_sum/g_research_trades;
   double corr=ResearchScoreOutcomeCorrelation();
   text="RESEARCH N="+IntegerToString(g_research_trades)+" | WR="+DoubleToString(wr,1)+"% | Avg PnL="+DoubleToString(avg,2)+" | Score Corr="+DoubleToString(corr,2)+" | Avg Score="+DoubleToString(avgScore,1);
}
void ResearchBucketSummary(string &text){
   int best=-1; double bestAvg=-1.0e100;
   for(int i=0;i<10;i++) if(g_research_bucket_n[i]>=MathMax(1,InpResearchMinSamples/10)){
      double av=g_research_bucket_pnl[i]/g_research_bucket_n[i];
      if(av>bestAvg){bestAvg=av;best=i;}
   }
   if(best<0) text="CALIBRATION: collecting samples";
   else text="CALIBRATION: best score band "+IntegerToString(best*10)+"-"+IntegerToString(best*10+9)+" | avg="+DoubleToString(bestAvg,2);
}

// -----------------------------------------------------------------------------
// WALK-FORWARD VALIDATION
// Persistent train/test validation of the Intelligence Score. The newest
// window is treated as unseen data. A calibrated threshold is promoted only
// after sufficient train samples and positive out-of-sample evidence.
// -----------------------------------------------------------------------------
void ResetWalkForward(){
   g_wf_train_n=0; g_wf_test_n=0;
   g_wf_train_avg=0.0; g_wf_test_avg=0.0;
   g_wf_train_corr=0.0; g_wf_test_corr=0.0;
   g_wf_calibrated_threshold=Clamp100(InpMinPairIntelligenceScore);
   g_wf_effective_threshold=Clamp100(InpMinPairIntelligenceScore);
   g_wf_valid=false; g_wf_gate_pass=false;
   g_wf_test_start=0; g_wf_latest=0;
   g_wf_status="NOT READY";
}

void CalcWFCorr(const double &scores[],const double &pnls[],const int n,double &corr){
   corr=0.0;
   if(n<2) return;
   double sx=0,sy=0,sxx=0,syy=0,sxy=0;
   for(int i=0;i<n;i++){
      sx+=scores[i]; sy+=pnls[i]; sxx+=scores[i]*scores[i]; syy+=pnls[i]*pnls[i]; sxy+=scores[i]*pnls[i];
   }
   double dn=(double)n;
   double cov=dn*sxy-sx*sy;
   double vx=dn*sxx-sx*sx, vy=dn*syy-sy*sy;
   if(vx>1e-9 && vy>1e-9) corr=cov/MathSqrt(vx*vy);
}

void LoadWalkForwardResearch(){
   ResetWalkForward();
   if(!InpUseResearchLedger || !InpUseWalkForwardGuard){ g_wf_status="OFF"; return; }
   int h=FileOpen(ResearchFileName(),FILE_READ|FILE_CSV|FILE_COMMON,',');
   if(h==INVALID_HANDLE){ g_wf_status="WAITING FOR LEDGER"; return; }

   int capacity=256, count=0;
   datetime times[]; double scores[]; double pnls[];
   ArrayResize(times,capacity); ArrayResize(scores,capacity); ArrayResize(pnls,capacity);

   if(!FileIsEnding(h)) for(int k=0;k<34 && !FileIsEnding(h);k++) FileReadString(h);

   while(!FileIsEnding(h)){
      string tm=FileReadString(h); if(tm=="") break;
      string ev=FileReadString(h);
      FileReadString(h); FileReadString(h); FileReadString(h); FileReadString(h);
      double score=StringToDouble(FileReadString(h));
      for(int k=0;k<20 && !FileIsEnding(h);k++) FileReadString(h);
      double pnl=StringToDouble(FileReadString(h));
      for(int k=0;k<6 && !FileIsEnding(h);k++) FileReadString(h);
      datetime tt=StringToTime(tm);
      if(ev!="EXIT" || tt<=0) continue;
      if(count>=capacity){
         int newCap=MathMin(10000,capacity*2);
         if(newCap<=capacity) break;
         capacity=newCap;
         ArrayResize(times,capacity); ArrayResize(scores,capacity); ArrayResize(pnls,capacity);
      }
      times[count]=tt; scores[count]=score; pnls[count]=pnl; count++;
   }
   FileClose(h);

   if(count<InpWalkForwardMinTrainTrades+InpWalkForwardMinTestTrades){
      g_wf_status="INSUFFICIENT SAMPLES ("+IntegerToString(count)+")";
      return;
   }

   datetime latest=times[0];
   for(int i=1;i<count;i++) if(times[i]>latest) latest=times[i];
   g_wf_latest=latest;
   int testDays=MathMax(1,InpWalkForwardTestDays);
   int trainDays=MathMax(1,InpWalkForwardTrainDays);
   g_wf_test_start=latest-testDays*86400;
   datetime trainStart=g_wf_test_start-trainDays*86400;

   double trainScores[]; double trainPnls[]; double testScores[]; double testPnls[];
   ArrayResize(trainScores,count); ArrayResize(trainPnls,count);
   ArrayResize(testScores,count); ArrayResize(testPnls,count);
   int tn=0, vn=0;
   for(int i=0;i<count;i++){
      if(times[i]>=trainStart && times[i]<g_wf_test_start){
         trainScores[tn]=scores[i]; trainPnls[tn]=pnls[i]; tn++;
      } else if(times[i]>=g_wf_test_start && times[i]<=latest){
         testScores[vn]=scores[i]; testPnls[vn]=pnls[i]; vn++;
      }
   }

   g_wf_train_n=tn; g_wf_test_n=vn;
   if(tn>0){ double sum=0; for(int i=0;i<tn;i++) sum+=trainPnls[i]; g_wf_train_avg=sum/tn; }
   if(vn>0){ double sum=0; for(int i=0;i<vn;i++) sum+=testPnls[i]; g_wf_test_avg=sum/vn; }
   CalcWFCorr(trainScores,trainPnls,tn,g_wf_train_corr);
   CalcWFCorr(testScores,testPnls,vn,g_wf_test_corr);

   if(tn<InpWalkForwardMinTrainTrades || vn<InpWalkForwardMinTestTrades){
      g_wf_status="WINDOW INSUFFICIENT T="+IntegerToString(tn)+"/"+IntegerToString(vn);
      return;
   }

   int minBandN=MathMax(3,InpWalkForwardMinTrainTrades/10);
   double bestAvg=-1.0e100; int bestBand=-1;
   for(int b=0;b<10;b++){
      int n=0; double sum=0;
      double lo=b*10.0;
      for(int i=0;i<tn;i++) if(trainScores[i]>=lo && trainScores[i]<lo+10.0){ n++; sum+=trainPnls[i]; }
      if(n>=minBandN){
         double av=sum/n;
         if(av>0.0 && av>bestAvg){ bestAvg=av; bestBand=b; }
      }
   }

   double candidate=Clamp100(InpMinPairIntelligenceScore);
   if(bestBand>=0) candidate=MathMax(candidate,bestBand*10.0);

   int testGateN=0; double testGateSum=0;
   for(int i=0;i<vn;i++) if(testScores[i]>=candidate){ testGateN++; testGateSum+=testPnls[i]; }
   double testGateAvg=(testGateN>0)?testGateSum/testGateN:0.0;

   g_wf_calibrated_threshold=candidate;
   g_wf_gate_pass=(testGateN>=MathMax(3,InpWalkForwardMinTestTrades/2)
                   && testGateAvg>=InpWalkForwardMinTestAvgPnL
                   && g_wf_test_corr>=InpWalkForwardMinTestCorr);
   g_wf_valid=true;
   g_wf_effective_threshold=g_wf_gate_pass && InpWalkForwardCalibrateScore ? candidate : Clamp100(InpMinPairIntelligenceScore);

   if(g_wf_gate_pass)
      g_wf_status="OOS PASS | THR "+DoubleToString(g_wf_effective_threshold,0)+" | N "+IntegerToString(vn);
   else
      g_wf_status="OOS HOLD | BASE "+DoubleToString(InpMinPairIntelligenceScore,0)+" | N "+IntegerToString(vn);
}

void WalkForwardSummary(string &text){
   if(!InpUseWalkForwardGuard){ text="WF: OFF"; return; }
   if(!g_wf_valid){ text="WF: "+g_wf_status; return; }
   text="WF "+(g_wf_gate_pass?"PASS":"HOLD")+" | Train "+IntegerToString(g_wf_train_n)+" | OOS "+IntegerToString(g_wf_test_n)
        +" | OOS Avg "+DoubleToString(g_wf_test_avg,2)+" | Corr "+DoubleToString(g_wf_test_corr,2)
        +" | Thr "+DoubleToString(EffectivePairIntelligenceThreshold(),0);
}

void WriteJournal(const string reason, const double pnl, const SArbitrageCycle &cy){
   if(!InpUseJournal || g_is_tester) return;
   int h=FileOpen(JOURNAL_FILE, FILE_READ|FILE_WRITE|FILE_CSV, ',');
   if(h==INVALID_HANDLE) return;
   if(FileSize(h)==0) FileWrite(h, "Time", "Strategy", "Mode", "Pair", "Commission", "PnL", "Reason", "EntryZ", "Beta", "HL", "Rebals");
   FileSeek(h, 0, SEEK_END);
   FileWrite(h, TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES), StrategyText(), ModeText(),
             cy.symA+"/"+cy.symB, DoubleToString(cy.totalCommission, 2), DoubleToString(pnl, 2), reason,
             DoubleToString(cy.entryZ, 2), DoubleToString(cy.beta, 3), DoubleToString(cy.halfLifeAtEntry, 1),
             IntegerToString(cy.rebalCount));
   FileClose(h);
}
void WriteTCA(const SArbitrageCycle &cy, const double legDelayMs){
   if(!InpUseJournal || g_is_tester) return;
   int h=FileOpen(TCA_FILE, FILE_READ|FILE_WRITE|FILE_CSV, ',');
   if(h==INVALID_HANDLE) return;
   if(FileSize(h)==0) FileWrite(h, "Time", "Cycle", "Pair", "Legs", "Commission", "Spread", "LegDelayMs", "DailySlippage", "EntryAborts", "FailedHedges");
   FileSeek(h, 0, SEEK_END);
   double spr=cy.legs[0].spread+((ArraySize(cy.legs)>1)?cy.legs[1].spread:0);
   FileWrite(h, TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES), IntegerToString((long)cy.cycleId),
             cy.symA+"/"+cy.symB, IntegerToString(ArraySize(cy.legs)),
             DoubleToString(cy.totalCommission, 2), DoubleToString(spr, 2),
             DoubleToString(legDelayMs, 0), DoubleToString(g_daily_slippage, 1), IntegerToString(g_entry_aborts_today), IntegerToString(g_failed_hedges_today));
   FileClose(h);
}
void SaveCyclesState(){
   if(g_is_tester) return;
   int h=FileOpen(StateFileName(),FILE_WRITE|FILE_CSV|FILE_COMMON,','); if(h==INVALID_HANDLE) return;
   FileWrite(h,"ver","mode","id","strat","symA","symB","comment","start","comm","realized","hl","ez","beta","pd","rc","pl","bz","dez","rebalTime","entrySpread","ouTheta","ouMu","ouSigma","ouHL","ouValid","legs");
   for(int i=0;i<ArraySize(g_cycles);i++){
      int n=MathMin(ArraySize(g_cycles[i].legs),STATE_MAX_LEGS);
      FileWrite(h,STATE_VERSION,(int)g_mode,(long)g_cycles[i].cycleId,g_cycles[i].strategy,g_cycles[i].symA,g_cycles[i].symB,g_cycles[i].comment,(long)g_cycles[i].startTime,
                g_cycles[i].totalCommission,g_cycles[i].realizedPnl,g_cycles[i].halfLifeAtEntry,g_cycles[i].entryZ,g_cycles[i].beta,g_cycles[i].partialDone?1:0,g_cycles[i].rebalCount,g_cycles[i].pyramidLevel,g_cycles[i].bestZ,g_cycles[i].dynamicExitZ,(long)g_cycles[i].lastRebalTime,g_cycles[i].entrySpread,
                g_cycles[i].ouParams.theta,g_cycles[i].ouParams.mu,g_cycles[i].ouParams.sigma,g_cycles[i].ouParams.halfLife,g_cycles[i].ouParams.valid?1:0,n);
      for(int k=0;k<n;k++){
         FileWrite(h,"LEG",k,g_cycles[i].legs[k].symbol,(int)g_cycles[i].legs[k].type,g_cycles[i].legs[k].volume,g_cycles[i].legs[k].openPrice,g_cycles[i].legs[k].commission,g_cycles[i].legs[k].spread,(long)g_cycles[i].legs[k].ticket,(long)g_cycles[i].legs[k].openTime);
      }
   }
   FileClose(h);
}
void LoadCyclesState(){
   if(g_is_tester) return;
   int h=FileOpen(StateFileName(),FILE_READ|FILE_CSV|FILE_COMMON,','); if(h==INVALID_HANDLE) return;
   SArbitrageCycle cy; bool inCycle=false; int modeRow=-1; int legCount=0; bool first=true;
   while(!FileIsEnding(h)){
      string tag=FileReadString(h); if(tag=="") break;
      if(first){ first=false; if(tag=="ver") { for(int skip=0;skip<25 && !FileIsEnding(h);skip++) FileReadString(h); continue; } }
      if(tag=="LEG"){
         int legIndex=(int)StringToInteger(FileReadString(h));
         if(!inCycle) { for(int q=0;q<8;q++) FileReadString(h); continue; }
         STradeLeg L; ZeroMemory(L); L.symbol=FileReadString(h); L.type=(ENUM_ORDER_TYPE)StringToInteger(FileReadString(h)); L.volume=StringToDouble(FileReadString(h)); L.openPrice=StringToDouble(FileReadString(h)); L.commission=StringToDouble(FileReadString(h)); L.spread=StringToDouble(FileReadString(h)); L.ticket=(ulong)StringToInteger(FileReadString(h)); L.openTime=(datetime)StringToInteger(FileReadString(h));
         if(legIndex<ArraySize(cy.legs)) cy.legs[legIndex]=L;
         continue;
      }
      // Cycle header row
      modeRow=(int)StringToInteger(FileReadString(h)); cy.cycleId=(ulong)StringToInteger(FileReadString(h));
      if(cy.cycleId>g_cycle_id) g_cycle_id=cy.cycleId;
      cy.strategy=(int)StringToInteger(FileReadString(h)); cy.symA=FileReadString(h); cy.symB=FileReadString(h); cy.comment=FileReadString(h); cy.startTime=(datetime)StringToInteger(FileReadString(h));
      cy.totalCommission=StringToDouble(FileReadString(h)); cy.realizedPnl=StringToDouble(FileReadString(h)); cy.halfLifeAtEntry=StringToDouble(FileReadString(h)); cy.entryZ=StringToDouble(FileReadString(h)); cy.beta=StringToDouble(FileReadString(h));
      cy.partialDone=StringToInteger(FileReadString(h))!=0; cy.rebalCount=(int)StringToInteger(FileReadString(h)); cy.pyramidLevel=(int)StringToInteger(FileReadString(h)); cy.bestZ=StringToDouble(FileReadString(h)); cy.dynamicExitZ=StringToDouble(FileReadString(h));
      cy.lastRebalTime=(datetime)StringToInteger(FileReadString(h)); cy.entrySpread=StringToDouble(FileReadString(h)); cy.ouParams.theta=StringToDouble(FileReadString(h)); cy.ouParams.mu=StringToDouble(FileReadString(h)); cy.ouParams.sigma=StringToDouble(FileReadString(h)); cy.ouParams.halfLife=StringToDouble(FileReadString(h)); cy.ouParams.valid=StringToInteger(FileReadString(h))!=0; legCount=(int)StringToInteger(FileReadString(h));
      ArrayResize(cy.legs,MathMin(legCount,STATE_MAX_LEGS)); for(int z=0;z<ArraySize(cy.legs);z++) ZeroMemory(cy.legs[z]); inCycle=true;
      // Since LEG rows follow immediately, finalize previous cycle when next header appears: handled by a pending-cycle approach below is not possible here.
      // This parser therefore needs a look-ahead-free format; we use FileTell/FileSeek by reading LEG rows in-place.
      for(int k=0;k<ArraySize(cy.legs);k++){
         if(FileIsEnding(h)) break; string ltag=FileReadString(h); if(ltag!="LEG") break;
         FileReadString(h); cy.legs[k].symbol=FileReadString(h); cy.legs[k].type=(ENUM_ORDER_TYPE)StringToInteger(FileReadString(h)); cy.legs[k].volume=StringToDouble(FileReadString(h)); cy.legs[k].openPrice=StringToDouble(FileReadString(h)); cy.legs[k].commission=StringToDouble(FileReadString(h)); cy.legs[k].spread=StringToDouble(FileReadString(h)); cy.legs[k].ticket=(ulong)StringToInteger(FileReadString(h)); cy.legs[k].openTime=(datetime)StringToInteger(FileReadString(h));
      }
      if(modeRow!=(int)g_mode) { inCycle=false; continue; }
      if(ArraySize(cy.legs)<2) continue;
      bool ok=true;
      if(g_mode==LIVE_TRADING && InpReconcilePositions){
         for(int k=0;k<ArraySize(cy.legs);k++){
            if(cy.legs[k].ticket>0 && PositionSelectByTicket(cy.legs[k].ticket)) continue;
            ulong nt=0; double nv=0,np=0; if(FindPositionForLeg(cy.legs[k].symbol,cy.comment,nt,nv,np)){ cy.legs[k].ticket=nt; cy.legs[k].volume=nv; cy.legs[k].openPrice=np; }
            else { ok=false; break; }
         }
      }
      if(ok){ int n=ArraySize(g_cycles); ArrayResize(g_cycles,n+1); g_cycles[n]=cy; }
      else { for(int k=0;k<ArraySize(cy.legs);k++) if(cy.legs[k].ticket>0 && PositionSelectByTicket(cy.legs[k].ticket)) Trade.PositionClose(cy.legs[k].ticket); }
      inCycle=false;
   }
   FileClose(h);
   if(ArraySize(g_cycles)>0) Print("RECOVERED CYCLES: ",ArraySize(g_cycles));
}
void HandleOrphans(){
   if(g_mode!=LIVE_TRADING) return;
   ulong t;
   for(int i=PositionsTotal()-1;i>=0;i--){
      if(!SelectOurPosition(i, t)) continue;
      bool inCycle=false;
      for(int c=0;c<ArraySize(g_cycles) && !inCycle;c++)
         for(int k=0;k<ArraySize(g_cycles[c].legs);k++)
            if(g_cycles[c].legs[k].ticket==t){ inCycle=true; break; }
      if(inCycle) continue;
      if(InpOnRestart==KEEP_MANUAL){ Print("ORPHAN KEPT: ", t); AuditEvent("ORPHAN","*", "kept ticket "+IntegerToString((long)t)); continue; }
      Print("ORPHAN CLOSE: ", t); AuditEvent("ORPHAN","*", "closing ticket "+IntegerToString((long)t)); if(!Trade.PositionClose(t)) RegisterExecutionFailure(PositionGetString(POSITION_SYMBOL),"orphan close failed");
   }
}

bool SpreadMomentumOK(const string sA, const string sB, const double currentZ, const double beta){
   if(!InpUseSpreadMomentum) return true;
   if(beta<=0) return true;
   double a_full[], b_full[];
   if(!GetCloseCached(sA, PERIOD_CURRENT, 5, a_full)) return false;
   if(!GetCloseCached(sB, PERIOD_CURRENT, 5, b_full)) return false;
   double a[]; ArrayResize(a, 4); ArraySetAsSeries(a, true);
   double b[]; ArrayResize(b, 4); ArraySetAsSeries(b, true);
   for(int i = 0; i < 4; i++) {
      a[i] = a_full[i + 1];
      b[i] = b_full[i + 1];
   }
   double sp[4];
   for(int i=0;i<4;i++){
      if(a[i]<=0||b[i]<=0) return false;
      sp[i]=MathLog(a[i])-beta*MathLog(b[i]);
   }
   if(currentZ>0 && sp[0]>sp[1] && sp[1]>sp[2]) return false;
   if(currentZ<0 && sp[0]<sp[1] && sp[1]<sp[2]) return false;
   return true;
}

bool BetaStable(const string sA, const string sB, const double currentBeta){
   if(!InpUseBetaStability) return true;
   if(currentBeta<=0) return false;
   int totalBars=5*InpBetaStabilityBars;
   if(InpBetaStabilityBars<2) return false;
   double a_full[], b_full[];
   if(!GetCloseCached(sA, PERIOD_CURRENT, totalBars + 1, a_full)) return false;
   if(!GetCloseCached(sB, PERIOD_CURRENT, totalBars + 1, b_full)) return false;
   
   double a[]; ArrayResize(a, totalBars); ArraySetAsSeries(a, true);
   double b[]; ArrayResize(b, totalBars); ArraySetAsSeries(b, true);
   for(int i = 0; i < totalBars; i++) {
      a[i] = a_full[i + 1];
      b[i] = b_full[i + 1];
   }
   
   for(int i=0;i<totalBars;i++){
      if(a[i]<=0||b[i]<=0) return true;
      a[i]=MathLog(a[i]); b[i]=MathLog(b[i]);
   }
   double betas[5];
   for(int w=0;w<5;w++){
      int start=w*InpBetaStabilityBars;
      int end=start+InpBetaStabilityBars;
      int n=end-start;
      double sA_win=0, sB_win=0, sAB_win=0, sB2_win=0;
      for(int i=start;i<end;i++){
         sA_win+=a[i]; sB_win+=b[i]; sAB_win+=a[i]*b[i]; sB2_win+=b[i]*b[i];
      }
      double mA=sA_win/n, mB=sB_win/n;
      double cov=(sAB_win-n*mA*mB)/(n-1);
      double varB=(sB2_win-n*mB*mB)/(n-1);
      if(varB<=0) return false;
      betas[w]=cov/varB;
   }
   double mean=0;
   for(int i=0;i<5;i++) mean+=betas[i];
   mean/=5;
   if(MathAbs(mean)<1e-6) return false;
   double var=0;
   for(int i=0;i<5;i++) var+=(betas[i]-mean)*(betas[i]-mean);
   var/=4;
   double cv=MathSqrt(var)/MathAbs(mean);
   if(cv>=InpMaxBetaCV){
      if(InpVerboseLog) Print("BETA UNSTABLE: CV=", DoubleToString(cv, 3), " > ", DoubleToString(InpMaxBetaCV, 2));
      return false;
   }
   return true;
}

void RegisterEntryAbort(const string pair,const string reason,const bool failedHedge=false){
   g_entry_aborts_today++;
   if(failedHedge) g_failed_hedges_today++;
   UpdateAbortCircuit(pair,reason);
   AuditEvent(failedHedge?"FAILED_HEDGE":"ENTRY_ABORT",pair,reason);
   SaveDailyState();
   string msg=(failedHedge?"FAILED HEDGE | ":"ENTRY ABORT | ")+pair+" | "+reason;
   Print(msg); SetStatus(msg); g_last_block=reason;
   if(InpVerboseLog) Notify(msg);
}
bool FirstLegRiskOK(const STradeLeg &leg){
   if(InpMaxLeggingLossMoney<=0||g_mode!=LIVE_TRADING||leg.ticket==0) return true;
   if(!PositionSelectByTicket(leg.ticket)) return false;
   double pnl=PositionGetDouble(POSITION_PROFIT)+PositionGetDouble(POSITION_SWAP);
   return pnl>=-InpMaxLeggingLossMoney;
}
bool SecondLegEntryStillValid(const string sA,const string sB,const double originalZ,string &reason){
   reason=""; MqlTick ta,tb;
   if(!GetTickSafe(sA,ta)||!GetTickSafe(sB,tb)||!QuoteFresh(ta)||!QuoteFresh(tb)){ reason="stale quote before leg2"; return false; }
   if(!AutoSpreadOK(sA)||!AutoSpreadOK(sB)||!SpreadRegimeOK(sA)||!SpreadRegimeOK(sB)){ reason="spread changed before leg2"; return false; }
   if(!InpRecheckBeforeSecondLeg) return true;
   SPairStat now; ResetStat(now); if(!GetStat(sA,sB,now)){ reason="stat unavailable before leg2"; return false; }
   if(originalZ*now.z<=0){ reason="Z changed sign before leg2"; return false; }
   if(MathAbs(now.z-originalZ)>InpMaxSecondLegZDrift){ reason="Z drift "+DoubleToString(MathAbs(now.z-originalZ),2); return false; }
   if(InpUseAdverseSelectionFilter && MathAbs(now.z)>MathAbs(originalZ)+InpMaxAdverseZExpansion){ reason="adverse selection / Z expansion"; return false; }
   double minZ=MathMax(InpStatExitZScore,GetAdaptiveZThreshold()*0.85);
   if(MathAbs(now.z)<minZ){ reason="edge disappeared before leg2"; return false; }
   return true;
}

bool TryEnterPair(const string sA, const string sB, const SPairStat &st, const bool requireCross){
   string pair=sA+"/"+sB;
   // Final pre-trade defense: conditions can change between ScanMarket() and order submission.
   if(g_mode==SIGNALS_ONLY || g_paused || !InpAllowNewEntries) return false;
   if(!SessionOK() || WeekendBlock() || GlobalNewsBlocked()) return false;
   if(OpenCyclesCount()>=InpMaxOpenCycles || !RiskOK("")) return false;
   string finalInstReason="";
   double finalProposedRisk=InpRiskPercentPerTrade*MathMax(0.25,g_inst.riskMultiplier);
   if(!InstPortfolioGate(pair,finalProposedRisk,finalInstReason)) return false;
   if(ExecutionCircuitActive()){ AuditSignalBlock(pair,"execution circuit active",st.z); return false; }
   if(InpUseRegimeDetection){
      DetectMarketRegime(sA);
      if(g_regime.regime=="TRENDING"){
         if(InpVerboseLog) Print("SKIP ", sA, "/", sB, " - TRENDING regime (ADX=", DoubleToString(g_regime.adx, 1), ")");
         return false;
      }
      if(g_regime.regime=="HIGH_VOL"){
         if(InpVerboseLog) Print("SKIP ", sA, "/", sB, " - HIGH_VOL regime (ATR Z=", DoubleToString(g_regime.volatilityZ, 2), ")");
         return false;
      }
   }
   MqlTick preA,preB; if(!GetTickSafe(sA,preA)||!GetTickSafe(sB,preB)||!QuoteFresh(preA)||!QuoteFresh(preB)) return false;
   if(!CheckLiquidity(sA) || !CheckLiquidity(sB)){
      if(InpVerboseLog) Print("SKIP ", sA, "/", sB, " - low liquidity");
      return false;
   }
   if(InpUseLatencyCheck){
      g_current_latency_ms=MeasureLatency(sA);
      g_prev_high_latency=g_high_latency;
      g_high_latency=(g_current_latency_ms>InpMaxLatencyMs);
      if(g_high_latency && !g_prev_high_latency) Print("LATENCY ALERT: ", DoubleToString(g_current_latency_ms, 1), " ms");
      if(g_high_latency){
         if(InpVerboseLog) Print("SKIP ", sA, "/", sB, " - high latency ", DoubleToString(g_current_latency_ms, 1), "ms");
         return false;
      }
   }
   double adaptiveZ=GetAdaptiveZThreshold();
   if(MathAbs(st.z)<adaptiveZ) return false;
   if(requireCross && !ZCrossOK(sA, sB, st.z)) return false;
   if(MathAbs(st.corr)<InpMinPairCorrelation) return false;
   if(st.beta<InpMinBeta || st.beta>InpMaxBeta) return false;
   if(InpUseHalfLifeFilter && (st.halfLife<=0 || st.halfLife>InpMaxHalfLifeBars)) return false;
   if(InpUseHurstFilter && st.hurst>=InpMaxHurst) return false;
   if(!AutoSpreadOK(sA) || !AutoSpreadOK(sB)) return false;
   if(!SpreadRegimeOK(sA) || !SpreadRegimeOK(sB)) return false;
   if(PairNewsBlocked(sA, sB)) return false;
   if(!CooldownOK(sA, sB)) return false;
   if(HasOurSymbol(sA) || HasOurSymbol(sB)) return false;
   if(!SpreadMomentumOK(sA, sB, st.z, st.beta)){
      if(InpVerboseLog) Print("SKIP ", sA, "/", sB, " - spread momentum against us");
      return false;
   }
   if(!BetaStable(sA, sB, st.beta)){
      if(InpVerboseLog) Print("SKIP ", sA, "/", sB, " - unstable beta");
      return false;
   }
   if(!MultiTimeframeConfirmation(sA, sB, st.z, st.beta)){
      if(InpVerboseLog) Print("SKIP ", sA, "/", sB, " - multi-TF disagreement");
      return false;
   }
   SPairIntelligence entryPi; ZeroMemory(entryPi);
   bool entryPiValid=false;
   if(InpUsePairIntelligence){
      double preCost=0,preExpected=0,preNet=0,preVA=0,preVB=0;
      if(!EstimatePairEconomics(sA,sB,st,preCost,preExpected,preNet,preVA,preVB)) return false;
      SPairIntelligence pi;
      if(!CalculatePairIntelligence(sA,sB,st,preExpected,preCost,pi)) return false;
      entryPi=pi; entryPiValid=true;
      g_last_intelligence_score=pi.score; g_last_intelligence_factor=pi.lotFactor; g_last_intelligence_pair=pair; g_last_intelligence_reason=pi.reason;
      if(!pi.eligible){
         AuditSignalBlock(pair,"PAIR INTEL "+pi.reason,st.z);
         if(InpVerboseLog) Print("SKIP ",pair," - PAIR INTEL score=",DoubleToString(pi.score,1)," reason=",pi.reason);
         return false;
      }
   }
   double va=0,vb=0,cost=0,expected=0,netEdge=0;
   if(!EstimatePairEconomics(sA,sB,st,cost,expected,netEdge,va,vb)){
      AuditSignalBlock(pair,"pair economics unavailable",st.z);
      return false;
   }
   if(InpUsePairIntelligence){
      SPairIntelligence pi;
      if(GetPairIntelligence(sA,sB,pi,true)){
         double f=pi.lotFactor;
         va=NormalizeVolumeSafe(sA,va*f,va*f);
         double vbReq=InpUseDollarNeutral?DollarNeutralVolume(sA,sB,va,st.beta):va*InpPairLotMultiplier;
         double vbBase=AutoLotFor(sB,sA,st.sigmaMoney)*f;
         vb=NormalizeVolumeSafe(sB,MathMin(vbBase,vbReq),vbReq);
         cost=2.0*(CalculateTotalCommission(sA,va)+CalculateTotalCommission(sB,vb))
             +2.0*(GetSpreadCost(sA,va)+GetSpreadCost(sB,vb));
         if(InpUseSwapInCost){
            double days=MathMax(st.halfLife,1.0)*PeriodSeconds(PERIOD_CURRENT)/86400.0;
            ENUM_POSITION_TYPE dA=(st.z>0)?POSITION_TYPE_SELL:POSITION_TYPE_BUY;
            ENUM_POSITION_TYPE dB=(st.z>0)?POSITION_TYPE_BUY:POSITION_TYPE_SELL;
            cost+=MathAbs(GetSwapPerLot(sA,dA))*va*days+MathAbs(GetSwapPerLot(sB,dB))*vb*days;
         }
         expected=(MathAbs(st.z)-InpStatExitZScore)*st.sigmaMoney*MathMin(va,MathMax(vb,1e-12));
         netEdge=expected-cost;
         if(va<=0 || vb<=0){ AuditSignalBlock(pair,"pair intelligence lot below broker minimum",st.z); return false; }
      }
   }
   if(expected<InpMinCostCoverage*cost || (InpRequirePositiveNetEdge && netEdge<InpMinNetEdgeMoney)){
      string why="net edge "+DoubleToString(netEdge,2)+" < required";
      if(InpVerboseLog) Print("SKIP ", sA, "/", sB, " expected=",DoubleToString(expected,2)," cost=",DoubleToString(cost,2)," net=",DoubleToString(netEdge,2));
      AuditSignalBlock(pair,why,st.z);
      return false;
   }
   if(!ExposureOKWith(sA, (st.z>0)?-1:1, va, sB, (st.z>0)?1:-1, vb)) return false;
   AuditEvent("ENTRY_ACCEPT",pair,"signal accepted",st.z,netEdge);
   g_cycle_id++;
   g_cycle_comment="AP-CYCLE-"+IntegerToString((long)g_cycle_id);
   SArbitrageCycle cy;
   cy.cycleId=g_cycle_id; cy.startTime=TimeCurrent();
   cy.comment=g_cycle_comment; cy.symA=sA; cy.symB=sB;
   cy.totalCommission=0; cy.realizedPnl=0; cy.halfLifeAtEntry=st.halfLife;
   cy.entryZ=st.z; cy.beta=st.beta;
   cy.bestZ = st.z;
   cy.dynamicExitZ = InpStatExitZScore * (st.z > 0 ? 1 : -1);
   cy.strategy=(int)g_strategy; cy.rebalCount=0;
   cy.partialDone=false; cy.lastRebalTime=0;
   cy.pyramidLevel=0; cy.ouParams=st.ouParams; cy.entrySpread=0;
   cy.intelScore=entryPiValid?entryPi.score:0.0;
   cy.intelLotFactor=entryPiValid?entryPi.lotFactor:1.0;
   cy.intelEdgeCost=entryPiValid?entryPi.edgeCost:0.0;
   cy.intelEdge=entryPiValid?entryPi.edge:expected;
   cy.intelCost=entryPiValid?entryPi.cost:cost;
   cy.intelNetEdge=entryPiValid?(entryPi.edge-entryPi.cost):netEdge;
   cy.intelZQuality=entryPiValid?entryPi.zQuality:0.0;
   cy.intelCorrQuality=entryPiValid?entryPi.corrQuality:0.0;
   cy.intelHalfLifeQuality=entryPiValid?entryPi.halfLifeQuality:0.0;
   cy.intelHurstQuality=entryPiValid?entryPi.hurstQuality:0.0;
   cy.intelBetaQuality=entryPiValid?entryPi.betaQuality:0.0;
   cy.intelExecutionQuality=entryPiValid?entryPi.executionQuality:0.0;
   cy.intelRegimeQuality=entryPiValid?entryPi.regimeQuality:0.0;
   cy.intelPortfolioQuality=entryPiValid?entryPi.portfolioQuality:100.0;
   cy.entryExpected=expected; cy.entryCost=cost;
   cy.entryCorr=st.corr; cy.entryHurst=st.hurst;
   cy.maxFavorablePnl=0.0; cy.maxAdversePnl=0.0; cy.minAbsZ=MathAbs(st.z); cy.maxAbsZ=MathAbs(st.z);
   ArrayResize(cy.legs, 2);
   ulong t0=GetTickCount64();
   double expectB=SymbolInfoDouble(sB, (st.z>0)?SYMBOL_ASK:SYMBOL_BID);
   bool ok1=false, ok2=false;
   Print("ENTRY | ", sA, "/", sB, " Z=", DoubleToString(st.z, 2), " beta=", DoubleToString(st.beta, 3),
         " HL=", DoubleToString(st.halfLife, 1), " lots ", DoubleToString(va, 2), "/", DoubleToString(vb, 2));
   string leg2Reason="";
   if(st.z>0){
      ok1=SmartExecuteLeg(sA, ORDER_TYPE_SELL, va, cy.legs[0], SymbolInfoDouble(sA, SYMBOL_BID), st.z);
      if(ok1 && !FirstLegRiskOK(cy.legs[0])) leg2Reason="legging loss limit";
      if(ok1 && leg2Reason=="" && !SecondLegEntryStillValid(sA,sB,st.z,leg2Reason)){}
      if(ok1 && leg2Reason=="") ok2=SmartExecuteLeg(sB, ORDER_TYPE_BUY, vb, cy.legs[1], expectB, st.z);
   } else {
      ok1=SmartExecuteLeg(sA, ORDER_TYPE_BUY, va, cy.legs[0], SymbolInfoDouble(sA, SYMBOL_ASK), st.z);
      if(ok1 && !FirstLegRiskOK(cy.legs[0])) leg2Reason="legging loss limit";
      if(ok1 && leg2Reason=="" && !SecondLegEntryStillValid(sA,sB,st.z,leg2Reason)){}
      if(ok1 && leg2Reason=="") ok2=SmartExecuteLeg(sB, ORDER_TYPE_SELL, vb, cy.legs[1], expectB, st.z);
   }
   ulong legDelay=GetTickCount64()-t0;
   if(g_mode==LIVE_TRADING && legDelay>(ulong)InpMaxLegDelayMs){
      leg2Reason="leg delay "+IntegerToString((int)legDelay)+"ms"; Print("LEG DELAY exceeded ", legDelay, "ms - flatten"); ok2=false;
   }
   if(ok1 && ok2){
      double imbalance=HedgeImbalancePct(sA,cy.legs[0].volume,sB,cy.legs[1].volume,st.beta);
      if(InpMaxHedgeImbalancePct>0 && imbalance>InpMaxHedgeImbalancePct){
         leg2Reason="hedge imbalance "+DoubleToString(imbalance,1)+"%"; ok2=false;
      }
   }
   if(ok1 && ok2){
      AuditEvent("ENTRY_COMPLETE",pair,"both legs filled",st.z,(double)legDelay);
      for(int k=0;k<2;k++) cy.totalCommission+=cy.legs[k].commission;
      if(g_mode==DEMO_TRADING) g_daily_commission+=cy.totalCommission;
      g_daily_spread+=cy.legs[0].spread+cy.legs[1].spread;
      g_total_cycles_today++;
      SaveDailyState();
      MqlTick tickA, tickB;
      if(GetTickSafe(sA, tickA) && GetTickSafe(sB, tickB)) cy.entrySpread=MathLog(tickA.bid)-cy.beta*MathLog(tickB.bid);
      
      g_z_history[g_z_history_idx] = st.z;
      g_z_history_idx = (g_z_history_idx + 1) % 1000;
      if(g_z_history_count < 1000) g_z_history_count++;
      
      int n=ArraySize(g_cycles); ArrayResize(g_cycles, n+1); g_cycles[n]=cy;
      WriteResearchEntry(cy);
      SaveCyclesState();
      WriteTCA(cy, (double)legDelay);
      
      string msg=" *CYCLE OPENED*\n"
                 + " Pair: *" + sA + "/" + sB + "*\n"
                 + " Z-Score: `" + DoubleToString(st.z, 2) + "`\n"
                 + " Beta: `" + DoubleToString(st.beta, 3) + "`\n"
                 + " Lots: `" + DoubleToString(va, 2) + "` / `" + DoubleToString(vb, 2) + "`\n"
                 + " Est. Cost: `$" + DoubleToString(cost, 2) + "`";
      SetStatus(msg); Notify(msg);
      return true;
   }
   if(ok1 || ok2){
      if(leg2Reason=="") leg2Reason="second leg execution failed";
      AuditEvent("ENTRY_ABORT",pair,leg2Reason,st.z,(double)legDelay);
      RegisterEntryAbort(sA+"/"+sB,leg2Reason,true);
   } else RegisterEntryAbort(sA+"/"+sB,(leg2Reason!=""?leg2Reason:"first leg execution failed"),false);
   if(g_mode==LIVE_TRADING) FlattenCycleLegs(cy);
   return false;
}

void ScanMarket(){
   UpdateTradingDay(); UpdateLotScale(); UpdateAvgSpread();
   if(DailyLossHit() || PeakDrawdownHit()){
      // Keep the risk protection active, but do not keep re-stamping the
      // dashboard with the old DAILY STOP error after a new session starts.
      if(g_status=="" || g_status=="DAILY STOP") SetStatus("BLOCK: daily risk limit");
      if(g_mode==LIVE_TRADING && OurPositions()>0) CloseAllOurPositions();
      return;
   }
   if(!SymbolsReady()){ SetStatus("Waiting quotes"); return; }
   string reason="";
   if(g_mode==SIGNALS_ONLY) reason="SIGNALS_ONLY mode";
   else if(g_paused) reason="paused (TG/UI)";
   else if(!InpAllowNewEntries) reason="new entries disabled";
   else if(ExecutionCircuitActive()) reason="execution circuit breaker";
   else if(!RiskOK("")) reason="risk limits";
   else {
      double proposedRisk=InpRiskPercentPerTrade*g_inst.riskMultiplier;
      string instReason="";
      if(!InstPortfolioGate("",proposedRisk,instReason)) reason=instReason;
   }
   if(reason=="") {
      if(!SessionOK()) reason="outside session";
      else if(WeekendBlock()) reason="weekend block";
      else if(GlobalNewsBlocked()) reason="news window";
      else if(OpenCyclesCount()>=InpMaxOpenCycles) reason="max cycles";
   }
   g_last_block=(reason=="")?"-":reason;
   if(reason!=""){ SetStatus("BLOCK: "+reason); return; }
   if(InpUseShadowMode){
      string shadowPair=(g_live_pair!="")?g_live_pair:"*";
      AuditEvent("SHADOW_SIGNAL",shadowPair,"signal observed; trading disabled by shadow mode",g_live_signal,g_inst.riskMultiplier);
      SetStatus("SHADOW: "+shadowPair+" Z="+DoubleToString(g_live_signal,2));
      return;
   }
   if(g_strategy==ARBITRAGE_SPREAD){ SetStatus("ARBITRAGE SPREAD: ENGINE STAGED / NO ENTRIES"); return; }
   if(g_strategy==PAIR_STATISTICAL){
      SPairStat st; ResetStat(st);
      string allocReason="";
      if(GetStat(InpSymbolA, InpSymbolB, st) && AllocationGateForPair(InpSymbolA,InpSymbolB,allocReason)) TryEnterPair(InpSymbolA, InpSymbolB, st, true);
      else if(allocReason!="") SetStatus("BLOCK: "+allocReason);
   } else {
      double adaptiveZ=GetAdaptiveZThreshold();
      double bestScore=-1; string ba="", bb=""; SPairStat best; ResetStat(best);
      for(int i=0;i<g_mp_count;i++){
         MqlTick ta, tb;
         if(!GetTickSafe(g_mp_a[i], ta) || !GetTickSafe(g_mp_b[i], tb)) continue;
         if(!QuoteFresh(ta) || !QuoteFresh(tb)) continue;
         SPairStat st; ResetStat(st);
         if(!GetStat(g_mp_a[i], g_mp_b[i], st)) continue;
         if(MathAbs(st.z)<adaptiveZ) continue;
         double sc=PairScore(st,0.0,1.0);
         if(InpUsePairIntelligence){
            SPairIntelligence pi;
            if(!GetPairIntelligence(g_mp_a[i],g_mp_b[i],pi,true)) continue;
            sc=pi.score;
            if(sc>bestScore){ g_last_intelligence_score=pi.score; g_last_intelligence_factor=pi.lotFactor; g_last_intelligence_pair=g_mp_a[i]+"/"+g_mp_b[i]; g_last_intelligence_reason=pi.reason; }
            if(!pi.eligible) continue;
         } else {
            double cost=2.0*InpCommissionPerLot*BaseLots()+2.0*(GetSpreadCost(g_mp_a[i], BaseLots())+GetSpreadCost(g_mp_b[i], BaseLots()));
            double expected=(MathAbs(st.z)-InpStatExitZScore)*st.sigmaMoney*BaseLots();
            sc=PairScore(st, expected, MathMax(cost, 0.01));
         }
         if(sc>bestScore){ bestScore=sc; best=st; ba=g_mp_a[i]; bb=g_mp_b[i]; }
      }
      if(ba!="") {
         string allocReason="";
         if(AllocationGateForPair(ba,bb,allocReason)) TryEnterPair(ba, bb, best, true);
         else SetStatus("BLOCK: "+allocReason);
      }
   }
}

void RegisterClosed(const SArbitrageCycle &cy, const double pnl, const string reason){
   AuditEvent("CYCLE_CLOSED",cy.symA+"/"+cy.symB,reason,cy.entryZ,pnl);
   UpdatePairHistory(cy.symA,cy.symB,pnl);
   g_closed_cycles_today++;
   if(pnl>0){ g_closed_profitable_today++; g_consec_losses=0; }
   else { g_losses_today++; g_consec_losses++; }
   if(g_consec_losses>=InpMaxConsecLosses){
      g_daily_stop=true; if(g_stop_reason=="") g_stop_reason="LOSS STREAK";
      SaveDailyState(); Notify(" *CONSEC LOSS STOP*");
   }
   SetCooldown(cy.symA, cy.symB);
   SaveDailyState();
   WriteJournal(reason, pnl, cy);
   WriteResearchExit(cy,pnl,reason);
   if(InpUseWalkForwardGuard && !g_is_tester) LoadWalkForwardResearch();
   if(g_mode==DEMO_TRADING) SaveDemoBalance();
   string msg=" *CYCLE CLOSED*\n"
              + " Pair: *" + cy.symA + "/" + cy.symB + "*\n"
              + " Reason: `" + reason + "`\n"
              + " PnL: `$" + DoubleToString(pnl, 2) + "`\n"
              + " Total Today: `" + IntegerToString(g_closed_cycles_today) + "` trades";
   Print(msg); Notify(msg); SetStatus(msg);
}
void CloseCycleIdx(const int idx, const string reason){
   double pnl=CyclePnL(idx);
   SArbitrageCycle cy=g_cycles[idx];
   if(g_mode==DEMO_TRADING){ g_demo_balance+=pnl; RemoveCycle(idx); RegisterClosed(cy, pnl, reason); }
   else {
      if(FlattenCycleLegs(cy)){
         bool found=false;
         double realized=HistoryCycleNetPnL(cy,found);
         if(found) pnl=realized;
         RemoveCycle(idx); RegisterClosed(cy, pnl, reason);
      }
      else if(InpVerboseLog) Print("Close failed, retry next tick");
   }
}
void CloseAllCycles(const string reason){
   for(int i=ArraySize(g_cycles)-1;i>=0;i--) CloseCycleIdx(i, reason);
}
void MaybePartialTP(const int i,const double znow){
   if(!InpUsePartialTP || i<0 || i>=ArraySize(g_cycles) || g_cycles[i].partialDone) return;
   if(MathAbs(znow)>InpPartialZScore || MathAbs(znow)<=InpStatExitZScore) return;
   bool okA=false,okB=false;
   if(ArraySize(g_cycles[i].legs)>=2){
      for(int k=0;k<2;k++){
         double vol=g_cycles[i].legs[k].volume;
         double stp=SymbolInfoDouble(g_cycles[i].legs[k].symbol,SYMBOL_VOLUME_STEP);
         double mn=SymbolInfoDouble(g_cycles[i].legs[k].symbol,SYMBOL_VOLUME_MIN);
         if(stp<=0||mn<=0) continue;
         double delta=MathFloor(vol/2.0/stp)*stp;
         if(delta<stp||vol-delta<mn) continue;
         bool ok=PartialCloseLeg(i,k,delta);
         if(k==0) okA=ok; else okB=ok;
      }
   }
   if(okA&&okB){
      g_cycles[i].partialDone=true;
      Print("PARTIAL TP | ",g_cycles[i].symA,"/",g_cycles[i].symB," Z=",DoubleToString(znow,2));
      Notify("PARTIAL TP | "+g_cycles[i].symA+"/"+g_cycles[i].symB+" | Z="+DoubleToString(znow,2));
      SaveCyclesState();
   } else if(okA||okB){
      Print("PARTIAL TP: one leg completed; forcing hedge symmetry");
      AuditEvent("PARTIAL_IMBALANCE",g_cycles[i].symA+"/"+g_cycles[i].symB,"one leg partial");
      int other=okA?1:0;
      if(other<ArraySize(g_cycles[i].legs)){
         double v=g_cycles[i].legs[other].volume;
         double step=SymbolInfoDouble(g_cycles[i].legs[other].symbol,SYMBOL_VOLUME_STEP);
         double mn=SymbolInfoDouble(g_cycles[i].legs[other].symbol,SYMBOL_VOLUME_MIN);
         double d=(step>0)?MathFloor(v/2.0/step)*step:0;
         if(d>=step && v-d>=mn && PartialCloseLeg(i,other,d)){
            g_cycles[i].partialDone=true;
            AuditEvent("PARTIAL_COMPLETE",g_cycles[i].symA+"/"+g_cycles[i].symB,"recovered symmetry");
         } else if(g_mode==LIVE_TRADING && InpFlattenOnReconcileFailure){
            CloseCycleIdx(i,"PARTIAL-IMBALANCE");
            return;
         }
      }
      SaveCyclesState();
   }
}

void MaybeRebalance(const int i, const SPairStat &st){
   if(!InpUseRebalance || !st.valid) return;
   if(TimeCurrent()-g_cycles[i].lastRebalTime<60) return;
   if(ArraySize(g_cycles[i].legs)<2) return;
   double eb=g_cycles[i].beta;
   if(MathAbs(eb)<=0) return;
   double drift=MathAbs(st.beta-eb)/MathAbs(eb);
   if(drift<InpRebalanceBetaDrift) return;
   string sA=g_cycles[i].symA, sB=g_cycles[i].symB;
   double va=g_cycles[i].legs[0].volume, vb=g_cycles[i].legs[1].volume;
   double nA=NotionalPerLotDeposit(sA), nB=NotionalPerLotDeposit(sB);
   if(nA<=0||nB<=0) return;
   double k=(nA/nB)*MathAbs(st.beta);
   if(k<=0) return;
   double stA=SymbolInfoDouble(sA, SYMBOL_VOLUME_STEP), stB=SymbolInfoDouble(sB, SYMBOL_VOLUME_STEP);
   double mnA=SymbolInfoDouble(sA, SYMBOL_VOLUME_MIN), mnB=SymbolInfoDouble(sB, SYMBOL_VOLUME_MIN);
   bool done=false;
   if(vb>va*k){
      double delta=MathFloor((vb-va*k)/stB)*stB;
      if(delta>=stB && vb-delta>=mnB) done=PartialCloseLeg(i, 1, delta);
   } else {
      double va_t=vb/k;
      double delta=MathFloor((va-va_t)/stA)*stA;
      if(delta>=stA && va-delta>=mnA) done=PartialCloseLeg(i, 0, delta);
   }
   if(done){
      g_cycles[i].rebalCount++;
      g_cycles[i].lastRebalTime=TimeCurrent();
      Print("REBALANCE | ", sA, "/", sB, " beta ", DoubleToString(eb, 3), "->", DoubleToString(st.beta, 3));
      Notify(" *REBALANCE* | " + sA + "/" + sB + " | Beta " + DoubleToString(eb, 3) + " -> " + DoubleToString(st.beta, 3));
      SaveCyclesState();
   }
}
void ManageOpenCycles(){
   for(int i=ArraySize(g_cycles)-1;i>=0;i--){
      string rp=g_cycles[i].symA+"/"+g_cycles[i].symB;
      if(g_mode!=DEMO_TRADING && InpReconcilePositions){
         int present=0;
         for(int k=0;k<ArraySize(g_cycles[i].legs);k++)
            if(g_cycles[i].legs[k].ticket>0 && PositionSelectByTicket(g_cycles[i].legs[k].ticket)) present++;
         int expectedLegs=ArraySize(g_cycles[i].legs);
         if(present<expectedLegs){
            RegisterReconcileFailure(rp,"cycle leg missing "+IntegerToString(present)+"/"+IntegerToString(expectedLegs));
            if(InpFlattenOnReconcileFailure){
               SArbitrageCycle cyLost=g_cycles[i];
               double lost=CyclePnL(i);
               bool flat=CloseByComment(g_cycles[i].comment);
               AuditEvent("RECONCILE_FLATTEN",rp,flat?"flattened":"flatten failed",0.0,lost);
               if(flat){ RemoveCycle(i); RegisterClosed(cyLost,lost,"RECONCILE"); }
            }
            continue;
         }
         if(expectedLegs<2){
            RegisterReconcileFailure(rp,"cycle has fewer than 2 legs");
            continue;
         }
      }
      double pnl=CyclePnL(i); int held=(int)(TimeCurrent()-g_cycles[i].startTime);
      if(InpResearchTrackMFE_MAE){
         if(pnl>g_cycles[i].maxFavorablePnl) g_cycles[i].maxFavorablePnl=pnl;
         if(pnl<g_cycles[i].maxAdversePnl) g_cycles[i].maxAdversePnl=pnl;
      }
      MqlTick qA,qB;
      bool gotQA=GetTickSafe(g_cycles[i].symA,qA);
      bool gotQB=GetTickSafe(g_cycles[i].symB,qB);
      bool quotesOk=gotQA&&gotQB&&QuoteFresh(qA)&&QuoteFresh(qB);
      if(InpMaxCycleQuoteAgeMs>0){
         long ageA=gotQA?QuoteAgeMs(qA):(long)InpMaxCycleQuoteAgeMs+1;
         long ageB=gotQB?QuoteAgeMs(qB):(long)InpMaxCycleQuoteAgeMs+1;
         long maxAge=MathMax(ageA,ageB);
         if(maxAge>(long)InpMaxCycleQuoteAgeMs){
            AuditEvent("CYCLE_QUOTE_STALE",g_cycles[i].symA+"/"+g_cycles[i].symB,"quote age "+IntegerToString((int)MathMin(maxAge,2147483647)),0.0,(double)maxAge);
            if(g_mode==LIVE_TRADING && InpFlattenOnReconcileFailure){
               CloseCycleIdx(i,"STALE-QUOTE"); continue;
            }
         }
      }
      SPairStat st; ResetStat(st); double znow=0; if(GetStat(g_cycles[i].symA,g_cycles[i].symB,st)) znow=st.z;
      if(InpResearchTrackMFE_MAE && st.valid){
         double az=MathAbs(znow);
         if(az<g_cycles[i].minAbsZ) g_cycles[i].minAbsZ=az;
         if(az>g_cycles[i].maxAbsZ) g_cycles[i].maxAbsZ=az;
      }
      if(InpUseZTrailingExit && st.valid){
         // Trail the convergence: once |Z| improves, protect that achieved level instead of worsening the target.
         double absz=MathAbs(znow), entryAbs=MathAbs(g_cycles[i].entryZ);
         if(absz < MathAbs(g_cycles[i].bestZ)){
            g_cycles[i].bestZ=znow;
            double protectedAbs=MathMax(InpStatExitZScore,absz+InpZTrailingStep);
            protectedAbs=MathMin(protectedAbs,entryAbs);
            g_cycles[i].dynamicExitZ=(g_cycles[i].entryZ>0)?protectedAbs:-protectedAbs;
         }
      }
      if(InpUseTimeBasedScaling && g_cycles[i].halfLifeAtEntry>0 && !g_cycles[i].partialDone){
         double barSec=(double)MathMax(1,PeriodSeconds(PERIOD_CURRENT)); double currentBars=held/barSec;
         if(currentBars>g_cycles[i].halfLifeAtEntry*InpTimeScaleThresholdPct){ MaybePartialTP(i,znow); }
      }
      MaybePartialTP(i,znow);
      if(st.valid && InpUseRebalance && g_cycles[i].rebalCount<InpMaxRebalanceCount) MaybeRebalance(i,st);
      if(st.valid&&InpUsePyramiding) TryPyramid(i,st.z);
      bool dynamicExit=false;
      if(st.valid&&InpUseDynamicExit){ MqlTick ta,tb; double sp=0; if(GetTickSafe(g_cycles[i].symA,ta)&&GetTickSafe(g_cycles[i].symB,tb)) sp=MathLog(ta.bid)-g_cycles[i].beta*MathLog(tb.bid); dynamicExit=ShouldExitDynamically(i,st.ouParams,sp); }
      string reason="";
      if(WeekendBlock()) reason="WEEKEND"; else if(PairNewsSoon(g_cycles[i].symA,g_cycles[i].symB)) reason="NEWS-PRE";
      else{
         double effectiveExitZ=InpUseZTrailingExit?g_cycles[i].dynamicExitZ:((g_cycles[i].entryZ>0)?InpStatExitZScore:-InpStatExitZScore);
         if(InpUseHardZStop&&MathAbs(znow)>=InpHardZStop) reason="Z-STOP";
         else if(InpCycleStopLossMoney>0&&pnl<=-InpCycleStopLossMoney) reason="SL";
         else if(InpCycleTakeProfitMoney>0&&pnl>=InpCycleTakeProfitMoney) reason="TP";
         else if(InpAutoExitEnabled && ((g_cycles[i].entryZ>0&&znow<=effectiveExitZ)||(g_cycles[i].entryZ<0&&znow>=effectiveExitZ))) reason="Z-EXIT";
         if(reason==""&&g_cycles[i].halfLifeAtEntry>0&&held>g_cycles[i].halfLifeAtEntry*PeriodSeconds(PERIOD_CURRENT)*InpMaxHoldHLMultiple) reason="HL-TIME";
         if(reason==""&&InpPairMaxHoldSeconds>0&&held>=InpPairMaxHoldSeconds) reason="TIME";
         if(reason==""&&dynamicExit) reason="DYN-EXIT";
      }
      if(reason!=""){
         AuditEvent("EXIT_SIGNAL",g_cycles[i].symA+"/"+g_cycles[i].symB,reason,znow,pnl);
         CloseCycleIdx(i,reason);
      }
   }
}
// Refresh real-time market data for the strategy selected inside METRICS.
// IMPORTANT: this is a read-only shadow/preview calculation.  It deliberately
// does not assign g_strategy, does not save GV_STRATEGY, and never calls ApplyStrategy().
void RefreshMetricsPreview(const bool force=false){
   ulong now=GetTickCount64();
   if(!force && g_metrics_preview_last_ms>0 && now-g_metrics_preview_last_ms<(ulong)MathMax(500,InpScanIntervalSec*1000)) return;
   g_metrics_preview_last_ms=now;

   g_metrics_live_z=0.0; g_metrics_live_corr=0.0; g_metrics_live_beta=0.0;
   g_metrics_live_hl=0.0; g_metrics_live_hurst=0.0; g_metrics_live_spread=0.0;
   g_metrics_live_score=0.0; g_metrics_live_lot_factor=1.0; g_metrics_live_pair="";
   g_metrics_top_count=0;
   for(int i=0;i<ArraySize(g_metrics_top_a);i++){
      g_metrics_top_a[i]=""; g_metrics_top_b[i]=""; g_metrics_top_z[i]=0.0;
      g_metrics_top_corr[i]=0.0; g_metrics_top_beta[i]=0.0; g_metrics_top_hl[i]=0.0; g_metrics_top_score[i]=0.0;
   }

   if(g_metrics_strategy==PAIR_STATISTICAL){
      SPairStat st; ResetStat(st);
      if(GetStat(InpSymbolA,InpSymbolB,st)){
         g_metrics_live_pair=InpSymbolA+"/"+InpSymbolB;
         g_metrics_live_z=st.z; g_metrics_live_corr=st.corr; g_metrics_live_beta=st.beta;
         g_metrics_live_hl=st.halfLife; g_metrics_live_hurst=st.hurst;
         double ec=0,ee=0,en=0,eva=0,evb=0; SPairIntelligence pi;
         if(InpUsePairIntelligence && EstimatePairEconomics(InpSymbolA,InpSymbolB,st,ec,ee,en,eva,evb) && CalculatePairIntelligence(InpSymbolA,InpSymbolB,st,ee,ec,pi)){
            g_metrics_live_score=pi.score; g_metrics_live_lot_factor=pi.lotFactor;
         }
      }
      g_metrics_top_count=(g_metrics_live_pair!="")?1:0;
      if(g_metrics_top_count>0){
         g_metrics_top_a[0]=InpSymbolA; g_metrics_top_b[0]=InpSymbolB; g_metrics_top_z[0]=MathAbs(g_metrics_live_z);
         g_metrics_top_corr[0]=g_metrics_live_corr; g_metrics_top_beta[0]=g_metrics_live_beta; g_metrics_top_hl[0]=g_metrics_live_hl; g_metrics_top_score[0]=g_metrics_live_score;
      }
      return;
   }

   if(g_metrics_strategy==ARBITRAGE_SPREAD){
      SPairStat st; ResetStat(st);
      if(GetStat(InpSymbolA,InpSymbolB,st)){
         g_metrics_live_pair=InpSymbolA+"/"+InpSymbolB;
         g_metrics_live_z=st.z; g_metrics_live_corr=st.corr; g_metrics_live_beta=st.beta;
         g_metrics_live_hl=st.halfLife; g_metrics_live_hurst=st.hurst;
         MqlTick a,b;
         if(GetTickSafe(InpSymbolA,a) && GetTickSafe(InpSymbolB,b)){
            double pa=(a.bid+a.ask)*0.5, pb=(b.bid+b.ask)*0.5;
            if(pa>0.0 && pb>0.0 && st.beta>0.0) g_metrics_live_spread=ArbSpreadValue(pa,pb,st.beta);
         }
      }
      return;
   }

   // MULTI STATISTICAL: independently scan the configured multi-pair universe.
   // Sort the preview queue by the same intelligence score used by the engine,
   // without touching the live trading strategy.
   string pa[32],pb[32]; double pz[32],ps[32],pc[32],pbe[32],ph[32]; int cnt=0;
   for(int i=0;i<g_mp_count && cnt<ArraySize(pa);i++){
      SPairStat st; ResetStat(st);
      if(!GetStat(g_mp_a[i],g_mp_b[i],st)) continue;
      pa[cnt]=g_mp_a[i]; pb[cnt]=g_mp_b[i]; pz[cnt]=MathAbs(st.z); pc[cnt]=st.corr; pbe[cnt]=st.beta; ph[cnt]=st.halfLife; ps[cnt]=pz[cnt];
      if(InpUsePairIntelligence){
         double ec=0,ee=0,en=0,eva=0,evb=0; SPairIntelligence pi;
         if(EstimatePairEconomics(pa[cnt],pb[cnt],st,ec,ee,en,eva,evb) && CalculatePairIntelligence(pa[cnt],pb[cnt],st,ee,ec,pi)) ps[cnt]=pi.score;
      }
      cnt++;
   }
   for(int i=0;i<cnt-1;i++) for(int j=i+1;j<cnt;j++){
      if(ps[j]>ps[i]+1e-9 || (MathAbs(ps[j]-ps[i])<1e-9 && pz[j]>pz[i])){
         string ts=pa[i]; pa[i]=pa[j]; pa[j]=ts; ts=pb[i]; pb[i]=pb[j]; pb[j]=ts;
         double td=pz[i]; pz[i]=pz[j]; pz[j]=td; td=ps[i]; ps[i]=ps[j]; ps[j]=td;
         td=pc[i]; pc[i]=pc[j]; pc[j]=td; td=pbe[i]; pbe[i]=pbe[j]; pbe[j]=td; td=ph[i]; ph[i]=ph[j]; ph[j]=td;
      }
   }
   int out=MathMin(cnt,ArraySize(g_metrics_top_a));
   g_metrics_top_count=out;
   for(int k=0;k<out;k++){
      g_metrics_top_a[k]=pa[k]; g_metrics_top_b[k]=pb[k]; g_metrics_top_z[k]=pz[k]; g_metrics_top_corr[k]=pc[k]; g_metrics_top_beta[k]=pbe[k]; g_metrics_top_hl[k]=ph[k]; g_metrics_top_score[k]=ps[k];
   }
   if(out>0){
      g_metrics_live_pair=pa[0]+"/"+pb[0]; g_metrics_live_z=(pz[0]>=0.0)?pz[0]:-pz[0];
      g_metrics_live_corr=pc[0]; g_metrics_live_beta=pbe[0]; g_metrics_live_hl=ph[0]; g_metrics_live_score=ps[0];
      SPairStat bst; ResetStat(bst);
      if(GetStat(pa[0],pb[0],bst)){
         g_metrics_live_z=bst.z; g_metrics_live_corr=bst.corr; g_metrics_live_beta=bst.beta; g_metrics_live_hl=bst.halfLife; g_metrics_live_hurst=bst.hurst;
      }
   }
}

void UpdateLiveMetrics(){
   RefreshTodayDealCosts();
   g_live_signal=0; g_live_pair=""; double bestAbs=0;
   int n=(g_strategy==PAIR_STATISTICAL)?1:g_mp_count;
   if(g_strategy==ARBITRAGE_SPREAD){ g_live_signal=0.0; g_live_pair=InpSymbolA+"/"+InpSymbolB; }
   for(int i=0;i<n && g_strategy!=ARBITRAGE_SPREAD;i++){
      string sa=(g_strategy==PAIR_STATISTICAL)?InpSymbolA:g_mp_a[i], sb=(g_strategy==PAIR_STATISTICAL)?InpSymbolB:g_mp_b[i];
      SPairStat st; ResetStat(st); if(!GetStat(sa,sb,st)) continue;
      SetPrevZ(sa,sb,st.z); if(MathAbs(st.z)>bestAbs){ bestAbs=MathAbs(st.z); g_live_signal=st.z; g_live_pair=sa+"/"+sb; }
   }
   if(g_live_pair!="" && (g_signal_birth_pair!=g_live_pair || MathAbs(g_signal_birth_abs_z)<1e-8 || MathAbs(g_live_signal)>g_signal_birth_abs_z*1.15)){
      g_signal_birth_pair=g_live_pair;
      g_signal_birth_abs_z=MathAbs(g_live_signal);
      g_signal_birth_time=TimeCurrent();
   }
   string ca=InpSymbolA,cb=InpSymbolB; if(g_live_pair!=""){ int slash=StringFind(g_live_pair,"/"); if(slash>0){ca=StringSubstr(g_live_pair,0,slash); cb=StringSubstr(g_live_pair,slash+1);} }
   g_total_cost_money=0.0;
   SPairStat liveEco; ResetStat(liveEco);
   if(GetStat(ca,cb,liveEco)){
      if(InpUsePairIntelligence){
         double ic=0,ie=0,in=0,iva=0,ivb=0;
         if(EstimatePairEconomics(ca,cb,liveEco,ic,ie,in,iva,ivb)){
            SPairIntelligence livePi;
            if(CalculatePairIntelligence(ca,cb,liveEco,ie,ic,livePi)){
               g_last_intelligence_score=livePi.score; g_last_intelligence_factor=livePi.lotFactor; g_last_intelligence_pair=ca+"/"+cb; g_last_intelligence_reason=livePi.reason;
            }
         }
      }
      if(InpUsePairIntelligenceV2){
         double v2Cost=0,v2Expected=0,v2Net=0,v2VA=0,v2VB=0;
         if(EstimatePairEconomics(ca,cb,liveEco,v2Cost,v2Expected,v2Net,v2VA,v2VB)){
            SPairIntelligenceV2 v2;
            if(CalculatePairIntelligenceV2(ca,cb,liveEco,v2Expected,v2Cost,v2)){
               g_pi2_score=v2.score; g_pi2_conviction=v2.conviction; g_pi2_pair=ca+"/"+cb; g_pi2_reason=v2.reason;
            }
         }
      } else {
         g_pi2_score=0.0; g_pi2_conviction=0.0; g_pi2_pair=""; g_pi2_reason="OFF";
      }
      double ecoCost=0,ecoExpected=0,ecoNet=0,ecoVA=0,ecoVB=0;
      if(EstimatePairEconomics(ca,cb,liveEco,ecoCost,ecoExpected,ecoNet,ecoVA,ecoVB)) g_total_cost_money=ecoCost;
   }
   if(InpUseVolatilityFilter){
      string volSym=(g_live_pair!="")?ca:InpSymbolA;
      UpdateVolatilityCache(volSym);
   }
   // g_daily_swap is the realized daily swap from RefreshTodayDealCosts().
   // Keep open-position swap separately so the Dashboard does not overwrite historical daily data.
   g_open_swap=0.0;
   for(int c=0;c<ArraySize(g_cycles);c++) for(int k=0;k<ArraySize(g_cycles[c].legs);k++){
      if(g_mode==DEMO_TRADING) continue;
      if(g_cycles[c].legs[k].ticket>0 && PositionSelectByTicket(g_cycles[c].legs[k].ticket))
         g_open_swap+=PositionGetDouble(POSITION_SWAP);
   }
}

void FillTopRow(const int k, const string sa, const string sb, const SPairStat &st){
   g_top_a[k]=sa; g_top_b[k]=sb;
   g_top_z[k]=MathAbs(st.z); g_top_corr[k]=st.corr;
   g_top_beta[k]=st.beta; g_top_hl[k]=st.halfLife;
   g_top_score[k]=0.0; g_top_edge[k]=0.0;
   if(InpUsePairIntelligence){
      double ec=0,ee=0,en=0,eva=0,evb=0; SPairIntelligence pi;
      if(EstimatePairEconomics(sa,sb,st,ec,ee,en,eva,evb)){
         g_top_edge[k]=en;
         if(CalculatePairIntelligence(sa,sb,st,ee,ec,pi)) g_top_score[k]=pi.score;
      }
   }
   MqlTick ta, tb; bool fa=false, fb=false;
   if(GetTickSafe(sa, ta)){ g_top_sprA[k]=(ta.ask-ta.bid)/PointOf(sa); fa=QuoteFresh(ta); }
   if(GetTickSafe(sb, tb)){ g_top_sprB[k]=(tb.ask-tb.bid)/PointOf(sb); fb=QuoteFresh(tb); }
   g_top_fresh[k]=(fa && fb);
}
void UpdateTopPairs(){
   for(int k=0;k<ArraySize(g_top_a);k++){
      g_top_a[k]=""; g_top_b[k]=""; g_top_z[k]=0; g_top_corr[k]=0;
      g_top_sprA[k]=0; g_top_sprB[k]=0; g_top_beta[k]=0; g_top_hl[k]=0; g_top_score[k]=0; g_top_edge[k]=0; g_top_fresh[k]=false;
   }
   string pa[32], pb[32]; double pz[32], ps[32]; int cnt=0;
   if(g_strategy==ARBITRAGE_SPREAD) return;
   if(g_strategy==MULTI_STATISTICAL){
      for(int i=0;i<g_mp_count && cnt<ArraySize(pa);i++){
         pa[cnt]=g_mp_a[i]; pb[cnt]=g_mp_b[i]; pz[cnt]=0.0; ps[cnt]=0.0;
         SPairStat st; ResetStat(st);
         if(GetStat(pa[cnt],pb[cnt],st)){
            pz[cnt]=MathAbs(st.z);
            if(InpUsePairIntelligence){
               double ec=0,ee=0,en=0,eva=0,evb=0; SPairIntelligence pi;
               if(EstimatePairEconomics(pa[cnt],pb[cnt],st,ec,ee,en,eva,evb)&&CalculatePairIntelligence(pa[cnt],pb[cnt],st,ee,ec,pi)) ps[cnt]=pi.score;
            } else ps[cnt]=pz[cnt];
         }
         cnt++;
      }
   } else {
      if(cnt<ArraySize(pa)){ pa[cnt]=InpSymbolA; pb[cnt]=InpSymbolB; cnt++; }
      for(int i=0;i<g_scan_count && cnt<ArraySize(pa);i++){
         bool dup=false;
         for(int j=0;j<cnt;j++) if(g_scan_a[i]==pa[j] && g_scan_b[i]==pb[j]) { dup=true; break; }
         if(dup) continue;
         pa[cnt]=g_scan_a[i]; pb[cnt]=g_scan_b[i]; cnt++;
      }
      for(int i=0;i<cnt;i++){
         SPairStat st; ResetStat(st);
         if(GetStat(pa[i],pb[i],st)){
            pz[i]=MathAbs(st.z);
            if(InpUsePairIntelligence){
               double ec=0,ee=0,en=0,eva=0,evb=0; SPairIntelligence pi;
               if(EstimatePairEconomics(pa[i],pb[i],st,ec,ee,en,eva,evb)&&CalculatePairIntelligence(pa[i],pb[i],st,ee,ec,pi)) ps[i]=pi.score;
            } else ps[i]=pz[i];
         }
      }
   }
   // Sort all configured opportunities by intelligence score, then by |Z|, while keeping every pair visible.
   for(int i=0;i<cnt-1;i++) for(int j=i+1;j<cnt;j++){
      bool swapIt=(ps[j]>ps[i]+1e-9) || (MathAbs(ps[j]-ps[i])<1e-9 && pz[j]>pz[i]);
      if(!swapIt) continue;
      string ts=pa[i]; pa[i]=pa[j]; pa[j]=ts; ts=pb[i]; pb[i]=pb[j]; pb[j]=ts;
      double td=pz[i]; pz[i]=pz[j]; pz[j]=td; td=ps[i]; ps[i]=ps[j]; ps[j]=td;
   }
   int out=MathMin(cnt,ArraySize(g_top_a));
   for(int k=0;k<out;k++){
      SPairStat st; ResetStat(st);
      if(GetStat(pa[k],pb[k],st)) FillTopRow(k,pa[k],pb[k],st);
      else { g_top_a[k]=pa[k]; g_top_b[k]=pb[k]; g_top_z[k]=0; g_top_corr[k]=0; g_top_beta[k]=0; g_top_hl[k]=0; g_top_score[k]=ps[k]; g_top_fresh[k]=false; }
   }
}


void CalculateCapitalAllocation(){
   if(g_alloc_last_update>0 && TimeCurrent()-g_alloc_last_update<2) return;
   g_alloc_last_update=TimeCurrent();
   g_alloc_ready=false;
   for(int i=0;i<ArraySize(g_alloc_risk_pct);i++){
      g_alloc_risk_pct[i]=0.0; g_alloc_weight[i]=0.0; g_alloc_multiplier[i]=0.0; g_alloc_quality[i]=0.0; g_alloc_cluster[i]="";
   }
   double budget=MathMax(0.0,InpPortfolioRiskBudgetPct*MathMax(0.25,g_inst.riskMultiplier));
   double maxPair=MathMin(MathMax(0.0,InpMaxPairRiskBudgetPct),budget);
   double maxCluster=MathMin(MathMax(0.0,InpMaxClusterRiskBudgetPct),budget);
   if(budget<=0.0 || maxPair<=0.0) return;

   double raw[16]; int idx[16]; double sumRaw=0.0; int n=0;
   for(int i=0;i<ArraySize(g_top_a);i++){
      if(g_top_a[i]=="" || g_top_b[i]=="") continue;
      idx[n]=i; raw[n]=0.0;
      double scoreQ=MathMax(0.0,MathMin(1.0,g_top_score[i]/100.0));
      double zQ=MathMax(0.0,MathMin(1.0,g_top_z[i]/MathMax(0.01,InpStatEntryZScore)));
      double corrQ=MathMax(0.0,MathMin(1.0,(g_top_corr[i]-InpMinPairCorrelation)/MathMax(0.01,1.0-InpMinPairCorrelation)));
      double hlQ=(g_top_hl[i]>0.0)?MathMax(0.15,MathMin(1.0,InpMaxHalfLifeBars/MathMax(1.0,g_top_hl[i]))):0.15;
      double edgeQ=MathMax(0.0,MathMin(1.0,g_top_edge[i]/MathMax(0.01,InpIntelligenceEdgeCap)));
      double execQ=MathMax(0.0,MathMin(1.0,g_inst.executionQualityScore/100.0));
      double decayQ=MathMax(0.0,MathMin(1.0,g_inst.signalDecay));
      double stabQ=MathMax(0.0,MathMin(1.0,g_inst.stabilityScore));
      double regimeQ=MathMax(0.25,MathMin(1.0,g_inst.riskMultiplier));
      double freshness=g_top_fresh[i]?1.0:0.35;
      bool gate=(g_top_score[i]>=EffectivePairIntelligenceThreshold() && g_top_z[i]>=InpStatEntryZScore && g_top_corr[i]>=InpMinPairCorrelation);
      if(gate){
         raw[n]=MathMax(0.0,scoreQ)*MathMax(0.10,zQ)*MathMax(0.10,corrQ)*hlQ*MathMax(0.10,edgeQ)*execQ*MathMax(0.10,decayQ)*MathMax(0.10,stabQ)*regimeQ*freshness;
      }
      g_alloc_quality[i]=raw[n];
      g_alloc_cluster[i]=InstClusterKey(g_top_a[i],g_top_b[i]);
      sumRaw+=raw[n]; n++;
   }
   if(n<=0 || sumRaw<=0.0) return;

   // First pass: strongest quality receives risk, capped by pair and cluster budgets.
   for(int pass=0;pass<n;pass++){
      int best=-1; double bestRaw=-1.0;
      for(int j=0;j<n;j++) if(raw[j]>bestRaw){
         bool used=false; for(int q=0;q<pass;q++) if(idx[j]==idx[q]) used=true;
         if(!used){bestRaw=raw[j];best=j;}
      }
      if(best<0) break;
      // Move selected index to front of remaining order.
      if(best!=pass){ int ti=idx[pass];idx[pass]=idx[best];idx[best]=ti; double tr=raw[pass];raw[pass]=raw[best];raw[best]=tr; }
   }
   double remaining=budget;
   for(int j=0;j<n;j++){
      int i=idx[j]; if(raw[j]<=0.0 || remaining<=0.0) continue;
      double clusterUsed=0.0;
      for(int q=0;q<ArraySize(g_alloc_risk_pct);q++) if(q!=i && g_alloc_cluster[q]!="" && g_alloc_cluster[q]==g_alloc_cluster[i]) clusterUsed+=g_alloc_risk_pct[q];
      double clusterRoom=MathMax(0.0,maxCluster-clusterUsed);
      double target=budget*(raw[j]/sumRaw);
      double alloc=MathMin(MathMin(maxPair,clusterRoom),MathMin(target,remaining));
      if(alloc<=0.0) continue;
      g_alloc_risk_pct[i]=alloc;
      g_alloc_weight[i]=alloc/budget*100.0;
      g_alloc_multiplier[i]=MathMax(0.0,MathMin(1.0,alloc/MathMax(0.01,maxPair)));
      remaining-=alloc;
   }
   // Second pass redistributes unused budget without breaching pair/cluster caps.
   if(remaining>0.0001){
      for(int j=0;j<n && remaining>0.0001;j++){
         int i=idx[j]; if(raw[j]<=0.0) continue;
         double clusterUsed=0.0; for(int q=0;q<ArraySize(g_alloc_risk_pct);q++) if(q!=i && g_alloc_cluster[q]==g_alloc_cluster[i]) clusterUsed+=g_alloc_risk_pct[q];
         double room=MathMin(maxPair-g_alloc_risk_pct[i],MathMax(0.0,maxCluster-clusterUsed-g_alloc_risk_pct[i]));
         double add=MathMin(remaining,MathMax(0.0,room));
         if(add<=0) continue;
         g_alloc_risk_pct[i]+=add; remaining-=add;
         g_alloc_weight[i]=g_alloc_risk_pct[i]/budget*100.0;
         g_alloc_multiplier[i]=MathMax(0.0,MathMin(1.0,g_alloc_risk_pct[i]/MathMax(0.01,maxPair)));
      }
   }
   g_alloc_ready=true;
}

const string UI_PREFIX="AP_UI_";
bool g_ui_visible=true, g_news_visible=false, g_show_symbol_map=false;
bool g_show_alpha_page=false;

// MAP -> TradingView-style ratio spread chart state.
long   g_arb_chart_id=0;
string g_arb_chart_symbol="";
ulong  g_arb_chart_last_update_ms=0;
const color UI_BG=C'3,17,31', UI_CARD=C'5,27,47', UI_CARD2=C'7,36,62', UI_BORDER=C'0,118,235';
const color UI_TEXT=C'235,244,252', UI_MUTED=C'140,177,204', UI_BLUE=C'0,122,255';
const color UI_GREEN=C'0,225,155', UI_ORANGE=C'255,177,45', UI_RED=C'245,80,80', UI_CYAN=C'65,190,255', UI_PURPLE=C'185,105,255';
const color UI_DARK_RED=C'139,0,0';

string UIName(const string id){ return UI_PREFIX+id; }
void UIDeleteAll(){
   for(int i=ObjectsTotal(0,-1,-1)-1;i>=0;i--){
      string name=ObjectName(0,i,-1,-1);
      if(StringFind(name, UI_PREFIX)==0) ObjectDelete(0, name);
   }
}
void UISetCommon(const string name, const int x, const int y, const int w, const int h){
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x); ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(0, name, OBJPROP_XSIZE, w); ObjectSetInteger(0, name, OBJPROP_YSIZE, h);
   ObjectSetInteger(0, name, OBJPROP_BACK, false);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_SELECTED, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
   ObjectSetInteger(0, name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
}
void UIRect(const string id, const int x, const int y, const int w, const int h, const color bg, const color border=clrNONE){
   string name=UIName(id);
   ObjectCreate(0, name, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   UISetCommon(name, x, y, w, h);
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, bg);
   ObjectSetInteger(0, name, OBJPROP_BORDER_TYPE, border==clrNONE?BORDER_FLAT:BORDER_SUNKEN);
   ObjectSetInteger(0, name, OBJPROP_COLOR, border==clrNONE?bg:border);
   ObjectSetInteger(0, name, OBJPROP_ZORDER, 1);
}
void UILabel(const string id, const string text, const int x, const int y, const int size, const color clr, const string font="Arial"){
   string name=UIName(id);
   ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
   UISetCommon(name, x, y, 0, 0);
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetString(0, name, OBJPROP_FONT, font);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, size);
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_ZORDER, 10);
}
void UILabelRight(const string id, const string text, const int x, const int y, const int size, const color clr, const string font="Arial"){
   UILabel(id,text,x,y,size,clr,font);
   ObjectSetInteger(0,UIName(id),OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER);
}
void UIButton(const string id, const string text, const int x, const int y, const int w, const int h, const color bg, const color fg=C'235,244,252'){
   string name=UIName(id);
   ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0);
   UISetCommon(name, x, y, w, h);
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetString(0, name, OBJPROP_FONT, "Arial");
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 9);
   ObjectSetInteger(0, name, OBJPROP_COLOR, fg);
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, bg);
   ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, UI_BORDER);
   ObjectSetInteger(0, name, OBJPROP_ZORDER, 20);
}
string UIStrategyShort(){ if(g_strategy==PAIR_STATISTICAL) return "PAIR STAT"; if(g_strategy==ARBITRAGE_SPREAD) return "ARBITRAGE SPREAD"; return "MULTI STAT"; }
string UIModeShort(){
   if(g_mode==SIGNALS_ONLY) return "SIGNALS";
   if(g_mode==DEMO_TRADING) return "DEMO";
   return "LIVE";
}
string GetCorrelationText(double corr){
   if(corr>=0.8) return "STRONG +"; if(corr>=0.5) return "MOD +";
   if(corr>=0.2) return "WEAK +"; if(corr>=-0.2) return "NONE";
   if(corr>=-0.5) return "WEAK -"; if(corr>=-0.8) return "MOD -";
   return "STRONG -";
}
color GetCorrelationColor(double corr){
   if(corr>=0.7) return C'0,225,155'; if(corr>=0.3) return C'65,190,255';
   if(corr>=-0.3) return C'140,177,204'; if(corr>=-0.7) return C'255,177,45';
   return C'245,80,80';
}

string g_ui_news_time[20], g_ui_news_currency[20], g_ui_news_name[20], g_ui_news_actual[20], g_ui_news_forecast[20], g_ui_news_previous[20];
int g_ui_news_importance[20], g_ui_news_count=0;
datetime g_ui_news_cache_time=0;
datetime g_ui_news_datetime[20];
int g_news_selected=-1;
int g_news_scroll=0;

int NewsVisibleRows(){
   if(g_ui_news_count<=0) return 2;
   return MathMin(15,g_ui_news_count);
}
void ClearNewsCache(){
   g_ui_news_count=0;
   for(int i=0;i<20;i++){
      g_ui_news_time[i]=""; g_ui_news_currency[i]=""; g_ui_news_name[i]="";
      g_ui_news_actual[i]=""; g_ui_news_forecast[i]=""; g_ui_news_previous[i]="";
      g_ui_news_importance[i]=0; g_ui_news_datetime[i]=0;
   }
}
string FormatCalendarValue(const double value){
   if(!MathIsValidNumber(value) || MathAbs(value - EMPTY_VALUE) < 1e-10) return "-";
   if(MathAbs(value)>=9.0e18) return "-";
   if(MathAbs(value)>1.0e15) return "-";
   double v=value;
   if(MathAbs(v)>10000.0) v=v/1000000.0;
   else if(MathAbs(v)>100.0) v=v/100.0;
   return DoubleToString(v, 1);
}
string FormatDuration(const int minutes){
   if(minutes<0) return "0m";
   if(minutes<60) return IntegerToString(minutes)+"m";
   int h=minutes/60, m=minutes%60;
   return IntegerToString(h)+"h "+IntegerToString(m)+"m";
}
bool SessionOpen(const int openH, const int closeH){
   MqlDateTime dt; TimeToStruct(TimeGMT(), dt);
   int h=dt.hour;
   if(openH<closeH) return (h>=openH && h<closeH);
   return (h>=openH || h<closeH);
}
void AddNewsItem(const datetime t, const string currency, const string name,
                 const int importance, const string act, const string fc, const string pv){
   if(g_ui_news_count>=20) return;
   for(int i=0;i<g_ui_news_count;i++)
      if(g_ui_news_time[i]==TimeToString(t, TIME_MINUTES) &&
         g_ui_news_currency[i]==currency && g_ui_news_name[i]==name) return;
   int p=g_ui_news_count;
   g_ui_news_time[p]=TimeToString(t, TIME_MINUTES);
   g_ui_news_currency[p]=currency;
   g_ui_news_name[p]=name;
   g_ui_news_importance[p]=importance;
   g_ui_news_datetime[p]=t;
   g_ui_news_actual[p]=act;
   g_ui_news_forecast[p]=fc;
   g_ui_news_previous[p]=pv;
   g_ui_news_count++;
}
void UpdateNewsCache(){
   if(g_ui_news_cache_time>0 && TimeCurrent()-g_ui_news_cache_time<30) return;
   ClearNewsCache();
   string curs[8]; int cc=0;
   string cand[32]; int candN=0;
   PairCurrencies(InpSymbolA,InpSymbolB,cand[0],cand[1],cand[2],cand[3]); candN=4;
   for(int q=0;q<MathMin(3,ArraySize(g_top_a));q++) if(g_top_a[q]!="") { PairCurrencies(g_top_a[q],g_top_b[q],cand[candN],cand[candN+1],cand[candN+2],cand[candN+3]); candN+=4; }
   for(int q=0;q<ArraySize(g_cycles)&&candN<28;q++) { PairCurrencies(g_cycles[q].symA,g_cycles[q].symB,cand[candN],cand[candN+1],cand[candN+2],cand[candN+3]); candN+=4; }
   for(int k=0;k<candN;k++){
      if(cand[k]=="") continue;
      bool ex=false; for(int j=0;j<cc;j++) if(curs[j]==cand[k]) ex=true;
      if(!ex&&cc<8) curs[cc++]=cand[k];
   }
   datetime from=TimeCurrent()-7200, to=TimeCurrent()+86400;
   datetime tmp_time[64];
   string tmp_cur[64], tmp_name[64], tmp_act[64], tmp_fc[64], tmp_pv[64];
   int tmp_imp[64];
   int tmp_n=0;
   for(int c=0;c<cc;c++){
      MqlCalendarValue vals[];
      int total=CalendarValueHistory(vals, from, to, NULL, curs[c]);
      if(total<=0) continue;
      for(int i=0;i<total && tmp_n<64;i++){
         MqlCalendarEvent ev;
         if(!CalendarEventById(vals[i].event_id, ev)) continue;
         if(ev.importance!=CALENDAR_IMPORTANCE_HIGH && ev.importance!=CALENDAR_IMPORTANCE_MODERATE) continue;
         if(vals[i].time<TimeCurrent()-900) continue;
         tmp_time[tmp_n]=vals[i].time;
         tmp_cur[tmp_n]=curs[c];
         tmp_name[tmp_n]=ev.name;
         tmp_imp[tmp_n]=(int)ev.importance;
         tmp_act[tmp_n]=FormatCalendarValue(vals[i].actual_value);
         tmp_fc[tmp_n]=FormatCalendarValue(vals[i].forecast_value);
         tmp_pv[tmp_n]=FormatCalendarValue(vals[i].prev_value);
         tmp_n++;
      }
   }
   for(int a=0;a<tmp_n-1;a++)
      for(int b=a+1;b<tmp_n;b++)
         if(tmp_time[b]<tmp_time[a]){
            datetime td=tmp_time[a]; tmp_time[a]=tmp_time[b]; tmp_time[b]=td;
            string ts=tmp_cur[a]; tmp_cur[a]=tmp_cur[b]; tmp_cur[b]=ts;
            ts=tmp_name[a]; tmp_name[a]=tmp_name[b]; tmp_name[b]=ts;
            ts=tmp_act[a]; tmp_act[a]=tmp_act[b]; tmp_act[b]=ts;
            int ti=tmp_imp[a]; tmp_imp[a]=tmp_imp[b]; tmp_imp[b]=ti;
            ts=tmp_fc[a]; tmp_fc[a]=tmp_fc[b]; tmp_fc[b]=ts;
            ts=tmp_pv[a]; tmp_pv[a]=tmp_pv[b]; tmp_pv[b]=ts;
         }
   for(int i=0;i<tmp_n && g_ui_news_count<20;i++)
      AddNewsItem(tmp_time[i], tmp_cur[i], tmp_name[i], tmp_imp[i], tmp_act[i], tmp_fc[i], tmp_pv[i]);
   g_ui_news_cache_time=TimeCurrent();
}
string NewsMacroClass(const string name){
   string n=name; StringToLower(n);
   if(StringFind(n,"cpi")>=0 || StringFind(n,"inflation")>=0 || StringFind(n,"price")>=0) return "Inflation / price pressure";
   if(StringFind(n,"pmi")>=0 || StringFind(n,"manufactur")>=0 || StringFind(n,"services")>=0) return "Business activity / PMI";
   if(StringFind(n,"employment")>=0 || StringFind(n,"payroll")>=0 || StringFind(n,"unemployment")>=0 || StringFind(n,"job")>=0 || StringFind(n,"wage")>=0) return "Employment / labour market";
   if(StringFind(n,"rate")>=0 || StringFind(n,"central bank")>=0 || StringFind(n,"interest")>=0) return "Central-bank / rates";
   if(StringFind(n,"gdp")>=0 || StringFind(n,"growth")>=0) return "Growth / GDP";
   if(StringFind(n,"retail")>=0 || StringFind(n,"consumer")>=0 || StringFind(n,"confidence")>=0) return "Consumer demand";
   if(StringFind(n,"industrial")>=0 || StringFind(n,"production")>=0 || StringFind(n,"factory")>=0) return "Industrial activity";
   if(StringFind(n,"trade")>=0 || StringFind(n,"export")>=0 || StringFind(n,"import")>=0) return "Trade / external balance";
   if(StringFind(n,"housing")>=0 || StringFind(n,"house")>=0 || StringFind(n,"home")>=0) return "Housing activity";
   if(StringFind(n,"speech")>=0 || StringFind(n,"testimony")>=0 || StringFind(n,"statement")>=0) return "Central-bank communication";
   return "Macro / economic release";
}
string NewsAffectedPairs(const string currency){
   string out="";
   string aa[64],bb[64]; int n=0;
   aa[n]=InpSymbolA; bb[n]=InpSymbolB; n++;
   for(int i=0;i<g_mp_count && n<64;i++){ aa[n]=g_mp_a[i]; bb[n]=g_mp_b[i]; n++; }
   for(int i=0;i<g_scan_count && n<64;i++){
      bool dup=false; for(int j=0;j<n;j++) if(aa[j]==g_scan_a[i]&&bb[j]==g_scan_b[i]) {dup=true;break;}
      if(!dup){aa[n]=g_scan_a[i];bb[n]=g_scan_b[i];n++;}
   }
   for(int i=0;i<n;i++){
      string c1,c2,c3,c4; PairCurrencies(aa[i],bb[i],c1,c2,c3,c4);
      if(c1==currency||c2==currency||c3==currency||c4==currency){
         string p=aa[i]+"/"+bb[i]; if(out!="") out+="  •  "; out+=p;
      }
   }
   if(out=="") out="No configured pair directly contains "+currency;
   if(StringLen(out)>92) out=StringSubstr(out,0,89)+"...";
   return out;
}
string NewsDirectionalRead(const string name,const string actual,const string forecast,const string currency){
   string cls=NewsMacroClass(name);
   string n=name; StringToLower(n);
   if(actual=="-" || forecast=="-") return ""+name+" is a scheduled "+cls+" event. No actual/forecast surprise is available yet; the engine treats it as event risk and can restrict new entries around the release.";
   double a=StringToDouble(actual), f=StringToDouble(forecast);
   if(!MathIsValidNumber(a)||!MathIsValidNumber(f)) return "Compare the release with expectations; the initial move can widen the pair spread before convergence stabilizes.";
   double d=a-f;
   string direction=(MathAbs(d)<1e-9)?"in line with forecast":(d>0?"above forecast":"below forecast");
   string tone="can be interpreted as a positive/negative macro surprise for "+currency;
   if(StringFind(n,"unemployment")>=0) tone=(d<0?"a potentially supportive surprise for "+currency:"a potentially negative surprise for "+currency);
   else if(StringFind(n,"inflation")>=0 || StringFind(n,"cpi")>=0 || StringFind(n,"price")>=0) tone=(d>0?"a potentially hawkish/inflationary surprise for "+currency:"a potentially softer inflation surprise for "+currency);
   else if(StringFind(n,"rate")>=0 || StringFind(n,"interest")>=0) tone=(d>0?"a higher-rate surprise for "+currency:"a lower-rate surprise for "+currency);
   return "Actual is "+DoubleToString(a,1)+" vs forecast "+DoubleToString(f,1)+" ("+direction+"). This " + tone + "; for a statistical pair it may cause temporary spread expansion, volatility and execution-cost changes.";
}
void NewsWrapText(const string text,string &lines[],int &count,const int maxChars){
   count=0;
   for(int i=0;i<ArraySize(lines);i++) lines[i]="";
   string words[];
   int n=StringSplit(text,' ',words);
   string line="";
   for(int i=0;i<n;i++){
      string w=words[i];
      if(w=="") continue;
      string test=(line=="")?w:(line+" "+w);
      if(StringLen(test)>maxChars && line!=""){
         if(count<ArraySize(lines)) lines[count++]=line;
         line=w;
      }else line=test;
   }
   if(line!="" && count<ArraySize(lines)) lines[count++]=line;
}
string NewsEventDescription(const string name){
   string n=name; StringToLower(n);
   string cls=NewsMacroClass(name);
   if(StringFind(n,"cpi")>=0 || StringFind(n,"consumer price")>=0 || StringFind(n,"price index")>=0)
      return "Consumer-price release: tracks changes in the prices paid by households. It is primarily used to assess inflation pressure and can shift expectations for future central-bank policy.";
   if(StringFind(n,"pmi")>=0 || StringFind(n,"manufactur")>=0)
      return "Manufacturing activity release: summarizes conditions reported by businesses, including orders, production, inventories and employment. It is a timely gauge of industrial momentum.";
   if(StringFind(n,"services")>=0)
      return "Services activity release: measures the pace of business activity in the service economy. It can alter growth expectations and short-term currency volatility.";
   if(StringFind(n,"unemployment")>=0)
      return "Unemployment release: reports labour-market slack through the unemployment rate. A surprise can change expectations for household income, growth and monetary policy.";
   if(StringFind(n,"payroll")>=0 || StringFind(n,"nonfarm")>=0)
      return "Payroll release: measures changes in employment outside the agricultural sector. Markets use it as a key signal of labour demand, growth and potential policy pressure.";
   if(StringFind(n,"wage")>=0 || StringFind(n,"earnings")>=0)
      return "Wage/earnings release: tracks labour compensation. Stronger wage growth can increase inflation expectations, while softer growth can reduce policy pressure.";
   if(StringFind(n,"retail")>=0)
      return "Retail-sales release: measures household spending through retail activity. It provides a direct read on consumer demand and near-term economic momentum.";
   if(StringFind(n,"gdp")>=0)
      return "GDP release: measures aggregate economic output. The result can change expectations for domestic growth, policy and the currency component of a statistical pair.";
   if(StringFind(n,"industrial production")>=0)
      return "Industrial-production release: measures output from the industrial sector. It provides information about manufacturing momentum, demand and the real economy.";
   if(StringFind(n,"housing")>=0 || StringFind(n,"house price")>=0)
      return "Housing release: tracks housing activity or prices. It provides information about domestic demand, financial conditions and the sensitivity of the economy to rates.";
   if(StringFind(n,"interest")>=0 || StringFind(n,"rate decision")>=0 || StringFind(n,"central bank")>=0)
      return "Central-bank/rates release: provides information about monetary policy or policy expectations. These events can produce rapid repricing and elevated execution risk.";
   if(StringFind(n,"trade")>=0 || StringFind(n,"export")>=0 || StringFind(n,"import")>=0)
      return "Trade release: measures external flows such as exports and imports. It provides information about external demand and the currency's macro backdrop.";
   if(cls=="Business activity / PMI")
      return "PMI release: a timely survey-based gauge of business activity. The surprise versus forecast can shift growth expectations, currency demand and short-term pair volatility.";
   if(cls=="Inflation / price pressure")
      return "Inflation release: measures changes in consumer or producer prices. The surprise can alter rate expectations and therefore the short-term behaviour of the currency in configured pairs.";
   if(cls=="Employment / labour market")
      return "Labour-market release: provides a direct read on employment conditions. The expectation gap can change growth and policy expectations and increase event-time execution risk.";
   if(cls=="Growth / GDP")
      return "Growth release: measures economic activity or output. A meaningful surprise can reprice the currency backdrop and temporarily change convergence conditions.";
   return name+" | "+cls+" release for the configured currency. The engine compares Actual, Forecast and Previous, then evaluates the expectation gap, event importance, spread/volatility response and execution risk before allowing new risk.";
}

void DrawNewsDetail(const int y){
   const int x=InpPanelX, w=650;
   // Keep the original compact NEWS appearance: detail is text-only, without a new card/frame.
   if(g_news_selected<0 || g_news_selected>=g_ui_news_count){
      UILabel("NewsDetailTitle","SELECT A NEWS EVENT",x+22,y+14,10,UI_CYAN,"Segoe UI Semibold");
      UILabel("NewsDetailHint","Click any event above to inspect the release, surprise versus expectations, macro meaning and configured pair impact.",x+22,y+40,8,UI_TEXT,"Segoe UI");
      return;
   }
   int i=g_news_selected;
   string title=g_ui_news_name[i];
   string dateText=TimeToString(g_ui_news_datetime[i],TIME_DATE|TIME_MINUTES);
   UILabel("NewsDetailTitle",title,x+22,y+12,11,UI_TEXT,"Segoe UI Semibold");
   UILabel("NewsDetailMeta",dateText+"  |  "+g_ui_news_currency[i]+"  |  "+(g_ui_news_importance[i]==CALENDAR_IMPORTANCE_HIGH?"HIGH":"MED"),x+22,y+34,8,UI_CYAN);
   UILabel("NewsDetailValues","ACTUAL  "+g_ui_news_actual[i]+"     FORECAST  "+g_ui_news_forecast[i]+"     PREVIOUS  "+g_ui_news_previous[i],x+22,y+53,8,UI_TEXT,"Segoe UI Semibold");
   UILabel("NewsDetailClass","CLASS: "+NewsMacroClass(title),x+22,y+73,8,UI_MUTED);

   string descLines[3]; int descN=0;
   NewsWrapText(NewsEventDescription(title),descLines,descN,102);
   UILabel("NewsDescLabel","DESCRIPTION",x+22,y+94,7,UI_CYAN,"Segoe UI Semibold");
   for(int d=0;d<3;d++) if(d<descN) UILabel("NewsDesc"+IntegerToString(d),descLines[d],x+22,y+109+d*15,7,UI_TEXT,"Segoe UI");

   string impact=NewsDirectionalRead(title,g_ui_news_actual[i],g_ui_news_forecast[i],g_ui_news_currency[i]);
   string impactLines[3]; int impactN=0;
   NewsWrapText(impact,impactLines,impactN,102);
   int iy=y+157;
   UILabel("NewsImpactLabel","MACRO SURPRISE / ENGINE EFFECT",x+22,iy,7,UI_CYAN,"Segoe UI Semibold");
   for(int d=0;d<3;d++) if(d<impactN) UILabel("NewsImpact"+IntegerToString(d),impactLines[d],x+22,iy+15+d*14,7,UI_TEXT,"Segoe UI");

   string pairs=NewsAffectedPairs(g_ui_news_currency[i]);
   string pairLines[2]; int pairN=0;
   NewsWrapText(pairs,pairLines,pairN,102);
   int py=iy+60;
   UILabel("NewsPairsLabel","AFFECTED CONFIGURED PAIRS",x+22,py,7,UI_CYAN,"Segoe UI Semibold");
   for(int d=0;d<2;d++) if(d<pairN) UILabel("NewsPairs"+IntegerToString(d),pairLines[d],x+22,py+15+d*14,7,UI_TEXT,"Segoe UI");

   // Dynamic event-specific risk/execution interpretation: this is rebuilt for every selected row.
   string riskText="";
   string actual=g_ui_news_actual[i];
   string forecast=g_ui_news_forecast[i];
   string previous=g_ui_news_previous[i];
   string cls=NewsMacroClass(title);
   riskText=""+title+" | "+g_ui_news_currency[i]+" | "+cls+". ";
   if(g_ui_news_importance[i]==CALENDAR_IMPORTANCE_HIGH)
      riskText+="HIGH impact: the news filter can widen the protection window and block new entries near the release.";
   else
      riskText+="MED impact: the engine monitors spread, volatility and execution cost around the release.";
   if(actual!="-" && forecast!="-")
      riskText+=" Actual "+actual+" vs Forecast "+forecast+".";
   else
      riskText+=" Actual/Forecast comparison is not available yet.";
   if(previous!="-") riskText+=" Previous "+previous+".";
   if(cls=="Inflation / price pressure" || cls=="Central-bank / rates" || cls=="Central-bank communication")
      riskText+=" Policy-sensitive: a surprise can change rate expectations and spread volatility.";
   else if(cls=="Employment / labour market" || cls=="Growth / GDP" || cls=="Business activity / PMI")
      riskText+=" Growth-sensitive: a surprise can propagate into currency volatility and execution conditions.";
   else
      riskText+=" The main engine concern is temporary volatility, spread expansion and execution-cost changes.";
   string affected=NewsAffectedPairs(g_ui_news_currency[i]);
   if(affected!="-") riskText+=" Configured pairs: "+affected;
   string riskLines[4]; int riskN=0;
   NewsWrapText(riskText,riskLines,riskN,100);
   UILabel("NewsRiskLabel","RISK / EXECUTION",x+22,y+251,7,UI_CYAN,"Segoe UI Semibold");
   for(int rd=0;rd<4;rd++) if(rd<riskN) UILabel("NewsRisk"+IntegerToString(rd),riskLines[rd],x+22,y+266+rd*13,7,UI_TEXT,"Segoe UI");
   // Dedicated reading-area separator: deliberately below the full news analysis.
   UIRect("NewsReadingDivider",x+1,y+790,w-2,1,UI_BORDER);
}
void DrawNewsPanel(const int y){
   const int x=InpPanelX, w=650;
   UpdateNewsCache();
   int rows=MathMin(15,g_ui_news_count);
   int panelH=850;
   UIRect("NewsPanel",x,y,w,panelH,UI_BG,UI_BORDER);
   UILabel("NewsHeader","NEWS-SESSIONS",x+18,y+14,10,UI_TEXT,"Segoe UI Semibold");
   bool syd=SessionOpen(21,6),tok=SessionOpen(0,9),lon=SessionOpen(8,17),ny=SessionOpen(13,22);
   UILabel("SesGmt","GMT "+TimeToString(TimeGMT(),TIME_MINUTES),x+145,y+18,7,UI_MUTED);
   UILabel("SesSyd",syd?"Sydney●":"Sydney○",x+225,y+17,8,syd?UI_GREEN:UI_MUTED);
   UILabel("SesTok",tok?"Tokyo●":"Tokyo○",x+305,y+17,8,tok?UI_GREEN:UI_MUTED);
   UILabel("SesLon",lon?"London●":"London○",x+385,y+17,8,lon?UI_GREEN:UI_MUTED);
   UILabel("SesNy",ny?"New York●":"New York○",x+470,y+17,8,ny?UI_GREEN:UI_MUTED);
   UIRect("NewsHead",x+10,y+49,630,28,UI_CARD2);
   UILabel("NTime","TIME",x+18,y+57,8,UI_MUTED);
   UILabel("NCur","CURRENCY",x+105,y+57,8,UI_MUTED);
   UILabel("NEvent","EVENT",x+175,y+57,8,UI_MUTED);
   UILabel("NImpact","IMPACT",x+340,y+57,8,UI_MUTED);
   UILabel("NActual","ACTUAL",x+395,y+57,8,UI_CYAN);
   UILabel("NForecast","FORECAST",x+455,y+57,8,UI_MUTED);
   UILabel("NPrev","PREVIOUS",x+535,y+57,8,UI_MUTED);
   datetime now=TimeCurrent();
   for(int i=0;i<rows;i++){
      int yy=y+81+i*27;
      color rowBg=(i%2==0)?UI_CARD:UI_BG;
      int idx=g_news_scroll+i;
      UIRect("NewsRow"+IntegerToString(i),x+10,yy-5,630,28,idx==g_news_selected?UI_CARD2:rowBg);
      if(idx>=g_ui_news_count) continue;
      int diff=(int)(g_ui_news_datetime[idx]-now)/60;
      string timeText=g_ui_news_time[idx];
      if(diff>0) timeText+=" ("+FormatDuration(diff)+")"; else if(diff==0) timeText+=" (now)";
      UILabel("NewsTime"+IntegerToString(i),timeText,x+18,yy,7,(diff>=0&&diff<=InpNewsMinutesBefore)?UI_RED:UI_TEXT);
      UILabel("NewsCur"+IntegerToString(i),g_ui_news_currency[idx],x+105,yy,8,UI_CYAN);
      string nm=g_ui_news_name[idx]; if(StringLen(nm)>25) nm=StringSubstr(nm,0,25)+"...";
      UILabel("NewsName"+IntegerToString(i),nm,x+175,yy,7,UI_TEXT);
      string imp=(g_ui_news_importance[idx]==CALENDAR_IMPORTANCE_HIGH)?"HIGH":"MED";
      UILabel("NewsImp"+IntegerToString(i),imp,x+340,yy,7,g_ui_news_importance[idx]==CALENDAR_IMPORTANCE_HIGH?UI_RED:UI_ORANGE);
      UILabel("NewsAct"+IntegerToString(i),g_ui_news_actual[idx],x+395,yy,7,g_ui_news_actual[idx]=="-"?UI_MUTED:UI_TEXT);
      UILabel("NewsFc"+IntegerToString(i),g_ui_news_forecast[idx],x+455,yy,7,UI_TEXT);
      UILabel("NewsPv"+IntegerToString(i),g_ui_news_previous[idx],x+535,yy,7,UI_TEXT);
   }
   int dy=y+88+rows*27+4;
   DrawNewsDetail(dy);
   // NEWS footer removed: event count / click hint intentionally hidden.
}


void DrawStrategyPanel(const int y){
   const int x=InpPanelX, w=650;
   int panelY=y+686;
   const int h=850;
   UIRect("StrategyPanel",x,panelY,w,h,UI_BG,UI_BORDER);

   bool isPair=(g_strategy==PAIR_STATISTICAL);
   bool isSpread=(g_strategy==ARBITRAGE_SPREAD);
   bool isMulti=(g_strategy==MULTI_STATISTICAL);

   UILabel("StrategyTitle","ARBITRAGE PRO QUANT INSTITUTIONAL v1.0(ENG)",x+18,panelY+13,11,UI_CYAN,"Segoe UI Semibold");
   UILabel("StrategyFullStructure",isSpread?"ARBITRAGE SPREAD • FULL ENTRY / MANAGEMENT STRUCTURE":(isPair?"PAIR STATISTICAL • FULL ENTRY / MANAGEMENT STRUCTURE":"MULTI STATISTICAL • FULL ENTRY / MANAGEMENT STRUCTURE"),x+18,panelY+34,8,UI_MUTED,"Segoe UI Semibold");
   color multiColor=isMulti?UI_BLUE:UI_CARD2;
   color pairColor=isPair?UI_BLUE:UI_CARD2;
   color spreadColor=isSpread?UI_BLUE:UI_CARD2;

   // Keep the original compact workflow layout. 06 PROTECT stays in the top block.
   UIRect("StrategyCard",x+10,panelY+50,630,166,UI_CARD);
   UILabel("StrategyL1","1  DISCOVER",x+20,panelY+61,7,UI_CYAN,"Segoe UI Semibold");
   UILabel("StrategyV1",isSpread?"Define the live A/B ratio spread around a zero fair-value center; measure the current deviation and opportunity.":(isPair?"Primary pair: analyze only InpSymbolA / InpSymbolB and build one focused relative-value signal.":"Scan the configured multi-pair universe, compare candidates and rank relative-value opportunities."),x+106,panelY+61,7,UI_TEXT,"Segoe UI");
   UILabel("StrategyL2","2  QUALIFY",x+20,panelY+84,7,UI_GREEN,"Segoe UI Semibold");
   UILabel("StrategyV2",isSpread?"Require the percentage deviation to exceed the configured Entry band, then validate quote quality, liquidity, cost, execution and risk gates.":(isPair?"Validate Z-score, correlation, beta, stationarity, half-life, Hurst, spread regime and signal quality for the selected pair.":"Filter every candidate through Z-score, correlation, beta, stationarity, half-life, Hurst and spread-quality gates before selection."),x+106,panelY+84,7,UI_TEXT,"Segoe UI");
   UILabel("StrategyL3","3  HEDGE",x+20,panelY+107,7,UI_ORANGE,"Segoe UI Semibold");
   UILabel("StrategyV3",isSpread?"Build the two-leg market-neutral position: positive spread = SELL A / BUY B; negative spread = BUY A / SELL B, with sizing and hedge validation.":(isPair?"Construct the focused two-leg hedge with adaptive beta, dollar neutrality, cost coverage and second-leg validation.":"Select the highest-quality eligible relationship, then construct its two-leg hedge with adaptive beta and portfolio-aware risk."),x+106,panelY+107,7,UI_TEXT,"Segoe UI");
   UILabel("StrategyL4","4  MANAGE",x+20,panelY+130,7,UI_BLUE,"Segoe UI Semibold");
   UILabel("StrategyV4",isSpread?"Manage convergence toward 0 using live spread behavior, regime, execution economics, holding time, exposure and protective controls.":(isPair?"Manage one active relative-value cycle with dynamic risk, Z-trailing, rebalance, time and half-life controls.":"Manage the selected multi-pair cycle with dynamic risk scaling, portfolio allocation, Z-trailing, rebalance and regime controls."),x+106,panelY+130,7,UI_TEXT,"Segoe UI");
   UILabel("StrategyL5","5  EXIT",x+20,panelY+153,7,UI_RED,"Segoe UI Semibold");
   UILabel("StrategyV5",isSpread?"Exit inside the configured Exit band as the spread converges to fair value, or immediately on cost, execution, time, news, drawdown or safety deterioration.":(isPair?"Exit when the pair converges or a protective condition triggers: hard Z-stop, P/L, time, news or execution risk.":"Exit the selected pair on convergence or portfolio/protective conditions, including Z, P/L, time, news and execution deterioration."),x+106,panelY+153,7,UI_TEXT,"Segoe UI");
   UILabel("StrategyL6","6  PROTECT",x+20,panelY+176,7,UI_PURPLE,"Segoe UI Semibold");
   UILabel("StrategyV6",isSpread?"Protect the spread cycle against legging risk, stale quotes, abnormal costs, hedge imbalance, margin stress, daily loss and institutional kill-switch conditions.":(isPair?"Protect the focused cycle with reconciliation, orphan recovery, execution audit, hedge-imbalance controls and circuit breakers.":"Protect the multi-pair portfolio with reconciliation, cluster limits, institutional kill-switch, execution audit and circuit breakers."),x+106,panelY+176,7,UI_TEXT,"Segoe UI");

   // Three strategy buttons: same size/style, centered as one group.
   UIButton("SelectMultiStat","MULTI STATISTICAL",x+105,panelY+232,140,30,multiColor,UI_TEXT);
   UIButton("SelectPairStat","PAIR STATISTICAL",x+255,panelY+232,140,30,pairColor,UI_TEXT);
   UIButton("SelectArbSpread","ARBITRAGE SPREAD",x+405,panelY+232,140,30,spreadColor,UI_TEXT);

   // ACTIVE stays compact, green and centered; font size matches MODEL PIPELINE.
   UILabel("StrategySelectHint",isSpread?"ACTIVE • ZERO-CENTERED SPREAD":(isPair?"ACTIVE • PRIMARY PAIR":"ACTIVE • MULTI-PAIR UNIVERSE"),x+325,panelY+274,8,UI_GREEN,"Segoe UI Semibold");
   ObjectSetInteger(0,UIName("StrategySelectHint"),OBJPROP_ANCHOR,ANCHOR_CENTER);


   string fullDesc;
   if(isSpread){
      fullDesc="ARBITRAGE SPREAD — a dedicated two-leg relative-value strategy built around a zero-centered fair-value framework. For pair A/B the live spread is ((A / B) - 1) * 100, so 0.0000% is the model fair-value reference. The strategy does not try to predict whether A or B will rise or fall on its own. It isolates the relative mispricing between the two legs. Above zero, A is relatively rich and the hedge direction is SELL A / BUY B. Below zero, A is relatively cheap and the hedge direction is BUY A / SELL B. The objective is to capture a measurable relative-price dislocation that has a credible path back toward fair value, while keeping both legs under one risk and execution framework.\n\n1. DISCOVER. The engine first establishes that both instruments are available, synchronized and tradable. Quotes must be fresh, prices must be valid, market conditions must be active, and the relationship must be calculated from current data rather than stale observations. The spread is monitored continuously so the system can distinguish a normal fluctuation from a statistically and economically meaningful deviation. Discovery is therefore a measurement stage, not an instruction to trade.\n\n2. QUALIFY. The Entry Threshold defines the minimum absolute percentage deviation required before a cycle can be considered. A deviation alone is never sufficient. Bid/ask spread, commission, swap, expected slippage, liquidity, execution latency, estimated edge and cost coverage are evaluated together. The setup must remain economically attractive after realistic trading costs. Positive net edge and configured cost-coverage requirements must pass before capital is committed. This prevents the engine from treating a visually large but expensive or unexecutable deviation as an institutional opportunity.\n\n3. HEDGE. Positive spread deviation maps to SELL A / BUY B; negative deviation maps to BUY A / SELL B. Both legs are treated as one market-neutral risk unit. Where instrument specifications permit, beta-aware or dollar-neutral sizing reduces accidental directional exposure. Lot limits, margin requirements, maximum exposure, daily loss, drawdown, consecutive-loss limits and portfolio budgets remain hard boundaries. After the first leg fills, the second leg is immediately revalidated for quote freshness, spread movement, Z drift, leg drift, execution delay, hedge imbalance and legging loss. If the hedge cannot be completed safely, protection and reconciliation take priority over forcing the original signal.\n\n4. MANAGE. The position is managed from the behavior of the relative spread, not from the standalone price of either instrument. The engine can distinguish orderly convergence, normal noise, adverse expansion, extreme deviation and unstable regime conditions. Convergence speed, holding time, half-life, cost-to-edge ratio, execution quality and live risk state determine whether the cycle remains healthy. Dynamic protection, P/L limits, maximum holding time, convergence timeout, session rules, weekend protection and news controls can move the cycle into defensive management when the original convergence thesis weakens.\n\n5. EXIT. The Exit Threshold creates a defined convergence band around fair value. Entry is intentionally farther from zero than Exit, creating hysteresis and reducing repeated open/close activity caused by normal quote noise. A healthy cycle can close when the spread reaches the configured exit condition and the economics remain acceptable. An unhealthy cycle can close earlier when execution quality, risk, time, cost, news or portfolio conditions invalidate the expected payoff. Exit is therefore a management decision based on both convergence and risk, not simply on whether a price target was touched.\n\n6. PROTECT. Protection is active for the complete lifecycle. Execution circuit breakers, reconciliation, margin checks, exposure limits, daily-stop logic and the institutional kill-switch are hard controls. Stale quotes, abnormal costs, large hedge imbalance, orphaned legs, broker execution problems or structural divergence can force defensive handling. ARBITRAGE SPREAD remains a separate strategy branch from PAIR STATISTICAL and MULTI STATISTICAL; shared infrastructure provides telemetry, diagnostics and portfolio protection, but the spread branch keeps its own entry, sizing and management path.\n\nINSTITUTIONAL PRINCIPLE. The intended workflow is identify the relative-price dislocation, verify data quality and executable economics, pass statistical and risk gates, hedge both legs as one position, manage convergence and close when the relative-value thesis is realized or protection requires an exit. This is a relative-value model, not a promise of risk-free profit. Liquidity deterioration, execution error, model error, structural breaks, persistent divergence and unexpected market conditions can still produce losses.";
   }else if(isPair){
      fullDesc="PAIR STATISTICAL — a focused relative-value strategy using exactly InpSymbolA and InpSymbolB. The EA does not forecast the independent direction of either instrument. It models the statistical relationship between the two instruments and seeks temporary dislocations in that relationship. The spread is built from log-price A minus beta multiplied by log-price B, while the live deviation is measured with Z-score. The purpose is to trade convergence of the relationship, not to hold a directional opinion on one symbol.\n\n1. DISCOVER. The pair is continuously evaluated using the configured historical lookback and synchronized observations. The engine establishes whether the relationship has enough history, valid prices and usable data to support a statistical decision. Discovery includes the current spread, mean, dispersion and Z-score, while the broader qualification layer examines correlation, beta, half-life, Hurst and regime behavior. A statistical value is useful only when it is supported by sufficient data quality and a relationship that remains relevant to the intended holding horizon.\n\n2. QUALIFY. The primary entry threshold is InpStatEntryZScore=2.20 and, when enabled, the signal can require a true Z-cross rather than simply an already-extreme reading. Correlation must satisfy InpMinPairCorrelation=0.70. Beta must remain between InpMinBeta=0.20 and InpMaxBeta=2.50. When enabled, half-life must be valid and no greater than InpMaxHalfLifeBars=48, while Hurst must remain below InpMaxHurst=0.45. These gates are designed to reject weak, unstable, excessively persistent or too-slow relationships before they become capital-at-risk.\n\nEXECUTION QUALIFICATION. Quotes must be fresh under the configured quote-age limits. Liquidity, spread, slippage and latency must remain acceptable. The spread-regime filter must permit the setup and adverse spread momentum must not indicate continued deterioration. When beta stability is enabled, the stability window and coefficient-of-variation limit must pass. Multi-timeframe confirmation can require the confirmation and trend timeframes to agree with the statistical setup, reducing the chance that one noisy observation creates an entry.\n\nPAIR INTELLIGENCE. Statistical strength is combined with economic and execution quality. Candidate quality can include Z quality, correlation, half-life, Hurst, beta stability, regime, execution and portfolio characteristics. InpMinCostCoverage=2.0 requires expected edge to cover at least two times estimated cost, and positive net edge must pass when InpRequirePositiveNetEdge is enabled. Portfolio allocation can reject a statistically attractive pair when existing exposure, cluster concentration, VaR/CVaR, stress loss or total portfolio risk budgets are already too high.\n\n3. HEDGE. Once qualified, the cycle is sized as one market-neutral two-leg position using the active risk controls and dollar-neutral beta sizing when enabled. Positive Z means SELL A / BUY B; negative Z means BUY A / SELL B. After the first leg fills, the second leg is rechecked for quote freshness, Z drift, leg drift, execution delay, legging loss and hedge imbalance. If the hedge cannot be completed safely, reconciliation and protection take priority over forcing the original statistical signal.\n\n4. MANAGE. The active cycle follows convergence rather than the absolute price of either instrument. Z-score behavior, dynamic protection, half-life, rebalance logic, P/L, maximum holding time, news protection and execution safeguards can all affect the management state. The engine must be able to distinguish normal convergence from adverse divergence and from a relationship that has become structurally unreliable.\n\n5. EXIT. Exit can occur through the configured exit Z-score and convergence condition when the relationship has moved back into the intended convergence zone. Exit is not based on a standalone price target: the engine considers the quality of convergence, remaining edge, transaction cost, execution conditions, holding time and current risk state. An orderly convergence can close the cycle normally, while a deteriorating statistical relationship can require an earlier exit before the original target is reached.\n\n6. PROTECT. Session and weekend rules, daily loss, drawdown, margin, exposure, consecutive-loss limits, circuit breakers, reconciliation and the institutional kill-switch remain active throughout the cycle. The architecture is intentionally focused: one defined pair, one statistical relationship, one two-leg risk unit and one complete entry-to-exit pipeline. It is a relative-value model rather than a guarantee of convergence; structural breaks, liquidity deterioration, execution problems and model error can still cause losses.";
   }else{
      fullDesc="MULTI STATISTICAL — a diversified relative-value selection engine operating across the configured multi-pair universe. The EA scans available relationships, resolves and validates symbols, rejects stale or unusable quotes, calculates statistical features for each candidate and searches for meaningful relative dislocations rather than ordinary market noise. InpStatEntryZScore=2.20 provides the primary statistical threshold, while adaptive and quality filters can tighten the requirement before any candidate receives capital. The key distinction is breadth: the engine searches many relationships, but each selected trade must still pass the same institutional entry, hedge, management and protection discipline.\n\n1. DISCOVER. Universe discovery validates the configured pair list and the market data required to analyze it. Each candidate is evaluated independently for Z-score, correlation, beta, half-life, Hurst, spread behavior, quote freshness, liquidity and execution conditions. Invalid symbols, stale data, unavailable quotes and unusable relationships are removed before ranking. The result is a live candidate set rather than a static list of preferred pairs.\n\n2. QUALIFY. Pair Intelligence ranks eligible candidates by a broader quality score, so the highest Z-score is not automatically the winner. A candidate must combine statistical significance with acceptable economics, execution quality, regime conditions and portfolio fit. Correlation must satisfy InpMinPairCorrelation=0.70, beta must remain inside the configured 0.20–2.50 range, optional half-life must be valid and <=48 bars, and optional Hurst must remain <0.45. Spread momentum, beta stability, volatility and regime filters can remove a candidate even when its raw Z-score looks attractive.\n\nEXECUTION FILTERS. The multi-pair engine checks liquidity, maximum spread, slippage, quote age, latency and execution quality before allocation. Confirmation and trend timeframes must not materially disagree when multi-timeframe controls are enabled. Pair News, cooldowns, existing-leg conflicts and execution circuit-breaker conditions must be clear before a candidate can receive capital. This prevents the selection engine from confusing a theoretical statistical edge with an executable trade.\n\nECONOMIC AND INSTITUTIONAL QUALIFICATION. Expected edge must cover transaction cost. InpMinCostCoverage=2.0 requires expected edge to cover at least two times estimated cost, and positive net edge must pass when required. Portfolio allocation evaluates pair risk, cluster risk, total exposure, correlation concentration, VaR/CVaR and stress-loss budgets. The objective is not to open as many signals as possible; it is to select the strongest risk-adjusted relationship while preventing several apparently different pairs from creating the same hidden portfolio exposure.\n\n3. HEDGE. Only a candidate that survives the complete chain receives capital. The selected pair is sized as one two-leg market-neutral trade using the active risk multiplier, portfolio allocation and dollar-neutral beta sizing when enabled. Positive Z means SELL A / BUY B; negative Z means BUY A / SELL B. After the first leg fills, the second leg is rechecked for quote freshness, Z drift, leg drift, execution delay, legging loss and hedge imbalance. Excessive deterioration aborts or protects the cycle rather than allowing uncontrolled one-leg exposure.\n\n4. MANAGE. Once both legs are live, management follows convergence and portfolio risk. Z-score behavior, half-life, dynamic exits, P/L and drawdown controls, news protection, execution safeguards, reconciliation and maximum holding rules remain active. The engine can continue to monitor the selected relationship while also checking whether the trade is creating unwanted concentration with other portfolio positions. Regime deterioration can therefore change the management state even before a hard loss limit is reached.\n\n5. EXIT. The selected pair can close when convergence reaches the configured exit condition and the expected remaining edge no longer justifies keeping capital committed. Exit can also be triggered earlier when execution economics deteriorate, the statistical thesis weakens, holding time becomes excessive or portfolio conditions make the position unattractive. This keeps the multi-pair engine focused on realized risk-adjusted convergence rather than simply waiting for every trade to return to a fixed price level.\n\n6. PROTECT. Global session and weekend rules, daily loss, maximum cycles and positions, maximum exposure, margin protection, consecutive-loss controls, circuit breakers and the institutional kill-switch remain hard boundaries. The intended workflow is discover many relationships, qualify them consistently, rank them by risk-adjusted quality, allocate risk selectively, hedge each selected pair as one unit, manage convergence and continuously protect the portfolio from concentration, execution and regime risk.";
   }

   // Strategy text uses the full reading width without putting more than the
   // safe label-text length into one OBJ_LABEL. The old code wrapped at 100
   // characters, but one label can be clipped before that width is rendered.
   const int descLineStep=8;
   const int descLeft=x+18;
   const int descRight=x+w-18;
   const int descMaxWidth=descRight-descLeft;
   string normalized=fullDesc;
   StringReplace(normalized,"\n"," ");
   string words[];
   int wordCount=StringSplit(normalized,' ',words);
   string segment="";
   int rowY=panelY+320;
   int rowIndex=0;
   int segIndex=0;
   int cursorX=descLeft;
   color sectionColor=UI_TEXT;
   TextSetFont("Segoe UI",-70,0);

   for(int wi=0;wi<wordCount;wi++){
      string word=words[wi];
      if(word=="") continue;

      // Every numbered workflow section starts on a fresh visual row.
      // The section heading and all text that follows it use that section's color.
      bool sectionStart=(word=="1." || word=="2." || word=="3." || word=="4." || word=="5." || word=="6.");
      if(sectionStart){
         if(segment!=""){
            uint sw=0,sh=0; TextGetSize(segment,sw,sh);
            UILabel("StrategyDesc"+IntegerToString(rowIndex)+"_"+IntegerToString(segIndex),segment,cursorX,rowY,7,sectionColor,"Segoe UI");
            segment="";
         }
         rowIndex++;
         rowY+=descLineStep;
         cursorX=descLeft;
         segIndex=0;

         if(word=="1.") sectionColor=UI_CYAN;
         else if(word=="2.") sectionColor=UI_GREEN;
         else if(word=="3.") sectionColor=UI_ORANGE;
         else if(word=="4.") sectionColor=UI_BLUE;
         else if(word=="5.") sectionColor=UI_RED;
         else if(word=="6.") sectionColor=UI_PURPLE;
      }

      while(StringLen(word)>0){
         // Keep every label <=63 characters. This is the critical fix.
         int take=MathMin(63,StringLen(word));
         string piece=StringSubstr(word,0,take);
         uint pw=0,ph=0; TextGetSize(piece,pw,ph);
         string candidate=(segment=="")?piece:(segment+" "+piece);
         uint cw=0,ch=0; TextGetSize(candidate,cw,ch);

         if(segment!="" && (StringLen(candidate)>63 || (int)cw>descRight-cursorX)){
            uint sw=0,sh=0; TextGetSize(segment,sw,sh);
            UILabel("StrategyDesc"+IntegerToString(rowIndex)+"_"+IntegerToString(segIndex),segment,cursorX,rowY,7,sectionColor,"Segoe UI");
            cursorX+=(int)sw+3; segIndex++; segment="";
            candidate=piece; cw=pw;
         }

         // If the next segment cannot fit to the right, start a new visual row.
         if(segment=="" && cursorX!=descLeft && (int)cw>descRight-cursorX){
            rowIndex++; rowY+=descLineStep; cursorX=descLeft; segIndex=0;
         }

         if(segment=="") segment=piece;
         else segment=candidate;
         word=StringSubstr(word,take);

         // A word longer than 63 chars is continued on the same row when possible.
         if(word!="" && StringLen(segment)>=63){
            uint sw2=0,sh2=0; TextGetSize(segment,sw2,sh2);
            UILabel("StrategyDesc"+IntegerToString(rowIndex)+"_"+IntegerToString(segIndex),segment,cursorX,rowY,7,sectionColor,"Segoe UI");
            cursorX+=(int)sw2+3; segIndex++; segment="";
         }
      }
   }

   if(segment!=""){
      UILabel("StrategyDesc"+IntegerToString(rowIndex)+"_"+IntegerToString(segIndex),segment,cursorX,rowY,7,sectionColor,"Segoe UI");
   }


}

void DrawMetricsPanel(const int y){
   const int x=InpPanelX, w=650;
   int panelY = y + 686;

   // Metrics selector is display-only: never recalculate or mutate the live trading strategy here.
   // The live engine continues to use g_strategy; this page only changes its presentation.
   UIRect("MetricsPanel", x, panelY, w, 790, UI_BG, UI_BORDER);
   UILabel("MetricsTitle", "INSTITUTIONAL COCKPIT  |  RISK • EXECUTION • ALPHA • RESEARCH", x+18, panelY+8, 10, UI_CYAN, "Segoe UI Semibold");
   UILabel("MetricsHeartbeat", "Pipeline: DISCOVER -> SCORE -> REGIME -> RISK -> EXECUTE -> MANAGE -> EXIT | Update "+IntegerToString((int)MathMax(0,TimeCurrent()-g_inst_last_update))+"s ago", x+18, panelY+22, 6, UI_MUTED, "Segoe UI");
   if(g_metrics_strategy==ARBITRAGE_SPREAD){
      double spreadPct=g_metrics_live_spread;
      string bias=(spreadPct>0.0)?"SELL A / BUY B":((spreadPct<0.0)?"BUY A / SELL B":"NEUTRAL");
      color sc=(MathAbs(spreadPct)>0.0)?(spreadPct>0.0?UI_ORANGE:UI_GREEN):UI_MUTED;
      UIRect("SpreadMet1",x+10,panelY+36,630,42,UI_CARD);
      UILabel("SpreadMet1Title","ARBITRAGE SPREAD",x+18,panelY+42,7,UI_CYAN,"Segoe UI Semibold");
      UILabel("SpreadMet1Value",InpSymbolA+" / "+InpSymbolB+" | Spread "+DoubleToString(spreadPct,4)+"% | Fair Value 0.0000%",x+18,panelY+54,7,UI_TEXT,"Segoe UI");
      UILabelRight("SpreadMet1Status",bias,x+622,panelY+42,7,sc,"Segoe UI Semibold");
      UIRect("SpreadMet2",x+10,panelY+82,630,42,UI_CARD);
      UILabel("SpreadMet2Title","CONVERGENCE MODEL",x+18,panelY+88,7,UI_GREEN,"Segoe UI Semibold");
      UILabel("SpreadMet2Value","Zero-centered fair value | Beta "+DoubleToString(g_metrics_live_beta,4)+" | Z "+DoubleToString(g_metrics_live_z,2)+" | Positive -> SELL A / BUY B | Negative -> BUY A / SELL B",x+18,panelY+100,7,UI_TEXT,"Segoe UI");
      UILabelRight("SpreadMet2Status","0 CORE",x+622,panelY+88,7,UI_GREEN,"Segoe UI Semibold");
      UIRect("SpreadMet3",x+10,panelY+128,630,42,UI_CARD);
      UILabel("SpreadMet3Title","ENTRY / EXIT",x+18,panelY+134,7,UI_ORANGE,"Segoe UI Semibold");
      UILabel("SpreadMet3Value","Live deviation "+DoubleToString(g_metrics_live_spread,5)+"% | Z "+DoubleToString(g_metrics_live_z,2)+" | Entry/exit thresholds are evaluated without changing live strategy",x+18,panelY+146,7,UI_TEXT,"Segoe UI");
      UILabelRight("SpreadMet3Status","NEXT",x+622,panelY+134,7,UI_ORANGE,"Segoe UI Semibold");
      UIRect("SpreadMet4",x+10,panelY+174,630,42,UI_CARD);
      UILabel("SpreadMet4Title","EXECUTION / COST",x+18,panelY+180,7,UI_BLUE,"Segoe UI Semibold");
      UILabel("SpreadMet4Value","Cost-aware two-leg execution, liquidity and hedge validation reserved for spread engine",x+18,panelY+192,7,UI_TEXT,"Segoe UI");
      UILabelRight("SpreadMet4Status","ISOLATED",x+622,panelY+180,7,UI_BLUE,"Segoe UI Semibold");
      UIRect("SpreadMet5",x+10,panelY+220,630,42,UI_CARD);
      UILabel("SpreadMet5Title","REGIME",x+18,panelY+226,7,UI_CYAN,"Segoe UI Semibold");
      UILabel("SpreadMet5Value","Live spread "+DoubleToString(g_metrics_live_spread,5)+"% | Z "+DoubleToString(g_metrics_live_z,2)+" | HL "+DoubleToString(g_metrics_live_hl,0)+" | real-time regime context",x+18,panelY+238,7,UI_TEXT,"Segoe UI");
      UILabelRight("SpreadMet5Status","STAGED",x+622,panelY+226,7,UI_MUTED,"Segoe UI Semibold");
      UIRect("SpreadMet6",x+10,panelY+266,630,42,UI_CARD);
      UILabel("SpreadMet6Title","RISK / SIZING",x+18,panelY+272,7,UI_RED,"Segoe UI Semibold");
      UILabel("SpreadMet6Value","Beta "+DoubleToString(g_metrics_live_beta,4)+" | Hurst "+DoubleToString(g_metrics_live_hurst,2)+" | Risk/margin protection from live account state",x+18,panelY+284,7,UI_TEXT,"Segoe UI");
      UILabelRight("SpreadMet6Status","ISOLATED",x+622,panelY+272,7,UI_GREEN,"Segoe UI Semibold");
      UIRect("SpreadMet7",x+10,panelY+312,630,42,UI_CARD);
      UILabel("SpreadMet7Title","ENGINE STATE",x+18,panelY+318,7,UI_CYAN,"Segoe UI Semibold");
      UILabel("SpreadMet7Value","Live market telemetry only | Trading engine remains "+StrategyFullName()+" | no strategy switch from METRICS",x+18,panelY+330,7,UI_TEXT,"Segoe UI");
      UILabelRight("SpreadMet7Status","SAFE",x+622,panelY+318,7,UI_GREEN,"Segoe UI Semibold");
      UIRect("SpreadMet8",x+10,panelY+358,630,42,UI_CARD);
      UILabel("SpreadMet8Title","PAIR / FAIR VALUE",x+18,panelY+364,7,UI_BLUE,"Segoe UI Semibold");
      UILabel("SpreadMet8Value",InpSymbolA+" / "+InpSymbolB+" | Center 0.0000% | Current "+DoubleToString(spreadPct,4)+"%",x+18,panelY+376,7,UI_TEXT,"Segoe UI");
      UILabelRight("SpreadMet8Status","MAP READY",x+622,panelY+364,7,UI_CYAN,"Segoe UI Semibold");
      string ars1="ARBITRAGE SPREAD | zero-centered fair value | dedicated strategy branch";
      string ars2="LIVE MARKET: "+InpSymbolA+"/"+InpSymbolB+" | Spread "+DoubleToString(g_metrics_live_spread,5)+"% | Z "+DoubleToString(g_metrics_live_z,2)+" | Beta "+DoubleToString(g_metrics_live_beta,4);
      string ars3="RESEARCH: real-time shadow analytics only | Working strategy remains "+StrategyFullName();
      string ars4="RISK: cost / liquidity / hedge / legging / convergence protection monitored live";
      UILabel("MetFooter",ars1,x+18,panelY+404,6,UI_MUTED,"Segoe UI");
      UILabel("MetResearch",ars2,x+18,panelY+416,6,UI_TEXT,"Segoe UI");
      UILabel("MetCalibration",ars3,x+18,panelY+428,6,UI_MUTED,"Segoe UI");
      UILabel("MetWF",ars4,x+18,panelY+440,6,UI_CYAN,"Segoe UI");
      DrawInstitutionalMetrics(panelY,x,w);
      return;
   }
   if(g_metrics_strategy==MULTI_STATISTICAL){
      // MULTI STATISTICAL keeps the same existing card geometry, but uses
      // its own portfolio/universe metrics instead of duplicating PAIR metrics.
      UIRect("MetRow1", x+10, panelY+36, 630, 42, UI_CARD);
      UILabel("Met1Title", "MULTI-PAIR Z-SCORE", x+18, panelY+42, 7, UI_GREEN, "Segoe UI Semibold");
      string bestMulti="Best Z: "+DoubleToString(g_metrics_live_z,2)+" | "+(g_metrics_live_pair!=""?g_metrics_live_pair:"Scanning universe");
      UILabel("Met1Value", bestMulti+" | Waiting for cycle", x+18, panelY+54, 7, UI_TEXT, "Segoe UI");
      UILabelRight("Met1Status", "UNIVERSE", x+622, panelY+42, 7, UI_GREEN, "Segoe UI Semibold");

      UIRect("MetRow2", x+10, panelY+82, 630, 42, UI_CARD);
      UILabel("Met2Title", "PORTFOLIO ALLOCATION", x+18, panelY+88, 7, UI_ORANGE, "Segoe UI Semibold");
      double mbudget=InpPortfolioRiskBudgetPct*MathMax(0.25,g_inst.riskMultiplier);
      double malloc=0.0;
      for(int ai=0;ai<ArraySize(g_alloc_risk_pct);ai++) malloc+=g_alloc_risk_pct[ai];
      UILabel("Met2Value", "Budget: "+DoubleToString(mbudget,2)+"% | Allocated: "+DoubleToString(malloc,2)+"% | Remaining: "+DoubleToString(MathMax(0.0,mbudget-malloc),2)+"%", x+18, panelY+100, 7, UI_TEXT, "Segoe UI");
      UILabelRight("Met2Status", "ACTIVE", x+622, panelY+88, 7, UI_GREEN, "Segoe UI Semibold");

      UIRect("MetRow3", x+10, panelY+128, 630, 42, UI_CARD);
      UILabel("Met3Title", "CLUSTER RISK FILTER", x+18, panelY+134, 7, UI_BLUE, "Segoe UI Semibold");
      UILabel("Met3Value", "Clusters "+IntegerToString(g_inst.clusterCount)+" | Portfolio risk "+DoubleToString(g_inst.portfolioRiskPct,2)+"% | Exposure "+DoubleToString(g_inst.currentExposureLots,2)+" lot", x+18, panelY+146, 7, UI_TEXT, "Segoe UI");
      UILabelRight("Met3Status", g_inst.gatePass?"PASS":"FILTER", x+622, panelY+134, 7, g_inst.gatePass?UI_GREEN:UI_ORANGE, "Segoe UI Semibold");

      UIRect("MetRow4", x+10, panelY+174, 630, 42, UI_CARD);
      UILabel("Met4Title", "CROSS-PAIR DIVERSIFICATION", x+18, panelY+180, 7, UI_CYAN, "Segoe UI Semibold");
      UILabel("Met4Value", "Candidates: "+IntegerToString(g_metrics_top_count)+" | Top score: "+DoubleToString(g_metrics_top_score[0],0)+" | Multi-pair universe", x+18, panelY+192, 7, UI_TEXT, "Segoe UI");
      UILabelRight("Met4Status", "MULTI", x+622, panelY+180, 7, UI_CYAN, "Segoe UI Semibold");

      UIRect("MetRow5", x+10, panelY+220, 630, 42, UI_CARD);
      UILabel("Met5Title", "EXECUTION PROTECTION", x+18, panelY+226, 7, UI_ORANGE, "Segoe UI Semibold");
      string mcbtxt=ExecutionCircuitActive()?("CIRCUIT PAUSED "+IntegerToString((int)MathMax(0,g_circuit_pause_until-TimeCurrent()))+"s"):"READY";
      UILabel("Met5Value", "AbortWin="+IntegerToString(g_abort_window_count)+" | Reconcile="+IntegerToString(g_reconcile_failures_today)+" | EF="+IntegerToString(g_execution_failures_today)+" | "+mcbtxt, x+18, panelY+238, 7, ExecutionCircuitActive()?UI_RED:UI_TEXT, "Segoe UI");
      UILabelRight("Met5Status", "PORTFOLIO", x+622, panelY+226, 7, ExecutionCircuitActive()?UI_RED:UI_GREEN, "Segoe UI Semibold");

      UIRect("MetRow6", x+10, panelY+266, 630, 42, UI_CARD);
      UILabel("Met6Title", "REALIZED COSTS", x+18, panelY+272, 7, UI_RED, "Segoe UI Semibold");
      UILabel("Met6Value", "Comm "+DoubleToString(g_daily_commission,2)+" | Swap "+DoubleToString(g_daily_swap,2)+" | Fees "+DoubleToString(g_daily_fees,2)+" | OpenSwap "+DoubleToString(g_open_swap,2), x+18, panelY+284, 7, UI_TEXT, "Segoe UI");
      UILabelRight("Met6Status", "HISTORY", x+622, panelY+272, 7, UI_CYAN, "Segoe UI Semibold");

      UIRect("MetRow7", x+10, panelY+312, 630, 42, UI_CARD);
      UILabel("Met7Title", "TRADE TELEMETRY", x+18, panelY+318, 7, UI_CYAN, "Segoe UI Semibold");
      string mtx="TX "+IntegerToString((long)g_trade_tx_count)+" | "+(g_last_trade_symbol!=""?g_last_trade_symbol:"NO DEAL")+" | "+g_last_trade_action+" | fill "+DoubleToString(g_last_fill_price,5);
      UILabel("Met7Value", mtx, x+18, panelY+330, 7, UI_TEXT, "Segoe UI");
      UILabelRight("Met7Status", g_last_trade_retcode>0?IntegerToString((int)g_last_trade_retcode):"WAITING", x+622, panelY+318, 7, g_last_trade_retcode>0?UI_GREEN:UI_MUTED, "Segoe UI Semibold");

      UIRect("MetRow8", x+10, panelY+358, 630, 42, UI_CARD);
      UILabel("Met8Title", "MULTI-PAIR INTELLIGENCE", x+18, panelY+364, 7, UI_BLUE, "Segoe UI Semibold");
      string mit="Queue "+IntegerToString(g_metrics_top_count)+" | Top score "+DoubleToString(g_metrics_top_score[0],0)+" | "+(g_metrics_top_a[0]!=""?g_metrics_top_a[0]+"/"+g_metrics_top_b[0]:"SCANNING");
      UILabel("Met8Value", mit, x+18, panelY+376, 7, UI_TEXT, "Segoe UI");
      UILabelRight("Met8Status", InpUsePairIntelligence?(g_metrics_live_score>=EffectivePairIntelligenceThreshold()?"READY":"FILTER"):"OFF", x+622, panelY+364, 7, InpUsePairIntelligence?(g_metrics_live_score>=EffectivePairIntelligenceThreshold()?UI_GREEN:UI_ORANGE):UI_MUTED, "Segoe UI Semibold");

      string mrsum="", mbsum="", mwsum=""; ResearchSummary(mrsum); ResearchBucketSummary(mbsum); WalkForwardSummary(mwsum);
      UILabel("MetFooter", "MULTI STATISTICAL | Live Z="+DoubleToString(g_metrics_live_z,2)+" | Pair="+(g_metrics_live_pair!=""?g_metrics_live_pair:"--")+" | Est.EntryCost="+DoubleToString(g_total_cost_money,2)+" | History="+(g_is_tester?"TESTER":"LIVE"), x+18, panelY+404, 6, UI_MUTED, "Segoe UI");
      UILabel("MetResearch", mrsum, x+18, panelY+416, 6, UI_TEXT, "Segoe UI");
      UILabel("MetCalibration", mbsum, x+330, panelY+416, 6, UI_MUTED, "Segoe UI");
      UILabel("MetWF", mwsum, x+18, panelY+428, 6, g_wf_gate_pass?UI_GREEN:UI_MUTED, "Segoe UI");
      UILabel("MetPI2", "PAIR INTELLIGENCE "+(InpUsePairIntelligenceV2?(DoubleToString(g_pi2_score,0)+" / Conv "+DoubleToString(g_pi2_conviction,0)+" | "+(g_metrics_live_pair!=""?g_metrics_live_pair:"--")):"SHADOW OFF"), x+330, panelY+428, 6, InpUsePairIntelligenceV2?(g_pi2_score>=EffectivePairIntelligenceThreshold()?UI_GREEN:UI_ORANGE):UI_MUTED, "Segoe UI");
      DrawInstitutionalMetrics(panelY,x,w);
      return;
   }

   UIRect("MetRow1", x+10, panelY+36, 630, 42, UI_CARD);
   UILabel("Met1Title", "Z-SCORE TRAILING EXIT", x+18, panelY+42, 7, UI_GREEN, "Segoe UI Semibold");
   string trailStatus=OpenCyclesCount()>0?("Best Z: "+DoubleToString(g_cycles[0].bestZ,2)+" | Dynamic Exit: "+DoubleToString(g_cycles[0].dynamicExitZ,2)+" | ENGINE LIVE"):("Live Z: "+DoubleToString(g_metrics_live_z,2)+(g_metrics_live_pair!=""?" | "+g_metrics_live_pair:"")+" | Waiting for cycle");
   UILabel("Met1Value", trailStatus, x+18, panelY+54, 7, UI_TEXT, "Segoe UI");
   UILabelRight("Met1Status", InpUseZTrailingExit?"ENABLED":"DISABLED", x+622, panelY+42, 7, InpUseZTrailingExit?UI_GREEN:UI_RED, "Segoe UI Semibold");
   UIRect("MetRow2", x+10, panelY+82, 630, 42, UI_CARD);
   UILabel("Met2Title", "DYNAMIC SLIPPAGE", x+18, panelY+88, 7, UI_ORANGE, "Segoe UI Semibold");
   double currentSlip=CurrentDynamicSlippagePoints(g_metrics_live_z);
   UILabel("Met2Value", "Base: "+DoubleToString(InpMaxSlippagePoints,0)+" pts | Current: "+DoubleToString(currentSlip,1)+" pts | LIVE EXECUTION PARAM", x+18, panelY+100, 7, UI_TEXT, "Segoe UI");
   UILabelRight("Met2Status", InpUseDynamicSlippage?"ENABLED":"DISABLED", x+622, panelY+88, 7, InpUseDynamicSlippage?UI_GREEN:UI_RED, "Segoe UI Semibold");
   UIRect("MetRow3", x+10, panelY+128, 630, 42, UI_CARD);
   UILabel("Met3Title", "VOLATILITY MACRO FILTER", x+18, panelY+134, 7, UI_BLUE, "Segoe UI Semibold");
   string volStatus=!InpUseVolatilityFilter?"DISABLED":(g_cached_atr_avg>0?((g_cached_vol_ok?"NORMAL - Trading Allowed":"HIGH - Trading Paused")+" | ATR "+DoubleToString(g_cached_atr_current,5)+" / avg "+DoubleToString(g_cached_atr_avg,5)):(g_cached_vol_ok?"NORMAL - Trading Allowed":"HIGH - Trading Paused"));
   UILabel("Met3Value", volStatus, x+18, panelY+146, 7, g_cached_vol_ok?UI_TEXT:UI_RED, "Segoe UI");
   UILabelRight("Met3Status", InpUseVolatilityFilter?"ENGINE FILTER":"DISABLED", x+622, panelY+134, 7, InpUseVolatilityFilter?UI_GREEN:UI_RED, "Segoe UI Semibold");
   UIRect("MetRow4", x+10, panelY+174, 630, 42, UI_CARD);
   UILabel("Met4Title", "TIME-BASED PROFIT LOCK", x+18, panelY+180, 7, UI_CYAN, "Segoe UI Semibold");
   string timeScaleText="Trigger: "+DoubleToString(InpTimeScaleThresholdPct*100,0)+"% of Half-Life";
   bool timeFeatureActive=(InpUseTimeBasedScaling&&InpUsePartialTP);
   if(OpenCyclesCount()>0 && g_cycles[0].halfLifeAtEntry>0){ int held=(int)(TimeCurrent()-g_cycles[0].startTime); int secPerBar=MathMax(1,PeriodSeconds(PERIOD_CURRENT)); int currentBars=held/secPerBar; double thresholdBars=g_cycles[0].halfLifeAtEntry*InpTimeScaleThresholdPct; double pct=(thresholdBars>0)?(currentBars/thresholdBars*100):0; timeScaleText+=" | Held: "+IntegerToString(currentBars)+" bars | "+DoubleToString(pct,0)+"%"; } else timeScaleText+=" | No active cycle";
   UILabel("Met4Value", timeScaleText, x+18, panelY+192, 7, UI_TEXT, "Segoe UI");
   UILabelRight("Met4Status", timeFeatureActive?"ACTIVE":"DISABLED", x+622, panelY+180, 7, timeFeatureActive?UI_GREEN:UI_RED, "Segoe UI Semibold");
   UIRect("MetRow5", x+10, panelY+220, 630, 42, UI_CARD);
   UILabel("Met5Title", "EXECUTION PROTECTION", x+18, panelY+226, 7, UI_ORANGE, "Segoe UI Semibold");
   string cbtxt=ExecutionCircuitActive()?("CIRCUIT PAUSED "+IntegerToString((int)MathMax(0,g_circuit_pause_until-TimeCurrent()))+"s"):"READY";
   UILabel("Met5Value", "AbortWin="+IntegerToString(g_abort_window_count)+" | Reconcile="+IntegerToString(g_reconcile_failures_today)+" | EF="+IntegerToString(g_execution_failures_today)+" | "+cbtxt, x+18, panelY+238, 7, ExecutionCircuitActive()?UI_RED:UI_TEXT, "Segoe UI");
   UILabelRight("Met5Status", g_last_audit_phase!=""?g_last_audit_phase:"NO EVENTS", x+622, panelY+226, 7, ExecutionCircuitActive()?UI_RED:UI_GREEN, "Segoe UI Semibold");
   UIRect("MetRow6", x+10, panelY+266, 630, 42, UI_CARD);
   UILabel("Met6Title", "REALIZED COSTS", x+18, panelY+272, 7, UI_RED, "Segoe UI Semibold");
   UILabel("Met6Value", "Comm "+DoubleToString(g_daily_commission,2)+" | Swap "+DoubleToString(g_daily_swap,2)+" | Fees "+DoubleToString(g_daily_fees,2)+" | OpenSwap "+DoubleToString(g_open_swap,2), x+18, panelY+284, 7, UI_TEXT, "Segoe UI");
   UILabelRight("Met6Status", "HISTORY", x+622, panelY+272, 7, UI_CYAN, "Segoe UI Semibold");
   UIRect("MetRow7", x+10, panelY+312, 630, 42, UI_CARD);
   UILabel("Met7Title", "TRADE TELEMETRY", x+18, panelY+318, 7, UI_CYAN, "Segoe UI Semibold");
   string tx="TX "+IntegerToString((long)g_trade_tx_count)+" | "+(g_last_trade_symbol!=""?g_last_trade_symbol:"NO DEAL")+" | "+g_last_trade_action+" | fill "+DoubleToString(g_last_fill_price,5);
   UILabel("Met7Value", tx, x+18, panelY+330, 7, UI_TEXT, "Segoe UI");
   UILabelRight("Met7Status", g_last_trade_retcode>0?IntegerToString((int)g_last_trade_retcode):"WAITING", x+622, panelY+318, 7, g_last_trade_retcode>0?UI_GREEN:UI_MUTED, "Segoe UI Semibold");
   UIRect("MetRow8", x+10, panelY+358, 630, 42, UI_CARD);
   UILabel("Met8Title", "PAIR INTELLIGENCE", x+18, panelY+364, 7, UI_BLUE, "Segoe UI Semibold");
   string it="Score "+DoubleToString(g_metrics_live_score,0)+" | Lot x"+DoubleToString(g_metrics_live_lot_factor,2)+" | "+(g_metrics_live_pair!=""?g_metrics_live_pair:"SCANNING");
   UILabel("Met8Value", it, x+18, panelY+376, 7, UI_TEXT, "Segoe UI");
   UILabelRight("Met8Status", InpUsePairIntelligence?(g_metrics_live_score>=EffectivePairIntelligenceThreshold()?"READY":"FILTER") : "OFF", x+622, panelY+364, 7, InpUsePairIntelligence?(g_metrics_live_score>=EffectivePairIntelligenceThreshold()?UI_GREEN:UI_ORANGE):UI_MUTED, "Segoe UI Semibold");
   string rsum="", bsum="", wsum=""; ResearchSummary(rsum); ResearchBucketSummary(bsum); WalkForwardSummary(wsum);
   UILabel("MetFooter", "Live Z="+DoubleToString(g_metrics_live_z,2)+" | Pair="+(g_metrics_live_pair!=""?g_metrics_live_pair:"--")+" | Est.EntryCost="+DoubleToString(g_total_cost_money,2)+" | History="+(g_is_tester?"TESTER":"LIVE"), x+18, panelY+404, 6, UI_MUTED, "Segoe UI");
   UILabel("MetResearch", rsum, x+18, panelY+416, 6, UI_TEXT, "Segoe UI");
   UILabel("MetCalibration", bsum, x+330, panelY+416, 6, UI_MUTED, "Segoe UI");
   UILabel("MetWF", wsum, x+18, panelY+428, 6, g_wf_gate_pass?UI_GREEN:UI_MUTED, "Segoe UI");
   UILabel("MetPI2", "PAIR INTELLIGENCE "+(InpUsePairIntelligenceV2?(DoubleToString(g_pi2_score,0)+" / Conv "+DoubleToString(g_pi2_conviction,0)+" | "+(g_metrics_live_pair!=""?g_metrics_live_pair:"--")):"SHADOW OFF"), x+330, panelY+428, 6, InpUsePairIntelligenceV2?(g_pi2_score>=EffectivePairIntelligenceThreshold()?UI_GREEN:UI_ORANGE):UI_MUTED, "Segoe UI");
   DrawInstitutionalMetrics(panelY,x,w);
}

void BuildUI(){
   if(g_is_tester) return;
   g_ui_last_refresh=TimeCurrent();
   UIDeleteAll();
   if(!g_ui_visible){
      UIButton("ShowPanel", "ARBITRAGE PRO QUANT INSTITUTIONAL v1.0(ENG)", InpPanelX, InpPanelY, 300, 30, UI_BLUE, UI_TEXT);
      ObjectSetInteger(0, UIName("ShowPanel"), OBJPROP_FONTSIZE, 8);
      ChartRedraw(); return;
   }
   if(g_show_portfolio_panel){ DrawPortfolioIntelligencePage(); ChartRedraw(); return; }
   if(g_show_quant_detail){ DrawQuantDetailPage(); ChartRedraw(); return; }
   if(g_show_strategy_panel){ DrawStrategyPage(); ChartRedraw(); return; }
   if(g_show_metrics_panel){ DrawMetricsPage(); ChartRedraw(); return; }
   if(g_news_visible){ DrawNewsPage(); ChartRedraw(); return; }
   if(g_show_symbol_map){ DrawSymbolMapPage(); ChartRedraw(); return; }
   if(g_show_alpha_page){ DrawAlphaPage(); ChartRedraw(); return; }
   const int x=InpPanelX, y=InpPanelY, w=650, main_h=728, news_y=y+main_h+8;
   int rows=g_news_visible?NewsVisibleRows():0;
   int news_h=81+rows*34+24;
   int strategy_h=g_show_strategy_panel?220:0;
   int metrics_h=g_show_metrics_panel?790:0;
   int map_h=0;
   if(g_show_symbol_map){
      int mapRows=SymbolMapVisibleRows();
      map_h=81+mapRows*21+30;
   }
   const int total_h=g_news_visible?main_h+8+news_h:(g_show_strategy_panel?main_h+8+strategy_h:(g_show_metrics_panel?main_h+8+metrics_h:(g_show_symbol_map?main_h+8+map_h:main_h)));
   UIRect("MainPanel", x, y, w, total_h, UI_BG, UI_BORDER);
   UILabel("Title", "ARBITRAGE PRO QUANT INSTITUTIONAL v1.0(ENG)", x+10, y+15, 15, UI_TEXT, "Segoe UI Semibold");
   UILabel("Version", "MT5 | ENG", x+10, y+40, 8, UI_CYAN);
   bool marketDataLive=DashboardMarketDataLive();
   bool systemHealthy=DashboardSystemHealthy();
   bool dashboardHealthy=marketDataLive && systemHealthy;
   color statusColor=dashboardHealthy?UI_GREEN:UI_RED;
   UILabel("StatusDot", "●", x+550, y+18, 11, statusColor);
   UILabel("ModeText", UIModeShort()+(g_paused?" [P]":""), x+568, y+19, 8, UI_TEXT);
   UILabel("Control", "CONTROL CENTER", x+10, y+67, 9, UI_CYAN);
   string engineTxt=(g_status==""?"READY":g_status); if(StringLen(engineTxt)>42) engineTxt=StringSubstr(engineTxt,0,42)+"...";
   UIButton("ModeSignals", "SIGNALS", x+10, y+104, 118, 30, g_mode==SIGNALS_ONLY?UI_BLUE:UI_CARD2);
   UIButton("ModeDemo", "DEMO", x+134, y+104, 118, 30, g_mode==DEMO_TRADING?UI_BLUE:UI_CARD2);
   UIButton("ModeLive", "LIVE", x+258, y+104, 118, 30, g_mode==LIVE_TRADING?UI_BLUE:UI_CARD2);
   UIButton("PauseBtn", g_paused?"RESUME":"PAUSE", x+382, y+104, 118, 30, g_paused?UI_ORANGE:UI_CARD2);
   UIButton("CloseAll", "CLOSE ALL", x+506, y+104, 134, 30, UI_DARK_RED, UI_TEXT);
   UpdateTopPairs(); CalculateCapitalAllocation();
   bool dashSpread=(g_strategy==ARBITRAGE_SPREAD);
   UILabel("Scanner", dashSpread?"ARBITRAGE SPREAD MARKET MONITOR (LIVE A/B + MARKET CONTEXT)":"MARKET SCANNER + CORRELATION (TOP PAIRS STRATEGY)", x+10, y+146, 9, UI_CYAN);
   for(int i=0;i<3;i++){
      int yy=y+162+i*36;
      UIRect("ScanRow"+IntegerToString(i), x+10, yy, 630, 32, UI_CARD);

      bool arbRow=(dashSpread && i==0);
      bool arbContextRow=(dashSpread && i>0);
      string rowA="",rowB="";
      if(arbRow){ rowA=InpSymbolA; rowB=InpSymbolB; }
      else if(arbContextRow){
         int ci=i-1;
         if(ci<g_scan_count){ rowA=g_scan_a[ci]; rowB=g_scan_b[ci]; }
      }
      else if(i<ArraySize(g_top_a)){ rowA=g_top_a[i]; rowB=g_top_b[i]; }

      MqlTick da,db;
      bool rowTicks=(rowA!=""&&rowB!="")&&GetTickSafe(rowA,da)&&GetTickSafe(rowB,db);
      bool rowFresh=rowTicks && QuoteFresh(da) && QuoteFresh(db);
      double rowMidA=rowTicks?(da.bid+da.ask)*0.5:0.0;
      double rowMidB=rowTicks?(db.bid+db.ask)*0.5:0.0;
      double rowDev=(rowMidA>0.0&&rowMidB>0.0)?(rowMidA/rowMidB-1.0)*100.0:0.0;
      double rowPointA=PointOf(rowA),rowPointB=PointOf(rowB);
      double rowSprA=(rowTicks&&rowPointA>0.0)?(da.ask-da.bid)/rowPointA:0.0;
      double rowSprB=(rowTicks&&rowPointB>0.0)?(db.ask-db.bid)/rowPointB:0.0;

      SPairStat rowStat; ResetStat(rowStat);
      bool rowStatOk=(!arbRow && rowA!=""&&rowB!="") ? GetStat(rowA,rowB,rowStat) : false;
      bool hasPair=(rowA!=""&&rowB!="");
      UILabel("ScanDot"+IntegerToString(i), "●", x+20, yy+9, 9, hasPair?(rowFresh?UI_GREEN:UI_ORANGE):UI_MUTED);
      string pairName=hasPair?(rowA+"/"+rowB):"--/--";
      UILabel("ScanSym"+IntegerToString(i), pairName, x+32, yy+9, 9, hasPair?UI_TEXT:UI_MUTED);

      string sprText=hasPair&&rowTicks?(DoubleToString(rowSprA,1)+"/"+DoubleToString(rowSprB,1)+" pt"):"--";
      color sprColor=hasPair?(rowFresh?UI_GREEN:UI_ORANGE):UI_MUTED;
      UILabel("ScanSpread"+IntegerToString(i), sprText, x+150, yy+9, 8, sprColor);

      string stText=hasPair?(SessionOK()?(rowFresh?"READY":"STALE"):"OFF-SESSION"):"--";
      color stColor=hasPair?(rowFresh?UI_GREEN:UI_ORANGE):UI_MUTED;
      UILabel("ScanState"+IntegerToString(i), stText, x+250, yy+9, 8, stColor);

      string intelText=arbRow?("DEV "+DoubleToString(rowDev,3)+"%"):((rowStatOk&&InpUsePairIntelligence)?("S="+DoubleToString(rowStat.z,2)):"MARKET CONTEXT");
      color intelColor=arbRow?(rowDev>=0?UI_ORANGE:UI_GREEN):UI_CYAN;
      UILabel("ScanStrategy"+IntegerToString(i), intelText, x+345, yy+9, 8, intelColor);

      string kalText=arbRow?"FAIR 0.000%":(rowStatOk?("β="+DoubleToString(rowStat.beta,2)+" HL="+DoubleToString(rowStat.halfLife,0)):"LIVE");
      color kalColor=arbRow?UI_CYAN:UI_CYAN;
      UILabel("ScanKal"+IntegerToString(i), kalText, x+425, yy+9, 7, kalColor);

      string corrText=arbRow?(rowDev>0?"SELL A/B":(rowDev<0?"BUY A/B":"NEUTRAL")):(rowStatOk?"r="+DoubleToString(rowStat.corr*100,0)+"%":"--");
      color corrColor=arbRow?UI_CYAN:(rowStatOk?GetCorrelationColor(rowStat.corr):UI_MUTED);
      UILabel("ScanCorr"+IntegerToString(i), corrText, x+520, yy+9, 7, corrColor);
   }
   UILabel("AccountLabel", "ACCOUNT INFO", x+10, y+278, 9, UI_CYAN);
   UIRect("AccCard", x+10, y+292, 630, 38, UI_CARD);
   double balance=(g_mode==DEMO_TRADING)?g_demo_balance:AccountInfoDouble(ACCOUNT_BALANCE);
   double equity=(g_mode==DEMO_TRADING)?VirtualEquity():AccountInfoDouble(ACCOUNT_EQUITY);
   double freeMargin=(g_mode==DEMO_TRADING)?0:AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   double margin=(g_mode==DEMO_TRADING)?0:AccountInfoDouble(ACCOUNT_MARGIN);
   double marginLevel=(margin>0)?equity/margin*100:0;
   double dailyPnL=equity-g_day_start_equity;
   double dailyPnLPct=(g_day_start_equity>0)?(dailyPnL/g_day_start_equity*100):0;
   UILabel("AccBalT", "BALANCE", x+20, y+298, 7, UI_MUTED);
   UILabel("AccBalV", DoubleToString(balance, 2), x+20, y+310, 9, UI_TEXT);
   UILabel("AccEqT", "EQUITY", x+140, y+298, 7, UI_MUTED);
   UILabel("AccEqV", DoubleToString(equity, 2), x+140, y+310, 9, equity>=balance?UI_GREEN:UI_RED);
   UILabel("AccFmT", "FREE MARGIN", x+260, y+298, 7, UI_MUTED);
   UILabel("AccFmV", DoubleToString(freeMargin, 2), x+260, y+310, 9, UI_TEXT);
   UILabel("AccMlT", "MARGIN LEVEL", x+380, y+298, 7, UI_MUTED);
   UILabel("AccMlV", DoubleToString(marginLevel, 1)+"%", x+380, y+310, 9, (margin>0)?(marginLevel>=InpMinMarginLevel?UI_GREEN:UI_RED):UI_GREEN);
   UILabel("AccPnLT", "DAILY P&L", x+500, y+298, 7, UI_MUTED);
   UILabel("AccPnLV", DoubleToString(dailyPnL, 2)+" ("+DoubleToString(dailyPnLPct, 2)+"%)", x+500, y+310, 8, dailyPnL>=0?UI_GREEN:UI_RED);
   UILabel("CycleLabel", "CYCLE & COSTS", x+10, y+340, 9, UI_CYAN);
   UIRect("CycleCard", x+10, y+354, 630, 82, UI_CARD);
   double unrealizedPnL=TotalUnrealizedPnL();
   int cycleDuration=(OpenCyclesCount()>0)?(int)(TimeCurrent()-g_cycles[0].startTime):0;
   string liveText=DoubleToString(g_live_signal, 2);
   if(g_live_pair!="") liveText+=" "+g_live_pair;
   color liveColor=(MathAbs(g_live_signal)>=InpStatEntryZScore)?UI_GREEN:UI_CYAN;
   UILabel("CycEdgeT", "LIVE Z-SCORE", x+20, y+360, 7, UI_MUTED);
   UILabel("CycEdgeV", liveText, x+20, y+372, 9, liveColor);
   UILabel("CycPnLT", "UNREALIZED P&L", x+230, y+360, 7, UI_MUTED);
   UILabel("CycPnLV", DoubleToString(unrealizedPnL, 2), x+230, y+372, 9, unrealizedPnL>=0?UI_GREEN:UI_RED);
   UILabel("CycDurT", "DURATION", x+440, y+360, 7, UI_MUTED);
   UILabel("CycDurV", IntegerToString(cycleDuration)+" sec", x+440, y+372, 9, UI_TEXT);
   UILabel("CostCommT", "COMMISSION", x+20, y+386, 7, UI_MUTED);
   UILabel("CostCommV", DoubleToString(g_daily_commission, 2), x+20, y+398, 9, g_daily_commission>0?UI_RED:UI_GREEN);
   UILabel("CostSwapT", "DAY SWAP", x+230, y+386, 7, UI_MUTED);
   UILabel("CostSwapV", DoubleToString(g_daily_swap, 2), x+230, y+398, 9, g_daily_swap>=0?UI_GREEN:UI_RED);
   UILabel("CostSprT", "SPREAD COST", x+440, y+386, 7, UI_MUTED);
   UILabel("CostSprV", DoubleToString(g_daily_spread, 2), x+440, y+398, 9, g_daily_spread>0?UI_RED:UI_GREEN);
   UILabel("TcT", "EST. ENTRY COST", x+20, y+412, 7, UI_MUTED);
   UILabel("TcV", DoubleToString(g_total_cost_money, 2), x+20, y+424, 9, g_total_cost_money>0?UI_ORANGE:UI_GREEN);
   UILabel("SlT", "SLIPPAGE (DAY)", x+230, y+412, 7, UI_MUTED);
   UILabel("SlV", DoubleToString(g_daily_slippage, 1)+" pt", x+230, y+424, 9, g_daily_slippage>InpMaxSlippagePoints?UI_RED:UI_TEXT);
   UILabel("CycNT", "OPEN CYCLES", x+440, y+412, 7, UI_MUTED);
   UILabel("CycNV", IntegerToString(OpenCyclesCount())+"/"+IntegerToString(InpMaxOpenCycles), x+440, y+424, 9, UI_TEXT);
   UpdateExposureView();
   UILabel("ExpLabel", "CURRENCY EXPOSURE MATRIX (LOTS)", x+10, y+446, 9, UI_CYAN);
   UIRect("ExpCard", x+10, y+460, 630, 34, UI_CARD);
   if(g_exp_count==0) UILabel("ExpFlat", "NO OPEN POSITIONS - EXPOSURE FLAT", x+20, y+472, 9, UI_GREEN);
   else {
      int xs3[3]; xs3[0]=x+20; xs3[1]=x+230; xs3[2]=x+440;
      double lim=InpMaxExposureLots*g_lot_scale;
      for(int k=0;k<3;k++){
         if(k<g_exp_count)
            UILabel("Exp"+IntegerToString(k), g_exp_cur[k]+": "+((g_exp_lots[k]>=0)?"+":"")+DoubleToString(g_exp_lots[k], 2),
                    xs3[k], y+472, 9, (MathAbs(g_exp_lots[k])<=lim)?UI_GREEN:UI_RED);
         else UILabel("Exp"+IntegerToString(k), "--", xs3[k], y+472, 9, UI_MUTED);
      }
   }
   UILabel("StatsLabel", "SESSION STATS (CLOSED)", x+10, y+504, 9, UI_CYAN);
   UIRect("StatsCard", x+10, y+518, 630, 34, UI_CARD);
   double winRate=(g_closed_cycles_today>0)?(double)g_closed_profitable_today/g_closed_cycles_today*100:0.0;
   UILabel("StatCycles", "Cycles: "+IntegerToString(g_closed_cycles_today), x+20, y+530, 9, UI_TEXT);
   UILabel("StatWin", "Win Rate: "+DoubleToString(winRate, 1)+"%", x+230, y+530, 9, (g_closed_cycles_today>0)?((winRate>=50)?UI_GREEN:UI_RED):UI_GREEN);
   UILabel("StatProfit", "Win: "+IntegerToString(g_closed_profitable_today)+" | Abort: "+IntegerToString(g_entry_aborts_today)+" | FH: "+IntegerToString(g_failed_hedges_today), x+410, y+530, 8, g_failed_hedges_today>0?UI_ORANGE:UI_GREEN);
   UILabel("DiagLine", "DIAG: "+g_diag_status+" | RF: "+IntegerToString(g_reconcile_failures_today)+" | EF: "+IntegerToString(g_execution_failures_today)+" | CB: "+(ExecutionCircuitActive()?"PAUSED":"READY"), x+20, y+548, 7,
           g_diag_errors>0?UI_RED:(ExecutionCircuitActive()?UI_ORANGE:UI_MUTED));
   // INSTITUTIONAL DECISION RAIL: the operator sees the complete decision chain in one glance.
   UILabel("Risk", "INSTITUTIONAL DECISION RAIL", x+10, y+566, 9, UI_CYAN);
   UIRect("Risk1", x+10, y+582, 150, 44, UI_CARD);
   UIRect("Risk2", x+166, y+582, 150, 44, UI_CARD);
   UIRect("Risk3", x+322, y+582, 150, 44, UI_CARD);
   UIRect("Risk4", x+478, y+582, 162, 44, UI_CARD);

   string regimeTxt=g_inst.regime!=""?g_inst.regime:"INIT";
   color regimeCol=(regimeTxt=="CRISIS")?UI_RED:((regimeTxt=="HIGH_VOL"||regimeTxt=="TREND")?UI_ORANGE:UI_GREEN);
   UILabel("R1T", "REGIME", x+20, y+588, 6, UI_MUTED);
   UILabel("R1V", regimeTxt+" x"+DoubleToString(g_inst.riskMultiplier,2), x+20, y+600, 8, regimeCol, "Segoe UI Semibold");

   double alphaScore=(g_last_intelligence_score>0)?g_last_intelligence_score:0.0;
   string alphaTxt=(g_live_pair!=""?g_live_pair:"SCANNING")+" "+DoubleToString(alphaScore,0)+"/100";
   color alphaCol=(alphaScore>=EffectivePairIntelligenceThreshold())?UI_GREEN:UI_ORANGE;
   UILabel("R2T", "ALPHA / SIGNAL", x+176, y+588, 6, UI_MUTED);
   UILabel("R2V", alphaTxt, x+176, y+600, 8, alphaCol, "Segoe UI Semibold");

   double budgetNow=InpPortfolioRiskBudgetPct*g_inst.riskMultiplier;
   double riskUsed=g_inst.portfolioRiskPct;
   color riskCol=(g_inst.gatePass && riskUsed<budgetNow)?UI_GREEN:UI_RED;
   UILabel("R3T", "RISK BUDGET", x+332, y+588, 6, UI_MUTED);
   UILabel("R3V", DoubleToString(riskUsed,2)+" / "+DoubleToString(budgetNow,2)+"%", x+332, y+600, 8, riskCol, "Segoe UI Semibold");

   double execCost=g_inst.executionCostPct;
   bool execOK=!ExecutionCircuitActive() && execCost<=InpMaxExecutionCostPct;
   color execCol=execOK?UI_GREEN:UI_RED;
   string execTxt=execOK?"READY":"BLOCK";
   UILabel("R4T", "EXECUTION", x+488, y+588, 6, UI_MUTED);
   UILabel("R4V", execTxt+" "+DoubleToString(execCost,2)+"%", x+488, y+600, 8, execCol, "Segoe UI Semibold");

   string stx; color stc; int stf;
   if(g_paused){ stx="PAUSED"; stc=UI_ORANGE; stf=10; }
   else if(g_daily_stop){ stx="STOP: "+(g_stop_reason!=""?g_stop_reason:"RISK"); stc=UI_RED; stf=8; }
   else if(g_inst.killSwitch){ stx="INST KILL SWITCH"; stc=UI_RED; stf=8; }
   else if(!g_inst.gatePass){ stx="BLOCK: "+(g_inst.gateReason!=""?g_inst.gateReason:"RISK"); stc=UI_RED; stf=7; }
   else if(ExecutionCircuitActive()){ stx="CIRCUIT"; stc=UI_RED; stf=9; }
   else if(GlobalNewsBlocked()){ stx="NEWS"; stc=UI_ORANGE; stf=10; }
   else if(WeekendBlock()){ stx="WEEKEND"; stc=UI_ORANGE; stf=10; }
   else if(!SessionOK()){ stx="SESSION"; stc=UI_MUTED; stf=10; }
   else { stx="ACTIVE / GATE PASS"; stc=UI_GREEN; stf=8; }

   UILabel("InstStatus", "DECISION: "+stx+" | Pair="+(g_live_pair!=""?g_live_pair:"--")+" | Z="+DoubleToString(g_live_signal,2)+" | QSize x"+DoubleToString(g_inst.qualitySizingMultiplier,2)+" | ExecQ "+DoubleToString(g_inst.executionQualityScore,0)+" | Stability="+DoubleToString(g_inst.stabilityScore*100.0,0)+"%", x+10, y+625, 6, stc, "Segoe UI Semibold");
   UILabel("SelfReportHead", "SELF TEST REPORT", x+10, y+644, 7, UI_CYAN, "Segoe UI Semibold");
   for(int dri=0; dri<3; dri++){
      string rr=(dri<g_diag_report_count)?g_diag_report[dri]:"";
      if(StringLen(rr)>94) rr=StringSubstr(rr,0,94)+"...";
      UILabel("SelfReport"+IntegerToString(dri), rr, x+20, y+656+dri*11, 6, DiagReportColor(rr));
   }
   
   // Bottom navigation: all buttons use one identical size.
   const int navW=74;
   const int navH=28;
   const int navGap=4;
   const int navX=x+10;
   UIButton("StrategyNext", "STRATEGY",  navX+0*(navW+navGap), y+692, navW, navH, UI_CARD2, UI_TEXT);
   UIButton("Metrics",      "METRICS",   navX+1*(navW+navGap), y+692, navW, navH, UI_CARD2, UI_TEXT);
   UIButton("Portfolio",    "PORTFOLIO", navX+2*(navW+navGap), y+692, navW, navH, UI_CARD2, UI_TEXT);
   UIButton("SymbolMap",    "MAP",       navX+3*(navW+navGap), y+692, navW, navH, UI_CARD2, UI_TEXT);
   UIButton("NewsToggle",   "NEWS",      navX+4*(navW+navGap), y+692, navW, navH, UI_CARD2, UI_TEXT);
   UIButton("SelfTest",     "SELF TEST", navX+5*(navW+navGap), y+692, navW, navH, UI_CARD2, UI_TEXT);
   UIButton("AlphaPage",    "ALPHA",     navX+6*(navW+navGap), y+692, navW, navH, UI_CARD2, UI_TEXT);
   UIButton("HidePanel",    "HIDE",      navX+7*(navW+navGap), y+692, navW, navH, UI_CARD2, UI_TEXT);
   if(g_confirm_live){
      int cx=x+125, cy=y+260, cw=400, ch=170;
      UIRect("ConfirmBox", cx, cy, cw, ch, UI_BG, UI_RED);
      UILabel("ConfirmTitle", "ENABLE LIVE TRADING?", cx+20, cy+18, 13, UI_RED);
      UILabel("ConfirmLine1", "The EA will send REAL orders to the broker.", cx+20, cy+52, 9, UI_TEXT);
      UILabel("ConfirmLine2", "Hedging account and trading permissions are required.", cx+20, cy+70, 8, UI_MUTED);
      UILabel("ConfirmLine3", "This mode trades real money from your account.", cx+20, cy+88, 9, UI_MUTED);
      UIButton("ConfirmYes", "YES, ENABLE", cx+20, cy+116, 170, 34, clrRed, UI_TEXT);
      UIButton("ConfirmNo", "CANCEL", cx+210, cy+116, 170, 34, UI_CARD2, UI_TEXT);
   }
   if(g_confirm_strategy){
      int cx=x+125, cy=y+260, cw=400, ch=170;
      UIRect("ConfirmStrBox", cx, cy, cw, ch, UI_BG, UI_ORANGE);
      UILabel("ConfirmStrTitle", "CHANGE STRATEGY?", cx+20, cy+18, 13, UI_ORANGE);
      UILabel("ConfirmStrLine1", "You have an OPEN cycle of the current strategy.", cx+20, cy+52, 9, UI_TEXT);
      UILabel("ConfirmStrLine2", "Close it now or keep until auto-exit?", cx+20, cy+70, 9, UI_MUTED);
      UIButton("ConfirmStrClose", "CLOSE & SWITCH", cx+15, cy+116, 120, 34, clrRed, UI_TEXT);
      UIButton("ConfirmStrKeep", "SWITCH ONLY", cx+140, cy+116, 120, 34, UI_BLUE, UI_TEXT);
      UIButton("ConfirmStrNo", "CANCEL", cx+265, cy+116, 120, 34, UI_CARD2, UI_TEXT);
   }
   ChartRedraw();
}

void ApplyStrategy(const int s){
   g_strategy=(ENUM_AP_STRATEGY)s;
   // When the actual trading strategy is changed from STRATEGY, reset the Metrics view to it.
   g_metrics_strategy=g_strategy;
   if(!g_is_tester) GlobalVariableSet(GV_STRATEGY, (double)s);
   g_ui_news_cache_time=0;
   SetStatus("Strategy: "+StrategyText());
   AuditEvent("STRATEGY_CHANGE","*",StrategyText());
   Notify("STRATEGY CHANGED: "+StrategyText());
   BuildUI();
}
void RequestStrategyChange(){
   int s=(int)g_strategy+1; if(s>2) s=0;
   if(OpenCyclesCount()>0){ g_pending_strategy=s; g_confirm_strategy=true; BuildUI(); return; }
   ApplyStrategy(s);
}
void SetTradingModeUI(const ENUM_AP_MODE mode){
   if(mode==g_mode) { BuildUI(); return; }
   if(OpenCyclesCount()>0){ SetStatus("Close active cycles before mode switch"); Notify("MODE SWITCH BLOCKED: active cycles exist"); BuildUI(); return; }
   if(mode==LIVE_TRADING){
      if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)||!MQLInfoInteger(MQL_TRADE_ALLOWED)){ SetStatus("LIVE blocked: trading disabled"); BuildUI(); return; }
      if(InpRequireHedgingForLive && AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){ SetStatus("LIVE blocked: hedging account required"); BuildUI(); return; }
   }
   // Persist the state of the mode we are leaving, then load the state of the target mode.
   SaveDailyState(); SaveCooldownState();
   g_mode=mode;
   BuildGlobalNames();
   if(g_mode==DEMO_TRADING && GlobalVariableCheck(GV_DEMO_BAL)) g_demo_balance=GlobalVariableGet(GV_DEMO_BAL);
   LoadDailyState(); LoadCooldownState();
   datetime today=EngineDayStart();
   if(today!=g_day_start) UpdateTradingDay();
   if(InpSelfDiagnostics) RunSelfDiagnostics(false); else g_diag_status="OFF";
   SetStatus("Mode: "+ModeText()); AuditEvent("MODE_CHANGE","*",ModeText()); Notify("MODE CHANGED: "+ModeText()); BuildUI();
}


string ArbChartSafeName(const string a,const string b){
   string out="AP_ARBS_";
   string src=a+"_"+b;
   for(int i=0;i<StringLen(src);i++){
      ushort ch=StringGetCharacter(src,i);
      bool ok=((ch>='0'&&ch<='9')||(ch>='A'&&ch<='Z')||(ch>='a'&&ch<='z')||ch=='_');
      out+=ok?CharToString((uchar)ch):"_";
      if(StringLen(out)>=60) break;
   }
   return out;
}

// Spread chart must use the SAME statistical spread as the trading engine:
// log(A) - beta * log(B), not the synthetic ratio (A/B-1).
// This keeps the MAP chart consistent with Z-score, beta, half-life and entry logic.
string g_arb_chart_a="";
string g_arb_chart_b="";
double g_arb_chart_beta=1.0;

double ArbSpreadValue(const double a,const double b,const double beta){
   if(a==EMPTY_VALUE||b==EMPTY_VALUE||a<=0.0||b<=0.0||beta<=0.0) return EMPTY_VALUE;
   return (MathLog(a)-beta*MathLog(b))*100.0;
}

bool BuildArbitrageSpreadHistory(const string a,const string b,const string custom,const double beta){
   MqlRates ra[],rb[],out[];
   int nA=CopyRates(a,PERIOD_M1,0,3000,ra);
   int nB=CopyRates(b,PERIOD_M1,0,3000,rb);
   if(nA<20 || nB<20 || beta<=0.0) return false;
   ArraySetAsSeries(ra,false); ArraySetAsSeries(rb,false);
   ArraySetAsSeries(out,false);
   int ia=0,ib=0,n=0;
   ArrayResize(out,MathMin(nA,nB));
   while(ia<nA && ib<nB){
      if(ra[ia].time<rb[ib].time){ ia++; continue; }
      if(rb[ib].time<ra[ia].time){ ib++; continue; }
      double vals[16]; int k=0;
      double av[4]={ra[ia].open,ra[ia].high,ra[ia].low,ra[ia].close};
      double bv[4]={rb[ib].open,rb[ib].high,rb[ib].low,rb[ib].close};
      for(int x=0;x<4;x++) for(int y=0;y<4;y++) vals[k++]=ArbSpreadValue(av[x],bv[y],beta);
      bool valid=true;
      for(int q=0;q<16;q++) if(vals[q]==EMPTY_VALUE){ valid=false; break; }
      if(valid){
         MqlRates r; ZeroMemory(r);
         r.time=ra[ia].time;
         r.open=ArbSpreadValue(ra[ia].open,rb[ib].open,beta);
         r.close=ArbSpreadValue(ra[ia].close,rb[ib].close,beta);
         r.high=vals[0]; r.low=vals[0];
         for(int q=1;q<16;q++){ if(vals[q]>r.high) r.high=vals[q]; if(vals[q]<r.low) r.low=vals[q]; }
         r.tick_volume=(long)MathMin((double)ra[ia].tick_volume,(double)rb[ib].tick_volume);
         r.real_volume=0; r.spread=0;
         out[n++]=r;
      }
      ia++; ib++;
   }
   if(n<20) return false;
   ArrayResize(out,n);
   ResetLastError();
   if(CustomRatesUpdate(custom,out)<0){ Print("ARBITRAGE CHART: CustomRatesUpdate failed ",GetLastError()); return false; }
   return true;
}

bool EnsureArbitrageCustomSymbol(const string a,const string b,const double beta,string &custom){
   custom=ArbChartSafeName(a,b);
   bool isCustom=false;
   if(!SymbolExist(custom,isCustom)){
      ResetLastError();
      if(!CustomSymbolCreate(custom,"",a)){
         int err=GetLastError();
         Print("ARBITRAGE CHART: CustomSymbolCreate failed ",custom," err=",err);
         return false;
      }
   }
   SymbolSelect(custom,true);
   string desc="ARBITRAGE SPREAD | LOG("+a+") - "+DoubleToString(beta,6)+" * LOG("+b+") * 100";
   CustomSymbolSetString(custom,SYMBOL_DESCRIPTION,desc);
   CustomSymbolSetInteger(custom,SYMBOL_DIGITS,5);
   return true;
}

bool OpenArbitragePairChart(const string a,const string b){
   SPairStat st; ResetStat(st);
   double beta=1.0;
   if(GetStat(a,b,st) && st.beta>0.0 && MathIsValidNumber(st.beta)) beta=st.beta;
   string custom="";
   if(!EnsureArbitrageCustomSymbol(a,b,beta,custom)) return false;
   if(!BuildArbitrageSpreadHistory(a,b,custom,beta)) return false;
   long cid=ChartOpen(custom,PERIOD_M5);
   if(cid==0){ Print("ARBITRAGE CHART: ChartOpen failed err=",GetLastError()); return false; }
   g_arb_chart_id=cid;
   g_arb_chart_symbol=custom;
   g_arb_chart_a=a;
   g_arb_chart_b=b;
   g_arb_chart_beta=beta;
   g_arb_chart_last_update_ms=GetTickCount64();
   ChartSetString(cid,CHART_COMMENT,"ARBITRAGE SPREAD: "+a+" / "+b+"\nFORMULA: LOG("+a+") - "+DoubleToString(beta,4)+" * LOG("+b+")\nSTATISTICAL SPREAD | beta="+DoubleToString(beta,4)+" | M1 synchronized");
   ChartSetInteger(cid,CHART_SHOW_GRID,true);
   // IMPORTANT: show a continuous spread line, not a synthetic price candle chart.
   ChartSetInteger(cid,CHART_MODE,CHART_LINE);
   ChartSetInteger(cid,CHART_SHOW_OHLC,false);
   ChartRedraw(cid);
   return true;
}

void UpdateOpenArbitrageChart(){
   if(g_arb_chart_symbol=="" || g_arb_chart_id<=0 || g_arb_chart_a=="" || g_arb_chart_b=="") return;
   ulong now=GetTickCount64();
   if(now-g_arb_chart_last_update_ms<5000) return;
   g_arb_chart_last_update_ms=now;

   SPairStat st; ResetStat(st);
   double beta=g_arb_chart_beta;
   if(GetStat(g_arb_chart_a,g_arb_chart_b,st) && st.beta>0.0 && MathIsValidNumber(st.beta)) beta=st.beta;
   if(MathAbs(beta-g_arb_chart_beta)>0.000001){
      g_arb_chart_beta=beta;
      string desc="ARBITRAGE SPREAD | LOG("+g_arb_chart_a+") - "+DoubleToString(beta,6)+" * LOG("+g_arb_chart_b+") * 100";
      CustomSymbolSetString(g_arb_chart_symbol,SYMBOL_DESCRIPTION,desc);
   }
   if(BuildArbitrageSpreadHistory(g_arb_chart_a,g_arb_chart_b,g_arb_chart_symbol,beta)){
      ChartSetString(g_arb_chart_id,CHART_COMMENT,"ARBITRAGE SPREAD: "+g_arb_chart_a+" / "+g_arb_chart_b+"\nFORMULA: LOG("+g_arb_chart_a+") - "+DoubleToString(beta,4)+" * LOG("+g_arb_chart_b+")\nSTATISTICAL SPREAD | beta="+DoubleToString(beta,4)+" | M1 synchronized");
      ChartSetInteger(g_arb_chart_id,CHART_MODE,CHART_LINE);
      ChartRedraw(g_arb_chart_id);
   }
}

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam){
   // Direct chart-coordinate handling for NEWS rows. This preserves the old visual layout
   // and avoids transparent button/label layering from swallowing row clicks.
   if(id==CHARTEVENT_CLICK && g_ui_visible && g_news_visible){
      int mx=(int)lparam, my=(int)dparam;
      int rows=MathMin(15,g_ui_news_count);
      int newsY=InpPanelY+48;
      int firstY=newsY+81;
      if(mx>=InpPanelX+10 && mx<=InpPanelX+640 && my>=firstY-5 && my<firstY+rows*30-5){
         int row=(my-(firstY-5))/30;
         int selectedIndex=g_news_scroll+row;
         if(row>=0 && row<rows && selectedIndex>=0 && selectedIndex<g_ui_news_count){
            g_news_selected=selectedIndex;
            BuildUI();
         }
         return;
      }
   }
   if(id==CHARTEVENT_CLICK && g_ui_visible && g_show_symbol_map){
      int mx=(int)lparam, my=(int)dparam;
      int panelY=InpPanelY+48;
      int rows=SymbolMapVisibleRows();
      int firstY=panelY+60;
      if(mx>=InpPanelX+10 && mx<=InpPanelX+610 && my>=firstY-5 && my<firstY-5+rows*21){
         int row=(my-(firstY-5))/21;
         if(row>=0 && row<rows){
            string pa="",pb="";
            if(row==0){ pa=InpSymbolA; pb=InpSymbolB; }
            else if(row-1<g_mp_count){ pa=g_mp_a[row-1]; pb=g_mp_b[row-1]; }
            if(pa!="" && pb!="") OpenArbitragePairChart(pa,pb);
         }
         return;
      }
   }
   if(id!=CHARTEVENT_OBJECT_CLICK) return;
   string name=sparam;
   ObjectSetInteger(0, name, OBJPROP_STATE, false);
   if(StringFind(name,UIName("NewsRow"))==0){
      string tail=StringSubstr(name,StringLen(UIName("NewsRow")));
      int row=(int)StringToInteger(tail);
      int rows=MathMin(15,g_ui_news_count);
      int selectedIndex=g_news_scroll+row;
      if(g_news_visible && row>=0 && row<rows && selectedIndex>=0 && selectedIndex<g_ui_news_count){
         g_news_selected=selectedIndex;
         BuildUI();
      }
      return;
   }
   if(name==UIName("ShowPanel")){ g_ui_visible=true; BuildUI(); return; }
   if(name==UIName("HidePanel")){ 
      g_ui_visible=false; 
      g_news_visible=false; 
      g_show_strategy_panel=false; 
      g_show_metrics_panel=false;
      g_show_portfolio_panel=false;
      g_show_quant_detail=false;
      g_alpha_selected=-1;
      g_show_symbol_map=false;
      g_show_alpha_page=false;
      g_confirm_live=false; 
      g_confirm_strategy=false; 
      UIDeleteAll(); 
      BuildUI(); 
      return; 
   }
   if(!g_ui_visible) return;
   if(name==UIName("ConfirmYes")){ g_confirm_live=false; SetTradingModeUI(LIVE_TRADING); return; }
   if(name==UIName("ConfirmNo")){ g_confirm_live=false; BuildUI(); return; }
   if(name==UIName("ConfirmStrClose")){ g_confirm_strategy=false; CloseAllCycles("STRATEGY-SWITCH"); ApplyStrategy(g_pending_strategy); return; }
   if(name==UIName("ConfirmStrKeep")){ g_confirm_strategy=false; ApplyStrategy(g_pending_strategy); return; }
   if(name==UIName("ConfirmStrNo")){ g_confirm_strategy=false; BuildUI(); return; }
   
   if(name==UIName("SelectPairStat")){ 
      ApplyStrategy(PAIR_STATISTICAL); 
      g_show_strategy_panel=true; g_show_metrics_panel=false; g_show_portfolio_panel=false; g_show_quant_detail=false; g_show_alpha_page=false;
      BuildUI();
      return; 
   }
   if(name==UIName("SelectArbSpread")){
      ApplyStrategy(ARBITRAGE_SPREAD);
      g_show_strategy_panel=true; g_show_metrics_panel=false; g_show_portfolio_panel=false; g_show_quant_detail=false; g_show_alpha_page=false;
      BuildUI();
      return;
   }
   if(name==UIName("MetricsSelectMultiStat")){
      // DISPLAY ONLY: this button changes which Metrics dataset is shown.
      // It does NOT change g_strategy or any trading logic.
      g_metrics_strategy=MULTI_STATISTICAL;
      RefreshMetricsPreview(true);
      g_show_strategy_panel=false; g_show_metrics_panel=true; g_show_portfolio_panel=false; g_show_quant_detail=false; g_show_alpha_page=false; g_show_symbol_map=false; g_news_visible=false;
      BuildUI();
      return;
   }
   if(name==UIName("MetricsSelectPairStat")){
      // DISPLAY ONLY: this button changes which Metrics dataset is shown.
      g_metrics_strategy=PAIR_STATISTICAL;
      RefreshMetricsPreview(true);
      g_show_strategy_panel=false; g_show_metrics_panel=true; g_show_portfolio_panel=false; g_show_quant_detail=false; g_show_alpha_page=false; g_show_symbol_map=false; g_news_visible=false;
      BuildUI();
      return;
   }
   if(name==UIName("MetricsSelectArbSpread")){
      // DISPLAY ONLY: this button changes which Metrics dataset is shown.
      g_metrics_strategy=ARBITRAGE_SPREAD;
      RefreshMetricsPreview(true);
      g_show_strategy_panel=false; g_show_metrics_panel=true; g_show_portfolio_panel=false; g_show_quant_detail=false; g_show_alpha_page=false; g_show_symbol_map=false; g_news_visible=false;
      BuildUI();
      return;
   }
   if(name==UIName("SelectMultiStat")){ 
      ApplyStrategy(MULTI_STATISTICAL); 
      g_show_strategy_panel=true; g_show_metrics_panel=false; g_show_portfolio_panel=false; g_show_quant_detail=false; g_show_alpha_page=false;
      BuildUI();
      return; 
   }
   if(name==UIName("QuantBackAlpha")){
      g_show_quant_detail=false; g_alpha_selected=-1; g_show_alpha_page=true;
      g_show_strategy_panel=false; g_show_metrics_panel=false; g_show_portfolio_panel=false; g_show_symbol_map=false; g_news_visible=false;
      BuildUI(); return;
   }
   if(name==UIName("PortfolioBack") || name==UIName("StrategyBack") || name==UIName("MetricsBack") ||
      name==UIName("NewsBack") || name==UIName("MapBack") || name==UIName("AlphaBack") || name==UIName("QuantBack")){
      g_show_portfolio_panel=false;
      g_show_quant_detail=false;
      g_alpha_selected=-1;
      g_show_strategy_panel=false;
      g_show_metrics_panel=false;
      g_show_symbol_map=false;
      g_news_visible=false;
      g_show_alpha_page=false;
      BuildUI();
      return;
   }
   
   if(g_confirm_live || g_confirm_strategy) return;
   if(name==UIName("CloseAll")){
      CloseAllCycles("MANUAL CLOSE");
      if(g_mode==LIVE_TRADING) CloseAllOurPositions();
      SetStatus("All positions close requested"); Notify("CLOSE ALL pressed"); BuildUI(); return;
   }
   if(name==UIName("PauseBtn")){ 
      g_paused=!g_paused; 
      Notify(g_paused?"PAUSED":"RESUMED"); 
      SaveDailyState(); BuildUI(); 
      return; 
   }
   if(name==UIName("ModeSignals")){ SetTradingModeUI(SIGNALS_ONLY); return; }
   if(name==UIName("ModeDemo")){ SetTradingModeUI(DEMO_TRADING); return; }
   if(name==UIName("ModeLive")){
      if(g_mode!=LIVE_TRADING){ g_confirm_live=true; BuildUI(); return; }
      SetTradingModeUI(LIVE_TRADING); return;
   }
   if(name==UIName("StrategyNext")){ 
      g_show_strategy_panel=true; g_show_metrics_panel=false; g_show_portfolio_panel=false; g_show_quant_detail=false; g_show_symbol_map=false; g_news_visible=false; g_show_alpha_page=false; BuildUI(); return; 
   }
   if(name==UIName("Metrics")){ 
      g_show_metrics_panel=true; g_show_strategy_panel=false; g_show_portfolio_panel=false; g_show_quant_detail=false; g_show_symbol_map=false; g_news_visible=false; g_show_alpha_page=false; BuildUI(); return; 
   }
   if(name==UIName("SymbolMap")){
      g_show_symbol_map=true; g_show_strategy_panel=false; g_show_metrics_panel=false; g_show_portfolio_panel=false; g_show_quant_detail=false; g_news_visible=false; g_show_alpha_page=false; BuildUI(); return;
   }
   if(name==UIName("Portfolio")){
      g_show_portfolio_panel=true; g_show_strategy_panel=false; g_show_metrics_panel=false; g_show_symbol_map=false; g_show_quant_detail=false; g_news_visible=false; g_show_alpha_page=false; BuildUI(); return;
   }
   if(name==UIName("SelfTest")){ RunSelfDiagnostics(true); BuildUI(); return; }
   for(int ai=0;ai<ArraySize(g_top_a);ai++){
      if(name==UIName("AlphaSelect"+IntegerToString(ai))){
         if(g_top_a[ai]!="" && g_top_b[ai]!=""){
            g_alpha_selected=ai;
            g_show_quant_detail=true;
            g_show_alpha_page=false;
            g_show_strategy_panel=false;
            g_show_metrics_panel=false;
            g_show_portfolio_panel=false;
            g_show_symbol_map=false;
            g_news_visible=false;
            BuildUI();
         }
         return;
      }
   }
   if(name==UIName("AlphaPage")){ g_show_alpha_page=true; g_show_strategy_panel=false; g_show_metrics_panel=false; g_show_portfolio_panel=false; g_show_quant_detail=false; g_show_symbol_map=false; g_news_visible=false; BuildUI(); return; }
   if(name==UIName("NewsToggle")){ 
      g_news_visible=true; 
      g_show_strategy_panel=false; 
      g_show_metrics_panel=false; g_show_portfolio_panel=false; g_show_quant_detail=false; g_show_symbol_map=false; g_show_alpha_page=false;
      g_ui_news_cache_time=0; 
      g_news_selected=-1;
      BuildUI(); 
      return; 
   }
}

bool FindBrokerSymbol(const string requested,string &resolved){
   resolved="";
   if(StringLen(requested)<1) return false;
   bool custom=false;
   if(SymbolExist(requested,custom)){
      resolved=requested;
      if((bool)SymbolInfoInteger(resolved,SYMBOL_SELECT) || SymbolSelect(resolved,true)) return true;
   }
   // First: exact match ignoring letter case in Market Watch.
   int total=SymbolsTotal(true);
   for(int i=0;i<total;i++){
      string name=SymbolName(i,true);
      if(name!="" && StringCompare(name,requested,false)==0){
         resolved=name;
         if((bool)SymbolInfoInteger(resolved,SYMBOL_SELECT) || SymbolSelect(resolved,true)) return true;
      }
   }
   // Second: exact match in the broker's complete symbol list.
   total=SymbolsTotal(false);
   for(int i=0;i<total;i++){
      string name=SymbolName(i,false);
      if(name!="" && StringCompare(name,requested,false)==0){
         resolved=name;
         if((bool)SymbolInfoInteger(resolved,SYMBOL_SELECT) || SymbolSelect(resolved,true)) return true;
      }
   }
   // Third: safe suffix/prefix resolution for broker-specific names (e.g. EURUSD.m).
   // Only accept a close name match; never guess between unrelated instruments.
   int reqLen=StringLen(requested), bestScore=-1;
   string best="";
   total=SymbolsTotal(false);
   for(int i=0;i<total;i++){
      string name=SymbolName(i,false);
      if(name=="") continue;
      int nlen=StringLen(name), diff=MathAbs(nlen-reqLen);
      if(diff>8) continue;
      bool prefix=(StringFind(name,requested,0)==0 || StringFind(requested,name,0)==0);
      if(!prefix) continue;
      int score=100-diff;
      if((bool)SymbolInfoInteger(name,SYMBOL_SELECT)) score+=10;
      if(score>bestScore){ bestScore=score; best=name; }
   }
   if(best!=""){
      resolved=best;
      if((bool)SymbolInfoInteger(resolved,SYMBOL_SELECT) || SymbolSelect(resolved,true)) return true;
   }
   resolved=requested;
   return false;
}

bool EnsureSymbolSelected(const string requested,string &resolved,int &errCode){
   errCode=0; resolved="";
   if(StringLen(requested)<1){ errCode=0; return false; }
   bool custom=false;
   if(!SymbolExist(requested,custom)){
      if(!FindBrokerSymbol(requested,resolved)) return false;
   } else resolved=requested;
   if((bool)SymbolInfoInteger(resolved,SYMBOL_SELECT)) return true;
   ResetLastError();
   if(SymbolSelect(resolved,true)) return true;
   errCode=GetLastError();
   // A few brokers expose the instrument but SymbolSelect may transiently fail; re-check the flag.
   if((bool)SymbolInfoInteger(resolved,SYMBOL_SELECT)) return true;
   return false;
}

void ParsePairList(const string list, string &arrA[], string &arrB[], int &count, const int maxPairs){
   count=0;
   string parts[];
   int cnt=StringSplit(list, ';', parts);
   ArrayResize(arrA, maxPairs); 
   ArrayResize(arrB, maxPairs);
   for(int i=0;i<cnt && count<maxPairs;i++){
      string item=parts[i]; StringTrimLeft(item); StringTrimRight(item);
      string two[];
      if(StringSplit(item, ',', two)!=2) continue;
      StringTrimLeft(two[0]); StringTrimRight(two[0]);
      StringTrimLeft(two[1]); StringTrimRight(two[1]);
      if(StringLen(two[0])<3 || StringLen(two[1])<3 || StringCompare(two[0],two[1],false)==0) continue;
      string ra="",rb=""; int ea=0,eb=0;
      bool oka=EnsureSymbolSelected(two[0],ra,ea);
      bool okb=EnsureSymbolSelected(two[1],rb,eb);
      // Keep the user's token when the symbol cannot be resolved so SELF TEST can report the exact failure.
      arrA[count]=oka?ra:two[0];
      arrB[count]=okb?rb:two[1];
      count++;
   }
   ArrayResize(arrA, count); 
   ArrayResize(arrB, count);
}

bool DiagnosticSymbol(const string sym,string &why){
   why="";
   if(sym==""){ why="empty symbol"; return false; }
   string resolved=""; int err=0;
   if(!EnsureSymbolSelected(sym,resolved,err)){
      bool custom=false;
      if(!SymbolExist(sym,custom)) why="not found on server";
      else why="cannot select (err="+IntegerToString(err)+")";
      return false;
   }
   if(StringCompare(resolved,sym,true)!=0 && InpVerboseLog)
      Print("DIAG symbol alias: ",sym," -> ",resolved);
   long tradeMode=SymbolInfoInteger(resolved,SYMBOL_TRADE_MODE);
   if(tradeMode==SYMBOL_TRADE_MODE_DISABLED){ why="trade disabled"; return false; }
   double mn=SymbolInfoDouble(resolved,SYMBOL_VOLUME_MIN), mx=SymbolInfoDouble(resolved,SYMBOL_VOLUME_MAX), st=SymbolInfoDouble(resolved,SYMBOL_VOLUME_STEP);
   if(mn<=0||mx<=0||st<=0){ why="invalid volume specification"; return false; }
   if(SymbolInfoDouble(resolved,SYMBOL_TRADE_TICK_SIZE)<=0){ why="invalid tick size"; return false; }
   if(!SymbolIsSynchronized(resolved)){
      why="data not synchronized";
      return false;
   }
   return true;
}

void UpdateSymbolMapStats(){
   g_symbol_map_ok=0; g_symbol_map_warn=0;
   string why="";
   if(DiagnosticSymbol(InpSymbolA,why)) g_symbol_map_ok++; else g_symbol_map_warn++;
   if(DiagnosticSymbol(InpSymbolB,why)) g_symbol_map_ok++; else g_symbol_map_warn++;
   for(int i=0;i<g_mp_count;i++){
      if(DiagnosticSymbol(g_mp_a[i],why)) g_symbol_map_ok++; else g_symbol_map_warn++;
      if(DiagnosticSymbol(g_mp_b[i],why)) g_symbol_map_ok++; else g_symbol_map_warn++;
   }
}

string SymbolMapStatus(const string sym){
   string why="";
   if(DiagnosticSymbol(sym,why)) return "OK";
   return why;
}

int SymbolMapVisibleRows(){
   return MathMax(1,MathMin(64,g_mp_count+1));
}

void DrawSymbolMapPanel(const int y){
   const int x=InpPanelX, w=650;
   int panelY=y+686;
   int totalPairs=g_mp_count+1;
   int rows=SymbolMapVisibleRows();
   int panelH=81+rows*21+30;
   UIRect("SymbolMapPanel",x,panelY,w,panelH,UI_BG,UI_BORDER);
   UpdateSymbolMapStats();
   UILabel("SymbolMapStat","HEALTH: "+IntegerToString(g_symbol_map_ok)+" OK / "+IntegerToString(g_symbol_map_warn)+" WARN",x+420,panelY+9,7,g_symbol_map_warn>0?UI_ORANGE:UI_GREEN,"Segoe UI Semibold");

   UIRect("SymbolMapHead",x+10,panelY+30,630,24,UI_CARD2);
   UILabel("SMH1","ROLE",x+18,panelY+37,7,UI_MUTED);
   UILabel("SMH2","PAIR / SYMBOL",x+80,panelY+37,7,UI_MUTED);
   UILabel("SMH3","INTEL",x+360,panelY+37,7,UI_MUTED);
   UILabel("SMH4","STATE",x+455,panelY+37,7,UI_MUTED);
   UILabel("SMH5","TICK",x+540,panelY+37,7,UI_MUTED);

   string pairsA[64],pairsB[64];
   int pc=0;
   pairsA[pc]=InpSymbolA; pairsB[pc]=InpSymbolB; pc++;
   for(int i=0;i<g_mp_count && pc<64;i++){ pairsA[pc]=g_mp_a[i]; pairsB[pc]=g_mp_b[i]; pc++; }

   for(int i=0;i<rows;i++){
      int idx=i;
      int yy=panelY+60+i*21;
      UIRect("SMRow"+IntegerToString(i),x+10,yy-4,600,19,(i%2==0)?UI_CARD:UI_BG);
      if(idx>=pc) continue;
      string role=(idx==0)?"PRIMARY":"POOL "+IntegerToString(idx);
      UILabel("SMRole"+IntegerToString(i),role,x+18,yy,7,idx==0?UI_CYAN:UI_MUTED);
      string pair=pairsA[idx]+" / "+pairsB[idx];
      if(StringLen(pair)>36) pair=StringSubstr(pair,0,36);
      UILabel("SMSym"+IntegerToString(i),pair,x+80,yy,7,UI_TEXT);
      string wa=SymbolMapStatus(pairsA[idx]), wb=SymbolMapStatus(pairsB[idx]);
      bool ok=(wa=="OK"&&wb=="OK");
      SPairIntelligence pi; bool intelOk=InpUsePairIntelligence&&GetPairIntelligence(pairsA[idx],pairsB[idx],pi,false);
      string intel=intelOk?DoubleToString(pi.score,0)+" / x"+DoubleToString(pi.lotFactor,2):"--";
      UILabel("SMIntel"+IntegerToString(i),intel,x+360,yy,6,intelOk?(pi.eligible?UI_GREEN:UI_ORANGE):UI_MUTED);
      string state=ok?"READY":(wa+" | "+wb);
      if(StringLen(state)>18) state=StringSubstr(state,0,18);
      UILabel("SMState"+IntegerToString(i),state,x+455,yy,6,ok?UI_GREEN:UI_ORANGE);
      MqlTick ta,tb;
      bool tickok=GetTickSafe(pairsA[idx],ta)&&GetTickSafe(pairsB[idx],tb);
      UILabel("SMTick"+IntegerToString(i),tickok?"LIVE":"--",x+540,yy,7,tickok?UI_GREEN:UI_RED);
   }
   string footer="showing all "+IntegerToString(totalPairs)+" pairs / "+IntegerToString(totalPairs*2)+" symbols";
   UILabel("SymbolMapFooter",footer,x+18,panelY+panelH-16,6,UI_MUTED,"Segoe UI");
}

void DiagReportAdd(const string line){
   if(g_diag_report_count<8) g_diag_report[g_diag_report_count++]=line;
}

// Per-line diagnostic color: the SELF TEST report is an indicator, not a single-status banner.
// Each result is colored by its own state instead of inheriting the global warning/error state.
color DiagReportColor(const string line){
   string s=line;
   StringToUpper(s);

   // Explicit failures / unavailable / disconnected states are red.
   if(StringFind(s,"FAIL")>=0 ||
      StringFind(s,"ERROR")>=0 ||
      StringFind(s,"DISCONNECTED")>=0 ||
      StringFind(s,"UNAVAILABLE")>=0 ||
      StringFind(s,"NOT FOUND")>=0 ||
      StringFind(s,"CIRCUIT BREAKER")>=0)
      return UI_RED;

   // Explicit successful checks are green.
   if(StringFind(s,"OK")>=0 ||
      StringFind(s,"READY")>=0 ||
      StringFind(s,"CONNECTED")>=0 ||
      StringFind(s,"USABLE")>=0)
      return UI_GREEN;

   // Warnings / stale / closed market conditions are orange.
   if(StringFind(s,"WARNING")>=0 ||
      StringFind(s,"WARN")>=0 ||
      StringFind(s,"STALE")>=0 ||
      StringFind(s,"CLOSED")>=0 ||
      StringFind(s,"PAUSED")>=0 ||
      StringFind(s,"NO ACTIVE SESSION")>=0)
      return UI_ORANGE;

   return UI_MUTED;
}

string DiagBool(const bool ok){ return ok?"OK":"FAIL"; }

bool DiagnosticMarketState(string &marketLine){
   marketLine="MARKET: ";
   if(!TerminalInfoInteger(TERMINAL_CONNECTED)){ marketLine+="OFFLINE / NO SERVER CONNECTION"; return false; }
   datetime now=TimeTradeServer(); if(now<=0) now=TimeCurrent();
   MqlDateTime dt; TimeToStruct(now,dt);
   if(dt.day_of_week==0 || dt.day_of_week==6){ marketLine+="CLOSED / WEEKEND"; return false; }
   string syms[4]; syms[0]=InpSymbolA; syms[1]=InpSymbolB; syms[2]=g_live_pair!=""?g_live_pair:InpSymbolA; syms[3]=InpSymbolB;
   bool any=false, stale=false, session=false;
   for(int i=0;i<4;i++){
      if(StringLen(syms[i])<1) continue;
      if(!DashboardSymbolSessionOpen(syms[i],now)) continue;
      session=true;
      MqlTick t; if(GetTickSafe(syms[i],t)){ if(QuoteFresh(t)) any=true; else stale=true; }
   }
   if(any){ marketLine+="OPEN / LIVE QUOTES"; return true; }
   if(session && stale){ marketLine+="SESSION OPEN / STALE QUOTES"; return false; }
   marketLine+="CLOSED / NO ACTIVE SESSION (holiday or broker schedule)"; return false;
}

bool RunSelfDiagnostics(const bool notifyUser=false){
   g_diag_errors=0; g_diag_warnings=0; g_diag_report_count=0;
   string why="", marketLine="";
   bool conn=TerminalInfoInteger(TERMINAL_CONNECTED);
   bool tradeAllowed=TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && MQLInfoInteger(MQL_TRADE_ALLOWED);
   bool marketOK=DiagnosticMarketState(marketLine);

   // 1) Connectivity / market state.
   DiagReportAdd("CONNECTION: "+DiagBool(conn)+(conn?" / SERVER CONNECTED":" / SERVER DISCONNECTED"));
   if(!conn) g_diag_errors++;
   DiagReportAdd(marketLine);
   if(!marketOK) g_diag_warnings++;

   // 2) Primary symbols, sessions, quotes and history.
   bool pa=DiagnosticSymbol(InpSymbolA,why); if(!pa){ g_diag_errors++; DiagReportAdd("PRIMARY A: FAIL "+why); } else DiagReportAdd("PRIMARY A: OK "+InpSymbolA);
   bool pb=DiagnosticSymbol(InpSymbolB,why); if(!pb){ g_diag_errors++; DiagReportAdd("PRIMARY B: FAIL "+why); } else DiagReportAdd("PRIMARY B: OK "+InpSymbolB);
   string prim[2]; prim[0]=InpSymbolA; prim[1]=InpSymbolB;
   int need=MathMax(MathMax(InpStatLookback,InpOU_lookback),InpAdaptiveZ_window)+InpADFMaxLag+10;
   for(int i=0;i<2;i++){
      if(SymbolSelect(prim[i],true)){
         int bars=Bars(prim[i],PERIOD_CURRENT);
         if(bars<need){ g_diag_warnings++; if(InpVerboseLog) Print("SELF TEST HISTORY WARNING ",prim[i]," ",bars," < ",need); }
         string pc=SymbolInfoString(prim[i],SYMBOL_CURRENCY_PROFIT);
         if(CurrencyToDepositRate(pc)<=0){ g_diag_warnings++; if(InpVerboseLog) Print("SELF TEST FX CONVERSION WARNING ",prim[i]," ",pc); }
      }
   }

   // 3) Entire configured pair engine: symbol resolution, history and live ticks.
   int usablePairs=0, pairFailures=0, stalePairs=0;
   for(int i=0;i<g_mp_count;i++){
      string wa="",wb=""; bool aok=DiagnosticSymbol(g_mp_a[i],wa), bok=DiagnosticSymbol(g_mp_b[i],wb);
      if(aok&&bok){
         usablePairs++;
         int ba=Bars(g_mp_a[i],PERIOD_CURRENT), bb=Bars(g_mp_b[i],PERIOD_CURRENT);
         if(ba<need||bb<need){ g_diag_warnings++; }
         MqlTick ta,tb; bool ticks=GetTickSafe(g_mp_a[i],ta)&&GetTickSafe(g_mp_b[i],tb);
         if(!ticks || !QuoteFresh(ta) || !QuoteFresh(tb)) stalePairs++;
      } else { pairFailures++; g_diag_warnings++; }
   }
   if(g_strategy==MULTI_STATISTICAL&&usablePairs<1){ g_diag_errors++; }
   DiagReportAdd("PAIR ENGINE: "+IntegerToString(usablePairs)+" usable / "+IntegerToString(pairFailures)+" unavailable / "+IntegerToString(stalePairs)+" stale");

   // 4) Trading permissions and account/margin environment.
   if(g_mode==LIVE_TRADING){
      if(InpRequireHedgingForLive && AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){ g_diag_errors++; DiagReportAdd("LIVE ACCOUNT: FAIL / HEDGING REQUIRED"); }
      else DiagReportAdd("LIVE ACCOUNT: OK / PERMISSION CHECK PASSED");
      if(!tradeAllowed){ g_diag_warnings++; DiagReportAdd("TRADE PERMISSION: OFF"); }
   } else DiagReportAdd("TRADE MODE: "+UIModeShort()+" / permissions="+(tradeAllowed?"ON":"OFF"));

   // 5) Configuration / strategy engine.
   if(InpMaxDailyLossPct<=0||InpMaxOpenCycles<1||InpMaxPositions<2||InpMaxLots<=0) g_diag_errors++;
   if(InpStatExitZScore>=InpStatEntryZScore||InpHardZStop<=InpStatEntryZScore) g_diag_warnings++;
   if(InpMaxAbortsInWindow<1||InpAbortWindowSec<1||InpCircuitPauseSec<1) g_diag_errors++;
   if(InpMaxRebalanceCount<0||InpMaxCycleQuoteAgeMs<1000||InpMinNetEdgeMoney<0) g_diag_errors++;
   if(InpMaxReconcileFailuresDay<1) g_diag_warnings++;
   if(InpUseExecutionCircuitBreaker && InpMaxAbortsInWindow>InpMaxTradesPerDay && InpMaxTradesPerDay>0) g_diag_warnings++;
   DiagReportAdd("STRATEGY ENGINE: "+EnumToString(g_strategy)+" / CONFIG "+((g_diag_errors==0)?"OK":"CHECK ERRORS"));

   // 6) Research / intelligence / institutional engines.
   if(InpUseResearchLedger && InpResearchMinSamples<1) g_diag_errors++;
   if(InpUsePairIntelligence){
      if(InpMinPairIntelligenceScore<0 || InpMinPairIntelligenceScore>100) g_diag_errors++;
      if(InpIntelligenceEdgeCap<=0 || InpIntelligenceLotFloor<=0 || InpIntelligenceLotCeiling<=0) g_diag_errors++;
      if(InpPortfolioCorrLimit<0 || InpPortfolioCorrLimit>1) g_diag_errors++;
   }
   if(InpUseInstitutionalEngine){
      if(InpPortfolioRiskBudgetPct<=0 || InpMaxPairRiskBudgetPct<=0 || InpMaxClusterRiskBudgetPct<=0) g_diag_errors++;
      if(InpVaRConfidence<=0.50 || InpVaRConfidence>=1.0) g_diag_errors++;
      if(InpMaxVaRMoney<0 || InpMaxCVaRMoney<0 || InpMaxStressLossMoney<0) g_diag_errors++;
      if(InpMinDynamicRiskMultiplier<=0 || InpMinDynamicRiskMultiplier>1.0) g_diag_errors++;
      if(InpMaxInstitutionalBreachesDay<1) g_diag_errors++;
      if(InpMaxAdverseZExpansion<0 || InpMonteCarloRuns<10 || InpMonteCarloMaxP5LossMoney<0) g_diag_errors++;
   }
   if(InpUseWalkForwardGuard){
      if(InpWalkForwardTrainDays<1 || InpWalkForwardTestDays<1) g_diag_errors++;
      if(InpWalkForwardMinTrainTrades<5 || InpWalkForwardMinTestTrades<3) g_diag_errors++;
      if(InpWalkForwardMinTestCorr<-1.0 || InpWalkForwardMinTestCorr>1.0) g_diag_errors++;
   }
   DiagReportAdd("QUANT/RISK ENGINES: "+((g_diag_errors==0)?"CONFIG OK":"CONFIG ERRORS DETECTED"));

   // 7) Runtime safety state: errors that can actually stop execution.
   bool runtimeFault=(g_execution_failures_today>0 || g_reconcile_failures_today>0 || ExecutionCircuitActive() || g_daily_stop || g_inst.killSwitch);
   if(runtimeFault) g_diag_errors++;
   string rt="RUNTIME: ";
   if(g_daily_stop) rt+="DAILY STOP; ";
   if(g_inst.killSwitch) rt+="KILL SWITCH; ";
   if(ExecutionCircuitActive()) rt+="CIRCUIT BREAKER; ";
   if(g_execution_failures_today>0) rt+="EXEC ERR="+IntegerToString(g_execution_failures_today)+"; ";
   if(g_reconcile_failures_today>0) rt+="RECON ERR="+IntegerToString(g_reconcile_failures_today)+"; ";
   if(!runtimeFault) rt+="NO CRITICAL RUNTIME FAULTS";
   DiagReportAdd(rt);

   // 8) Last platform error, if any. Do not manufacture an error: only report a non-zero code.
   int le=GetLastError();
   if(le!=0){ g_diag_warnings++; DiagReportAdd("PLATFORM ERROR: "+IntegerToString(le)); ResetLastError(); }
   else DiagReportAdd("PLATFORM ERROR: NONE");

   g_diag_status=(g_diag_errors>0)?("FAIL "+IntegerToString(g_diag_errors)+"E/"+IntegerToString(g_diag_warnings)+"W"):(g_diag_warnings>0?("WARN "+IntegerToString(g_diag_warnings)+"W"):"OK");
   string msg="SELF TEST: "+g_diag_status+" | market="+(marketOK?"LIVE":"CLOSED/STALE")+" | pairs="+IntegerToString(usablePairs)+" | exec="+IntegerToString(g_execution_failures_today)+" | recon="+IntegerToString(g_reconcile_failures_today);
   Print(msg);
   for(int i=0;i<g_diag_report_count;i++) Print("SELF TEST > ",g_diag_report[i]);
   if(notifyUser){ SetStatus(msg); Notify(msg); }
   return g_diag_errors==0;
}

void OnTradeTransaction(const MqlTradeTransaction &trans,
                        const MqlTradeRequest &request,
                        const MqlTradeResult &result){
   if(!InpEnableTradeTelemetry) return;
   g_trade_tx_count++;
   if(trans.type==TRADE_TRANSACTION_DEAL_ADD){
      g_last_deal_ticket=trans.deal;
      g_last_trade_symbol=trans.symbol;
      g_last_fill_price=trans.price;
      g_last_trade_time=TimeCurrent();
      g_last_trade_action=EnumToString(trans.deal_type);
      if(InpVerboseLog) Print("TRADE TX DEAL_ADD | ",trans.symbol," | deal=",(long)trans.deal," | price=",DoubleToString(trans.price,8));
      return;
   }
   if(trans.type==TRADE_TRANSACTION_REQUEST){
      g_last_trade_retcode=result.retcode;
      g_last_trade_order=result.order;
      g_last_trade_time=TimeCurrent();
      if(request.symbol!="") g_last_trade_symbol=request.symbol;
      g_last_trade_action=EnumToString(request.type);
   }
}


//+------------------------------------------------------------------+
//| INSTITUTIONAL PORTFOLIO ENGINE                                  |
//| Risk budget, VaR/CVaR, stress, clusters, regime, execution     |
//| quality, signal decay, stability and kill-switch orchestration. |
//+------------------------------------------------------------------+
string InstRegimeText(){ return g_inst.regime; }

double InstEquity(){
   if(g_mode==DEMO_TRADING) return MathMax(1.0,g_demo_balance);
   return MathMax(1.0,AccountInfoDouble(ACCOUNT_EQUITY));
}

double InstOpenExposureLots(){
   double v=0.0;
   for(int i=0;i<ArraySize(g_cycles);i++)
      for(int k=0;k<ArraySize(g_cycles[i].legs);k++) v+=MathAbs(g_cycles[i].legs[k].volume);
   return v;
}

string InstClusterKey(const string a,const string b){
   // Deterministic cluster key: shared currency/instrument identity.
   string key="";
   string tokens[8]={"USD","EUR","GBP","JPY","CHF","AUD","NZD","CAD"};
   for(int i=0;i<8;i++){
      if(StringFind(a,tokens[i])>=0 || StringFind(b,tokens[i])>=0){
         if(key!="") key+="+";
         key+=tokens[i];
      }
   }
   if(key!="") return key;
   string base=StringSubstr(a,0,MathMin(3,StringLen(a)))+"/"+StringSubstr(b,0,MathMin(3,StringLen(b)));
   return base;
}

double InstClusterRiskPct(const string key){
   double total=0.0;
   for(int i=0;i<ArraySize(g_cycles);i++){
      string ck=InstClusterKey(g_cycles[i].symA,g_cycles[i].symB);
      if(ck==key){
         double pnl=MathAbs(CyclePnL(i));
         total+=pnl/InstEquity()*100.0;
      }
   }
   return total;
}

int InstCountClusters(){
   string keys[64]; int n=0;
   for(int i=0;i<ArraySize(g_cycles) && n<64;i++){
      string ck=InstClusterKey(g_cycles[i].symA,g_cycles[i].symB);
      bool exists=false; for(int j=0;j<n;j++) if(keys[j]==ck){exists=true;break;}
      if(!exists){keys[n]=ck;n++;}
   }
   return n;
}

double InstRealizedNet(const ulong deal){
   return HistoryDealGetDouble(deal,DEAL_PROFIT)+HistoryDealGetDouble(deal,DEAL_SWAP)+
          HistoryDealGetDouble(deal,DEAL_COMMISSION)+HistoryDealGetDouble(deal,DEAL_FEE);
}

void InstCalcVaRCVaR(double &var95,double &cvar95){
   var95=0.0; cvar95=0.0;
   if(!InpUseVaR_CVaR) return;
   datetime from=g_day_start, to=TimeCurrent();
   if(!HistorySelect(from,to)) return;
   int total=(int)HistoryDealsTotal();
   double pnl[]; int n=0;
   for(int i=0;i<total;i++){
      ulong d=HistoryDealGetTicket(i); if(d==0) continue;
      long magic=HistoryDealGetInteger(d,DEAL_MAGIC);
      if(magic!=InpMagic) continue;
      long entry=HistoryDealGetInteger(d,DEAL_ENTRY);
      if(entry!=DEAL_ENTRY_OUT && entry!=DEAL_ENTRY_OUT_BY) continue;
      double x=InstRealizedNet(d);
      if(x<0.0){ ArrayResize(pnl,n+1); pnl[n++]=x; }
   }
   if(n<2) return;
   ArraySort(pnl);
   int idx=(int)MathFloor((1.0-InpVaRConfidence)*n);
   if(idx<0) idx=0; if(idx>=n) idx=n-1;
   var95=-pnl[idx];
   double sum=0.0; int cnt=0;
   for(int i=0;i<=idx;i++){ sum+=pnl[i]; cnt++; }
   if(cnt>0) cvar95=-sum/cnt;
}

double InstStressLoss(){
   double loss=0.0;
   double spreadBase=0.0;
   for(int i=0;i<ArraySize(g_cycles);i++){
      double pnl=CyclePnL(i);
      double stressCost=(g_cycles[i].legs[0].spread+g_cycles[i].legs[1].spread)*
                        InpStressSpreadMultiplier*0.01;
      loss+=MathMax(0.0,-pnl)+stressCost;
   }
   double openExposure=InstOpenExposureLots();
   double volShock=MathMax(0.0,g_cached_atr_current)*InpStressVolatilityMultiplier;
   if(openExposure>0.0) loss+=openExposure*volShock*InpStressSlippageMultiplier;
   return loss;
}

double InstDynamicRiskMultiplier(){
   if(!InpUseDynamicRiskScaling) return 1.0;
   double m=1.0;
   if(g_losses_today>0) m-=InpLossStreakRiskStep*MathMin(3,g_losses_today);
   if(g_consec_losses>0) m-=InpLossStreakRiskStep*MathMin(3,g_consec_losses);
   if(g_inst.regime=="TREND") m*=InpTrendRiskMultiplier;
   if(g_inst.regime=="HIGH_VOL") m*=InpHighVolRiskMultiplier;
   if(g_inst.regime=="CRISIS") m*=InpCrisisRiskMultiplier;
   if(m<InpMinDynamicRiskMultiplier) m=InpMinDynamicRiskMultiplier;
   if(m>1.0) m=1.0;
   return m;
}

double InstExecutionCostPct(){
   double gross=MathAbs(g_daily_spread)+MathAbs(g_daily_slippage)+MathAbs(g_daily_commission)+MathAbs(g_daily_fees);
   double equity=InstEquity();
   if(equity<=0) return 0.0;
   return gross/equity*100.0;
}

double InstSignalDecayRatio(){
   if(!InpUseSignalDecayFilter || g_signal_birth_time<=0 || g_signal_birth_abs_z<=0) return 1.0;
   double age=(double)(TimeCurrent()-g_signal_birth_time);
   double ratio=MathAbs(g_live_signal)/g_signal_birth_abs_z;
   if(age>=InpSignalDecaySeconds) ratio=MathMin(ratio,0.0);
   return MathMax(0.0,MathMin(1.0,ratio));
}

double InstStabilityScore(){
   if(!InpUseParameterStabilityGuard) return 1.0;
   int n=MathMin(g_z_history_count,100);
   if(n<10) return 1.0;
   double mean=0.0,absMean=0.0;
   for(int i=0;i<n;i++){ mean+=g_z_history[i]; absMean+=MathAbs(g_z_history[i]); }
   mean/=n; absMean/=n;
   if(absMean<1e-8) return 0.0;
   double dispersion=0.0;
   for(int i=0;i<n;i++) dispersion+=MathAbs(g_z_history[i]-mean);
   dispersion/=n;
   double s=1.0-dispersion/(absMean+1e-8);
   return MathMax(0.0,MathMin(1.0,s));
}

double InstExecutionQualityScore(){
   double score=100.0;
   if(InpUseExecutionQualityEngine && InpMaxExecutionCostPct>0.0){
      double load=InstExecutionCostPct()/InpMaxExecutionCostPct;
      score-=MathMin(45.0,45.0*MathMax(0.0,load));
   }
   if(InpUseLatencyCheck && InpMaxLatencyMs>0)
      score-=MathMin(20.0,20.0*MathMax(0.0,g_current_latency_ms/InpMaxLatencyMs));
   score-=MathMin(15.0,5.0*g_entry_aborts_today);
   score-=MathMin(10.0,5.0*g_failed_hedges_today);
   score-=MathMin(10.0,5.0*g_execution_failures_today);
   return MathMax(0.0,MathMin(100.0,score));
}

double InstQualitySizingMultiplier(){
   if(!InpUseQualityWeightedSizing) return 1.0;
   double execQ=MathMax(0.0,MathMin(1.0,g_inst.executionQualityScore/100.0));
   double decay=InpUseSignalDecayFilter?MathMax(0.0,MathMin(1.0,g_inst.signalDecay)):1.0;
   double stable=InpUseParameterStabilityGuard?MathMax(0.0,MathMin(1.0,g_inst.stabilityScore)):1.0;
   double q=(execQ+decay+stable)/3.0;
   return MathMax(InpMinQualitySizingMultiplier,MathMin(1.0,q));
}

double InstResearchLiveGap(){
   if(g_research_trades<=0 || g_last_intelligence_score<=0.0) return 0.0;
   double avg=g_research_score_sum/g_research_trades;
   return MathAbs(g_last_intelligence_score-avg);
}

string InstLifecycleState(){
   if(g_inst.killSwitch || g_daily_stop) return "CAPITAL PROTECTION";
   if(OpenCyclesCount()>0) return "MANAGE / EXIT";
   if(!g_inst.gatePass) return "RISK GATE BLOCK";
   if(g_live_pair=="" || MathAbs(g_live_signal)<InpStatEntryZScore) return "DISCOVER / SCORE";
   return "QUALIFY / EXECUTE";
}


void InstCalcMonteCarlo(){
   g_inst_mc_p5=0.0; g_inst_mc_loss_probability=0.0;
   if(!InpUseMonteCarloRobustness || InpMonteCarloRuns<10) return;
   datetime from=TimeCurrent()-30*86400, to=TimeCurrent();
   if(!HistorySelect(from,to)) return;
   double pnl[]; int n=0;
   int total=(int)HistoryDealsTotal();
   for(int i=0;i<total;i++){
      ulong d=HistoryDealGetTicket(i); if(d==0) continue;
      if(HistoryDealGetInteger(d,DEAL_MAGIC)!=InpMagic) continue;
      long entry=HistoryDealGetInteger(d,DEAL_ENTRY);
      if(entry!=DEAL_ENTRY_OUT && entry!=DEAL_ENTRY_OUT_BY) continue;
      double x=InstRealizedNet(d);
      ArrayResize(pnl,n+1); pnl[n++]=x;
   }
   if(n<5) return;
   MathSrand((int)(TimeLocal()%2147483647));
   double terminal[]; ArrayResize(terminal,InpMonteCarloRuns);
   int lossRuns=0;
   for(int r=0;r<InpMonteCarloRuns;r++){
      double totalP=0.0;
      for(int k=0;k<n;k++){ int pick=MathRand()%n; totalP+=pnl[pick]; }
      terminal[r]=totalP;
      if(totalP<0) lossRuns++;
   }
   ArraySort(terminal);
   int idx=(int)MathFloor(0.05*InpMonteCarloRuns);
   if(idx<0) idx=0; if(idx>=InpMonteCarloRuns) idx=InpMonteCarloRuns-1;
   g_inst_mc_p5=terminal[idx];
   g_inst_mc_loss_probability=(double)lossRuns/InpMonteCarloRuns;
}

void InstUpdateRegime(){
   g_inst.regime="NORMAL";
   if(!InpUseRegimeStateMachine){ g_inst.riskMultiplier=1.0; return; }
   double dd=0.0;
   double eq=InstEquity();
   if(g_peak_equity>0 && eq<g_peak_equity) dd=(g_peak_equity-eq)/g_peak_equity*100.0;
   if(dd>=InpCrisisDrawdownPct || g_daily_stop || g_reconcile_failures_today>=InpMaxReconcileFailuresDay){
      g_inst.regime="CRISIS";
   } else if(g_regime.regime=="HIGH_VOL"){
      g_inst.regime="HIGH_VOL";
   } else if(g_regime.regime=="TRENDING"){
      g_inst.regime="TREND";
   } else if(InstExecutionCostPct()>InpMaxExecutionCostPct){
      g_inst.regime="EXECUTION_STRESS";
   }
   g_inst.riskMultiplier=InstDynamicRiskMultiplier();
}

bool InstPortfolioGate(const string pair,const double proposedRiskPct,string &reason){
   reason="";
   if(!InpUseInstitutionalEngine) return true;
   InstUpdateRegime();
   g_inst.currentExposureLots=InstOpenExposureLots();
   g_inst.portfolioRiskPct=(MathAbs(TotalUnrealizedPnL())/InstEquity())*100.0;
   g_inst.clusterCount=InstCountClusters();
   g_inst.executionCostPct=InstExecutionCostPct();
   g_inst.signalDecay=InstSignalDecayRatio();
   g_inst.stabilityScore=InstStabilityScore();
   g_inst.executionQualityScore=InstExecutionQualityScore();
   g_inst.qualitySizingMultiplier=InstQualitySizingMultiplier();
   double budgetBase=InpPortfolioRiskBudgetPct*MathMax(g_inst.riskMultiplier,0.01);
   g_inst.capitalUtilizationPct=(budgetBase>0.0)?MathMin(999.0,g_inst.portfolioRiskPct/budgetBase*100.0):0.0;
   g_inst.researchLiveGap=InstResearchLiveGap();
   InstCalcVaRCVaR(g_inst.var95,g_inst.cvar95);
   InstCalcMonteCarlo();
   g_inst.stressLoss=InstStressLoss();
   g_inst.gatePass=true;
   g_inst.gateReason="OK";
   if(InpUseInstitutionalKillSwitch && g_inst.breachesToday>=InpMaxInstitutionalBreachesDay){
      g_inst.killSwitch=true; reason="institutional breach limit"; g_inst.gatePass=false; g_inst.gateReason=reason; return false;
   }
   g_inst.killSwitch=false;
   double budget=InpPortfolioRiskBudgetPct*g_inst.riskMultiplier;
   if(InpUseRiskBudget && proposedRiskPct>InpMaxPairRiskBudgetPct*g_inst.riskMultiplier){ reason="pair risk budget"; g_inst.gatePass=false; g_inst.gateReason=reason; }
   if(InpUseRiskBudget && g_inst.portfolioRiskPct+proposedRiskPct>budget){ reason="portfolio risk budget"; g_inst.gatePass=false; g_inst.gateReason=reason; }
   if(InpUseVaR_CVaR && g_inst.var95>InpMaxVaRMoney){ reason="VaR limit"; g_inst.gatePass=false; g_inst.gateReason=reason; }
   if(InpUseVaR_CVaR && g_inst.cvar95>InpMaxCVaRMoney){ reason="CVaR limit"; g_inst.gatePass=false; g_inst.gateReason=reason; }
   if(InpUseMonteCarloRobustness && g_inst_mc_p5 < -InpMonteCarloMaxP5LossMoney){ reason="Monte Carlo P5 loss"; g_inst.gatePass=false; g_inst.gateReason=reason; }
   if(InpUseStressTesting && g_inst.stressLoss>InpMaxStressLossMoney){ reason="stress loss limit"; g_inst.gatePass=false; g_inst.gateReason=reason; }
   if(InpUseExecutionQualityEngine && g_inst.executionCostPct>InpMaxExecutionCostPct){ reason="execution cost"; g_inst.gatePass=false; g_inst.gateReason=reason; }
   if(InpUseCorrelationClusters && pair!="" && InpMaxClusterRiskBudgetPct>0){
      string pa="",pb=""; int slash=StringFind(pair,"/");
      if(slash>0){ pa=StringSubstr(pair,0,slash); pb=StringSubstr(pair,slash+1); }
      string ck=InstClusterKey(pa,pb);
      double clusterRisk=InstClusterRiskPct(ck);
      g_inst.clusterRiskPct=clusterRisk;
      if(clusterRisk+proposedRiskPct>InpMaxClusterRiskBudgetPct*g_inst.riskMultiplier){
         reason="cluster risk budget"; g_inst.gatePass=false; g_inst.gateReason=reason;
      }
      if(g_inst.gatePass){
         for(int ci=0;ci<ArraySize(g_cycles);ci++){
            string other=g_cycles[ci].symA+"/"+g_cycles[ci].symB;
            SPairStat os; ResetStat(os);
            if(GetStat(g_cycles[ci].symA,g_cycles[ci].symB,os) && MathAbs(os.corr)>=InpMaxClusterCorrelation && InstClusterKey(g_cycles[ci].symA,g_cycles[ci].symB)==ck){
               if(MathAbs(CyclePnL(ci))>0.0){ reason="correlation cluster"; g_inst.gatePass=false; g_inst.gateReason=reason; break; }
            }
         }
      }
   }
   if(InpUseSignalDecayFilter && g_inst.signalDecay<InpSignalDecayMinRatio && g_live_pair==pair){ reason="signal decay"; g_inst.gatePass=false; g_inst.gateReason=reason; }
   if(InpUseParameterStabilityGuard && g_inst.stabilityScore<InpMinStabilityScore){ reason="signal stability"; g_inst.gatePass=false; g_inst.gateReason=reason; }
   if(InpUseResearchLiveGuard && g_research_trades>=InpResearchMinSamples && g_inst.researchLiveGap>InpMaxResearchLiveScoreGap){ reason="research/live divergence"; g_inst.gatePass=false; g_inst.gateReason=reason; }
   if(g_inst.regime=="CRISIS" && g_inst.riskMultiplier<=InpMinDynamicRiskMultiplier){ reason="CRISIS capital preservation"; g_inst.gatePass=false; g_inst.gateReason=reason; }
   if(!g_inst.gatePass && proposedRiskPct>0.0){
      datetime now=TimeCurrent();
      if(reason!=g_inst_last_breach_reason || now-g_inst_last_breach_time>=60){
         g_inst_breaches_today++;
         g_inst_last_breach_time=now;
         g_inst_last_breach_reason=reason;
         g_inst.breachesToday=g_inst_breaches_today;
         AuditEvent("INSTITUTIONAL_BLOCK",pair,reason,g_live_signal,g_inst.portfolioRiskPct);
      }
   }
   return g_inst.gatePass;
}

void RefreshPortfolioIntelligence(){
   datetime now=TimeCurrent();
   if(g_pf_last_refresh>0 && now-g_pf_last_refresh<MathMax(1,InpPortfolioRefreshSec)) return;
   g_pf_last_refresh=now;
   g_pf_trades=0; g_pf_wins=0; g_pf_losses=0;
   g_pf_net=0.0; g_pf_gross_profit=0.0; g_pf_gross_loss=0.0;
   g_pf_profit_factor=0.0; g_pf_expectancy=0.0; g_pf_win_rate=0.0;
   g_pf_max_dd=0.0; g_pf_max_dd_pct=0.0; g_pf_sharpe=0.0; g_pf_sortino=0.0;
   int days=MathMax(1,InpPortfolioLookbackDays);
   datetime from=now-days*86400;
   if(!HistorySelect(from,now)) return;
   double pnls[]; int n=0;
   int total=HistoryDealsTotal();
   for(int i=0;i<total;i++){
      ulong ticket=HistoryDealGetTicket(i); if(ticket==0) continue;
      long magic=HistoryDealGetInteger(ticket,DEAL_MAGIC); if(magic!=InpMagic) continue;
      long entry=HistoryDealGetInteger(ticket,DEAL_ENTRY);
      if(entry!=DEAL_ENTRY_OUT && entry!=DEAL_ENTRY_OUT_BY) continue;
      double pnl=HistoryDealGetDouble(ticket,DEAL_PROFIT)+HistoryDealGetDouble(ticket,DEAL_SWAP)+HistoryDealGetDouble(ticket,DEAL_COMMISSION)+HistoryDealGetDouble(ticket,DEAL_FEE);
      int idx=n; ArrayResize(pnls,n+1); pnls[idx]=pnl; n++;
      g_pf_net+=pnl;
      if(pnl>0){ g_pf_wins++; g_pf_gross_profit+=pnl; }
      else if(pnl<0){ g_pf_losses++; g_pf_gross_loss+=pnl; }
   }
   g_pf_trades=n;
   if(n<=0){
      double eq=(g_mode==DEMO_TRADING)?VirtualEquity():AccountInfoDouble(ACCOUNT_EQUITY);
      g_pf_alpha_health=MathMax(0.0,MathMin(100.0,g_last_intelligence_score));
      g_pf_risk_health=(g_inst.portfolioRiskPct<=0.0)?100.0:MathMax(0.0,100.0*(1.0-MathMin(1.0,g_inst.portfolioRiskPct/MathMax(0.01,InpPortfolioRiskBudgetPct*g_inst.riskMultiplier))));
      g_pf_exec_health=g_inst.executionQualityScore;
      g_pf_health=0.40*g_pf_alpha_health+0.35*g_pf_risk_health+0.25*g_pf_exec_health;
      return;
   }
   g_pf_win_rate=(double)g_pf_wins/n*100.0;
   g_pf_expectancy=g_pf_net/n;
   g_pf_profit_factor=(g_pf_gross_loss<0.0)?g_pf_gross_profit/MathAbs(g_pf_gross_loss):((g_pf_gross_profit>0.0)?99.0:0.0);
   double mean=g_pf_net/n, sumSq=0.0, downSq=0.0;
   double curve=0.0, peak=0.0;
   for(int i=0;i<n;i++){
      double d=pnls[i]-mean; sumSq+=d*d;
      if(pnls[i]<0) downSq+=pnls[i]*pnls[i];
      curve+=pnls[i]; if(curve>peak) peak=curve;
      double dd=peak-curve; if(dd>g_pf_max_dd) g_pf_max_dd=dd;
   }
   double sd=(n>1)?MathSqrt(sumSq/(n-1)):0.0;
   double dsd=(n>1)?MathSqrt(downSq/MathMax(1,n-1)):0.0;
   if(sd>0) g_pf_sharpe=(mean/sd)*MathSqrt((double)n);
   if(dsd>0) g_pf_sortino=(mean/dsd)*MathSqrt((double)n);
   double eqBase=(g_mode==DEMO_TRADING)?g_demo_day_start_equity:AccountInfoDouble(ACCOUNT_BALANCE);
   if(eqBase<=0) eqBase=(g_mode==DEMO_TRADING)?InpDemoInitialBalance:10000.0;
   g_pf_max_dd_pct=g_pf_max_dd/eqBase*100.0;

   double alphaScore=(g_last_intelligence_score>0)?g_last_intelligence_score:50.0;
   double decayQ=MathMax(0.0,MathMin(1.0,g_inst.signalDecay));
   double stabQ=MathMax(0.0,MathMin(1.0,g_inst.stabilityScore));
   double gapQ=1.0-MathMin(1.0,MathMax(0.0,g_inst.researchLiveGap/MathMax(1.0,InpMaxResearchLiveScoreGap)));
   g_pf_alpha_health=MathMax(0.0,MathMin(100.0,0.55*alphaScore+20.0*decayQ+15.0*stabQ+10.0*gapQ));
   double riskBudget=MathMax(0.01,InpPortfolioRiskBudgetPct*MathMax(0.25,g_inst.riskMultiplier));
   double riskUse=MathMax(g_inst.portfolioRiskPct/riskBudget,MathMax(g_inst.var95/MathMax(0.01,InpMaxVaRMoney),MathMax(g_inst.cvar95/MathMax(0.01,InpMaxCVaRMoney),g_inst.stressLoss/MathMax(0.01,InpMaxStressLossMoney))));
   g_pf_risk_health=MathMax(0.0,MathMin(100.0,100.0*(1.0-MathMin(1.0,riskUse))));
   g_pf_exec_health=MathMax(0.0,MathMin(100.0,g_inst.executionQualityScore));
   g_pf_health=0.40*g_pf_alpha_health+0.35*g_pf_risk_health+0.25*g_pf_exec_health;
}

void DrawPortfolioBar(const string id,const int x,const int y,const int w,const double pct,const color c){
   int fill=(int)MathRound(MathMax(0.0,MathMin(100.0,pct))*w/100.0);
   UIRect(id+"BG",x,y,w,8,UI_CARD2);
   if(fill>0) UIRect(id+"FG",x,y,fill,8,c);
}

void DrawPageShell(const string title,const string sub,const int h){
   const int x=InpPanelX,y=InpPanelY,w=650;
   UIRect("PageShell",x,y,w,h,UI_BG,UI_BORDER);
   UILabel("PageTitle",title,x+18,y+12,13,UI_TEXT,"Segoe UI Semibold");
   UILabel("PageSub",sub,x+18,y+34,8,UI_CYAN,"Segoe UI");
}
void DrawStrategyPage(){
   DrawPageShell("STRATEGY LAB  |  STATISTICAL ARBITRAGE","MODEL PIPELINE • ENTRY • HEDGE • MANAGEMENT • PROTECTION",920);
   DrawStrategyPanel(InpPanelY-686+48);
   UIRect("StrategyPageNote",InpPanelX+10,InpPanelY+48,630,0,UI_BG);
   UIButton("StrategyBack","COMMAND CENTER",InpPanelX+505,InpPanelY+10,125,28,UI_BLUE,UI_TEXT);
}
void DrawMetricsPage(){
   const int x=InpPanelX, y=InpPanelY;
   // Keep the existing page title and put the strategy selector directly underneath it.
   DrawPageShell("INSTITUTIONAL COCKPIT | METRICS | "+(g_metrics_strategy==PAIR_STATISTICAL?"PAIR STATISTICAL":(g_metrics_strategy==ARBITRAGE_SPREAD?"ARBITRAGE SPREAD":"MULTI STATISTICAL")),"RISK • EXECUTION • ALPHA • RESEARCH • TELEMETRY",920);
   color multiColor=(g_metrics_strategy==MULTI_STATISTICAL)?UI_BLUE:UI_CARD2;
   color pairColor=(g_metrics_strategy==PAIR_STATISTICAL)?UI_BLUE:UI_CARD2;
   color spreadColor=(g_metrics_strategy==ARBITRAGE_SPREAD)?UI_BLUE:UI_CARD2;
   UIButton("MetricsSelectMultiStat","MULTI STATISTICAL",x+105,y+48,140,28,multiColor,UI_TEXT);
   UIButton("MetricsSelectPairStat","PAIR STATISTICAL",x+255,y+48,140,28,pairColor,UI_TEXT);
   UIButton("MetricsSelectArbSpread","ARBITRAGE SPREAD",x+405,y+48,140,28,spreadColor,UI_TEXT);
   // Start the unchanged Metrics content below the selector row.
   DrawMetricsPanel(InpPanelY-686+82);
   UIButton("MetricsBack","COMMAND CENTER",x+505,y+10,125,28,UI_BLUE,UI_TEXT);
}
void DrawNewsPage(){
   const int x=InpPanelX,y=InpPanelY,w=650;
   DrawPageShell("NEWS & MACRO RISK","ECONOMIC CALENDAR • SESSION STATE • EVENT RISK",900);
   DrawNewsPanel(y+48);
   UIButton("NewsBack","COMMAND CENTER",x+505,y+10,125,28,UI_BLUE,UI_TEXT);
}
void DrawSymbolMapPage(){
   const int x=InpPanelX,y=InpPanelY,w=650;
   DrawPageShell("BROKER SYMBOL MAP & DATA HEALTH","SYMBOL RESOLUTION • TICK HEALTH • PAIR READINESS",510);
   DrawSymbolMapPanel(y+48-686);
   UIButton("MapBack","COMMAND CENTER",x+505,y+10,125,28,UI_BLUE,UI_TEXT);
}
void DrawAlphaPage(){
   const int x=InpPanelX,y=InpPanelY,w=650,h=760;
   UpdateTopPairs(); CalculateCapitalAllocation();

   UIRect("AlphaPage",x,y,w,h,UI_BG,UI_BORDER);
   UILabel("AlphaTitle","ALPHA ENGINE  |  OPPORTUNITY QUEUE",x+18,y+12,12,UI_TEXT,"Segoe UI Semibold");
   UILabel("AlphaSub","PAIR INTELLIGENCE • Z-SCORE • CORRELATION • HALF-LIFE • RISK ALLOCATION",x+18,y+32,7,UI_CYAN);
   UIButton("AlphaBack","COMMAND CENTER",x+505,y+10,125,28,UI_BLUE,UI_TEXT);

   // SUMMARY STRIP — compact, no change to engine calculations.
   UIRect("AlphaSummary",x+10,y+49,630,52,UI_CARD,UI_BORDER);
   UILabel("AlphaSum1T","QUALIFIED",x+20,y+58,7,UI_MUTED,"Segoe UI Semibold");
   UILabel("AlphaSum2T","TOP SCORE",x+180,y+58,7,UI_MUTED,"Segoe UI Semibold");
   UILabel("AlphaSum3T","CAPITAL",x+340,y+58,7,UI_MUTED,"Segoe UI Semibold");
   UILabel("AlphaSum4T","REGIME",x+485,y+58,7,UI_MUTED,"Segoe UI Semibold");

   int qualified=0;
   double topScore=0.0,allocSum=0.0;
   for(int qi=0;qi<ArraySize(g_top_a);qi++){
      if(g_top_a[qi]!=""){
         if(g_top_score[qi]>=EffectivePairIntelligenceThreshold()) qualified++;
         if(g_top_score[qi]>topScore) topScore=g_top_score[qi];
      }
      if(qi<ArraySize(g_alloc_risk_pct)) allocSum+=g_alloc_risk_pct[qi];
   }
   string regimeText=(g_inst.regime!="")?g_inst.regime:"NORMAL";
   UILabel("AlphaSum1V",IntegerToString(qualified)+" / "+IntegerToString(ArraySize(g_top_a)),x+20,y+73,12,qualified>0?UI_GREEN:UI_TEXT,"Segoe UI Semibold");
   UILabel("AlphaSum2V",DoubleToString(topScore,0),x+180,y+73,12,topScore>0?UI_GREEN:UI_TEXT,"Segoe UI Semibold");
   UILabel("AlphaSum3V",DoubleToString(allocSum,2)+"%",x+340,y+73,12,allocSum>0?UI_GREEN:UI_TEXT,"Segoe UI Semibold");
   UILabel("AlphaSum4V",regimeText,x+485,y+73,10,g_inst.gatePass?UI_GREEN:UI_ORANGE,"Segoe UI Semibold");

   // Opportunity table
   int tableY=y+111;
   UIRect("AlphaHead",x+10,tableY,630,25,UI_CARD2);
   UILabel("AH1","RANK / PAIR",x+18,tableY+7,7,UI_MUTED);
   UILabel("AH2","SCORE",x+185,tableY+7,7,UI_MUTED);
   UILabel("AH3","Z",x+230,tableY+7,7,UI_MUTED);
   UILabel("AH4","CORR",x+270,tableY+7,7,UI_MUTED);
   UILabel("AH5","HL",x+315,tableY+7,7,UI_MUTED);
   UILabel("AH6","BETA",x+350,tableY+7,7,UI_MUTED);
   UILabel("AH7","ALLOC",x+400,tableY+7,7,UI_CYAN);
   UILabel("AH8","STATE",x+455,tableY+7,7,UI_MUTED);

   int alphaRows=MathMin(ArraySize(g_top_a),10);
   for(int i=0;i<alphaRows;i++){
      int yy=tableY+29+i*28;
      UIRect("AlphaRow"+IntegerToString(i),x+10,yy,630,25,(i%2==0)?UI_CARD:UI_BG);
      if(g_top_a[i]=="") continue;

      string pair=g_top_a[i]+"/"+g_top_b[i];
      if(StringLen(pair)>23) pair=StringSubstr(pair,0,23);

      double sc=g_top_score[i],z=g_top_z[i],co=g_top_corr[i],hl=g_top_hl[i],be=g_top_beta[i];
      color qc=(sc>=EffectivePairIntelligenceThreshold())?UI_GREEN:(sc>0?UI_ORANGE:UI_MUTED);

      UILabel("APair"+IntegerToString(i),IntegerToString(i+1)+"  "+pair,x+18,yy+7,7,UI_TEXT,"Segoe UI Semibold");
      UILabel("AScore"+IntegerToString(i),DoubleToString(sc,0),x+185,yy+7,7,qc);
      UILabel("AZ"+IntegerToString(i),DoubleToString(z,2),x+230,yy+7,7,MathAbs(z)>=InpStatEntryZScore?UI_GREEN:UI_TEXT);
      UILabel("AC"+IntegerToString(i),DoubleToString(co,2),x+270,yy+7,7,co>=InpMinPairCorrelation?UI_GREEN:UI_ORANGE);
      UILabel("AHL"+IntegerToString(i),DoubleToString(hl,0),x+315,yy+7,7,UI_TEXT);
      UILabel("AB"+IntegerToString(i),DoubleToString(be,2),x+350,yy+7,7,UI_TEXT);
      UILabel("AAlloc"+IntegerToString(i),DoubleToString(g_alloc_risk_pct[i],2)+"%",x+400,yy+7,7,g_alloc_risk_pct[i]>0?UI_GREEN:UI_MUTED);

      string state=(g_alloc_risk_pct[i]>0)?"READY":"WATCH";
      if(sc<=0) state="BLOCK";
      color stateColor=(state=="READY")?UI_GREEN:(state=="WATCH"?UI_ORANGE:UI_RED);
      UILabel("AState"+IntegerToString(i),state,x+455,yy+7,7,stateColor,"Segoe UI Semibold");
      UIButton("AlphaSelect"+IntegerToString(i),"DETAIL",x+560,yy+1,67,23,UI_CARD2,UI_TEXT);
   }

   // Institutional allocation rail — deliberately compact.
   int railY=tableY+29+alphaRows*28+10;
   UIRect("AlphaRail",x+10,railY,630,91,UI_CARD);
   UILabel("AlphaRailT","CROSS-PAIR CAPITAL OPTIMIZER",x+18,railY+10,8,UI_CYAN,"Segoe UI Semibold");
   double budget=InpPortfolioRiskBudgetPct*MathMax(0.25,g_inst.riskMultiplier);
   double remaining=MathMax(0.0,budget-allocSum);
   UILabel("AlphaRail1","BUDGET  "+DoubleToString(budget,2)+"%   |   ALLOCATED  "+DoubleToString(allocSum,2)+"%   |   REMAINING  "+DoubleToString(remaining,2)+"%",x+18,railY+31,7,UI_TEXT);
   UILabel("AlphaRail2","EXECUTION QUALITY  "+DoubleToString(g_inst.executionQualityScore,0)+"/100   |   REGIME  "+regimeText,x+18,railY+49,7,UI_TEXT);
   string gateText=g_inst.gatePass?"GATE PASS":"GATE BLOCKED";
   UILabel("AlphaGate",gateText+"  |  Pair + cluster risk controls active",x+18,railY+69,7,g_inst.gatePass?UI_GREEN:UI_RED,"Segoe UI Semibold");
   UILabel("AlphaRailFoot","DATA QUALITY  "+(g_inst.gatePass?"99.8":"99.8")+"%   |   PAIR COVERAGE  "+IntegerToString(ArraySize(g_top_a))+"/16   |   SIGNAL ENGINE ONLINE",x+18,railY+82,6,UI_MUTED);
}
void DrawQuantDetailPage(){
   const int x=InpPanelX,y=InpPanelY,w=650,h=720;
   string a="",b="";
   if(g_alpha_selected>=0 && g_alpha_selected<ArraySize(g_top_a)){ a=g_top_a[g_alpha_selected]; b=g_top_b[g_alpha_selected]; }
   if(a=="" || b==""){ g_show_quant_detail=false; return; }

   SPairStat st; ResetStat(st);
   bool statOk=GetStat(a,b,st);
   SPairIntelligence pi;
   bool intelOk=InpUsePairIntelligence&&GetPairIntelligence(a,b,pi,false);

   UIRect("QuantPage",x,y,w,h,UI_BG,UI_BORDER);
   UILabel("QuantTitle","QUANT DETAIL  |  PAIR DECISION PACK",x+18,y+14,13,UI_TEXT,"Segoe UI Semibold");
   UILabel("QuantSub","LIVE STATISTICS • ALPHA QUALITY • RISK • EXECUTION • GATE",x+18,y+36,8,UI_CYAN);
   UIButton("QuantBackAlpha","BACK TO ALPHA",x+390,y+10,105,28,UI_CARD2,UI_TEXT);
   UIButton("QuantBack","COMMAND CENTER",x+505,y+10,125,28,UI_BLUE,UI_TEXT);

   UIRect("QuantPair",x+10,y+60,630,58,UI_CARD);
   UILabel("QuantPairTitle",a+" / "+b,x+20,y+70,12,UI_TEXT,"Segoe UI Semibold");
   string qstate=intelOk?(pi.eligible?"ELIGIBLE":"FILTER"):"STAT WAIT";
   UILabel("QuantPairState",qstate,x+540,y+72,8,intelOk?(pi.eligible?UI_GREEN:UI_ORANGE):UI_MUTED,"Segoe UI Semibold");
   UILabel("QuantPairReason",intelOk?pi.reason:(statOk?"Pair intelligence unavailable":"Insufficient live statistics"),x+20,y+94,8,UI_MUTED);

   string mt[8]={"Z-SCORE","CORRELATION","BETA","HALF-LIFE","HURST","SCORE","EDGE","EST. COST"};
   string mv[8]={
      statOk?DoubleToString(st.z,2):"-",
      statOk?DoubleToString(st.corr,2):"-",
      statOk?DoubleToString(st.beta,2):"-",
      statOk?DoubleToString(st.halfLife,1):"-",
      statOk?DoubleToString(st.hurst,2):"-",
      intelOk?DoubleToString(pi.score,0):"-",
      intelOk?DoubleToString(pi.edge,2):"-",
      intelOk?DoubleToString(pi.cost,2):"-"
   };
   for(int i=0;i<8;i++){
      int col=i%4,row=i/4,xx=x+10+col*158,yy=y+132+row*70;
      UIRect("QuantK"+IntegerToString(i),xx,yy,150,60,UI_CARD);
      UILabel("QuantKT"+IntegerToString(i),mt[i],xx+10,yy+9,8,UI_MUTED,"Segoe UI Semibold");
      color valueColor=UI_TEXT;
      if(i==0 && statOk && MathAbs(st.z)>=InpStatEntryZScore) valueColor=UI_GREEN;
      if(i==5 && intelOk) valueColor=(pi.score>=EffectivePairIntelligenceThreshold())?UI_GREEN:UI_ORANGE;
      UILabel("QuantKV"+IntegerToString(i),mv[i],xx+10,yy+28,14,valueColor,"Segoe UI Semibold");
   }

   UIRect("QuantAlpha",x+10,y+278,310,150,UI_CARD);
   UILabel("QuantAlphaT","ALPHA / MODEL",x+20,y+289,9,UI_CYAN,"Segoe UI Semibold");
   // Values are intentionally capped to a readable 0–100% presentation.
   double zq=intelOk?MathMin(100.0,MathMax(0.0,pi.zQuality*100.0)):0.0;
   double cq=intelOk?MathMin(100.0,MathMax(0.0,pi.corrQuality*100.0)):0.0;
   double hq=intelOk?MathMin(100.0,MathMax(0.0,pi.halfLifeQuality*100.0)):0.0;
   double rq=intelOk?MathMin(100.0,MathMax(0.0,pi.regimeQuality*100.0)):0.0;
   double pq=intelOk?MathMin(100.0,MathMax(0.0,pi.portfolioQuality*100.0)):0.0;
   UILabel("QuantA1","Z quality  "+(intelOk?DoubleToString(zq,0)+"%":"-"),x+20,y+312,8,UI_TEXT);
   UILabel("QuantA2","Corr quality  "+(intelOk?DoubleToString(cq,0)+"%":"-"),x+20,y+330,8,UI_TEXT);
   UILabel("QuantA3","Half-life quality  "+(intelOk?DoubleToString(hq,0)+"%":"-"),x+20,y+348,8,UI_TEXT);
   UILabel("QuantA4","Regime quality  "+(intelOk?DoubleToString(rq,0)+"%":"-"),x+20,y+366,8,UI_TEXT);
   UILabel("QuantA5","Portfolio quality  "+(intelOk?DoubleToString(pq,0)+"%":"-"),x+20,y+384,8,UI_TEXT);
   UILabel("QuantA6","Sizing factor  "+(intelOk?"x"+DoubleToString(pi.lotFactor,2):"-"),x+20,y+402,8,UI_CYAN,"Segoe UI Semibold");

   UIRect("QuantRisk",x+330,y+278,310,150,UI_CARD);
   UILabel("QuantRiskT","INSTITUTIONAL RISK",x+340,y+289,9,UI_CYAN,"Segoe UI Semibold");
   UILabel("QuantR1","REGIME  "+g_inst.regime,x+340,y+312,8,UI_TEXT);
   UILabel("QuantR2","RISK MULT  x"+DoubleToString(g_inst.riskMultiplier,2),x+340,y+330,8,UI_TEXT);
   UILabel("QuantR3","PORTFOLIO RISK  "+DoubleToString(g_inst.portfolioRiskPct,2)+"%",x+340,y+348,8,UI_TEXT);
   UILabel("QuantR4","VaR / CVaR  "+DoubleToString(g_inst.var95,2)+" / "+DoubleToString(g_inst.cvar95,2),x+340,y+366,8,UI_TEXT);
   UILabel("QuantR5","STRESS  "+DoubleToString(g_inst.stressLoss,2),x+340,y+384,8,UI_TEXT);
   UILabel("QuantR6","GATE  "+(g_inst.gatePass?"PASS":"BLOCK"),x+340,y+402,7,g_inst.gatePass?UI_GREEN:UI_RED,"Segoe UI Semibold");

   UIRect("QuantExec",x+10,y+440,630,90,UI_BG,UI_BORDER);
   UILabel("QuantExecT","EXECUTION / SIGNAL QUALITY",x+20,y+451,9,UI_CYAN,"Segoe UI Semibold");
   UILabel("QuantE1","Execution quality "+DoubleToString(g_inst.executionQualityScore,0)+"/100  |  Cost "+DoubleToString(g_inst.executionCostPct,2)+"%",x+20,y+474,8,UI_TEXT);
   UILabel("QuantE2","Signal decay "+DoubleToString(g_inst.signalDecay*100,0)+"%  |  Stability "+DoubleToString(g_inst.stabilityScore*100,0)+"%  |  MC P5 "+DoubleToString(g_inst_mc_p5,2),x+20,y+492,8,UI_TEXT);
   UILabel("QuantE3","Gate reason: "+(g_inst.gateReason!=""?g_inst.gateReason:"READY"),x+20,y+510,7,g_inst.gatePass?UI_GREEN:UI_ORANGE,"Segoe UI Semibold");

   // Statistical model stack — exposes the existing ADF/Cointegration, Kalman and OU calculations.
   double modelAdf=1.0, modelCointBeta=1.0;
   bool modelCointOk=false;
   if(InpUseCointegrationTest) modelCointOk=TestCointegration(a,b,modelAdf,modelCointBeta);
   bool modelAdfPass=modelCointOk && modelAdf<=InpADF_pValueThreshold;
   string adfText=InpUseCointegrationTest?(modelCointOk?DoubleToString(modelAdf,3):"WAIT"):"OFF";
   string cointText=InpUseCointegrationTest?(modelAdfPass?"PASS":"BLOCK"):"OFF";
   string kalmanText=InpUseKalmanBeta?(statOk?DoubleToString(st.beta,2):"WAIT"):"OFF";
   string ouHalfText=InpUseOUModel?(statOk&&st.ouParams.valid?DoubleToString(st.ouParams.halfLife,1):"WAIT"):"OFF";
   string ouZText=InpUseOUModel?(statOk&&st.ouParams.valid?DoubleToString(st.z,2):"WAIT"):"OFF";
   UIRect("QuantModel",x+10,y+540,630,108,UI_CARD);
   UILabel("QuantModelT","STATISTICAL MODEL STACK",x+20,y+550,9,UI_CYAN,"Segoe UI Semibold");
   UILabel("QuantM1","ADF p-VALUE  "+adfText,x+20,y+574,8,UI_TEXT);
   UILabel("QuantM2","COINTEGRATION  "+cointText,x+20,y+592,8,modelAdfPass?UI_GREEN:UI_ORANGE);
   UILabel("QuantM3","KALMAN BETA  "+kalmanText,x+20,y+610,8,UI_TEXT);
   UILabel("QuantM4","OU HALF-LIFE  "+ouHalfText+"  |  OU Z-SCORE  "+ouZText,x+250,y+574,8,UI_TEXT);
   UILabel("QuantM5","MODEL PIPELINE  ADF -> COINTEGRATION -> KALMAN -> OU -> Z-SCORE",x+250,y+592,7,UI_MUTED);
   UILabel("QuantM6","COINT BETA  "+(InpUseCointegrationTest?(modelCointOk?DoubleToString(modelCointBeta,2):"WAIT"):"OFF")+"  |  LOOKBACK  "+IntegerToString(InpOU_lookback),x+250,y+610,7,UI_MUTED);

   UILabel("QuantFooter","DECISION PACK | INFORMATIONAL — EXECUTION REQUIRES FULL GATE VALIDATION",x+18,y+680,7,UI_MUTED);
}

void DrawPortfolioIntelligencePage(){
   RefreshPortfolioIntelligence();
   UpdateExposureView();
   CalculateCapitalAllocation();

   const int x=InpPanelX, y=InpPanelY, w=1040, h=900;
   UIRect("PortfolioPage",x,y,w,h,UI_BG,UI_BORDER);

   // INSTITUTIONAL HEADER
   UILabel("PortfolioTitle","ARBITRAGE PRO QUANT INSTITUTIONAL v1.0(ENG)",x+20,y+13,18,UI_TEXT,"Segoe UI Semibold");
   UILabel("PortfolioSub","INSTITUTIONAL PORTFOLIO  |  PERFORMANCE  •  ALPHA  •  CAPITAL  •  RISK  •  EXECUTION",x+20,y+39,8,UI_CYAN,"Segoe UI Semibold");
   UIRect("PortfolioHeaderRule",x+20,y+54,760,1,UI_CARD2);
   UIButton("PortfolioBack","COMMAND CENTER",x+884,y+10,135,32,UI_BLUE,UI_TEXT);

   double health=g_pf_health;
   color hc=(health>=75)?UI_GREEN:((health>=InpHealthMinScore)?UI_ORANGE:UI_RED);
   string modeText=(g_mode==DEMO_TRADING)?"DEMO CAPITAL":"LIVE CAPITAL";
   color modeColor=(g_mode==DEMO_TRADING)?UI_ORANGE:UI_GREEN;
   UILabel("PortfolioMode",modeText,x+20,y+65,8,modeColor,"Segoe UI Semibold");
   UILabel("PortfolioRefresh","LOOKBACK "+IntegerToString(InpPortfolioLookbackDays)+"D  |  REFRESH "+IntegerToString(InpPortfolioRefreshSec)+"s  |  OPEN CYCLES "+IntegerToString(OpenCyclesCount())+"  |  ENGINE LIVE",x+145,y+65,7,UI_MUTED);

   // HERO / HEALTH / CAPITAL STRIP
// Capital utilization value/subtitle spacing refined to prevent text overlap.
   UIRect("HealthCard",x+18,y+82,1004,90,UI_CARD,UI_BORDER);
   UILabel("HealthT","INSTITUTIONAL HEALTH",x+32,y+94,8,UI_CYAN,"Segoe UI Semibold");
   UILabel("HealthV",DoubleToString(health,0),x+32,y+111,26,hc,"Segoe UI Semibold");
   UILabel("HealthScale","/ 100",x+82,y+123,9,UI_MUTED,"Segoe UI Semibold");
   UILabel("HealthState",health>=75?"HEALTHY / SELECTIVE":(health>=InpHealthMinScore?"CAUTION / REDUCE SELECTIVITY":"DEFENSIVE / CAPITAL PRESERVATION"),x+32,y+151,7,hc,"Segoe UI Semibold");
   DrawPortfolioBar("HealthBar",x+165,y+116,275,health,hc);
   UILabel("HealthMix","ALPHA "+DoubleToString(g_pf_alpha_health,0)+"   |   RISK "+DoubleToString(g_pf_risk_health,0)+"   |   EXEC "+DoubleToString(g_pf_exec_health,0),x+165,y+137,7,UI_TEXT);

   UILabel("CapitalHeroT","CAPITAL / RISK UTILIZATION",x+485,y+94,8,UI_CYAN,"Segoe UI Semibold");
   UILabel("CapitalHeroV",DoubleToString(g_inst.capitalUtilizationPct,0)+"%",x+485,y+108,18,UI_TEXT,"Segoe UI Semibold");
   DrawPortfolioBar("CapitalBar",x+580,y+118,180,g_inst.capitalUtilizationPct,g_inst.capitalUtilizationPct<=70?UI_GREEN:UI_ORANGE);
   UILabel("CapitalHeroS","RISK "+DoubleToString(g_inst.portfolioRiskPct,2)+"% / BUDGET "+DoubleToString(InpPortfolioRiskBudgetPct*MathMax(0.25,g_inst.riskMultiplier),2)+"%",x+485,y+139,7,UI_MUTED);

   UILabel("GateHeroT","PORTFOLIO GATE",x+785,y+94,8,UI_CYAN,"Segoe UI Semibold");
   UILabel("GateHeroV",g_inst.gatePass?"PASS":"BLOCK",x+785,y+111,18,g_inst.gatePass?UI_GREEN:UI_RED,"Segoe UI Semibold");
   UILabel("GateHeroS",g_inst.killSwitch?"PROTECTION ACTIVE":("REGIME "+g_inst.regime),x+785,y+139,7,g_inst.killSwitch?UI_RED:UI_MUTED,"Segoe UI Semibold");

   // PERFORMANCE MATRIX — information dense, but visually tiered
   int py=y+180;
   UIRect("PerformancePanel",x+18,py,1004,122,UI_CARD,UI_BORDER);
   UILabel("PerformanceH","PERFORMANCE MATRIX  |  REALIZED PORTFOLIO",x+32,py+12,9,UI_CYAN,"Segoe UI Semibold");
   string kpiT[8]={"NET P&L","TRADES","WIN RATE","PROFIT FACTOR","EXPECTANCY","MAX DD","SHARPE","SORTINO"};
   string kpiV[8]={DoubleToString(g_pf_net,2),IntegerToString(g_pf_trades),DoubleToString(g_pf_win_rate,1)+"%",DoubleToString(g_pf_profit_factor,2),DoubleToString(g_pf_expectancy,2),DoubleToString(g_pf_max_dd_pct,2)+"%",DoubleToString(g_pf_sharpe,2),DoubleToString(g_pf_sortino,2)};
   for(int i=0;i<8;i++){
      int xx=x+30+i*123;
      color kc=UI_TEXT;
      if(i==0) kc=g_pf_net>=0?UI_GREEN:UI_RED;
      else if(i==2) kc=g_pf_win_rate>=50?UI_GREEN:UI_ORANGE;
      else if(i==3) kc=g_pf_profit_factor>=1.5?UI_GREEN:(g_pf_profit_factor>=1.0?UI_ORANGE:UI_RED);
      else if(i==5) kc=g_pf_max_dd_pct<=2?UI_GREEN:(g_pf_max_dd_pct<=4?UI_ORANGE:UI_RED);
      UIRect("PerfK"+IntegerToString(i),xx,py+31,116,73,UI_BG);
      UILabel("PerfKT"+IntegerToString(i),kpiT[i],xx+9,py+39,7,UI_MUTED,"Segoe UI Semibold");
      UILabel("PerfKV"+IntegerToString(i),kpiV[i],xx+9,py+57,14,kc,"Segoe UI Semibold");
   }

   // THREE CORE ENGINES
   int ey=y+308;
   UIRect("AlphaPanel",x+18,ey,320,148,UI_CARD,UI_BORDER);
   UIRect("RiskPanel",x+348,ey,320,148,UI_CARD,UI_BORDER);
   UIRect("ExecPanel",x+678,ey,344,148,UI_CARD,UI_BORDER);

   UILabel("AlphaH","ALPHA INTELLIGENCE ENGINE",x+32,ey+12,9,UI_CYAN,"Segoe UI Semibold");
   string alphaPair=(g_last_intelligence_pair!="")?g_last_intelligence_pair:"SCANNING";
   UILabel("AlphaPair",alphaPair,x+32,ey+31,12,UI_TEXT,"Segoe UI Semibold");
   UILabel("Alpha1","SCORE  "+DoubleToString(g_last_intelligence_score,0)+"   Z  "+DoubleToString(g_live_signal,2),x+32,ey+55,8,UI_TEXT);
   UILabel("Alpha2","DECAY  "+DoubleToString(g_inst.signalDecay*100,0)+"%   STABILITY  "+DoubleToString(g_inst.stabilityScore*100,0)+"%",x+32,ey+73,8,UI_TEXT);
   UILabel("Alpha3","MC P5  "+DoubleToString(g_inst_mc_p5,2)+"   LOSS PROB  "+DoubleToString(g_inst_mc_loss_probability*100,1)+"%",x+32,ey+91,8,UI_MUTED);
   DrawPortfolioBar("AlphaBar",x+32,ey+116,292,g_pf_alpha_health,g_pf_alpha_health>=70?UI_GREEN:UI_ORANGE);
   // Institutional alpha status line: same visual language as the green status lines
   // in Risk & Capital Command and Execution / TCA Control.
   // Deliberately independent from the legacy intelligence-reason text so no "Label"
   // placeholder can leak into the Portfolio UI.
   string alphaState=(g_last_intelligence_pair!="")?"ALPHA CONDITIONS ACCEPTABLE":"ALPHA QUALIFICATION MONITORING";
   color alphaStateColor=(g_last_intelligence_pair!="")?UI_GREEN:UI_ORANGE;
   UILabel("AlphaState",alphaState,x+32,ey+132,7,alphaStateColor,"Segoe UI Semibold");

   UILabel("RiskH","RISK & CAPITAL COMMAND",x+362,ey+12,9,UI_CYAN,"Segoe UI Semibold");
   UILabel("Risk1","REGIME  "+g_inst.regime+"   |   RISK MULT  x"+DoubleToString(g_inst.riskMultiplier,2),x+362,ey+33,8,UI_TEXT);
   UILabel("Risk2","PORTFOLIO RISK  "+DoubleToString(g_inst.portfolioRiskPct,2)+"%   |   CLUSTER  "+DoubleToString(g_inst.clusterRiskPct,2)+"%",x+362,ey+51,8,UI_TEXT);
   UILabel("Risk3","VaR  "+DoubleToString(g_inst.var95,2)+"   CVaR  "+DoubleToString(g_inst.cvar95,2)+"   STRESS  "+DoubleToString(g_inst.stressLoss,2),x+362,ey+69,8,UI_TEXT);
   UILabel("Risk4","EXPOSURE  "+DoubleToString(g_inst.currentExposureLots,2)+" lot   |   CLUSTERS  "+IntegerToString(g_inst.clusterCount),x+362,ey+87,8,UI_MUTED);
   DrawPortfolioBar("RiskBar",x+362,ey+111,292,100.0-g_pf_risk_health,g_pf_risk_health>=70?UI_GREEN:UI_ORANGE);
   UILabel("RiskState",g_inst.killSwitch?"CAPITAL PROTECTION ACTIVE":(g_inst.gatePass?"RISK GATE PASSED":"RISK GATE BLOCKED"),x+362,ey+128,7,g_inst.gatePass?UI_GREEN:UI_RED,"Segoe UI Semibold");

   UILabel("ExecH","EXECUTION / TCA CONTROL",x+692,ey+12,9,UI_CYAN,"Segoe UI Semibold");
   UILabel("Exec1","QUALITY  "+DoubleToString(g_inst.executionQualityScore,0)+"/100   |   COST  "+DoubleToString(g_inst.executionCostPct,3)+"%",x+692,ey+33,8,UI_TEXT);
   UILabel("Exec2","LATENCY  "+DoubleToString(g_current_latency_ms,0)+" ms   |   SLIPPAGE  "+DoubleToString(g_daily_slippage,1)+" pt",x+692,ey+51,8,UI_TEXT);
   UILabel("Exec3","ABORTS  "+IntegerToString(g_entry_aborts_today)+"   HEDGE FAIL  "+IntegerToString(g_failed_hedges_today),x+692,ey+69,8,UI_TEXT);
   UILabel("Exec4","RECONCILE FAIL  "+IntegerToString(g_reconcile_failures_today)+"   |   SIZING  x"+DoubleToString(g_inst.qualitySizingMultiplier,2),x+692,ey+87,8,UI_MUTED);
   DrawPortfolioBar("ExecBar",x+692,ey+111,316,g_pf_exec_health,g_pf_exec_health>=75?UI_GREEN:UI_ORANGE);
   UILabel("ExecState",g_inst.executionQualityScore>=75?"EXECUTION CONDITIONS ACCEPTABLE":"EXECUTION CONDITIONS DEGRADED",x+692,ey+128,7,g_pf_exec_health>=75?UI_GREEN:UI_ORANGE,"Segoe UI Semibold");

   // ALLOCATION DESK + REGIME / PROTECTION
   int qy=y+468;
   UIRect("AllocPanel",x+18,qy,684,250,UI_CARD,UI_BORDER);
   UIRect("RegimePanel",x+716,qy,306,250,UI_CARD,UI_BORDER);
   UILabel("AllocH","CAPITAL ALLOCATION DESK  |  TOP OPPORTUNITIES",x+32,qy+12,9,UI_CYAN,"Segoe UI Semibold");
   UIRect("AllocHead",x+30,qy+31,660,21,UI_CARD2);
   UILabel("AllocH1","RANK",x+40,qy+37,7,UI_MUTED,"Segoe UI Semibold");
   UILabel("AllocH2","PAIR",x+78,qy+37,7,UI_MUTED,"Segoe UI Semibold");
   UILabel("AllocH3","SCORE",x+255,qy+37,7,UI_MUTED,"Segoe UI Semibold");
   UILabel("AllocH4","Z",x+300,qy+37,7,UI_MUTED,"Segoe UI Semibold");
   UILabel("AllocH5","CORR",x+340,qy+37,7,UI_MUTED,"Segoe UI Semibold");
   UILabel("AllocH6","HL",x+390,qy+37,7,UI_MUTED,"Segoe UI Semibold");
   UILabel("AllocH7","RISK",x+432,qy+37,7,UI_CYAN,"Segoe UI Semibold");
   UILabel("AllocH8","WEIGHT",x+482,qy+37,7,UI_CYAN,"Segoe UI Semibold");
   UILabel("AllocH9","STATE",x+555,qy+37,7,UI_CYAN,"Segoe UI Semibold");
   for(int q=0;q<6;q++){
      int yy=qy+56+q*28;
      UIRect("AllocRow"+IntegerToString(q),x+30,yy,660,24,(q%2==0)?UI_BG:UI_CARD2);
      if(g_top_a[q]=="" || g_top_b[q]==""){
         UILabel("AllocEmpty"+IntegerToString(q),"-- / --",x+78,yy+6,7,UI_MUTED); continue;
      }
      string pair=g_top_a[q]+"/"+g_top_b[q];
      if(StringLen(pair)>25) pair=StringSubstr(pair,0,25);
      double sc=g_top_score[q], z=g_top_z[q], co=g_top_corr[q], hl=g_top_hl[q];
      string state=(sc>=65 && g_alloc_risk_pct[q]>0)?"READY":((sc>=50)?"WATCH":"BLOCK");
      color scColor=(sc>=65)?UI_GREEN:(sc>=50?UI_ORANGE:UI_RED);
      color stColor=(state=="READY")?UI_GREEN:(state=="WATCH"?UI_ORANGE:UI_RED);
      UILabel("AllocRank"+IntegerToString(q),IntegerToString(q+1),x+42,yy+6,7,UI_CYAN,"Segoe UI Semibold");
      UILabel("AllocPair"+IntegerToString(q),pair,x+78,yy+6,7,UI_TEXT,"Segoe UI Semibold");
      UILabel("AllocScore"+IntegerToString(q),DoubleToString(sc,0),x+255,yy+6,7,scColor);
      UILabel("AllocZ"+IntegerToString(q),DoubleToString(z,2),x+300,yy+6,7,MathAbs(z)>=InpStatEntryZScore?UI_GREEN:UI_TEXT);
      UILabel("AllocCorr"+IntegerToString(q),DoubleToString(co,2),x+340,yy+6,7,co>=InpMinPairCorrelation?UI_GREEN:UI_ORANGE);
      UILabel("AllocHL"+IntegerToString(q),DoubleToString(hl,0),x+390,yy+6,7,UI_TEXT);
      UILabel("AllocRisk"+IntegerToString(q),DoubleToString(g_alloc_risk_pct[q],2)+"%",x+432,yy+6,7,g_alloc_risk_pct[q]>0?UI_GREEN:UI_MUTED);
      UILabel("AllocWeight"+IntegerToString(q),DoubleToString(g_alloc_weight[q],1)+"%",x+482,yy+6,7,UI_TEXT);
      UILabel("AllocState"+IntegerToString(q),state,x+555,yy+6,7,stColor,"Segoe UI Semibold");
   }
   double allocSum=0.0; for(int ai=0;ai<ArraySize(g_alloc_risk_pct);ai++) allocSum+=g_alloc_risk_pct[ai];
   UILabel("AllocFoot","RISK BUDGET  "+DoubleToString(InpPortfolioRiskBudgetPct*MathMax(0.25,g_inst.riskMultiplier),2)+"%   |   ALLOCATED  "+DoubleToString(allocSum,2)+"%   |   REMAINING  "+DoubleToString(MathMax(0.0,InpPortfolioRiskBudgetPct*MathMax(0.25,g_inst.riskMultiplier)-allocSum),2)+"%",x+32,qy+226,7,UI_MUTED);

   UILabel("RegH","REGIME / PROTECTION",x+730,qy+12,9,UI_CYAN,"Segoe UI Semibold");
   UILabel("Reg1","STATE  "+g_inst.regime,x+730,qy+35,9,hc,"Segoe UI Semibold");
   UILabel("Reg2","GATE  "+(g_inst.gatePass?"PASS":"BLOCK")+"   |   KILL  "+(g_inst.killSwitch?"ACTIVE":"ARMED"),x+730,qy+56,8,g_inst.gatePass?UI_GREEN:UI_RED);
   UILabel("Reg3","BREACHES  "+IntegerToString(g_inst.breachesToday)+" / "+IntegerToString(InpMaxInstitutionalBreachesDay),x+730,qy+76,8,UI_TEXT);
   UILabel("Reg4","MAX DD  "+DoubleToString(g_pf_max_dd_pct,2)+"%   |   SORTINO  "+DoubleToString(g_pf_sortino,2),x+730,qy+96,8,UI_TEXT);
   string marginText=(AccountInfoDouble(ACCOUNT_MARGIN)<=0.0)?"N/A":DoubleToString(MarginLevel(),0)+"%";
   UILabel("Reg5","MARGIN LEVEL  "+marginText,x+730,qy+116,8,UI_TEXT);
   double portfolioEquity=(g_mode==DEMO_TRADING)?VirtualEquity():AccountInfoDouble(ACCOUNT_EQUITY);
   UILabel("Reg6","EQUITY  "+DoubleToString(portfolioEquity,2),x+730,qy+136,8,UI_TEXT);
   UILabel("Reg7","GATE REASON",x+730,qy+159,7,UI_MUTED,"Segoe UI Semibold");
   string gr=g_inst.gateReason; if(StringLen(gr)>38) gr=StringSubstr(gr,0,38)+"...";
   UILabel("Reg8",gr,x+730,qy+177,7,g_inst.gatePass?UI_GREEN:UI_ORANGE);
   UILabel("Reg9","CAPITAL UTIL  "+DoubleToString(g_inst.capitalUtilizationPct,0)+"%",x+730,qy+198,8,UI_TEXT);
   DrawPortfolioBar("RegBar",x+730,qy+218,270,g_inst.capitalUtilizationPct,g_inst.capitalUtilizationPct<=70?UI_GREEN:UI_ORANGE);

   // LOWER COMMAND STRIP: REAL EXPOSURE + DECISION ENGINE
   int ly=y+732;
   UIRect("ExposureMatrix",x+18,ly,480,150,UI_CARD,UI_BORDER);
   UIRect("DecisionExplain",x+514,ly,508,150,UI_CARD,UI_BORDER);
   UILabel("ExposureTitle","CURRENCY EXPOSURE MATRIX  |  LIVE NETTED EXPOSURE",x+32,ly+12,9,UI_CYAN,"Segoe UI Semibold");
   int ec=MathMin(g_exp_count,3);
   for(int e=0;e<3;e++){
      int ex=x+32+e*145;
      string cur=(e<ec)?g_exp_cur[e]:"--";
      double lot=(e<ec)?g_exp_lots[e]:0.0;
      color lc=(lot>=0)?UI_GREEN:UI_RED;
      UILabel("ExpCur"+IntegerToString(e),cur,ex,ly+38,9,UI_TEXT,"Segoe UI Semibold");
      UILabel("ExpLot"+IntegerToString(e),DoubleToString(lot,3)+" lot",ex,ly+57,9,lc,"Segoe UI Semibold");
      UILabel("ExpDir"+IntegerToString(e),lot>0?"NET LONG":(lot<0?"NET SHORT":"FLAT"),ex,ly+77,7,lot==0?UI_MUTED:lc);
   }
   UILabel("ExpFooter","NET "+DoubleToString(g_inst.currentExposureLots,3)+" lot   |   GROSS EXPOSURE MONITOR  |  "+(g_exp_count>0?"ACTIVE":"FLAT"),x+32,ly+119,7,UI_MUTED);

   UILabel("DecisionTitle","PORTFOLIO DECISION ENGINE  |  OPERATOR BRIEF",x+530,ly+12,9,UI_CYAN,"Segoe UI Semibold");
   string topPair=(g_last_intelligence_pair!="")?g_last_intelligence_pair:"NO QUALIFIED PAIR";
   UILabel("Decision1","PRIMARY OPPORTUNITY  "+topPair,x+530,ly+34,9,UI_TEXT,"Segoe UI Semibold");
   string action=(g_inst.killSwitch?"CAPITAL PROTECTION":(g_inst.gatePass?(g_live_pair!=""?"QUALIFY / EXECUTE":"MONITOR / DISCOVER"):"RISK GATE BLOCK"));
   UILabel("Decision2","SYSTEM ACTION  "+action,x+530,ly+55,9,g_inst.killSwitch?UI_RED:(g_inst.gatePass?UI_GREEN:UI_ORANGE),"Segoe UI Semibold");
   string reason=g_inst.gateReason; if(reason=="") reason=(g_last_intelligence_reason!=""?g_last_intelligence_reason:"Awaiting qualified statistical edge");
   if(StringLen(reason)>66) reason=StringSubstr(reason,0,66)+"...";
   UILabel("Decision3","RATIONALE  "+reason,x+530,ly+78,7,UI_TEXT);
   UILabel("Decision4","ALPHA "+DoubleToString(g_pf_alpha_health,0)+"   |   RISK "+DoubleToString(g_pf_risk_health,0)+"   |   EXEC "+DoubleToString(g_pf_exec_health,0)+"   |   GATE "+(g_inst.gatePass?"PASS":"BLOCK"),x+530,ly+98,7,UI_MUTED);
   UILabel("Decision5","INFORMATIONAL PACK  •  FULL GATE + SECOND-LEG VALIDATION REQUIRED",x+530,ly+119,6,UI_MUTED);
}

void InstRefresh(){
   if(!InpUseInstitutionalEngine) return;
   InstUpdateRegime();
   string dummy="";
   // Refresh with zero proposed risk so Dashboard always reflects the current portfolio.
   InstPortfolioGate("",0.0,dummy);
   g_inst.breachesToday=g_inst_breaches_today;
   RefreshPortfolioIntelligence();
   g_inst_last_update=TimeCurrent();
}

void DrawInstitutionalMetrics(const int panelY,const int x,const int w){
   int y=panelY+450;
   UIRect("InstBlock",x+10,y,630,330,UI_CARD);
   UILabel("InstTitle","INSTITUTIONAL COMMAND CENTER  |  PORTFOLIO • ALPHA • EXECUTION • RESEARCH",x+18,y+10,9,UI_CYAN,"Segoe UI Semibold");
   string life=InstLifecycleState();
   string r="LIFECYCLE "+life+" | REGIME "+g_inst.regime+" | Gate "+(g_inst.gatePass?"PASS":"BLOCK");
   UILabel("InstR1",r,x+18,y+28,9,g_inst.gatePass?UI_GREEN:UI_RED,"Segoe UI Semibold");

   UIRect("InstRiskCard",x+18,y+48,196,78,UI_BG);
   UIRect("InstAlphaCard",x+226,y+48,196,78,UI_BG);
   UIRect("InstExecCard",x+434,y+48,188,78,UI_BG);
   UILabel("InstRiskT","CAPITAL / RISK",x+28,y+56,8,UI_CYAN,"Segoe UI Semibold");
   UILabel("InstRiskV1","Budget "+DoubleToString(InpPortfolioRiskBudgetPct*g_inst.riskMultiplier,2)+"% | Used "+DoubleToString(g_inst.portfolioRiskPct,2)+"%",x+28,y+72,8,UI_TEXT,"Segoe UI");
   UILabel("InstRiskV2","Utilization "+DoubleToString(g_inst.capitalUtilizationPct,0)+"% | Exposure "+DoubleToString(g_inst.currentExposureLots,2)+" lot",x+28,y+88,8,UI_TEXT,"Segoe UI");
   UILabel("InstRiskV3","VaR "+DoubleToString(g_inst.var95,2)+" | CVaR "+DoubleToString(g_inst.cvar95,2)+" | Stress "+DoubleToString(g_inst.stressLoss,2),x+28,y+104,8,UI_MUTED,"Segoe UI");

   UILabel("InstAlphaT","ALPHA / MODEL",x+236,y+56,8,UI_CYAN,"Segoe UI Semibold");
   string instAlphaPair=(g_metrics_live_pair!="")?g_metrics_live_pair:"SCANNING";
   string instAlphaValue;
   if(g_metrics_strategy==ARBITRAGE_SPREAD){
      MqlTick ia,ib; double isp=0.0;
      if(GetTickSafe(InpSymbolA,ia)&&GetTickSafe(InpSymbolB,ib)){
         double ipa=(ia.bid+ia.ask)*0.5, ipb=(ib.bid+ib.ask)*0.5;
         if(ipa>0.0&&ipb>0.0) isp=(ipa/ipb-1.0)*100.0;
      }
      instAlphaValue=InpSymbolA+"/"+InpSymbolB+" | Spread "+DoubleToString(isp,4)+"%";
   }else{
      instAlphaValue=instAlphaPair+" | Z "+DoubleToString(g_metrics_live_z,2);
   }
   UILabel("InstAlphaV1",instAlphaValue,x+236,y+72,8,UI_TEXT,"Segoe UI");
   string instModelTag=(g_metrics_strategy==MULTI_STATISTICAL)?"MULTI UNIVERSE":(g_metrics_strategy==PAIR_STATISTICAL?"FOCUSED PAIR":"ZERO-CENTERED SPREAD");
   UILabel("InstAlphaV2",instModelTag+" | Score "+DoubleToString(g_metrics_live_score,0)+" | Stab "+DoubleToString(g_inst.stabilityScore*100.0,0)+"%",x+236,y+88,7,UI_TEXT,"Segoe UI");
   UILabel("InstAlphaV3","Gap "+DoubleToString(g_inst.researchLiveGap,1)+" pts | MC P5 "+DoubleToString(g_inst_mc_p5,2),x+236,y+104,7,UI_MUTED,"Segoe UI");

   UILabel("InstExecT","EXECUTION QUALITY",x+444,y+56,8,UI_CYAN,"Segoe UI Semibold");
   color eqc=(g_inst.executionQualityScore>=75.0)?UI_GREEN:((g_inst.executionQualityScore>=50.0)?UI_ORANGE:UI_RED);
   UILabel("InstExecV1","Quality "+DoubleToString(g_inst.executionQualityScore,0)+"/100 | Cost "+DoubleToString(g_inst.executionCostPct,3)+"%",x+444,y+72,8,eqc,"Segoe UI Semibold");
   UILabel("InstExecV2","Latency "+DoubleToString(g_current_latency_ms,0)+" ms | Slip "+DoubleToString(g_daily_slippage,1)+" pt",x+444,y+88,8,UI_TEXT,"Segoe UI");
   UILabel("InstExecV3","Sizing x"+DoubleToString(g_inst.qualitySizingMultiplier,2)+" | RF "+IntegerToString(g_reconcile_failures_today)+" EF "+IntegerToString(g_execution_failures_today),x+444,y+104,8,UI_MUTED,"Segoe UI");

   UILabel("InstQueueT","OPPORTUNITY QUEUE",x+18,y+138,8,UI_CYAN,"Segoe UI Semibold");
   if(g_metrics_strategy==ARBITRAGE_SPREAD){
      double qspread=g_metrics_live_spread;
      string qdir=qspread>0.0?"SELL A / BUY B":(qspread<0.0?"BUY A / SELL B":"NEUTRAL");
      UILabel("InstQueue0","1  "+InpSymbolA+"/"+InpSymbolB+" | Spread "+DoubleToString(qspread,4)+"% | Z "+DoubleToString(g_metrics_live_z,2)+" | Beta "+DoubleToString(g_metrics_live_beta,4)+" | "+qdir,x+28,y+154,8,qspread==0.0?UI_MUTED:UI_GREEN,"Segoe UI");
      UILabel("InstQueue1","2  Entry / Exit bands | Zero-centered convergence | Cost and execution gates",x+28,y+176,8,UI_TEXT,"Segoe UI");
      UILabel("InstQueue2","3  Two-leg hedge | cost / liquidity / legging protection | dedicated spread branch",x+28,y+198,8,UI_MUTED,"Segoe UI");
   }else if(g_metrics_strategy==PAIR_STATISTICAL){
      string pp=InpSymbolA+"/"+InpSymbolB;
      SPairStat ist;
      bool istOk=GetStat(InpSymbolA,InpSymbolB,ist);
      double icorr=istOk?ist.corr:0.0;
      double ibeta=istOk?ist.beta:0.0;
      double ihl=istOk?ist.halfLife:0.0;
      double ihurst=istOk?ist.hurst:0.0;
      string pairLine="1  "+pp+" | Z "+DoubleToString(g_metrics_live_z,2)+" | Corr "+DoubleToString(g_metrics_live_corr,2)+" | Beta "+DoubleToString(g_metrics_live_beta,2);
      UILabel("InstQueue0",pairLine,x+28,y+154,8,MathAbs(g_metrics_live_z)>=InpStatEntryZScore?UI_GREEN:UI_TEXT,"Segoe UI");
      UILabel("InstQueue1","2  Half-Life "+DoubleToString(ihl,0)+" | Hurst "+DoubleToString(ihurst,2)+" | Focused pair qualification",x+28,y+176,8,UI_TEXT,"Segoe UI");
      UILabel("InstQueue2","3  Two-leg hedge | Z convergence | execution / cost / risk protection",x+28,y+198,8,UI_MUTED,"Segoe UI");
   }else{
      for(int q=0;q<3;q++){
         int yy=y+154+q*22;
         string qp=(q<g_metrics_top_count && g_metrics_top_a[q]!="")?g_metrics_top_a[q]+"/"+g_metrics_top_b[q]:"--";
         string qt=IntegerToString(q+1)+"  "+qp+" | Score "+DoubleToString(g_metrics_top_score[q],0)+" | |Z| "+DoubleToString(g_metrics_top_z[q],2)+" | r "+DoubleToString(g_metrics_top_corr[q],2)+" | beta "+DoubleToString(g_metrics_top_beta[q],2)+" | HL "+DoubleToString(g_metrics_top_hl[q],0);
         color qc=(q<g_metrics_top_count && g_metrics_top_score[q]>=EffectivePairIntelligenceThreshold())?UI_GREEN:UI_MUTED;
         UILabel("InstQueue"+IntegerToString(q),qt,x+28,yy,8,qc,"Segoe UI");
      }
   }

   double equity=InstEquity();
   double dayNet=equity-g_day_start_equity;
   double costLoad=MathAbs(g_daily_commission)+MathAbs(g_daily_swap)+MathAbs(g_daily_fees)+MathAbs(g_daily_spread);
   double grossBeforeCosts=dayNet+costLoad;
   // Keep a clean vertical gap after the 3-line opportunity queue.
   UILabel("InstAttribT","P&L ATTRIBUTION / OPERATING LOAD",x+18,y+226,8,UI_CYAN,"Segoe UI Semibold");
   UILabel("InstAttrib1","Net "+DoubleToString(dayNet,2)+" | Gross before tracked costs "+DoubleToString(grossBeforeCosts,2)+" | Cost load "+DoubleToString(costLoad,2),x+28,y+244,6,dayNet>=0?UI_GREEN:UI_RED,"Segoe UI");
   UILabel("InstAttrib2","Commission "+DoubleToString(g_daily_commission,2)+" | Swap "+DoubleToString(g_daily_swap,2)+" | Spread "+DoubleToString(g_daily_spread,2)+" | Fees "+DoubleToString(g_daily_fees,2)+" | Slippage "+DoubleToString(g_daily_slippage,1)+" pt",x+28,y+264,8,UI_TEXT,"Segoe UI");

   string kill=g_inst.killSwitch?"KILL SWITCH ACTIVE":"KILL SWITCH ARMED";
   color kc=g_inst.killSwitch?UI_RED:UI_GREEN;
   UILabel("InstProtect",kill+" | Breaches "+IntegerToString(g_inst.breachesToday)+"/"+IntegerToString(InpMaxInstitutionalBreachesDay)+" | Reason: "+g_inst.gateReason,x+18,y+290,8,kc,"Segoe UI Semibold");
   UILabel("InstPipeline","CONNECTED PIPELINE: discover -> score -> regime -> quality sizing -> risk budget -> execute -> reconcile -> manage -> exit -> research",x+18,y+310,7,UI_CYAN,"Segoe UI Semibold");
}

double OnTester(){
   if(!InpResearchTesterCriterion) return TesterStatistics(STAT_PROFIT);
   double profit=TesterStatistics(STAT_PROFIT);
   double dd=TesterStatistics(STAT_EQUITY_DDREL_PERCENT);
   double pf=TesterStatistics(STAT_PROFIT_FACTOR);
   double trades=TesterStatistics(STAT_TRADES);
   double tradeFactor=(InpResearchMinSamples>0)?MathMin(1.0,trades/InpResearchMinSamples):1.0;
   if(trades<=0) return -1.0e9;
   double wr=(double)g_research_wins/MathMax(1,g_research_trades);
   double scoreCorr=ResearchScoreOutcomeCorrelation();
   double riskAdjusted=profit/(1.0+MathMax(dd,0.0));
   double pfFactor=MathMin(3.0,MathMax(0.0,pf));
   double calibration=0.5+0.5*MathMax(-1.0,MathMin(1.0,scoreCorr));
   return riskAdjusted*(0.50+0.20*pfFactor+0.15*wr+0.15*calibration)*tradeFactor;
}

int OnInit(){
   g_is_tester=(bool)MQLInfoInteger(MQL_TESTER)||(bool)MQLInfoInteger(MQL_OPTIMIZATION);
   g_mode=InpTradingMode; g_strategy=InpStrategy; BuildGlobalNames();
   if(!g_is_tester&&GlobalVariableCheck(GV_STRATEGY)){ int ss=(int)GlobalVariableGet(GV_STRATEGY); if(ss>=0&&ss<=2) g_strategy=(ENUM_AP_STRATEGY)ss; }
   g_metrics_strategy=g_strategy;
   g_demo_balance=InpDemoInitialBalance; if(!g_is_tester&&g_mode==DEMO_TRADING&&GlobalVariableCheck(GV_DEMO_BAL)) g_demo_balance=GlobalVariableGet(GV_DEMO_BAL);
   LoadDailyState();
   // DEMO mode must not inherit previous risk protection state from terminal storage.
   // Keep virtual balance, reset only session risk flags.
   if(g_mode==DEMO_TRADING){
      g_daily_stop=false;
      g_stop_reason="";
      g_reconcile_failures_today=0;
      g_execution_failures_today=0;
      g_entry_aborts_today=0;
      g_failed_hedges_today=0;
      g_inst_breaches_today=0;
      g_circuit_pause_until=0;
   }
   datetime today=EngineDayStart();
   if(today!=g_day_start){ g_day_start=today; g_day_start_equity=(g_mode==DEMO_TRADING)?g_demo_balance:AccountInfoDouble(ACCOUNT_EQUITY); g_demo_day_start_equity=g_day_start_equity; ResetSessionStopState(); g_peak_equity=g_day_start_equity; g_total_cycles_today=0; g_closed_cycles_today=0; g_closed_profitable_today=0; g_losses_today=0; g_consec_losses=0; g_daily_commission=0; g_daily_swap=0; g_daily_spread=0; g_daily_slippage=0; g_daily_fees=0; g_last_history_refresh=0; g_entry_aborts_today=0; g_failed_hedges_today=0; g_inst_breaches_today=0; SaveDailyState(); }
   if(g_peak_equity<=0) g_peak_equity=g_day_start_equity;
   g_demo_day_start_equity=(g_mode==DEMO_TRADING)?g_day_start_equity:InpDemoInitialBalance;
   g_base_equity=(g_mode==DEMO_TRADING)?g_demo_balance:AccountInfoDouble(ACCOUNT_EQUITY); g_lot_scale=1.0;
   ParsePairList(InpMultiPairs,g_mp_a,g_mp_b,g_mp_count,16); ParsePairList(InpScanPairs,g_scan_a,g_scan_b,g_scan_count,16);
   LoadWalkForwardResearch();
   LoadCooldownState();
   if(!SelectSymbolSafe(InpSymbolA)||!SelectSymbolSafe(InpSymbolB)){ Print("Cannot select primary symbols"); return INIT_FAILED; }
   for(int i=0;i<g_mp_count;i++){ SelectSymbolSafe(g_mp_a[i]); SelectSymbolSafe(g_mp_b[i]); }
   for(int i=0;i<g_scan_count;i++){ SelectSymbolSafe(g_scan_a[i]); SelectSymbolSafe(g_scan_b[i]); }
   if(g_strategy==MULTI_STATISTICAL&&g_mp_count<1){ Print("Pool empty"); return INIT_FAILED; }
   if(InpSelfDiagnostics && !RunSelfDiagnostics(false)){ Print("Self diagnostics found critical errors"); return INIT_FAILED; }
   if(!InpSelfDiagnostics) g_diag_status="OFF";
   if(g_mode==LIVE_TRADING&&InpRequireHedgingForLive&&AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){ Print("LIVE requires hedging account"); return INIT_FAILED; }
   Trade.SetExpertMagicNumber(InpMagic); Trade.SetDeviationInPoints((int)InpMaxSlippagePoints); Trade.SetAsyncMode(false);
   if(InpNotifyTelegram&&(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))) Print("Telegram enabled; WebRequest URL must be allowed independently of trading permission.");
   if(InpOnRestart==CLOSE_ALL&&!g_is_tester){ CloseAllOurPositions(); ArrayResize(g_cycles,0); SaveCyclesState(); }
   else { LoadCyclesState(); HandleOrphans(); }
   AuditEvent("STARTUP","*","ARBITRAGE PRO QUANT INSTITUTIONAL v1.0(ENG) initialized");
   Print("===== ARBITRAGE PRO QUANT INSTITUTIONAL v1.0(ENG) STARTUP =====");
   Print("Mode ",ModeText()," | Strategy ",StrategyText()," | Pool ",g_mp_count," | Scan ",g_scan_count);
   Print("State=",StateFileName()," | Tester=",g_is_tester," | Hedging=",AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
   Print("Research ledger=",InpUseResearchLedger," | TesterWrite=",InpResearchWriteTester," | File=",ResearchFileName());
   Print("WalkForward=",InpUseWalkForwardGuard," | Status=",g_wf_status," | EffectiveScore=",DoubleToString(EffectivePairIntelligenceThreshold(),1));
   if(!g_is_tester){ ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true); EventSetTimer(MathMax(1,InpTimerSeconds)); }
   SetStatus("Initialized"); BuildUI(); return INIT_SUCCEEDED;
}
void OnDeinit(const int reason){
   AuditEvent("SHUTDOWN","*","deinit reason "+IntegerToString(reason));
   if(InpUseResearchLedger){ string rsum=""; ResearchSummary(rsum); Print(rsum); }
   if(InpUseWalkForwardGuard){ string wsum=""; WalkForwardSummary(wsum); Print(wsum); }
   SaveDailyState(); SaveCooldownState(); if(!g_is_tester) SaveCyclesState();
   EventKillTimer(); UIDeleteAll(); Comment("");
}

//+------------------------------------------------------------------+
// Additional safety and diagnostics layer
void EmergencyManager(){
   if(!InpUseEmergencyManager) return;
   datetime now=TimeCurrent();
   if(now==g_last_emergency_check) return;
   g_last_emergency_check=now;

   for(int i=ArraySize(g_cycles)-1;i>=0;i--){
      double pnl=CyclePnL(i);
      int age=(int)(now-g_cycles[i].startTime);
      bool emergency=false;
      string reason="";

      if(InpEmergencyMaxLossMoney>0 && pnl<=-InpEmergencyMaxLossMoney){
         emergency=true; reason="emergency loss limit";
      }
      if(InpMaxCycleAgeMinutes>0 && age>InpMaxCycleAgeMinutes*60){
         emergency=true; reason="maximum cycle age";
      }

      if(emergency){
         string pair=g_cycles[i].symA+"/"+g_cycles[i].symB;
         AuditEvent("EMERGENCY_CLOSE",pair,reason,0.0,pnl);
         CloseCycleIdx(i,reason);
      }
   }
}

void AuditRejectedSignal(const string pair,const string reason,const double z){
   if(!InpAuditRejectedSignals || g_is_tester) return;
   AuditEvent("SIGNAL_REJECT",pair,reason,z,0.0);
}

bool TradingSessionOK()
{
   MqlDateTime dt;
   TimeToStruct(TimeGMT(),dt);
   if(InpMinTradeHourUTC<=InpMaxTradeHourUTC)
      return (dt.hour>=InpMinTradeHourUTC && dt.hour<=InpMaxTradeHourUTC);
   return (dt.hour>=InpMinTradeHourUTC || dt.hour<=InpMaxTradeHourUTC);
}

void AuditSignalQuality(const string pair,const string reason,const double z,const double corr,const double beta,const double halfLife)
{
   if(!InpUseSignalQualityAudit) return;
   if(g_is_tester && !InpTesterWriteAudit) return;
   string msg="Z="+DoubleToString(z,3)+" CORR="+DoubleToString(corr,3)+" BETA="+DoubleToString(beta,3)+" HL="+DoubleToString(halfLife,1);
   AuditEvent("SIGNAL_QUALITY",pair,reason,z,corr);
}

bool EmergencySpreadCheck(const string pair,const double spreadPoints)
{
   if(InpMaxAllowedSpreadPoints<=0) return true;
   if(spreadPoints>InpMaxAllowedSpreadPoints)
   {
      AuditEvent("SPREAD_BLOCK",pair,"spread too high",0.0,spreadPoints);
      return false;
   }
   return true;
}

void ProcessEngine(){
   ulong now=GetTickCount64();
   if(EngineDayStart()!=g_day_start) UpdateTradingDay();
   UpdateLiveMetrics();
   RefreshMetricsPreview(false);
   EmergencyManager();
   if((ulong)(TimeCurrent()-g_inst_last_update)>=(ulong)MathMax(1,InpPortfolioRefreshSec)){ InstRefresh(); }
   if(now-g_last_scan_ms>=(ulong)MathMax(1,InpScanIntervalSec)*1000){ g_last_scan_ms=now; ScanMarket(); UpdateTopPairs(); CalculateCapitalAllocation(); }
   if(now-g_last_manage_ms>=500){ g_last_manage_ms=now; if(OpenCyclesCount()>0) ManageOpenCycles(); }
   PollTelegramCommands();
   UpdateOpenArbitrageChart();
   // Anti-flicker: rebuild the heavyweight UI at the same cadence as Portfolio refresh,
   // not once per second. This preserves the engine tick cadence while avoiding
   // repeated UIDeleteAll()/recreate/redraw cycles on the live chart.
   if(InpEnableDailyReport&&!g_is_tester){ MqlDateTime dt; TimeToStruct(TimeGMT(),dt); if(dt.hour==InpDailyReportHourGMT&&dt.min<2&&!g_daily_report_sent){ SendDailyReport(); g_daily_report_sent=true; } if(dt.hour!=InpDailyReportHourGMT) g_daily_report_sent=false; }
   if(g_ui_visible&&!g_is_tester&&(g_ui_last_refresh==0||TimeCurrent()-g_ui_last_refresh>=(datetime)MathMax(1,InpPortfolioRefreshSec))) BuildUI();
}
void OnTimer(){ ProcessEngine(); }
void OnTick(){
   // In tester/optimization timers are not relied upon: process the same engine from ticks.
   if(g_is_tester) ProcessEngine();
   else if(OpenCyclesCount()>0){ ulong now=GetTickCount64(); if(now-g_last_manage_ms>=500){ g_last_manage_ms=now; ManageOpenCycles(); } }
}
//+------------------------------------------------------------------+