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


//+------------------------------------------------------------------+
//|                                  ARBITRAGE PRO v1.0 QUANT INSTITUTIONAL.mq5 |
//+------------------------------------------------------------------+
#property version   "1.0"
#property description "ARBITRAGE PRO v1.0 QUANT INSTITUTIONAL DASHBOARD(ENG)"
#property description "Stat-Arb | Beta Hedge | Risk | Execution | Research"
#property description "Portfolio | VaR/CVaR | Stress | Regime | Quality | Kill Switch"
#property description "Modes: SIGNALS | DEMO | LIVE | M5-M15"

#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 };
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 "=== 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;
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";
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[];


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(){ return (g_strategy==PAIR_STATISTICAL)?"PAIR STAT":"MULTI STAT"; }
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;
}

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="+InpTelegramChatId+"&text="+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="";
   if(WebRequest("POST",url,hdr,5000,data,res,rh)<0)
      Print("Telegram send failed err=",GetLastError()," (Ensure https://api.telegram.org is in Allowed URLs)");
}
void Notify(const string msg){
   if(g_is_tester) return;
   if(InpNotifyPush) SendNotification(msg);
   if(InpNotifyEmail) SendMail("ArbitragePro",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: 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="";
   while(f<StringLen(res)){
      ushort ch=StringGetCharacter(res,f);
      if(ch=='"') break;
      out+=CharToString((uchar)ch);
      f++;
   }
   return out;
}
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==""||g_is_tester) return;
   ulong now=GetTickCount64();
   if(now-g_tg_last_poll<(ulong)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="";
   if(WebRequest("GET",url,hdr,3000,req,res,rh)<0) return;
   string body=CharArrayToString(res,0,WHOLE_ARRAY,CP_UTF8);
   long lastId=TGLastUpdateId(body);
   if(lastId>0) g_tg_offset=lastId+1;
   string txt=TGFirstText(body);
   if(txt=="") return;
   if(StringFind(txt,"/status")>=0) SendTelegramMessage(StatusSummary());
   else if(StringFind(txt,"/pause")>=0){ g_paused=true; SendTelegramMessage(" *PAUSED* via Telegram"); }
   else if(StringFind(txt,"/resume")>=0){ 
      g_paused=false; 
      SendTelegramMessage(" *RESUMED* via Telegram"); 
   }
   else if(StringFind(txt,"/close")>=0){ CloseAllCycles("TG-CLOSE"); SendTelegramMessage(" *CLOSE ALL* executed via Telegram"); }
   else if(StringFind(txt,"/report")>=0) 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=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;
}
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 2.0 — 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="V2 PASS";
   else if(!base.eligible) v2.reason="BASE "+base.reason;
   else v2.reason="V2 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==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);
      }
   }
}
void UpdateLiveMetrics(){
   RefreshTodayDealCosts();
   g_live_signal=0; g_live_pair=""; double bestAbs=0;
   int n=(g_strategy==PAIR_STATISTICAL)?1:g_mp_count;
   for(int i=0;i<n;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==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';
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 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(){ return (g_strategy==PAIR_STATISTICAL)?"PAIR STAT":"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;
   // 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");
}
void DrawNewsPanel(const int y){
   const int x=InpPanelX, w=650;
   UpdateNewsCache();
   int rows=MathMin(15,g_ui_news_count);
   int panelH=81+rows*27+342;
   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=560;
   UIRect("StrategyPanel",x,panelY,w,h,UI_BG,UI_BORDER);
   UILabel("StrategyTitle","ARBITRAGE PRO v1.0 QUANT INSTITUTIONAL  |  MARKET-NEUTRAL STATISTICAL ARBITRAGE",x+18,panelY+13,11,UI_CYAN,"Segoe UI Semibold");
   UILabel("StrategySub","Live strategy map: discovery -> statistical qualification -> risk gates -> two-leg hedge -> convergence management -> protected exit.",x+18,panelY+34,8,UI_TEXT,"Georgia");

   bool isPair=(g_strategy==PAIR_STATISTICAL);
   color multiColor=isPair?UI_CARD2:UI_BLUE;
   color pairColor=isPair?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",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",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",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",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",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_ORANGE,"Segoe UI Semibold");
   UILabel("StrategyV6",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");

   UIButton("SelectMultiStat","MULTI STATISTICAL",x+145,panelY+232,175,30,multiColor,UI_TEXT);
   UIButton("SelectPairStat","PAIR STATISTICAL",x+335,panelY+232,175,30,pairColor,UI_TEXT);
   UILabel("StrategySelectHint",isPair?"ACTIVE • PRIMARY PAIR":"ACTIVE • MULTI-PAIR UNIVERSE",x+520,panelY+241,6,isPair?UI_GREEN:UI_CYAN,"Segoe UI Semibold");

   UILabel("StrategyDescTitle",isPair?"PAIR STATISTICAL  •  FULL ENTRY / MANAGEMENT STRUCTURE":"MULTI STATISTICAL  •  FULL ENTRY / MANAGEMENT STRUCTURE",x+18,panelY+302,9,UI_CYAN,"Segoe UI Semibold");

   string fullDesc;
   if(isPair){
      fullDesc="PAIR STATISTICAL — focused relative-value cycle using exactly InpSymbolA and InpSymbolB. The EA does not forecast the direction of either symbol independently; it evaluates their statistical relationship and trades a temporary relative-value dislocation. The spread is constructed from log-price A minus beta multiplied by log-price B, and the live deviation is measured with Z-score. Entry begins only when the adaptive entry condition is satisfied; the base InpStatEntryZScore is 2.20, and when cross-entry is enabled the Z-score must cross the threshold rather than simply remain beyond it. Structural quality must pass correlation >= InpMinPairCorrelation=0.70 and beta inside InpMinBeta=0.20 to InpMaxBeta=2.50. When enabled, half-life must be valid and <= InpMaxHalfLifeBars=48 and Hurst must be < InpMaxHurst=0.45. Quotes must be fresh under the configured quote-age limits, liquidity must be acceptable, maximum spread and slippage must pass, the spread-regime filter must allow the setup, and latency must remain below InpMaxLatencyMs when the latency filter is enabled. Spread momentum must not show continued adverse expansion. When beta stability is enabled, the configured stability window and CV limit must pass, and the confirmation and trend timeframes must not disagree with the statistical signal. Pair Intelligence and Portfolio Allocation must approve the candidate when enabled. Expected edge must cover estimated trading cost by InpMinCostCoverage=2.0, meaning expected edge must cover at least 2x estimated cost, and positive net edge must pass when InpRequirePositiveNetEdge is enabled. Session and weekend filters, macro-news protection, cooldown, existing-leg conflict checks, daily loss and trade limits, consecutive-loss protection, drawdown and margin protection, maximum open cycles, positions and exposure, portfolio and cluster risk budgets, VaR/CVaR, stress controls and institutional kill-switch rules must all allow the trade. Only then is capital sized as one two-leg market-neutral position. With automatic sizing and dollar-neutral beta sizing enabled, the hedge ratio is reflected in the leg allocation so the cycle is treated as one relative-value risk unit. Direction comes only from the spread: positive Z means SELL A / BUY B; negative Z means BUY A / SELL B. After the first leg fills, the second leg is revalidated for quote freshness, Z drift, maximum leg drift, legging loss, execution delay and hedge imbalance. If those checks fail, the cycle is aborted or protected instead of leaving an uncontrolled one-leg position. Once both legs are valid, management follows convergence and risk: Z-score exits, dynamic protection, time and half-life controls, P/L limits, news protection, execution protection and reconciliation remain active until convergence or a protective exit closes the cycle.";
   }else{
      fullDesc="MULTI STATISTICAL — diversified relative-value selection across the configured multi-pair universe. The EA scans available configured pairs, resolves and validates symbols, rejects stale or invalid quotes, calculates the statistical relationship for each candidate and first requires the absolute Z-score to reach the adaptive entry threshold. With the base InpStatEntryZScore=2.20, the engine searches for statistically unusual relative dislocations rather than normal small fluctuations. Pair Intelligence then evaluates eligible candidates and ranks them by the configured quality/intelligence score; the highest Z-score is not automatically selected because statistical quality, economics, execution and portfolio risk must also pass. Each candidate must satisfy correlation >= InpMinPairCorrelation=0.70, beta inside the configured 0.20–2.50 range, optional half-life >0 and <=48 bars and optional Hurst <0.45. The spread must pass cost and quality checks, liquidity, maximum spread and slippage, quote freshness, latency and execution-quality controls. Spread-regime and volatility filters must permit the setup; the regime engine can block TRENDING and HIGH_VOL states when they are outside the strategy design. Spread momentum must not show continued adverse expansion, beta stability must pass when enabled, and confirmation and trend timeframes must not disagree with the statistical signal. Pair News, cooldown, existing-leg conflicts and execution circuit-breaker conditions must be clear. Pair Intelligence and Portfolio Allocation then work together with the institutional risk-budget engine: pair risk, cluster risk, total portfolio exposure, correlation concentration, VaR/CVaR and stress-loss limits must remain within their configured budgets. Expected edge must cover estimated transaction cost; InpMinCostCoverage=2.0 requires expected edge to cover at least 2x estimated cost, and positive net edge must pass when InpRequirePositiveNetEdge is enabled. Global session and weekend rules, macro-news protection, daily loss and drawdown limits, maximum cycles and positions, maximum exposure, consecutive-loss controls, margin protection and the institutional kill-switch must also be clear. Only a candidate that survives the complete chain receives capital. The selected pair is sized as one market-neutral two-leg trade using the active risk multiplier, portfolio allocation and dollar-neutral beta sizing when enabled. Direction is defined only by spread Z: 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, legging loss, execution delay and hedge imbalance. Excessive deterioration aborts or protects the cycle rather than allowing an uncontrolled hedge. Once both legs are live, the cycle is managed by convergence, Z-score behavior, half-life, dynamic exits, P/L and drawdown controls, news and execution protection and reconciliation. The key distinction is architectural: MULTI STATISTICAL searches and ranks a universe of statistically dislocated relationships, then selects only a candidate that passes the full risk-adjusted entry and execution pipeline; it is not a directional forecast engine and does not open a trade merely because one individual symbol looks strong or weak.";
   }

   string wrapped[32];
   int wn=0;
   // Full text is preserved. Only wrapping is controlled so the prose uses the existing 650px panel width.
   NewsWrapText(fullDesc,wrapped,wn,102);
   int maxLines=MathMin(23,wn);
   for(int i=0;i<maxLines;i++)
      UILabel("StrategyDesc"+IntegerToString(i),wrapped[i],x+18,panelY+320+i*9,7,UI_TEXT,"Segoe UI");

   UILabel("StrategyFooter","ENTRY: statistical dislocation -> qualification -> institutional risk gate -> two-leg execution.  EXIT: convergence/protection, not directional prediction.",x+18,panelY+540,6,UI_MUTED,"Segoe UI");
}

void DrawMetricsPanel(const int y){
   const int x=InpPanelX, w=650;
   int panelY = y + 686;
   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");
   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_live_signal,2)+(g_live_pair!=""?" | "+g_live_pair:"")+" | Waiting for cycle");
   UILabel("Met1Value", trailStatus, x+18, panelY+54, 7, UI_TEXT, "Segoe UI");
   UILabel("Met1Status", InpUseZTrailingExit?"ENABLED":"DISABLED", x+540, 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_live_signal);
   UILabel("Met2Value", "Base: "+DoubleToString(InpMaxSlippagePoints,0)+" pts | Current: "+DoubleToString(currentSlip,1)+" pts | LIVE EXECUTION PARAM", x+18, panelY+100, 7, UI_TEXT, "Segoe UI");
   UILabel("Met2Status", InpUseDynamicSlippage?"ENABLED":"DISABLED", x+540, 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");
   UILabel("Met3Status", InpUseVolatilityFilter?"ENGINE FILTER":"DISABLED", x+520, 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");
   UILabel("Met4Status", timeFeatureActive?"ACTIVE":"DISABLED", x+540, 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");
   UILabel("Met5Status", g_last_audit_phase!=""?g_last_audit_phase:"NO EVENTS", x+520, 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");
   UILabel("Met6Status", "HISTORY", x+540, 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");
   UILabel("Met7Status", g_last_trade_retcode>0?IntegerToString((int)g_last_trade_retcode):"WAITING", x+540, 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_last_intelligence_score,0)+" | Lot x"+DoubleToString(g_last_intelligence_factor,2)+" | "+(g_last_intelligence_pair!=""?g_last_intelligence_pair:"SCANNING");
   UILabel("Met8Value", it, x+18, panelY+376, 7, UI_TEXT, "Segoe UI");
   UILabel("Met8Status", InpUsePairIntelligence?(g_last_intelligence_score>=EffectivePairIntelligenceThreshold()?"READY":"FILTER") : "OFF", x+540, panelY+364, 7, InpUsePairIntelligence?(g_last_intelligence_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_live_signal,2)+" | Pair="+(g_live_pair!=""?g_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", "PI 2.0 "+(InpUsePairIntelligenceV2?(DoubleToString(g_pi2_score,0)+" / Conv "+DoubleToString(g_pi2_conviction,0)+" | "+(g_pi2_pair!=""?g_pi2_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 v1.0 QUANT INSTITUTIONAL", InpPanelX, InpPanelY, 260, 32, UI_BLUE, UI_TEXT);
      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=686, 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 v1.0 QUANT INSTITUTIONAL", x+10, y+15, 15, UI_TEXT, "Segoe UI Semibold");
   UILabel("Version", "MT5 | ENG", x+10, y+40, 8, UI_CYAN);
   bool terminalAlgo=(bool)TerminalInfoInteger(TERMINAL_TRADE_ALLOWED);
   bool eaAlgo=(bool)MQLInfoInteger(MQL_TRADE_ALLOWED);
   color statusColor=(!terminalAlgo)?UI_RED:(!eaAlgo?UI_ORANGE:UI_GREEN);
   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)+"...";
   UILabel("EngineState", "ENGINE: "+engineTxt+" | DIAG: "+g_diag_status, x+170, y+67, 7, g_diag_errors>0?UI_RED:(g_diag_warnings>0?UI_ORANGE:(g_paused?UI_ORANGE:UI_MUTED)));
   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();
   UILabel("Scanner", "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);
      UILabel("ScanDot"+IntegerToString(i), "●", x+20, yy+9, 9, UI_GREEN);
      string pairName=(g_top_a[i]!="")?g_top_a[i]+"/"+g_top_b[i]:"--/--";
      UILabel("ScanSym"+IntegerToString(i), pairName, x+32, yy+9, 9, UI_TEXT);
      string sprText=(g_top_a[i]!="")?DoubleToString(g_top_sprA[i], 1)+"/"+DoubleToString(g_top_sprB[i], 1)+" pt":"--";
      UILabel("ScanSpread"+IntegerToString(i), sprText, x+150, yy+9, 8, g_top_fresh[i]?UI_GREEN:UI_RED);
      string stText=(g_top_a[i]!="")?(SessionOK()?(g_top_fresh[i]?"READY":"STALE"):"OFF-SESSION"):"--";
      color stColor=(g_top_a[i]!="")?(SessionOK()?(g_top_fresh[i]?UI_GREEN:UI_ORANGE):UI_ORANGE):UI_MUTED;
      UILabel("ScanState"+IntegerToString(i), stText, x+250, yy+9, 8, stColor);
      string intelText=(g_top_a[i]!=""&&InpUsePairIntelligence)?"S="+DoubleToString(g_top_score[i],0):UIStrategyShort();
      UILabel("ScanStrategy"+IntegerToString(i), intelText, x+345, yy+9, 8, (g_top_score[i]>=EffectivePairIntelligenceThreshold())?UI_GREEN:UI_ORANGE);
      string kalText=(g_top_a[i]!="")?"Kβ="+DoubleToString(g_top_beta[i], 2)+" HL="+DoubleToString(g_top_hl[i], 0):"--";
      color kalColor=(g_top_a[i]!="")?((g_top_hl[i]>0 && g_top_hl[i]<=InpMaxHalfLifeBars)?UI_CYAN:UI_ORANGE):UI_MUTED;
      UILabel("ScanKal"+IntegerToString(i), kalText, x+425, yy+9, 7, kalColor);
      string corrText=(g_top_a[i]!="")?"r="+DoubleToString(g_top_corr[i]*100, 0)+"% "+GetCorrelationText(g_top_corr[i]):"--";
      UILabel("ScanCorr"+IntegerToString(i), corrText, x+520, yy+9, 7, GetCorrelationColor(g_top_corr[i]));
   }
   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 (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");
   
   UIButton("StrategyNext", "STRATEGY", x+10, y+650, 78, 28, UI_CARD2, UI_TEXT);
   UIButton("Metrics", "METRICS", x+92, y+650, 78, 28, UI_CARD2, UI_TEXT);
   UIButton("Portfolio", "PORTFOLIO", x+174, y+650, 86, 28, UI_CARD2, UI_TEXT);
   UIButton("SymbolMap", "MAP", x+264, y+650, 60, 28, UI_CARD2, UI_TEXT);
   UIButton("NewsToggle", "NEWS", x+328, y+650, 60, 28, UI_CARD2, UI_TEXT);
   UIButton("SelfTest", "SELF TEST", x+392, y+650, 78, 28, g_diag_errors>0?UI_DARK_RED:(g_diag_warnings>0?UI_ORANGE:UI_CARD2), UI_TEXT);
   UIButton("AlphaPage", "ALPHA", x+476, y+650, 70, 28, UI_CARD2, UI_TEXT);
   UIButton("HidePanel", "HIDE", x+550, y+650, 70, 28, 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;
   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>1) 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;
}

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

bool BuildArbitrageSpreadHistory(const string a,const string b,const string custom){
   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) 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]);
      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);
         r.close=ArbSpreadValue(ra[ia].close,rb[ib].close);
         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,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);
   CustomSymbolSetString(custom,SYMBOL_DESCRIPTION,"ARBITRAGE SPREAD % | ("+a+" / "+b+" - 1) * 100");
   CustomSymbolSetInteger(custom,SYMBOL_DIGITS,5);
   return true;
}

bool OpenArbitragePairChart(const string a,const string b){
   string custom="";
   if(!EnsureArbitrageCustomSymbol(a,b,custom)) return false;
   if(!BuildArbitrageSpreadHistory(a,b,custom)) 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_last_update_ms=GetTickCount64();
   ChartSetString(cid,CHART_COMMENT,"ARBITRAGE PAIR: "+a+" / "+b+"\nFORMULA: ("+a+" / "+b+" - 1) * 100\nM1 synchronized ratio spread | MT5 aggregation");
   ChartSetInteger(cid,CHART_SHOW_GRID,true);
   ChartSetInteger(cid,CHART_MODE,CHART_CANDLES);
   ChartRedraw(cid);
   return true;
}

void UpdateOpenArbitrageChart(){
   if(g_arb_chart_symbol=="" || g_arb_chart_id<=0) return;
   ulong now=GetTickCount64();
   if(now-g_arb_chart_last_update_ms<5000) return;
   g_arb_chart_last_update_ms=now;
   string desc=SymbolInfoString(g_arb_chart_symbol,SYMBOL_DESCRIPTION);
   int p=StringFind(desc,"ARBITRAGE SPREAD % | (");
   if(p<0) return;
   string formula=StringSubstr(desc,StringLen("ARBITRAGE SPREAD % | ("));
   int slash=StringFind(formula," / ");
   int tail=StringFind(formula," - 1)");
   if(slash<=0 || tail<=slash) return;
   string a=StringSubstr(formula,0,slash);
   string b=StringSubstr(formula,slash+3,tail-(slash+3));
   if(BuildArbitrageSpreadHistory(a,b,g_arb_chart_symbol)) 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("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");
}

bool RunSelfDiagnostics(const bool notifyUser=false){
   g_diag_errors=0; g_diag_warnings=0; string why="";
   // Primary pair is mandatory.
   if(!DiagnosticSymbol(InpSymbolA,why)){ g_diag_errors++; if(InpVerboseLog) Print("DIAG ERROR ",InpSymbolA,": ",why); }
   if(!DiagnosticSymbol(InpSymbolB,why)){ g_diag_errors++; if(InpVerboseLog) Print("DIAG ERROR ",InpSymbolB,": ",why); }
   int usablePairs=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 need=MathMax(MathMax(InpStatLookback,InpOU_lookback),InpAdaptiveZ_window)+InpADFMaxLag+10;
         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++; if(InpVerboseLog) Print("DIAG WARNING ",g_mp_a[i],"/",g_mp_b[i],": insufficient history ",ba,"/",bb," need ",need); }
      } else {
         g_diag_warnings++; if(InpVerboseLog) Print("DIAG WARNING pair unavailable ",g_mp_a[i],"/",g_mp_b[i]," | ",wa," ",wb);
      }
   }
   if(g_strategy==MULTI_STATISTICAL&&usablePairs<1) g_diag_errors++;
   string prim[2]; prim[0]=InpSymbolA; prim[1]=InpSymbolB;
   for(int i=0;i<2;i++){
      if(!SymbolSelect(prim[i],true)) continue;
      int need=MathMax(MathMax(InpStatLookback,InpOU_lookback),InpAdaptiveZ_window)+InpADFMaxLag+10;
      int bars=Bars(prim[i],PERIOD_CURRENT); if(bars<need){ g_diag_warnings++; if(InpVerboseLog) Print("DIAG WARNING ",prim[i],": history ",bars," < ",need); }
      string pc=SymbolInfoString(prim[i],SYMBOL_CURRENCY_PROFIT); if(CurrencyToDepositRate(pc)<=0){ g_diag_warnings++; if(InpVerboseLog) Print("DIAG WARNING ",prim[i],": no deposit conversion for ",pc); }
   }
   if(g_mode==LIVE_TRADING){
      if(InpRequireHedgingForLive&&AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) g_diag_errors++;
      if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)||!MQLInfoInteger(MQL_TRADE_ALLOWED)) g_diag_warnings++;
   }
   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++;
   if(InpUseResearchLedger){
      if(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++;
   }
   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)):"OK");
   string msg="SELF TEST: "+g_diag_status+" | usable pairs="+IntegerToString(usablePairs); Print(msg); 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",520);
   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(){
   DrawPageShell("INSTITUTIONAL COCKPIT  |  METRICS","RISK • EXECUTION • ALPHA • RESEARCH • TELEMETRY",860);
   DrawMetricsPanel(InpPanelY-686+48);
   UIButton("MetricsBack","COMMAND CENTER",InpPanelX+505,InpPanelY+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",980);
   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,11,UI_TEXT,"Segoe UI Semibold");
   UILabel("AlphaSub","PAIR INTELLIGENCE • Z-SCORE • CORRELATION • HALF-LIFE • RISK ALLOCATION",x+18,y+31,7,UI_CYAN);
   UIButton("AlphaBack","COMMAND CENTER",x+505,y+10,125,28,UI_BLUE,UI_TEXT);
   UIRect("AlphaHead",x+10,y+48,630,24,UI_CARD2);
   UILabel("AH1","RANK / PAIR",x+18,y+55,7,UI_MUTED); UILabel("AH2","SCORE",x+185,y+55,7,UI_MUTED); UILabel("AH3","Z",x+230,y+55,7,UI_MUTED);
   UILabel("AH4","CORR",x+270,y+55,7,UI_MUTED); UILabel("AH5","HL",x+315,y+55,7,UI_MUTED); UILabel("AH6","BETA",x+350,y+55,7,UI_MUTED);
   UILabel("AH7","ALLOC",x+400,y+55,7,UI_CYAN); UILabel("AH8","STATE",x+455,y+55,7,UI_MUTED);
   int alphaRows=MathMin(ArraySize(g_top_a),16);
   for(int i=0;i<alphaRows;i++){
      int yy=y+76+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)?"ALLOC":"FILTER";
      UILabel("AState"+IntegerToString(i),state,x+455,yy+7,7,state=="ALLOC"?UI_GREEN:UI_ORANGE);
      UIButton("AlphaSelect"+IntegerToString(i),"DETAIL",x+560,yy+1,67,23,UI_CARD2,UI_TEXT);
   }
   int railY=y+76+alphaRows*28+7;
   if(railY>y+650) railY=y+650;
   UIRect("AlphaRail",x+10,railY,630,72,UI_CARD);
   UILabel("AlphaRailT","CROSS-PAIR CAPITAL OPTIMIZER",x+18,railY+9,7,UI_CYAN,"Segoe UI Semibold");
   double allocSum=0.0;
   for(int i=0;i<ArraySize(g_alloc_risk_pct);i++) allocSum+=g_alloc_risk_pct[i];
   UILabel("AlphaRail1","Budget "+DoubleToString(InpPortfolioRiskBudgetPct*MathMax(0.25,g_inst.riskMultiplier),2)+"% | Allocated "+DoubleToString(allocSum,2)+"% | Exec "+DoubleToString(g_inst.executionQualityScore,0)+"/100",x+18,railY+27,7,UI_TEXT);
   UILabel("AlphaRail2","Used "+DoubleToString(allocSum,2)+"% risk | Remaining "+DoubleToString(MathMax(0.0,InpPortfolioRiskBudgetPct*MathMax(0.25,g_inst.riskMultiplier)-allocSum),2)+"% | Regime "+g_inst.regime,x+18,railY+44,7,UI_TEXT);
   UILabel("AlphaGate","GATE "+(g_inst.gatePass?"PASS":"BLOCK")+" | Allocation respects pair + cluster risk caps",x+18,railY+60,7,g_inst.gatePass?UI_GREEN:UI_ORANGE);
}


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");
      UILabel("QuantKV"+IntegerToString(i),mv[i],xx+10,yy+28,14,(i==0&&statOk&&MathAbs(st.z)>=InpStatEntryZScore)?UI_GREEN:UI_TEXT,"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");
   UILabel("QuantA1","Z quality  "+(intelOk?DoubleToString(pi.zQuality*100,0)+"%":"-"),x+20,y+312,8,UI_TEXT);
   UILabel("QuantA2","Corr quality  "+(intelOk?DoubleToString(pi.corrQuality*100,0)+"%":"-"),x+20,y+330,8,UI_TEXT);
   UILabel("QuantA3","Half-life quality  "+(intelOk?DoubleToString(pi.halfLifeQuality*100,0)+"%":"-"),x+20,y+348,8,UI_TEXT);
   UILabel("QuantA4","Regime quality  "+(intelOk?DoubleToString(pi.regimeQuality*100,0)+"%":"-"),x+20,y+366,8,UI_TEXT);
   UILabel("QuantA5","Portfolio quality  "+(intelOk?DoubleToString(pi.portfolioQuality*100,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");

   UILabel("QuantFooter","Decision pack is informational; execution remains subject to the full portfolio gate and second-leg validation.",x+18,y+550,8,UI_MUTED);
}

void DrawPortfolioIntelligencePage(){
   RefreshPortfolioIntelligence();
   const int x=InpPanelX, y=InpPanelY, w=900, h=720;
   UIRect("PortfolioPage",x,y,w,h,UI_BG,UI_BORDER);
   UILabel("PortfolioTitle","ARBITRAGE PRO v1.0  |  PORTFOLIO INTELLIGENCE",x+18,y+14,15,UI_TEXT,"Segoe UI Semibold");
   UILabel("PortfolioSub","30D REALIZED PERFORMANCE  •  CAPITAL ALLOCATION  •  RISK  •  ALPHA  •  EXECUTION",x+18,y+39,9,UI_CYAN);
   UIButton("PortfolioBack","COMMAND CENTER",x+748,y+12,135,30,UI_BLUE,UI_TEXT);

   double health=g_pf_health;
   color hc=(health>=75)?UI_GREEN:((health>=InpHealthMinScore)?UI_ORANGE:UI_RED);
   UIRect("HealthCard",x+18,y+64,864,66,UI_CARD);
   UILabel("HealthT","INSTITUTIONAL HEALTH SCORE",x+30,y+74,9,UI_CYAN,"Segoe UI Semibold");
   UILabel("HealthV",DoubleToString(health,0)+" / 100",x+30,y+91,18,hc,"Segoe UI Semibold");
   UILabel("HealthB","ALPHA "+DoubleToString(g_pf_alpha_health,0)+"  |  RISK "+DoubleToString(g_pf_risk_health,0)+"  |  EXEC "+DoubleToString(g_pf_exec_health,0),x+160,y+94,8,UI_TEXT);
   DrawPortfolioBar("HealthBar",x+500,y+91,350,health,hc);
   UILabel("HealthState",health>=75?"INSTITUTIONAL HEALTHY":(health>=InpHealthMinScore?"CAUTION / REDUCE SELECTIVITY":"DEFENSIVE / CAPITAL PRESERVATION"),x+500,y+106,7,hc,"Segoe UI Semibold");

   int cy=y+144;
   string kpiT[6]={"NET P&L","WIN RATE","PROFIT FACTOR","EXPECTANCY","MAX DD","TRADE SHARPE"};
   string kpiV[6]={DoubleToString(g_pf_net,2),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)};
   for(int i=0;i<6;i++){
      int xx=x+18+i*143;
      UIRect("Kpi"+IntegerToString(i),xx,cy,136,76,UI_CARD);
      UILabel("KpiT"+IntegerToString(i),kpiT[i],xx+10,cy+10,8,UI_MUTED,"Segoe UI Semibold");
      color kc=UI_TEXT;
      if(i==0) kc=g_pf_net>=0?UI_GREEN:UI_RED;
      else if(i==1) kc=(g_pf_win_rate>=50)?UI_GREEN:UI_ORANGE;
      else if(i==2) kc=(g_pf_profit_factor>=1.5)?UI_GREEN:((g_pf_profit_factor>=1.0)?UI_ORANGE:UI_RED);
      else if(i==4) kc=(g_pf_max_dd_pct<=2)?UI_GREEN:((g_pf_max_dd_pct<=4)?UI_ORANGE:UI_RED);
      UILabel("KpiV"+IntegerToString(i),kpiV[i],xx+10,cy+30,14,kc,"Segoe UI Semibold");
      UILabel("KpiS"+IntegerToString(i),"Trades "+IntegerToString(g_pf_trades),xx+10,cy+57,8,UI_MUTED);
   }

   int ry=y+236;
   UIRect("AlphaPanel",x+18,ry,276,132,UI_CARD);
   UIRect("RiskPanel",x+312,ry,276,132,UI_CARD);
   UIRect("ExecPanel",x+606,ry,276,132,UI_CARD);
   UILabel("AlphaH","ALPHA / MODEL",x+30,ry+12,9,UI_CYAN,"Segoe UI Semibold");
   UILabel("Alpha1","Score "+DoubleToString(g_last_intelligence_score,0)+"  Pair "+(g_last_intelligence_pair!=""?g_last_intelligence_pair:"--"),x+30,ry+32,8,UI_TEXT);
   UILabel("Alpha2","Z "+DoubleToString(g_live_signal,2)+"  Decay "+DoubleToString(g_inst.signalDecay*100,0)+"%",x+30,ry+50,8,UI_TEXT);
   UILabel("Alpha3","Stability "+DoubleToString(g_inst.stabilityScore*100,0)+"%  Research gap "+DoubleToString(g_inst.researchLiveGap,1),x+30,ry+68,8,UI_TEXT);
   UILabel("Alpha4","MC P5 "+DoubleToString(g_inst_mc_p5,2)+"  Loss P "+DoubleToString(g_inst_mc_loss_probability*100,1)+"%",x+30,ry+86,8,UI_MUTED);
   DrawPortfolioBar("AlphaBar",x+30,ry+108,245,g_pf_alpha_health,(g_pf_alpha_health>=70)?UI_GREEN:UI_ORANGE);

   UILabel("RiskH","CAPITAL / RISK",x+324,ry+12,9,UI_CYAN,"Segoe UI Semibold");
   UILabel("RiskA","Regime "+g_inst.regime+"  Risk x"+DoubleToString(g_inst.riskMultiplier,2),x+324,ry+32,8,UI_TEXT);
   UILabel("RiskB","Used "+DoubleToString(g_inst.portfolioRiskPct,2)+"% / Budget "+DoubleToString(InpPortfolioRiskBudgetPct*g_inst.riskMultiplier,2)+"%",x+324,ry+50,8,UI_TEXT);
   UILabel("RiskC","VaR "+DoubleToString(g_inst.var95,2)+"  CVaR "+DoubleToString(g_inst.cvar95,2)+"  Stress "+DoubleToString(g_inst.stressLoss,2),x+324,ry+68,8,UI_TEXT);
   UILabel("RiskD","Exposure "+DoubleToString(g_inst.currentExposureLots,2)+" lot  Clusters "+IntegerToString(g_inst.clusterCount),x+324,ry+86,8,UI_MUTED);
   DrawPortfolioBar("RiskBar",x+324,ry+108,245,100.0-g_pf_risk_health,g_pf_risk_health>=70?UI_GREEN:UI_ORANGE);

   UILabel("ExecH","EXECUTION / TCA",x+618,ry+12,9,UI_CYAN,"Segoe UI Semibold");
   UILabel("ExecA","Quality "+DoubleToString(g_inst.executionQualityScore,0)+"/100  Cost "+DoubleToString(g_inst.executionCostPct,3)+"%",x+618,ry+32,8,UI_TEXT);
   UILabel("ExecB","Latency "+DoubleToString(g_current_latency_ms,0)+" ms  Slippage "+DoubleToString(g_daily_slippage,1)+" pt",x+618,ry+50,8,UI_TEXT);
   UILabel("ExecC","Abort "+IntegerToString(g_entry_aborts_today)+"  HedgeFail "+IntegerToString(g_failed_hedges_today)+"  Reconcile "+IntegerToString(g_reconcile_failures_today),x+618,ry+68,8,UI_TEXT);
   UILabel("ExecD","Quality sizing x"+DoubleToString(g_inst.qualitySizingMultiplier,2),x+618,ry+86,8,UI_MUTED);
   DrawPortfolioBar("ExecBar",x+618,ry+108,245,g_pf_exec_health,g_pf_exec_health>=75?UI_GREEN:UI_ORANGE);

   int qy=y+382;
   UIRect("AllocPanel",x+18,qy,564,250,UI_CARD);
   UIRect("RegimePanel",x+596,qy,286,250,UI_CARD);
   UILabel("AllocH","CAPITAL ALLOCATION QUEUE",x+30,qy+12,9,UI_CYAN,"Segoe UI Semibold");
   CalculateCapitalAllocation();
   UIRect("AllocHead",x+28,qy+29,544,20,UI_CARD2);
   UILabel("AllocH1","RANK",x+38,qy+35,7,UI_MUTED,"Segoe UI Semibold");
   UILabel("AllocH2","PAIR",x+70,qy+35,7,UI_MUTED,"Segoe UI Semibold");
   UILabel("AllocH3","SCORE",x+230,qy+35,7,UI_MUTED,"Segoe UI Semibold");
   UILabel("AllocH4","Z",x+278,qy+35,7,UI_MUTED,"Segoe UI Semibold");
   UILabel("AllocH5","CORR",x+328,qy+35,7,UI_MUTED,"Segoe UI Semibold");
   UILabel("AllocH6","HL",x+375,qy+35,7,UI_MUTED,"Segoe UI Semibold");
   UILabel("AllocH7","RISK ALLOC",x+440,qy+35,7,UI_CYAN,"Segoe UI Semibold");
   for(int q=0;q<5;q++){
      int yy=qy+52+q*32;
      UIRect("AllocRow"+IntegerToString(q),x+28,yy,544,27,UI_BG);
      string pair=(g_top_a[q]!="")?g_top_a[q]+"/"+g_top_b[q]:"--/--";
      UILabel("AllocRank"+IntegerToString(q),"0"+IntegerToString(q+1),x+38,yy+7,7,UI_CYAN);
      UILabel("AllocPair"+IntegerToString(q),pair,x+70,yy+7,7,UI_TEXT);
      UILabel("AllocScore"+IntegerToString(q),"S "+DoubleToString(g_top_score[q],0),x+230,yy+7,7,UI_TEXT);
      UILabel("AllocZ"+IntegerToString(q),"Z "+DoubleToString(g_top_z[q],2),x+278,yy+7,7,UI_TEXT);
      UILabel("AllocR"+IntegerToString(q),"r "+DoubleToString(g_top_corr[q],2),x+328,yy+7,7,UI_TEXT);
      UILabel("AllocHL"+IntegerToString(q),"HL "+DoubleToString(g_top_hl[q],0),x+375,yy+7,7,UI_TEXT);
      UILabel("AllocQ"+IntegerToString(q),DoubleToString(g_alloc_risk_pct[q],2)+"%",x+440,yy+7,7,g_alloc_risk_pct[q]>0?UI_GREEN:UI_MUTED);
   }
   UILabel("AllocFoot","Optimizer uses score, expected edge, Z, correlation, half-life, execution, signal quality and cluster concentration.",x+30,qy+218,7,UI_MUTED);

   UILabel("RegH","REGIME / PROTECTION",x+608,qy+12,9,UI_CYAN,"Segoe UI Semibold");
   UILabel("Reg1","STATE  "+g_inst.regime,x+608,qy+34,8,hc,"Segoe UI Semibold");
   UILabel("Reg2","Gate  "+(g_inst.gatePass?"PASS":"BLOCK"),x+608,qy+55,9,g_inst.gatePass?UI_GREEN:UI_RED);
   UILabel("Reg3","Kill  "+(g_inst.killSwitch?"ACTIVE":"ARMED"),x+608,qy+76,8,g_inst.killSwitch?UI_RED:UI_GREEN);
   UILabel("Reg4","Breaches "+IntegerToString(g_inst.breachesToday)+" / "+IntegerToString(InpMaxInstitutionalBreachesDay),x+608,qy+97,8,UI_TEXT);
   UILabel("Reg5","Max DD  "+DoubleToString(g_pf_max_dd_pct,2)+"%",x+608,qy+118,8,UI_TEXT);
   UILabel("Reg6","Sortino "+DoubleToString(g_pf_sortino,2),x+608,qy+139,8,UI_TEXT);
   UILabel("Reg7","Gate reason",x+608,qy+163,8,UI_MUTED);
   string gr=g_inst.gateReason; if(StringLen(gr)>38) gr=StringSubstr(gr,0,38)+"...";
   UILabel("Reg8",gr,x+608,qy+177,7,g_inst.gatePass?UI_GREEN:UI_ORANGE);
   UILabel("Reg9","LOOKBACK "+IntegerToString(InpPortfolioLookbackDays)+"D | REFRESH "+IntegerToString(InpPortfolioRefreshSec)+"s",x+608,qy+205,8,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");
   UILabel("InstAlphaV1",(g_live_pair!=""?g_live_pair:"SCANNING")+" | Z "+DoubleToString(g_live_signal,2),x+236,y+72,8,UI_TEXT,"Segoe UI");
   UILabel("InstAlphaV2","Score "+DoubleToString(g_last_intelligence_score,0)+" | Decay "+DoubleToString(g_inst.signalDecay*100.0,0)+"% | Stability "+DoubleToString(g_inst.stabilityScore*100.0,0)+"%",x+236,y+88,8,UI_TEXT,"Segoe UI");
   UILabel("InstAlphaV3","Research gap "+DoubleToString(g_inst.researchLiveGap,1)+" pts | MC P5 "+DoubleToString(g_inst_mc_p5,2),x+236,y+104,8,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");
   for(int q=0;q<3;q++){
      int yy=y+154+q*22;
      string qp=(g_top_a[q]!="")?g_top_a[q]+"/"+g_top_b[q]:"--";
      string qt=IntegerToString(q+1)+"  "+qp+" | Score "+DoubleToString(g_top_score[q],0)+" | |Z| "+DoubleToString(g_top_z[q],2)+" | r "+DoubleToString(g_top_corr[q],2)+" | beta "+DoubleToString(g_top_beta[q],2)+" | HL "+DoubleToString(g_top_hl[q],0);
      color qc=(g_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;
   UILabel("InstAttribT","P&L ATTRIBUTION / OPERATING LOAD",x+18,y+198,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+216,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+238,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+264,8,kc,"Segoe UI Semibold");
   UILabel("InstPipeline","CONNECTED PIPELINE: discover -> score -> regime -> quality sizing -> risk budget -> execute -> reconcile -> manage -> exit -> research",x+18,y+284,8,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<=1) g_strategy=(ENUM_AP_STRATEGY)ss; }
   g_demo_balance=InpDemoInitialBalance; if(!g_is_tester&&g_mode==DEMO_TRADING&&GlobalVariableCheck(GV_DEMO_BAL)) g_demo_balance=GlobalVariableGet(GV_DEMO_BAL);
   LoadDailyState();
   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 v1.0 QUANT INSTITUTIONAL initialized");
   Print("===== ARBITRAGE PRO v1.0 QUANT INSTITUTIONAL 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("");
}

void ProcessEngine(){
   ulong now=GetTickCount64();
   if(EngineDayStart()!=g_day_start) UpdateTradingDay();
   UpdateLiveMetrics();
   if(now-g_inst_last_update>=1){ 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();
   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>=1)) 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(); } }
}
//+------------------------------------------------------------------+