| //+------------------------------------------------------------------+
|
| //| LiveEA.mq5 |
|
| //| DualEA Live Trading Expert - Reconstructed |
|
| //+------------------------------------------------------------------+
|
| #property copyright "DualEA"
|
| #property version "2.0"
|
| #property strict
|
|
|
| //+------------------------------------------------------------------+
|
| //| INCLUDES |
|
| //+------------------------------------------------------------------+
|
| // New Architecture: Auto-learning gate system (unified with PaperEA)
|
| #include "..\Include\GateSystemAutoLearning.mqh"
|
|
|
| #include "..\Include\IStrategy.mqh"
|
| #include "..\Include\CPositionManager.mqh"
|
| #include "..\Include\TradeManager.mqh"
|
| #include "..\Include\Telemetry.mqh"
|
| #include "..\Include\TelemetryStandard.mqh"
|
| #include "..\Include\SessionManager.mqh"
|
| #include "..\Include\CorrelationManager.mqh"
|
| #include "..\Include\VolatilitySizer.mqh"
|
| #include "..\Include\InsightsLoader.mqh"
|
| #include "..\Include\IncrementalInsightEngine.mqh" // Real-time O(1) statistics engine
|
| #include "LiveEA_StrategyBridge.mqh"
|
|
|
| // Undefine logging macros from included headers to avoid clashes with LogLevel enum
|
| #ifdef LOG_DEBUG
|
| #undef LOG_DEBUG
|
| #endif
|
| #ifdef LOG_INFO
|
| #undef LOG_INFO
|
| #endif
|
| #ifdef LOG_WARNING
|
| #undef LOG_WARNING
|
| #endif
|
| #ifdef LOG_ERROR
|
| #undef LOG_ERROR
|
| #endif
|
|
|
| //+------------------------------------------------------------------+
|
| //| ENUMS AND CONSTANTS |
|
| //+------------------------------------------------------------------+
|
| enum LogLevel { LOG_ERROR=0, LOG_WARN=1, LOG_INFO=2, LOG_DEBUG=3 };
|
|
|
| //+------------------------------------------------------------------+
|
| //| INPUT PARAMETERS - CORE SETTINGS |
|
| //+------------------------------------------------------------------+
|
| input long MagicNumber = 123456;
|
| input int Verbosity = 1; // 0=errors,1=warn,2=info,3=debug
|
| input bool TelemetryEnabled = false; // enable telemetry emission
|
| input bool NoConstraintsMode = false; // shadow-gate only, do not block
|
| //+------------------------------------------------------------------+
|
| //| INPUT PARAMETERS - TELEMETRY |
|
| //+------------------------------------------------------------------+
|
| input string TelemetryExperiment = "live";
|
| input int TelemetryBufferMax = 1000;
|
|
|
| //+------------------------------------------------------------------+
|
| //| INPUT PARAMETERS - RISK MANAGEMENT |
|
| //+------------------------------------------------------------------+
|
| input double SpreadMaxPoints = 0.0; // 0=disabled
|
| input int SessionStartHour = 0; // 0..23
|
| input int SessionMaxTrades = 0; // 0=disabled
|
| input double MaxDailyLossPct = 0.0; // 0=disabled
|
| input double MaxDrawdownPct = 0.0; // 0=disabled
|
| input double MinMarginLevel = 0.0; // 0=disabled, e.g. 100.0
|
| input int ConsecutiveLossLimit = 0; // 0=disabled
|
| input bool P5_PersistLossCounters = false; // persist counters (minimal in-memory here)
|
|
|
| //+------------------------------------------------------------------+
|
| //| INPUT PARAMETERS - INSIGHTS GATING |
|
| //+------------------------------------------------------------------+
|
| input bool UseInsightsGating = true;
|
| input int GateMinTrades = 0; // loosened for bootstrap
|
| input double GateMinWinRate = 0.00; // loosened for bootstrap
|
| input double GateMinExpectancyR = -10.0; // loosened for bootstrap
|
| input double GateMaxDrawdownR = 1000000.0;// loosened for bootstrap
|
| input double GateMinProfitFactor = 0.00; // loosened for bootstrap
|
|
|
| //+------------------------------------------------------------------+
|
| //| INPUT PARAMETERS - EXPLORATION MODE |
|
| //+------------------------------------------------------------------+
|
| input bool ExploreOnNoSlice = true; // allow limited trades when slice has no data
|
| input int ExploreMaxPerSlice = 100; // loosened for bootstrap
|
| input int ExploreMaxPerSlicePerDay = 100; // loosened for bootstrap
|
|
|
| //+------------------------------------------------------------------+
|
| //| INPUT PARAMETERS - POLICY GATING |
|
| //+------------------------------------------------------------------+
|
| input bool UsePolicyGating = true;
|
| input bool DefaultPolicyFallback = true; // allow neutral trading when policy slice is missing
|
| input bool FallbackDemoOnly = true; // restrict fallback to demo accounts
|
| input bool FallbackWhenNoPolicy = true; // allow fallback when policy file is not loaded
|
|
|
| //+------------------------------------------------------------------+
|
| //| INPUT PARAMETERS - CORRELATION MANAGEMENT |
|
| //+------------------------------------------------------------------+
|
| input double P5_CorrMax = 1.0; // 1.0=no pruning by default
|
| input int P5_CorrLookbackDays = 5; // used if PositionManager is not available
|
| input bool UsePositionManager = false; // optional PositionManager usage
|
|
|
| // News / Promotion / Regime / Cooldown inputs
|
| input bool UseNewsFilter = false; // block around defined news windows
|
| input int NewsBufferBeforeMin = 30; // minutes before an event
|
| input int NewsBufferAfterMin = 30; // minutes after an event
|
| input int NewsImpactMin = 2; // 1=low, 2=medium, 3=high
|
| input bool NewsUseFile = true; // read blackouts from CSV in Common files
|
| input string NewsFileRelPath = "DualEA\\news_blackouts.csv";
|
|
|
| // --- Insights live auto-reload controls
|
| input bool InsightsAutoReload = true; // if stale/missing, request rebuild and auto-reload when ready
|
| input int InsightsLiveFreshMinutes = 10; // consider insights fresh if modified within N minutes
|
| input int InsightsReadyPollSec = 5; // poll frequency for ready signal
|
|
|
| input bool UsePromotionGate = false; // allow only during configured windows
|
| input bool PromoLiveOnly = false; // apply only on live accounts
|
| input int PromoStartHour = 0; // inclusive, server time
|
| input int PromoEndHour = 24; // exclusive, supports wrap if less than start
|
|
|
| input bool UseRegimeGate = false; // filter by volatility regimes
|
| input int RegimeATRPeriod = 14;
|
| input double RegimeMinATRPct = 0.0; // 0=disabled
|
| input double RegimeMaxATRPct = 1000.0; // 1000=disabled
|
|
|
| input int CircuitCooldownSec = 0; // 0=disabled
|
|
|
| // --- FR-02 Session Manager inputs
|
| input bool UseSessionManager = true;
|
| input int SessionEndHour = 20;
|
| input int MaxTradesPerSession = 10;
|
|
|
| // --- FR-05 Correlation Manager inputs
|
| input bool UseCorrelationManager = true;
|
| input double MaxCorrelationLimit = 0.7;
|
| input int CorrLookbackDays = 30;
|
|
|
| // --- FR-04 Position Manager inputs
|
| input int PMMaxOpenPositions = 10; // 0=disabled; cap total open positions before PM gate blocks
|
|
|
| // --- FR-06 Volatility Sizer inputs
|
| input bool UseVolatilitySizer = false;
|
| input int VolSizerATRPeriod = 14;
|
| input double VolSizerBaseATRPct = 1.0;
|
| input double VolSizerMinMult = 0.1;
|
| input double VolSizerMaxMult = 3.0;
|
| input double VolSizerTargetRisk = 1.0;
|
| input double BaseLotSize = 0.01; // base lot size used when sizing via VolatilitySizer
|
|
|
| //+------------------------------------------------------------------+
|
| //| GLOBAL VARIABLES |
|
| //+------------------------------------------------------------------+
|
| CEfficientGateManagerEnhanced* g_gate_manager = NULL; // New unified gate system
|
| CTelemetry *g_telemetry = NULL;
|
| CTelemetryStandard* g_tel_standard = NULL;
|
| CSessionManager* g_session_manager = NULL;
|
| CCorrelationManager* g_correlation_manager = NULL;
|
| CVolatilitySizer* g_volatility_sizer = NULL;
|
| // g_position_manager and g_insight_engine are defined in their respective modules
|
| // CPositionManager* g_position_manager;
|
| // CIncrementalInsightEngine* g_insight_engine;
|
| bool g_eval_busy = false;
|
| datetime g_last_trade_placed= 0;
|
|
|
| // --- Insights gating cache (loaded from insights.json via InsightsLoader)
|
| string g_gate_strat[];
|
| string g_gate_sym[];
|
| int g_gate_tf[];
|
| int g_gate_cnt[];
|
| double g_gate_wr[];
|
| double g_gate_avgR[];
|
| double g_gate_pf[];
|
| double g_gate_dd[];
|
|
|
| // --- Policy gating cache (loaded from policy.json)
|
| bool g_policy_loaded = false;
|
| double g_policy_min_conf= 0.0;
|
| string g_pol_strat[];
|
| string g_pol_sym[];
|
| int g_pol_tf[];
|
| double g_pol_p[];
|
| double g_pol_sl[];
|
| double g_pol_tp[];
|
| double g_pol_trail[];
|
|
|
| // Live insights reload state
|
| bool g_waiting_insights = false; // set after we request reload; reset when ready is consumed
|
|
|
| // --- Exploration Mode tracking (weekly + daily persistent)
|
| string g_exp_keys[]; // slice key: strategy|symbol|timeframe
|
| int g_exp_weeks[]; // week bucket id (Monday yyyymmdd)
|
| int g_exp_counts[]; // count within week
|
| string g_explore_pending_key = ""; // set by Insights_Allow when allowing explore
|
| // Daily exploration tracking (persistent)
|
| string g_exp_day_keys[];
|
| int g_exp_day_days[];
|
| int g_exp_day_counts[];
|
|
|
| // --- News blackout cache (from CSV)
|
| string g_news_key[];
|
| datetime g_news_from[];
|
| datetime g_news_to[];
|
| int g_news_impact[];
|
|
|
| //+------------------------------------------------------------------+
|
| //| UTILITY FUNCTIONS |
|
| //+------------------------------------------------------------------+
|
| bool ShouldLog(const int level) { return (Verbosity >= level); }
|
|
|
| void LogMessage(const int level, const string message)
|
| {
|
| if (!ShouldLog(level)) return;
|
|
|
| string prefix = "";
|
| switch(level)
|
| {
|
| case LOG_ERROR: prefix = "[ERROR] "; break;
|
| case LOG_WARN: prefix = "[WARN] "; break;
|
| case LOG_INFO: prefix = "[INFO] "; break;
|
| case LOG_DEBUG: prefix = "[DEBUG] "; break;
|
| }
|
|
|
| Print(prefix + message);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| SERVICE INITIALIZATION FUNCTIONS |
|
| //+------------------------------------------------------------------+
|
| void EnsureTelemetry()
|
| {
|
| if(!TelemetryEnabled) return;
|
| if(CheckPointer(g_telemetry)==POINTER_INVALID)
|
| {
|
| g_telemetry = new CTelemetry(_Symbol, TelemetryExperiment, TelemetryBufferMax, (int)_Period);
|
| if(CheckPointer(g_telemetry)==POINTER_INVALID)
|
| {
|
| Print("Failed to create telemetry instance");
|
| return;
|
| }
|
| g_tel_standard = new CTelemetryStandard(g_telemetry);
|
| }
|
| }
|
|
|
| void EnsurePM()
|
| {
|
| if(!UsePositionManager) return;
|
| if(CheckPointer(g_position_manager)==POINTER_INVALID)
|
| {
|
| g_position_manager = new CPositionManager();
|
| if(CheckPointer(g_position_manager)!=POINTER_INVALID)
|
| (*g_position_manager).SetMagicNumber((int)MagicNumber);
|
| }
|
| }
|
|
|
| void EnsureSessionManager()
|
| {
|
| if(!UseSessionManager) return;
|
| if(CheckPointer(g_session_manager)==POINTER_INVALID)
|
| {
|
| g_session_manager = new CSessionManager(_Symbol, _Period);
|
| g_session_manager.SetEnabled(true);
|
| g_session_manager.SetSessionHours(SessionStartHour, SessionEndHour);
|
| g_session_manager.SetMaxTradesPerSession(MaxTradesPerSession);
|
| g_session_manager.SetMaxDailyLossPct(MaxDailyLossPct);
|
| }
|
| }
|
|
|
| void EnsureCorrelationManager()
|
| {
|
| if(!UseCorrelationManager) return;
|
| if(CheckPointer(g_correlation_manager)==POINTER_INVALID)
|
| {
|
| g_correlation_manager = new CCorrelationManager(_Symbol, _Period);
|
| g_correlation_manager.SetEnabled(true);
|
| g_correlation_manager.SetMaxCorrelation(MaxCorrelationLimit);
|
| g_correlation_manager.SetLookbackDays(CorrLookbackDays);
|
| }
|
| }
|
|
|
| void EnsureVolatilitySizer()
|
| {
|
| if(!UseVolatilitySizer) return;
|
| if(CheckPointer(g_volatility_sizer)==POINTER_INVALID)
|
| {
|
| g_volatility_sizer = new CVolatilitySizer(_Symbol, _Period);
|
| g_volatility_sizer.SetEnabled(true);
|
| g_volatility_sizer.SetATRPeriod(VolSizerATRPeriod);
|
| g_volatility_sizer.SetBaseATRPercent(VolSizerBaseATRPct);
|
| g_volatility_sizer.SetMultiplierRange(VolSizerMinMult, VolSizerMaxMult);
|
| g_volatility_sizer.SetTargetRiskPercent(VolSizerTargetRisk);
|
| }
|
| }
|
|
|
| // --- Spread/Session/Risk gating helpers and session tracking
|
| datetime g_session_start = 0;
|
| int g_session_day = 0;
|
| double g_session_equity_start = 0.0;
|
| double g_equity_highwater = 0.0;
|
|
|
| double CurrentSpreadPoints()
|
| {
|
| double bid=0.0, ask=0.0;
|
| SymbolInfoDouble(_Symbol, SYMBOL_BID, bid);
|
| SymbolInfoDouble(_Symbol, SYMBOL_ASK, ask);
|
| if(ask>0.0 && bid>0.0 && _Point>0.0) return (ask - bid)/_Point;
|
| return 0.0;
|
| }
|
|
|
| datetime ComputeSessionBoundary(datetime now)
|
| {
|
| MqlDateTime dt; TimeToStruct(now, dt);
|
| dt.hour = SessionStartHour;
|
| dt.min = 0;
|
| dt.sec = 0;
|
| datetime today_boundary = StructToTime(dt);
|
| if(now < today_boundary) return today_boundary - 86400;
|
| return today_boundary;
|
| }
|
|
|
| void EnsureSessionRollover()
|
| {
|
| datetime b = ComputeSessionBoundary(TimeCurrent());
|
| MqlDateTime dts; TimeToStruct(b, dts);
|
| int day = dts.year*10000 + dts.mon*100 + dts.day;
|
| if(g_session_start==0 || g_session_day!=day)
|
| {
|
| g_session_start = b;
|
| g_session_day = day;
|
| g_session_equity_start = AccountInfoDouble(ACCOUNT_EQUITY);
|
| g_equity_highwater = g_session_equity_start;
|
| if(ShouldLog(LOG_INFO)) PrintFormat("Session rollover: start=%s equity=%.2f",
|
| TimeToString(g_session_start, TIME_DATE|TIME_MINUTES), g_session_equity_start);
|
| }
|
| }
|
|
|
| int CountNewTradesSince(const datetime t0)
|
| {
|
| int count=0;
|
| if(!HistorySelect(t0, TimeCurrent())) return 0;
|
| int total = (int)HistoryDealsTotal();
|
| for(int i=0;i<total;++i)
|
| {
|
| ulong ticket = HistoryDealGetTicket(i);
|
| if(HistoryDealGetString(ticket, DEAL_SYMBOL)!=_Symbol) continue;
|
| long mag = (long)HistoryDealGetInteger(ticket, DEAL_MAGIC);
|
| if(mag != MagicNumber) continue;
|
| int entry = (int)HistoryDealGetInteger(ticket, DEAL_ENTRY);
|
| if(entry==DEAL_ENTRY_IN) count++;
|
| }
|
| return count;
|
| }
|
|
|
| int CountConsecutiveLosses()
|
| {
|
| int consec=0;
|
| datetime t0 = TimeCurrent() - 120*86400;
|
| if(!HistorySelect(t0, TimeCurrent())) return 0;
|
| for(int i=HistoryDealsTotal()-1; i>=0; --i)
|
| {
|
| ulong ticket = HistoryDealGetTicket(i);
|
| if((int)HistoryDealGetInteger(ticket, DEAL_ENTRY)!=DEAL_ENTRY_OUT) continue;
|
| long mag = (long)HistoryDealGetInteger(ticket, DEAL_MAGIC);
|
| if(mag != MagicNumber) continue;
|
| double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT);
|
| if(profit < 0.0) consec++;
|
| else break;
|
| }
|
| return consec;
|
| }
|
|
|
| // --- Minimal persistent consecutive loss counters (in-memory)
|
| string g_loss_sym[];
|
| long g_loss_mag[];
|
| int g_loss_cnt[];
|
|
|
| int GetConsecLossPersist(const string sym, const long mag)
|
| {
|
| for(int i=0;i<ArraySize(g_loss_sym);++i)
|
| if(g_loss_sym[i]==sym && g_loss_mag[i]==mag) return g_loss_cnt[i];
|
| return 0;
|
| }
|
|
|
| void SetConsecLossPersist(const string sym, const long mag, const int val)
|
| {
|
| for(int i=0;i<ArraySize(g_loss_sym);++i)
|
| if(g_loss_sym[i]==sym && g_loss_mag[i]==mag) { g_loss_cnt[i]=(val<0?0:val); return; }
|
| int n=ArraySize(g_loss_sym);
|
| ArrayResize(g_loss_sym,n+1); ArrayResize(g_loss_mag,n+1); ArrayResize(g_loss_cnt,n+1);
|
| g_loss_sym[n]=sym; g_loss_mag[n]=mag; g_loss_cnt[n]=(val<0?0:val);
|
| }
|
|
|
| void IncConsecLossPersist(const string sym, const long mag)
|
| {
|
| for(int i=0;i<ArraySize(g_loss_sym);++i)
|
| if(g_loss_sym[i]==sym && g_loss_mag[i]==mag) { g_loss_cnt[i]=g_loss_cnt[i]+1; return; }
|
| int n=ArraySize(g_loss_sym);
|
| ArrayResize(g_loss_sym,n+1); ArrayResize(g_loss_mag,n+1); ArrayResize(g_loss_cnt,n+1);
|
| g_loss_sym[n]=sym; g_loss_mag[n]=mag; g_loss_cnt[n]=1;
|
| }
|
|
|
| bool SpreadAllowed(string &reason)
|
| {
|
| reason = "ok";
|
| if(SpreadMaxPoints<=0.0) return true;
|
| double spr = CurrentSpreadPoints();
|
| if(spr <= SpreadMaxPoints) return true;
|
| reason = "spread_cap";
|
| return false;
|
| }
|
|
|
| bool SessionAllowed(string &reason)
|
| {
|
| reason = "ok";
|
| if(SessionMaxTrades<=0) return true;
|
| int used = CountNewTradesSince(g_session_start);
|
| if(used < SessionMaxTrades) return true;
|
| reason = "session_cap";
|
| return false;
|
| }
|
|
|
| bool RiskAllowed(string &reason)
|
| {
|
| reason = "ok";
|
| double eq = AccountInfoDouble(ACCOUNT_EQUITY);
|
| if(eq>g_equity_highwater) g_equity_highwater = eq;
|
| if(MaxDailyLossPct>0.0 && g_session_equity_start>0.0)
|
| {
|
| double dd = 100.0*(g_session_equity_start - eq)/g_session_equity_start;
|
| if(dd >= MaxDailyLossPct) { reason="max_daily_loss"; return false; }
|
| }
|
| if(MaxDrawdownPct>0.0 && g_equity_highwater>0.0)
|
| {
|
| double ddh = 100.0*(g_equity_highwater - eq)/g_equity_highwater;
|
| if(ddh >= MaxDrawdownPct) { reason="max_drawdown"; return false; }
|
| }
|
| if(MinMarginLevel>0.0)
|
| {
|
| double ml = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);
|
| if(ml>0.0 && ml < MinMarginLevel) { reason="margin_level"; return false; }
|
| }
|
| if(ConsecutiveLossLimit>0)
|
| {
|
| int cl = (P5_PersistLossCounters ? GetConsecLossPersist(_Symbol, (long)MagicNumber)
|
| : CountConsecutiveLosses());
|
| if(cl >= ConsecutiveLossLimit) { reason="consec_losses"; return false; }
|
| }
|
| return true;
|
| }
|
|
|
| // Helper: append unique key to a dynamic string array
|
| void AddUniqueKey(string &arr[], const string k)
|
| {
|
| for(int i=0;i<ArraySize(arr);++i)
|
| if(arr[i]==k) return;
|
| int n=ArraySize(arr);
|
| ArrayResize(arr, n+1);
|
| arr[n]=k;
|
| }
|
|
|
| bool NewsAllowed(string &reason)
|
| {
|
| reason = "ok";
|
| if(!UseNewsFilter)
|
| return true;
|
|
|
| // Derive keys to match against news file entries
|
| string keys[];
|
| string sU = _Symbol; StringToUpper(sU);
|
| // Add full symbol and wildcard
|
| AddUniqueKey(keys, sU);
|
| AddUniqueKey(keys, "*");
|
| // Try to extract currency codes
|
| string ccy[] = { "USD","EUR","GBP","JPY","CHF","AUD","NZD","CAD","SEK","NOK","DKK","SGD","HKD","CNY","CNH","ZAR","MXN","TRY" };
|
| for(int i=0;i<ArraySize(ccy);++i) if(StringFind(sU, ccy[i])>=0) AddUniqueKey(keys, ccy[i]);
|
| // Index heuristics -> base currency
|
| if(StringFind(sU, "US30")>=0 || StringFind(sU, "US500")>=0 || StringFind(sU, "NAS")>=0 || StringFind(sU, "DJ")>=0 || StringFind(sU, "SPX")>=0)
|
| AddUniqueKey(keys, "USD");
|
| if(StringFind(sU, "GER")>=0 || StringFind(sU, "DAX")>=0 || StringFind(sU, "FRA40")>=0 || StringFind(sU, "CAC")>=0)
|
| AddUniqueKey(keys, "EUR");
|
| if(StringFind(sU, "UK100")>=0 || StringFind(sU, "FTSE")>=0)
|
| AddUniqueKey(keys, "GBP");
|
| if(StringFind(sU, "JPN")>=0 || StringFind(sU, "JP")>=0 || StringFind(sU, "NIK")>=0)
|
| AddUniqueKey(keys, "JPY");
|
| if(StringFind(sU, "HK50")>=0 || StringFind(sU, "HSI")>=0)
|
| AddUniqueKey(keys, "HKD");
|
| if(StringFind(sU, "AUS")>=0)
|
| AddUniqueKey(keys, "AUD");
|
|
|
| datetime now = TimeCurrent();
|
| // Scan loaded blackout windows
|
| for(int i=0;i<ArraySize(g_news_key);++i)
|
| {
|
| // impact threshold
|
| if(g_news_impact[i] < NewsImpactMin) continue;
|
| // key match
|
| bool match=false;
|
| for(int j=0;j<ArraySize(keys) && !match;++j)
|
| if(g_news_key[i]==keys[j]) match=true;
|
| if(!match) continue;
|
| // window with buffers
|
| datetime from = g_news_from[i] - (NewsBufferBeforeMin*60);
|
| datetime to = g_news_to[i] + (NewsBufferAfterMin*60);
|
| if(now>=from && now<=to)
|
| {
|
| reason = "news_window";
|
| return false;
|
| }
|
| }
|
| return true;
|
| }
|
|
|
| bool PromotionAllowed(string &reason)
|
| {
|
| reason = "ok";
|
| if(!UsePromotionGate)
|
| return true;
|
| // Apply only on live accounts if configured
|
| if(PromoLiveOnly && AccountInfoInteger(ACCOUNT_TRADE_MODE)!=ACCOUNT_TRADE_MODE_REAL)
|
| return true;
|
| MqlDateTime dt; TimeToStruct(TimeCurrent(), dt);
|
| int h = dt.hour;
|
| // handle windows possibly spanning midnight
|
| bool in_window = false;
|
| if(PromoStartHour<=PromoEndHour)
|
| in_window = (h>=PromoStartHour && h<PromoEndHour);
|
| else
|
| in_window = (h>=PromoStartHour || h<PromoEndHour);
|
| if(!in_window)
|
| { reason = "promo_window"; return false; }
|
| return true;
|
| }
|
|
|
| bool RegimeAllowed(string &reason)
|
| {
|
| reason = "ok";
|
| if(!UseRegimeGate)
|
| return true;
|
| int atr_handle = iATR(_Symbol, _Period, RegimeATRPeriod);
|
| if(atr_handle==INVALID_HANDLE)
|
| return true;
|
| double atr_buf[]; ArrayResize(atr_buf,1);
|
| int copied = CopyBuffer(atr_handle, 0, 0, 1, atr_buf);
|
| IndicatorRelease(atr_handle);
|
| if(copied!=1)
|
| return true;
|
| double atr = atr_buf[0];
|
| double px = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
| if(atr<=0.0 || px<=0.0)
|
| return true; // cannot assess -> allow
|
| double atr_pct = 100.0 * atr / px;
|
| if(RegimeMinATRPct>0.0 && atr_pct < RegimeMinATRPct)
|
| { reason = "regime_low_atr"; return false; }
|
| if(RegimeMaxATRPct>0.0 && atr_pct > RegimeMaxATRPct)
|
| { reason = "regime_high_atr"; return false; }
|
| return true;
|
| }
|
|
|
| bool CircuitCooldownAllowed(string &reason)
|
| {
|
| reason = "ok";
|
| if(CircuitCooldownSec<=0)
|
| return true;
|
| if(g_last_trade_placed<=0)
|
| return true;
|
| datetime now = TimeCurrent();
|
| if((now - g_last_trade_placed) < CircuitCooldownSec)
|
| { reason = "circuit_cooldown"; return false; }
|
| return true;
|
| }
|
|
|
| bool P5_CorrPruneAllow(const TradeOrder &order, string &reason)
|
| {
|
| reason = "ok";
|
| // No open positions -> nothing to prune
|
| int npos = PositionsTotal();
|
| if(npos<=0)
|
| return true;
|
|
|
| // Prefer PositionManager's portfolio correlation if available
|
| double avg_abs_corr = 0.0;
|
| bool corr_from_pm = (CheckPointer(g_position_manager)!=POINTER_INVALID);
|
| if(corr_from_pm)
|
| {
|
| avg_abs_corr = (*g_position_manager).GetPortfolioCorrelation(_Symbol);
|
| }
|
| else
|
| {
|
| // Manual weighted average absolute correlation to each open symbol
|
| int ps = PeriodSeconds(_Period);
|
| if(ps<=0) ps = 60;
|
| int approx_bars = (int)MathMax(50, MathMin(2000, (P5_CorrLookbackDays*86400)/ps));
|
| double sum=0.0, wsum=0.0;
|
| for(int i=0;i<npos;++i)
|
| {
|
| ulong t = PositionGetTicket(i);
|
| if(t==0) continue;
|
| if(!PositionSelectByTicket(t)) continue;
|
| string osym = PositionGetString(POSITION_SYMBOL);
|
| if(osym=="" || osym==_Symbol) continue;
|
| double vol = PositionGetDouble(POSITION_VOLUME);
|
| // Compute Pearson correlation between closes of _Symbol and osym
|
| double a[]; double b[];
|
| int ca = CopyClose(_Symbol, _Period, 0, approx_bars, a);
|
| int cb = CopyClose(osym, _Period, 0, approx_bars, b);
|
| int m = MathMin(ca, cb);
|
| if(m<10) continue;
|
| double ma=0.0, mb=0.0; for(int k=0;k<m;++k){ ma+=a[k]; mb+=b[k]; }
|
| ma/=m; mb/=m;
|
| double cov=0.0, va=0.0, vb=0.0;
|
| for(int k=0;k<m;++k)
|
| {
|
| double da=a[k]-ma; double db=b[k]-mb;
|
| cov+=da*db; va+=da*da; vb+=db*db;
|
| }
|
| if(va<=0.0 || vb<=0.0) continue;
|
| double c = cov / MathSqrt(va*vb);
|
| if(c>1.0) c=1.0; if(c<-1.0) c=-1.0;
|
| sum += MathAbs(c) * vol;
|
| wsum += vol;
|
| }
|
| avg_abs_corr = (wsum>0.0? sum/wsum : 0.0);
|
| }
|
|
|
| if(avg_abs_corr > P5_CorrMax)
|
| {
|
| reason = StringFormat("corr=%.2f>max=%.2f", avg_abs_corr, P5_CorrMax);
|
| return false;
|
| }
|
| return true;
|
| }
|
|
|
| // --- Insights/Policy helpers and loaders
|
| // Trim helper
|
| string TrimCopy(string s){ StringTrimLeft(s); StringTrimRight(s); return s; }
|
|
|
| // Normalize broker-specific suffixes to base symbol (e.g., "AUDNZD_otc" -> "AUDNZD")
|
| string NormalizeSymbol(string s)
|
| {
|
| string lowers = s; StringToLower(lowers);
|
| // Common suffixes/prefixes to strip
|
| string suf[] = { "_otc", "_pro", "_ecn", "_mini", "_micro", ".r", ".i", ".pro", ".ecn", ".m" };
|
| for(int i=0;i<ArraySize(suf);++i)
|
| {
|
| int p = StringFind(lowers, suf[i], StringLen(lowers)-StringLen(suf[i]));
|
| if(p>=0 && p==StringLen(lowers)-StringLen(suf[i]))
|
| {
|
| // Remove suffix of same length from original string
|
| s = StringSubstr(s, 0, StringLen(s)-StringLen(suf[i]));
|
| break;
|
| }
|
| }
|
| return s;
|
| }
|
|
|
| // Load policy.json from Common Files and cache min_confidence and slice_probs
|
| bool Policy_Load()
|
| {
|
| g_policy_loaded = false;
|
| g_policy_min_conf = 0.0;
|
| ArrayResize(g_pol_strat,0); ArrayResize(g_pol_sym,0); ArrayResize(g_pol_tf,0); ArrayResize(g_pol_p,0);
|
| ArrayResize(g_pol_sl,0); ArrayResize(g_pol_tp,0); ArrayResize(g_pol_trail,0);
|
| string path = "DualEA\\policy.json";
|
| int h = FileOpen(path, FILE_READ|FILE_TXT|FILE_COMMON|FILE_ANSI);
|
| if(h==INVALID_HANDLE)
|
| {
|
| if(ShouldLog(LOG_WARN)) PrintFormat("Policy gating: cannot open %s (Common). Err=%d", path, GetLastError());
|
| return false;
|
| }
|
| string cur_s="", cur_y=""; int cur_tf=-1; double cur_p=-1.0;
|
| double cur_sl=1.0, cur_tp=1.0, cur_tr=1.0; // optional scales default to 1.0
|
| while(!FileIsEnding(h))
|
| {
|
| string line = FileReadString(h);
|
| if(line=="" && FileIsEnding(h)) break;
|
| int p;
|
| // min_confidence
|
| p = StringFind(line, "\"min_confidence\"", 0);
|
| if(p>=0)
|
| {
|
| int c = StringFind(line, ":", p);
|
| if(c>=0){ string num = TrimCopy(StringSubstr(line, c+1)); double v = StringToDouble(num); if(v>0) g_policy_min_conf = v; }
|
| }
|
| // strategy
|
| p = StringFind(line, "\"strategy\"", 0);
|
| if(p>=0)
|
| {
|
| int q = StringFind(line, ",", p+1); string seg = (q>p? StringSubstr(line, p, q-p) : StringSubstr(line, p));
|
| int c3=StringFind(seg, "\"", 0); c3 = StringFind(seg, "\"", c3+1); int c4=StringFind(seg, "\"", c3+1); int c5=StringFind(seg, "\"", c4+1);
|
| if(c4>0 && c5>c4) cur_s = StringSubstr(seg, c4+1, c5-c4-1);
|
| }
|
| // symbol
|
| p = StringFind(line, "\"symbol\"", 0);
|
| if(p>=0)
|
| {
|
| int q = StringFind(line, ",", p+1); string seg = (q>p? StringSubstr(line, p, q-p) : StringSubstr(line, p));
|
| int c3=StringFind(seg, "\"", 0); c3 = StringFind(seg, "\"", c3+1); int c4=StringFind(seg, "\"", c3+1); int c5=StringFind(seg, "\"", c4+1);
|
| if(c4>0 && c5>c4) cur_y = StringSubstr(seg, c4+1, c5-c4-1);
|
| }
|
| // timeframe
|
| p = StringFind(line, "\"timeframe\"", 0);
|
| if(p>=0)
|
| {
|
| int c = StringFind(line, ":", p); if(c>=0){ string num = TrimCopy(StringSubstr(line, c+1)); cur_tf = (int)StringToInteger(num); }
|
| }
|
| // p_win
|
| p = StringFind(line, "\"p_win\"", 0);
|
| if(p>=0)
|
| {
|
| int c = StringFind(line, ":", p); if(c>=0){ string num = TrimCopy(StringSubstr(line, c+1)); cur_p = StringToDouble(num); }
|
| }
|
| // optional: sl_scale
|
| p = StringFind(line, "\"sl_scale\"", 0);
|
| if(p>=0)
|
| {
|
| int c = StringFind(line, ":", p); if(c>=0){ string num = TrimCopy(StringSubstr(line, c+1)); cur_sl = StringToDouble(num); }
|
| }
|
| // optional: tp_scale
|
| p = StringFind(line, "\"tp_scale\"", 0);
|
| if(p>=0)
|
| {
|
| int c = StringFind(line, ":", p); if(c>=0){ string num = TrimCopy(StringSubstr(line, c+1)); cur_tp = StringToDouble(num); }
|
| }
|
| // optional: trail_scale
|
| p = StringFind(line, "\"trail_scale\"", 0);
|
| if(p>=0)
|
| {
|
| int c = StringFind(line, ":", p); if(c>=0){ string num = TrimCopy(StringSubstr(line, c+1)); cur_tr = StringToDouble(num); }
|
| }
|
| // commit when we have strat+symbol (timeframe may be -1)
|
| if(cur_s!="" && cur_y!="" && cur_tf!=-2 && cur_p>=0.0)
|
| {
|
| int n = ArraySize(g_pol_strat);
|
| ArrayResize(g_pol_strat,n+1); ArrayResize(g_pol_sym,n+1); ArrayResize(g_pol_tf,n+1);
|
| ArrayResize(g_pol_p,n+1); ArrayResize(g_pol_sl,n+1); ArrayResize(g_pol_tp,n+1); ArrayResize(g_pol_trail,n+1);
|
| g_pol_strat[n]=cur_s; g_pol_sym[n]=cur_y; g_pol_tf[n]=cur_tf; g_pol_p[n]=cur_p; g_pol_sl[n]=cur_sl; g_pol_tp[n]=cur_tp; g_pol_trail[n]=cur_tr;
|
| // reset slice accumulators for next object
|
| cur_s=""; cur_y=""; cur_tf=-1; cur_p=-1.0; cur_sl=1.0; cur_tp=1.0; cur_tr=1.0;
|
| }
|
| }
|
| FileClose(h);
|
| g_policy_loaded = (ArraySize(g_pol_strat)>0);
|
| if(ShouldLog(LOG_INFO)) PrintFormat("Policy gating: min_conf=%.3f slices=%d", g_policy_min_conf, ArraySize(g_pol_strat));
|
| return g_policy_loaded;
|
| }
|
|
|
| double GetPolicyProb(const string strategy, const string symbol, const int timeframe)
|
| {
|
| string symN = NormalizeSymbol(symbol);
|
| // 1) Exact slice: strat+symbol+timeframe
|
| for(int i=0;i<ArraySize(g_pol_strat);++i)
|
| if(g_pol_strat[i]==strategy && g_pol_sym[i]==symN && g_pol_tf[i]==timeframe)
|
| return g_pol_p[i];
|
| // 2) Symbol aggregate across TFs: strat+symbol, tf=-1
|
| for(int i=0;i<ArraySize(g_pol_strat);++i)
|
| if(g_pol_strat[i]==strategy && g_pol_sym[i]==symN && g_pol_tf[i]==-1)
|
| return g_pol_p[i];
|
| // 3) Strategy aggregate across symbols/TFs: strat only, symbol="*", tf=-1
|
| for(int i=0;i<ArraySize(g_pol_strat);++i)
|
| if(g_pol_strat[i]==strategy && g_pol_sym[i]=="*" && g_pol_tf[i]==-1)
|
| return g_pol_p[i];
|
| return -1.0;
|
| }
|
|
|
| double GetPolicyScaleSL(const string strategy, const string symbol, const int timeframe)
|
| {
|
| string symN = NormalizeSymbol(symbol);
|
| for(int i=0;i<ArraySize(g_pol_strat);++i)
|
| if(g_pol_strat[i]==strategy && g_pol_sym[i]==symN && g_pol_tf[i]==timeframe)
|
| return (g_pol_sl[i]>0? g_pol_sl[i] : 1.0);
|
| for(int i=0;i<ArraySize(g_pol_strat);++i)
|
| if(g_pol_strat[i]==strategy && g_pol_sym[i]==symN && g_pol_tf[i]==-1)
|
| return (g_pol_sl[i]>0? g_pol_sl[i] : 1.0);
|
| for(int i=0;i<ArraySize(g_pol_strat);++i)
|
| if(g_pol_strat[i]==strategy && g_pol_sym[i]=="*" && g_pol_tf[i]==-1)
|
| return (g_pol_sl[i]>0? g_pol_sl[i] : 1.0);
|
| return 1.0;
|
| }
|
|
|
| double GetPolicyScaleTP(const string strategy, const string symbol, const int timeframe)
|
| {
|
| string symN = NormalizeSymbol(symbol);
|
| for(int i=0;i<ArraySize(g_pol_strat);++i)
|
| if(g_pol_strat[i]==strategy && g_pol_sym[i]==symN && g_pol_tf[i]==timeframe)
|
| return (g_pol_tp[i]>0? g_pol_tp[i] : 1.0);
|
| for(int i=0;i<ArraySize(g_pol_strat);++i)
|
| if(g_pol_strat[i]==strategy && g_pol_sym[i]==symN && g_pol_tf[i]==-1)
|
| return (g_pol_tp[i]>0? g_pol_tp[i] : 1.0);
|
| for(int i=0;i<ArraySize(g_pol_strat);++i)
|
| if(g_pol_strat[i]==strategy && g_pol_sym[i]=="*" && g_pol_tf[i]==-1)
|
| return (g_pol_tp[i]>0? g_pol_tp[i] : 1.0);
|
| return 1.0;
|
| }
|
|
|
| double GetPolicyScaleTrail(const string strategy, const string symbol, const int timeframe)
|
| {
|
| string symN = NormalizeSymbol(symbol);
|
| for(int i=0;i<ArraySize(g_pol_strat);++i)
|
| if(g_pol_strat[i]==strategy && g_pol_sym[i]==symN && g_pol_tf[i]==timeframe)
|
| return (g_pol_trail[i]>0? g_pol_trail[i] : 1.0);
|
| for(int i=0;i<ArraySize(g_pol_strat);++i)
|
| if(g_pol_strat[i]==strategy && g_pol_sym[i]==symN && g_pol_tf[i]==-1)
|
| return (g_pol_trail[i]>0? g_pol_trail[i] : 1.0);
|
| for(int i=0;i<ArraySize(g_pol_strat);++i)
|
| if(g_pol_strat[i]==strategy && g_pol_sym[i]=="*" && g_pol_tf[i]==-1)
|
| return (g_pol_trail[i]>0? g_pol_trail[i] : 1.0);
|
| return 1.0;
|
| }
|
|
|
| // Apply policy-driven scaling to SL/TP/trailing of an order. Entry price approximated.
|
| void ApplyPolicyScaling(TradeOrder &order, const string symbol, const int timeframe, const double ppol)
|
| {
|
| if(!UsePolicyGating || !g_policy_loaded) return;
|
| // fetch scales
|
| double sls = GetPolicyScaleSL(order.strategy_name, symbol, timeframe);
|
| double tps = GetPolicyScaleTP(order.strategy_name, symbol, timeframe);
|
| double trs = GetPolicyScaleTrail(order.strategy_name, symbol, timeframe);
|
|
|
| // determine entry reference
|
| double entry = 0.0;
|
| if(order.price>0.0)
|
| entry = order.price; // pending
|
| else
|
| entry = (order.action==ACTION_BUY? SymbolInfoDouble(symbol, SYMBOL_ASK) : SymbolInfoDouble(symbol, SYMBOL_BID));
|
|
|
| // Adjust SL
|
| if(order.stop_loss>0.0 && entry>0.0 && sls>0.0 && sls!=1.0)
|
| {
|
| double d = MathAbs(entry - order.stop_loss);
|
| double nd = d * sls;
|
| if(order.action==ACTION_BUY)
|
| order.stop_loss = entry - nd;
|
| else if(order.action==ACTION_SELL)
|
| order.stop_loss = entry + nd;
|
| }
|
| // Adjust TP
|
| if(order.take_profit>0.0 && entry>0.0 && tps>0.0 && tps!=1.0)
|
| {
|
| double d = MathAbs(order.take_profit - entry);
|
| double nd = d * tps;
|
| if(order.action==ACTION_BUY)
|
| order.take_profit = entry + nd;
|
| else if(order.action==ACTION_SELL)
|
| order.take_profit = entry - nd;
|
| }
|
| // Adjust trailing
|
| if(order.trailing_enabled && trs>0.0 && trs!=1.0)
|
| {
|
| if(order.trailing_type==TRAIL_ATR)
|
| order.atr_multiplier *= trs;
|
| else if(order.trailing_type==TRAIL_FIXED_POINTS)
|
| {
|
| order.trail_distance_points *= trs;
|
| order.trail_step_points *= trs;
|
| }
|
| }
|
| if(ShouldLog(LOG_DEBUG))
|
| PrintFormat("[POLICY] scales applied %s/%s tf=%d p=%.3f sl=%.2f tp=%.2f tr=%.2f",
|
| order.strategy_name, symbol, timeframe, ppol, sls, tps, trs);
|
| }
|
|
|
| // --- Exploration counters (weekly/daily) persistence
|
| int WeekMondayId(datetime t)
|
| {
|
| MqlDateTime dt; TimeToStruct(t, dt);
|
| // MT5: day_of_week 0=Sunday, 1=Monday, ... 6=Saturday
|
| int dow = dt.day_of_week;
|
| int delta_days = (dow==0 ? 6 : (dow-1));
|
| datetime monday = t - (delta_days * 86400);
|
| MqlDateTime md; TimeToStruct(monday, md);
|
| return (md.year*10000 + md.mon*100 + md.day);
|
| }
|
|
|
| // P0-3: Checksum calculation for exploration counters
|
| uint CalculateExploreChecksum(string key, int week, int count)
|
| {
|
| string data = key + "|" + IntegerToString(week) + "|" + IntegerToString(count);
|
| uint checksum = 0;
|
| for(int i = 0; i < StringLen(data); i++)
|
| {
|
| checksum = ((checksum << 5) + checksum) + (uchar)StringGetCharacter(data, i);
|
| }
|
| return checksum;
|
| }
|
|
|
| string ExploreCountsPath()
|
| {
|
| return "DualEA\\explore_counts.csv"; // FILE_COMMON
|
| }
|
|
|
| void SaveExploreCounts()
|
| {
|
| string path = ExploreCountsPath();
|
| int h = FileOpen(path, FILE_WRITE|FILE_CSV|FILE_ANSI|FILE_COMMON, ',');
|
| if(h==INVALID_HANDLE){ PrintFormat("Explore persist: cannot open %s for write. Err=%d", path, GetLastError()); return; }
|
| // P0-3: Add checksum column to header
|
| FileWrite(h, "key", "week_monday_yyyymmdd", "count", "checksum");
|
| for(int i=0;i<ArraySize(g_exp_keys);++i)
|
| {
|
| // P0-3: Calculate and write checksum
|
| uint checksum = CalculateExploreChecksum(g_exp_keys[i], g_exp_weeks[i], g_exp_counts[i]);
|
| FileWrite(h, g_exp_keys[i], IntegerToString(g_exp_weeks[i]), IntegerToString(g_exp_counts[i]), IntegerToString((long)checksum));
|
| }
|
| FileClose(h);
|
| // P0-3: Verify file was written correctly
|
| if(!VerifyExploreCountsFile())
|
| {
|
| Print("[P0-3] WARNING: Explore counts file verification failed - data may be corrupted");
|
| }
|
| }
|
|
|
| // P0-3: Verify the saved file by reading it back and checking consistency
|
| bool VerifyExploreCountsFile()
|
| {
|
| string path = ExploreCountsPath();
|
| int h = FileOpen(path, FILE_READ|FILE_CSV|FILE_ANSI|FILE_COMMON, ',');
|
| if(h==INVALID_HANDLE) return false;
|
|
|
| bool first = true;
|
| int row = 0;
|
| bool valid = true;
|
|
|
| while(!FileIsEnding(h) && valid)
|
| {
|
| string k = FileReadString(h);
|
| if(k=="" && FileIsEnding(h)) break;
|
|
|
| string wk_s = FileReadString(h);
|
| string cnt_s = FileReadString(h);
|
| string checksum_s = FileReadString(h); // P0-3: Read checksum
|
|
|
| if(first) { first = false; continue; } // Skip header
|
|
|
| // P0-3: Verify checksum
|
| if(StringLen(checksum_s) > 0)
|
| {
|
| uint expected = CalculateExploreChecksum(k, (int)StringToInteger(wk_s), (int)StringToInteger(cnt_s));
|
| uint actual = (uint)StringToInteger(checksum_s);
|
| if(expected != actual)
|
| {
|
| PrintFormat("[P0-3] Checksum mismatch at row %d: key=%s", row, k);
|
| valid = false;
|
| }
|
| }
|
| row++;
|
| }
|
| FileClose(h);
|
| return valid;
|
| }
|
|
|
| bool LoadExploreCounts()
|
| {
|
| ArrayResize(g_exp_keys,0); ArrayResize(g_exp_weeks,0); ArrayResize(g_exp_counts,0);
|
| string path = ExploreCountsPath();
|
| int h = FileOpen(path, FILE_READ|FILE_CSV|FILE_ANSI|FILE_COMMON, ',');
|
| if(h==INVALID_HANDLE) { if(ShouldLog(LOG_INFO)) PrintFormat("Explore persist: no prior %s (ok)", path); return true; }
|
| bool first=true;
|
| int corrupted_rows = 0;
|
| int total_rows = 0;
|
|
|
| while(!FileIsEnding(h))
|
| {
|
| string k = FileReadString(h); if(k=="" && FileIsEnding(h)) break;
|
| string wk_s = FileReadString(h);
|
| string cnt_s = FileReadString(h);
|
| string checksum_s = FileReadString(h); // P0-3: Read checksum column
|
|
|
| // skip header if present
|
| if(first && (StringFind(k, "key", 0)==0)) { first=false; continue; }
|
| first=false;
|
| total_rows++;
|
|
|
| // P0-3: Validate checksum before loading
|
| bool valid = true;
|
| if(StringLen(checksum_s) > 0)
|
| {
|
| uint expected = CalculateExploreChecksum(k, (int)StringToInteger(wk_s), (int)StringToInteger(cnt_s));
|
| uint actual = (uint)StringToInteger(checksum_s);
|
| if(expected != actual)
|
| {
|
| PrintFormat("[P0-3] Corrupted row detected and skipped: key=%s (checksum mismatch)", k);
|
| corrupted_rows++;
|
| valid = false;
|
| }
|
| }
|
|
|
| if(valid)
|
| {
|
| int n = ArraySize(g_exp_keys);
|
| ArrayResize(g_exp_keys,n+1); ArrayResize(g_exp_weeks,n+1); ArrayResize(g_exp_counts,n+1);
|
| g_exp_keys[n]=k; g_exp_weeks[n]=(int)StringToInteger(wk_s); g_exp_counts[n]=(int)StringToInteger(cnt_s);
|
| }
|
| }
|
| FileClose(h);
|
|
|
| // P0-3: Log corruption statistics
|
| if(corrupted_rows > 0)
|
| {
|
| PrintFormat("[P0-3] LoadExploreCounts: Recovered from %d corrupted rows out of %d total", corrupted_rows, total_rows);
|
| }
|
|
|
| return true;
|
| }
|
|
|
| int DayId(datetime t)
|
| {
|
| MqlDateTime dt; TimeToStruct(t, dt);
|
| return (dt.year*10000 + dt.mon*100 + dt.day);
|
| }
|
|
|
| string ExploreDayCountsPath()
|
| {
|
| return "DualEA\\explore_counts_day.csv";
|
| }
|
|
|
| void SaveExploreCountsDay()
|
| {
|
| string path = ExploreDayCountsPath();
|
| int h = FileOpen(path, FILE_WRITE|FILE_CSV|FILE_ANSI|FILE_COMMON, ',');
|
| if(h==INVALID_HANDLE){ PrintFormat("Explore persist(day): cannot open %s for write. Err=%d", path, GetLastError()); return; }
|
| FileWrite(h, "key,day_yyyymmdd,count");
|
| for(int i=0;i<ArraySize(g_exp_day_keys);++i)
|
| FileWrite(h, g_exp_day_keys[i], IntegerToString(g_exp_day_days[i]), IntegerToString(g_exp_day_counts[i]));
|
| FileClose(h);
|
| }
|
|
|
| bool LoadExploreCountsDay()
|
| {
|
| ArrayResize(g_exp_day_keys,0); ArrayResize(g_exp_day_days,0); ArrayResize(g_exp_day_counts,0);
|
| string path = ExploreDayCountsPath();
|
| int h = FileOpen(path, FILE_READ|FILE_CSV|FILE_ANSI|FILE_COMMON, ',');
|
| if(h==INVALID_HANDLE) { if(ShouldLog(LOG_INFO)) PrintFormat("Explore persist(day): no prior %s (ok)", path); return true; }
|
| bool first=true;
|
| while(!FileIsEnding(h))
|
| {
|
| string k = FileReadString(h); if(k=="" && FileIsEnding(h)) break;
|
| string d = FileReadString(h);
|
| string c = FileReadString(h);
|
| if(first && (StringFind(k, "key", 0)==0)) { first=false; continue; }
|
| first=false;
|
| int n = ArraySize(g_exp_day_keys);
|
| ArrayResize(g_exp_day_keys,n+1); ArrayResize(g_exp_day_days,n+1); ArrayResize(g_exp_day_counts,n+1);
|
| g_exp_day_keys[n]=k; g_exp_day_days[n]=(int)StringToInteger(d); g_exp_day_counts[n]=(int)StringToInteger(c);
|
| }
|
| FileClose(h);
|
| return true;
|
| }
|
|
|
| string SliceKey(const string strategy, const string symbol, const int timeframe)
|
| {
|
| return strategy + "|" + symbol + "|" + IntegerToString(timeframe);
|
| }
|
|
|
| int GetExploreCount(const string key)
|
| {
|
| int wk = WeekMondayId(TimeCurrent());
|
| for(int i=0;i<ArraySize(g_exp_keys);++i)
|
| if(g_exp_keys[i]==key && g_exp_weeks[i]==wk) return g_exp_counts[i];
|
| return 0;
|
| }
|
|
|
| int GetExploreCountDay(const string key)
|
| {
|
| int d = DayId(TimeCurrent());
|
| for(int i=0;i<ArraySize(g_exp_day_keys);++i)
|
| if(g_exp_day_keys[i]==key && g_exp_day_days[i]==d) return g_exp_day_counts[i];
|
| return 0;
|
| }
|
|
|
| void IncExploreCount(const string key)
|
| {
|
| int wk = WeekMondayId(TimeCurrent());
|
| for(int i=0;i<ArraySize(g_exp_keys);++i)
|
| if(g_exp_keys[i]==key && g_exp_weeks[i]==wk)
|
| {
|
| g_exp_counts[i] = g_exp_counts[i] + 1;
|
| SaveExploreCounts();
|
| // also bump daily
|
| int dd = DayId(TimeCurrent()); bool day_found=false;
|
| for(int j=0;j<ArraySize(g_exp_day_keys);++j)
|
| if(g_exp_day_keys[j]==key && g_exp_day_days[j]==dd){ g_exp_day_counts[j]+=1; day_found=true; break; }
|
| if(!day_found)
|
| {
|
| int m = ArraySize(g_exp_day_keys);
|
| ArrayResize(g_exp_day_keys,m+1); ArrayResize(g_exp_day_days,m+1); ArrayResize(g_exp_day_counts,m+1);
|
| g_exp_day_keys[m]=key; g_exp_day_days[m]=dd; g_exp_day_counts[m]=1;
|
| }
|
| SaveExploreCountsDay();
|
| return;
|
| }
|
| int n = ArraySize(g_exp_keys);
|
| ArrayResize(g_exp_keys, n+1);
|
| ArrayResize(g_exp_weeks, n+1);
|
| ArrayResize(g_exp_counts, n+1);
|
| g_exp_keys[n] = key;
|
| g_exp_weeks[n] = wk;
|
| g_exp_counts[n] = 1;
|
| SaveExploreCounts();
|
| // init daily row
|
| int dd = DayId(TimeCurrent());
|
| int m = ArraySize(g_exp_day_keys);
|
| ArrayResize(g_exp_day_keys,m+1); ArrayResize(g_exp_day_days,m+1); ArrayResize(g_exp_day_counts,m+1);
|
| g_exp_day_keys[m]=key; g_exp_day_days[m]=dd; g_exp_day_counts[m]=1;
|
| SaveExploreCountsDay();
|
| }
|
|
|
| // Check if insights has an existing slice for exact strategy/symbol/timeframe
|
| bool HasSlice(const string strategy, const string symbol, const int timeframe)
|
| {
|
| for(int i=0;i<ArraySize(g_gate_strat);++i)
|
| if(g_gate_strat[i]==strategy && g_gate_sym[i]==symbol && g_gate_tf[i]==timeframe && g_gate_cnt[i]>0)
|
| return true;
|
| return false;
|
| }
|
|
|
| bool Insights_Load()
|
| {
|
| int loaded = Insights_Load_Default(
|
| g_gate_strat,
|
| g_gate_sym,
|
| g_gate_tf,
|
| g_gate_cnt,
|
| g_gate_wr,
|
| g_gate_avgR,
|
| g_gate_pf,
|
| g_gate_dd
|
| );
|
| if(loaded<0)
|
| return false;
|
| if(ShouldLog(LOG_INFO)) PrintFormat("Insights gating: loaded %d slices", ArraySize(g_gate_strat));
|
| return ArraySize(g_gate_strat)>0;
|
| }
|
|
|
| bool Insights_Allow(const string strategy, const string symbol, const int timeframe, string &reason, const bool shadow=false, const bool no_side_effects=false)
|
| {
|
| reason = "";
|
| // Global bypass for data collection
|
| if(NoConstraintsMode && !shadow)
|
| {
|
| reason = "no_constraints";
|
| return true;
|
| }
|
| // Default policy fallback when no policy is loaded
|
| if(DefaultPolicyFallback && UsePolicyGating && FallbackWhenNoPolicy && !g_policy_loaded)
|
| {
|
| bool demo_ok0 = (!FallbackDemoOnly) || (AccountInfoInteger(ACCOUNT_TRADE_MODE)==ACCOUNT_TRADE_MODE_DEMO);
|
| if(demo_ok0)
|
| {
|
| reason = "fallback_no_policy";
|
| return true;
|
| }
|
| }
|
| // Default policy fallback: allow neutral trading when policy slice is missing
|
| if(DefaultPolicyFallback && UsePolicyGating && g_policy_loaded)
|
| {
|
| double pchk = GetPolicyProb(strategy, symbol, timeframe);
|
| if(pchk < 0.0)
|
| {
|
| bool demo_ok = (!FallbackDemoOnly) || (AccountInfoInteger(ACCOUNT_TRADE_MODE)==ACCOUNT_TRADE_MODE_DEMO);
|
| if(demo_ok)
|
| {
|
| reason = "fallback_policy_miss";
|
| return true;
|
| }
|
| }
|
| }
|
| // Find matching slice
|
| for(int i=0;i<ArraySize(g_gate_strat);++i)
|
| if(g_gate_strat[i]==strategy && g_gate_sym[i]==symbol && g_gate_tf[i]==timeframe)
|
| {
|
| if(g_gate_cnt[i] < GateMinTrades)
|
| {
|
| reason = "min_trades"; return false;
|
| }
|
| if(g_gate_wr[i] < GateMinWinRate)
|
| {
|
| reason = "win_rate"; return false;
|
| }
|
| if(g_gate_avgR[i]< GateMinExpectancyR)
|
| {
|
| reason = "expectancy"; return false;
|
| }
|
| if(g_gate_dd[i] > GateMaxDrawdownR)
|
| {
|
| reason = "drawdown"; return false;
|
| }
|
| if(g_gate_pf[i] < GateMinProfitFactor)
|
| {
|
| reason = "profit_factor"; return false;
|
| }
|
| // Policy check for exact slice
|
| if(UsePolicyGating && g_policy_loaded)
|
| {
|
| double ppol = GetPolicyProb(strategy, symbol, timeframe);
|
| if(ppol>=0.0 && ppol < g_policy_min_conf)
|
| { reason = "policy_min_conf"; return false; }
|
| }
|
| return true;
|
| }
|
| // If no slices loaded at all, allow cold-start to bootstrap data
|
| if(ArraySize(g_gate_strat)==0)
|
| { reason = "cold_start"; return true; }
|
|
|
| // Fallback 1: aggregate by strategy+symbol across all timeframes
|
| int total=0; double wins=0.0; double sumR=0.0; double pf_sum=0.0; int pf_cnt=0; double worst_dd=0.0;
|
| for(int j=0;j<ArraySize(g_gate_strat);++j)
|
| if(g_gate_strat[j]==strategy && g_gate_sym[j]==symbol)
|
| {
|
| total += g_gate_cnt[j];
|
| wins += g_gate_wr[j]*g_gate_cnt[j];
|
| sumR += g_gate_avgR[j]*g_gate_cnt[j];
|
| pf_sum+= g_gate_pf[j]; pf_cnt++;
|
| if(g_gate_dd[j]>worst_dd) worst_dd=g_gate_dd[j];
|
| }
|
| if(total>0)
|
| {
|
| double wr = (total>0? wins/total : 0.0);
|
| double avgR = (total>0? sumR/total : 0.0);
|
| double pf = (pf_cnt>0? pf_sum/pf_cnt : 0.0);
|
| double dd = worst_dd;
|
| if(total < GateMinTrades)
|
| {
|
| reason = "min_trades_agg_sym"; return false;
|
| }
|
| if(wr < GateMinWinRate)
|
| {
|
| reason = "win_rate_agg_sym"; return false;
|
| }
|
| if(avgR < GateMinExpectancyR)
|
| {
|
| reason = "expectancy_agg_sym"; return false;
|
| }
|
| if(dd > GateMaxDrawdownR)
|
| {
|
| reason = "drawdown_agg_sym"; return false;
|
| }
|
| if(pf < GateMinProfitFactor)
|
| {
|
| reason = "profit_factor_agg_sym"; return false;
|
| }
|
| // Policy check for aggregated slice (if available)
|
| if(UsePolicyGating && g_policy_loaded)
|
| {
|
| double ppol = GetPolicyProb(strategy, symbol, timeframe);
|
| if(ppol>=0.0 && ppol < g_policy_min_conf)
|
| { reason = "policy_min_conf_agg_sym"; return false; }
|
| }
|
| reason = "agg_sym_ok"; return true;
|
| }
|
|
|
| // Fallback 2: aggregate by strategy across all symbols/timeframes
|
| total=0; wins=0.0; sumR=0.0; pf_sum=0.0; pf_cnt=0; worst_dd=0.0;
|
| for(int j=0;j<ArraySize(g_gate_strat);++j)
|
| if(g_gate_strat[j]==strategy)
|
| {
|
| total += g_gate_cnt[j];
|
| wins += g_gate_wr[j]*g_gate_cnt[j];
|
| sumR += g_gate_avgR[j]*g_gate_cnt[j];
|
| pf_sum+= g_gate_pf[j]; pf_cnt++;
|
| if(g_gate_dd[j]>worst_dd) worst_dd=g_gate_dd[j];
|
| }
|
| if(total>0)
|
| {
|
| double wr = (total>0? wins/total : 0.0);
|
| double avgR = (total>0? sumR/total : 0.0);
|
| double pf = (pf_cnt>0? pf_sum/pf_cnt : 0.0);
|
| double dd = worst_dd;
|
| if(total < GateMinTrades)
|
| { reason = "min_trades_agg_strat"; return false; }
|
| if(wr < GateMinWinRate)
|
| { reason = "win_rate_agg_strat"; return false; }
|
| if(avgR < GateMinExpectancyR)
|
| { reason = "expectancy_agg_strat"; return false; }
|
| if(dd > GateMaxDrawdownR)
|
| { reason = "drawdown_agg_strat"; return false; }
|
| if(pf < GateMinProfitFactor)
|
| { reason = "profit_factor_agg_strat"; return false; }
|
| // Policy check for aggregated strategy (if available)
|
| if(UsePolicyGating && g_policy_loaded)
|
| {
|
| double ppol = GetPolicyProb(strategy, symbol, timeframe);
|
| if(ppol>=0.0 && ppol < g_policy_min_conf)
|
| { reason = "policy_min_conf_agg_strat"; return false; }
|
| }
|
| reason = "agg_strat_ok"; return true;
|
| }
|
|
|
| // If slice not found and gating disabled, allow
|
| if(!UseInsightsGating) return true;
|
| // Else, no exact slice exists. Exploration Mode: allow only when no slice, with day+week caps
|
| if(ExploreOnNoSlice)
|
| {
|
| // Strict policy: allow exploration ONLY when no slice exists
|
| bool has_slice = HasSlice(strategy, symbol, timeframe);
|
| if(has_slice)
|
| {
|
| reason = "slice_exists";
|
| return false;
|
| }
|
| string key = SliceKey(strategy, symbol, timeframe);
|
| int used_w = GetExploreCount(key);
|
| int used_d = GetExploreCountDay(key);
|
| if(ExploreMaxPerSlicePerDay > 0 && used_d >= ExploreMaxPerSlicePerDay)
|
| { reason = "explore_cap_day"; return false; }
|
| if(ExploreMaxPerSlice > 0 && used_w >= ExploreMaxPerSlice)
|
| { reason = "explore_cap_week"; return false; }
|
| if(!no_side_effects)
|
| g_explore_pending_key = key;
|
| reason = "explore_allow";
|
| return true;
|
| }
|
| reason = "no_slice";
|
| return false;
|
| }
|
|
|
| // --- Evaluation and execution orchestrator
|
| void EvaluateAndMaybeExecute(const TradeOrder &order)
|
| {
|
| if(g_eval_busy) return; // prevent reentry
|
| g_eval_busy = true;
|
| ulong t0 = GetTickCount();
|
|
|
| EnsureSessionRollover();
|
| EnsureTelemetry();
|
| EnsurePM();
|
| EnsureSessionManager();
|
| EnsureCorrelationManager();
|
| EnsureVolatilitySizer();
|
|
|
| // Shadow gates (diagnostic only)
|
| if(NoConstraintsMode && TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| {
|
| int lat=0;
|
| string r;
|
| r=""; bool s_ok = SpreadAllowed(r); lat=(int)(GetTickCount()-t0);
|
| (*g_telemetry).LogGatingShadow(order.strategy_name, _Symbol, (int)_Period, "spread", s_ok, (s_ok?"ok":r)+";p6_latency="+IntegerToString(lat)+"ms", true);
|
| r=""; bool ses_ok = SessionAllowed(r); lat=(int)(GetTickCount()-t0);
|
| (*g_telemetry).LogGatingShadow(order.strategy_name, _Symbol, (int)_Period, "session", ses_ok, (ses_ok?"ok":r)+";p6_latency="+IntegerToString(lat)+"ms", true);
|
| r=""; bool r_ok = RiskAllowed(r); lat=(int)(GetTickCount()-t0);
|
| (*g_telemetry).LogGatingShadow(order.strategy_name, _Symbol, (int)_Period, "risk", r_ok, (r_ok?"ok":r)+";p6_latency="+IntegerToString(lat)+"ms", true);
|
| r=""; bool news_ok = NewsAllowed(r); lat=(int)(GetTickCount()-t0);
|
| (*g_telemetry).LogGatingShadow(order.strategy_name, _Symbol, (int)_Period, "news", news_ok, (news_ok?"ok":r)+";p6_latency="+IntegerToString(lat)+"ms", true);
|
| r=""; bool promo_ok = PromotionAllowed(r); lat=(int)(GetTickCount()-t0);
|
| (*g_telemetry).LogGatingShadow(order.strategy_name, _Symbol, (int)_Period, "promotion", promo_ok, (promo_ok?"ok":r)+";p6_latency="+IntegerToString(lat)+"ms", true);
|
| r=""; bool regime_ok = RegimeAllowed(r); lat=(int)(GetTickCount()-t0);
|
| (*g_telemetry).LogGatingShadow(order.strategy_name, _Symbol, (int)_Period, "regime", regime_ok, (regime_ok?"ok":r)+";p6_latency="+IntegerToString(lat)+"ms", true);
|
| r=""; bool cool_ok = CircuitCooldownAllowed(r); lat=(int)(GetTickCount()-t0);
|
| (*g_telemetry).LogGatingShadow(order.strategy_name, _Symbol, (int)_Period, "circuit_cooldown", cool_ok, (cool_ok?"ok":r)+";p6_latency="+IntegerToString(lat)+"ms", true);
|
| // Shadow insights gating (no side effects, ignore NoConstraints bypass)
|
| if(UseInsightsGating)
|
| {
|
| string rshadow=""; bool allow_shadow = Insights_Allow(order.strategy_name, _Symbol, (int)_Period, rshadow, true, true);
|
| (*g_telemetry).LogGatingShadow(order.strategy_name, _Symbol, (int)_Period, "insights", allow_shadow, rshadow, true);
|
| }
|
| }
|
|
|
| // Early gates (fail-closed unless NoConstraintsMode)
|
| if(!NoConstraintsMode)
|
| {
|
| // Spread gate
|
| string r_spread_early=""; if(!SpreadAllowed(r_spread_early))
|
| {
|
| if(ShouldLog(LOG_INFO)) PrintFormat("GATE: blocked %s on %s/%s reason=%s (spread)", order.strategy_name, _Symbol, EnumToString(_Period), r_spread_early);
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogGating(_Symbol, (int)_Period, order.strategy_name, "spread", false, r_spread_early+";p6_latency="+IntegerToString((int)(GetTickCount()-t0))+"ms");
|
| g_eval_busy = false; return;
|
| }
|
| // Session gate
|
| string r_sess_early=""; if(!SessionAllowed(r_sess_early))
|
| {
|
| if(ShouldLog(LOG_INFO)) PrintFormat("GATE: blocked %s on %s/%s reason=%s (session)", order.strategy_name, _Symbol, EnumToString(_Period), r_sess_early);
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogGating(_Symbol, (int)_Period, order.strategy_name, "session", false, r_sess_early+";p6_latency="+IntegerToString((int)(GetTickCount()-t0))+"ms");
|
| g_eval_busy = false; return;
|
| }
|
| string rr=""; if(!RiskAllowed(rr))
|
| {
|
| if(ShouldLog(LOG_INFO)) PrintFormat("GATE: blocked %s on %s/%s reason=%s (eq=%.2f base=%.2f high=%.2f ml=%.2f)", order.strategy_name, _Symbol, EnumToString(_Period), rr, AccountInfoDouble(ACCOUNT_EQUITY), g_session_equity_start, g_equity_highwater, AccountInfoDouble(ACCOUNT_MARGIN_LEVEL));
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogGating(_Symbol, (int)_Period, order.strategy_name, "risk", false, rr+";p6_latency="+IntegerToString((int)(GetTickCount()-t0))+"ms");
|
| g_eval_busy = false; return;
|
| }
|
| string r_news_early=""; if(!NewsAllowed(r_news_early))
|
| {
|
| if(ShouldLog(LOG_INFO)) PrintFormat("GATE: blocked %s on %s/%s reason=%s (news)", order.strategy_name, _Symbol, EnumToString(_Period), r_news_early);
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogGating(_Symbol, (int)_Period, order.strategy_name, "news", false, r_news_early+";p6_latency="+IntegerToString((int)(GetTickCount()-t0))+"ms");
|
| g_eval_busy = false; return;
|
| }
|
| string r_promo_early=""; if(!PromotionAllowed(r_promo_early))
|
| {
|
| if(ShouldLog(LOG_INFO)) PrintFormat("GATE: blocked %s on %s/%s reason=%s (promotion)", order.strategy_name, _Symbol, EnumToString(_Period), r_promo_early);
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogGating(_Symbol, (int)_Period, order.strategy_name, "promotion", false, r_promo_early+";p6_latency="+IntegerToString((int)(GetTickCount()-t0))+"ms");
|
| g_eval_busy = false; return;
|
| }
|
| string r_regime_early=""; if(!RegimeAllowed(r_regime_early))
|
| {
|
| if(ShouldLog(LOG_INFO)) PrintFormat("GATE: blocked %s on %s/%s reason=%s (regime)", order.strategy_name, _Symbol, EnumToString(_Period), r_regime_early);
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogGating(_Symbol, (int)_Period, order.strategy_name, "regime", false, r_regime_early+";p6_latency="+IntegerToString((int)(GetTickCount()-t0))+"ms");
|
| g_eval_busy = false; return;
|
| }
|
| string r_cool_early=""; if(!CircuitCooldownAllowed(r_cool_early))
|
| {
|
| if(ShouldLog(LOG_INFO)) PrintFormat("GATE: blocked %s on %s/%s reason=%s (circuit_cooldown)", order.strategy_name, _Symbol, EnumToString(_Period), r_cool_early);
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogGating(_Symbol, (int)_Period, order.strategy_name, "circuit_cooldown", false, r_cool_early+";p6_latency="+IntegerToString((int)(GetTickCount()-t0))+"ms");
|
| g_eval_busy = false; return;
|
| }
|
| }
|
|
|
| // Insights/Policy gate (fail-closed unless NoConstraintsMode)
|
| if(!NoConstraintsMode)
|
| {
|
| // Check IncrementalInsightEngine for auto-promoted strategies
|
| if(CheckPointer(g_insight_engine) != POINTER_INVALID)
|
| {
|
| if(!g_insight_engine.IsStrategyApproved(order.strategy_name, _Symbol, (int)_Period))
|
| {
|
| // Strategy not yet promoted - block trade
|
| if(ShouldLog(LOG_INFO)) PrintFormat("[INSIGHTS] blocked %s on %s/%s reason=not_approved",
|
| order.strategy_name, _Symbol, EnumToString(_Period));
|
| g_eval_busy=false; return;
|
| }
|
| }
|
|
|
| string gate_reason="";
|
| bool ins_ok = Insights_Allow(order.strategy_name, _Symbol, (int)_Period, gate_reason);
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogGating(_Symbol, (int)_Period, order.strategy_name, "insights", ins_ok, (ins_ok?"ok":gate_reason)+";p6_latency="+IntegerToString((int)(GetTickCount()-t0))+"ms");
|
| if(!ins_ok)
|
| {
|
| if(ShouldLog(LOG_INFO)) PrintFormat("[INSIGHTS] blocked %s on %s/%s reason=%s", order.strategy_name, _Symbol, EnumToString(_Period), gate_reason);
|
| g_eval_busy=false; return;
|
| }
|
| }
|
|
|
| // Correlation gate
|
| string r_corr=""; bool corr_ok = P5_CorrPruneAllow(order, r_corr);
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogGating(_Symbol, (int)_Period, order.strategy_name, "corr", corr_ok, (corr_ok?"ok":r_corr)+";p6_latency="+IntegerToString((int)(GetTickCount()-t0))+"ms");
|
| if(!corr_ok) { if(ShouldLog(LOG_INFO)) PrintFormat("[CORR] blocked %s on %s/%s reason=%s", order.strategy_name, _Symbol, EnumToString(_Period), r_corr); g_eval_busy=false; return; }
|
|
|
| // Final RISK4 gates just before execution
|
| string r_rk_exec="";
|
| if(!RiskAllowed(r_rk_exec))
|
| {
|
| if(ShouldLog(LOG_INFO)) PrintFormat("[RISK4] blocked %s on %s/%s reason=%s", order.strategy_name, _Symbol, EnumToString(_Period), r_rk_exec);
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogGating(_Symbol, (int)_Period, order.strategy_name, "risk4", false, r_rk_exec+";p6_latency="+IntegerToString((int)(GetTickCount()-t0))+"ms");
|
| g_eval_busy = false; return;
|
| }
|
| string r_spread_exec="";
|
| if(!SpreadAllowed(r_spread_exec))
|
| {
|
| if(ShouldLog(LOG_INFO)) PrintFormat("[RISK4] blocked %s on %s/%s reason=%s (spread)", order.strategy_name, _Symbol, EnumToString(_Period), r_spread_exec);
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogGating(_Symbol, (int)_Period, order.strategy_name, "risk4_spread", false, r_spread_exec+";p6_latency="+IntegerToString((int)(GetTickCount()-t0))+"ms");
|
| g_eval_busy = false; return;
|
| }
|
| string r_sess_exec="";
|
| if(!SessionAllowed(r_sess_exec))
|
| {
|
| if(ShouldLog(LOG_INFO)) PrintFormat("[RISK4] blocked %s on %s/%s reason=%s (session)", order.strategy_name, _Symbol, EnumToString(_Period), r_sess_exec);
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogGating(_Symbol, (int)_Period, order.strategy_name, "risk4_session", false, r_sess_exec+";p6_latency="+IntegerToString((int)(GetTickCount()-t0))+"ms");
|
| g_eval_busy = false; return;
|
| }
|
| string r_news_exec="";
|
| if(!NewsAllowed(r_news_exec))
|
| {
|
| if(ShouldLog(LOG_INFO)) PrintFormat("[RISK4] blocked %s on %s/%s reason=%s (news)", order.strategy_name, _Symbol, EnumToString(_Period), r_news_exec);
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogGating(_Symbol, (int)_Period, order.strategy_name, "risk4_news", false, r_news_exec+";p6_latency="+IntegerToString((int)(GetTickCount()-t0))+"ms");
|
| g_eval_busy = false; return;
|
| }
|
| string r_promo_exec="";
|
| if(!PromotionAllowed(r_promo_exec))
|
| {
|
| if(ShouldLog(LOG_INFO)) PrintFormat("[RISK4] blocked %s on %s/%s reason=%s (promotion)", order.strategy_name, _Symbol, EnumToString(_Period), r_promo_exec);
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogGating(_Symbol, (int)_Period, order.strategy_name, "risk4_promotion", false, r_promo_exec+";p6_latency="+IntegerToString((int)(GetTickCount()-t0))+"ms");
|
| g_eval_busy = false; return;
|
| }
|
| string r_regime_exec="";
|
| if(!RegimeAllowed(r_regime_exec))
|
| {
|
| if(ShouldLog(LOG_INFO)) PrintFormat("[RISK4] blocked %s on %s/%s reason=%s (regime)", order.strategy_name, _Symbol, EnumToString(_Period), r_regime_exec);
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogGating(_Symbol, (int)_Period, order.strategy_name, "risk4_regime", false, r_regime_exec+";p6_latency="+IntegerToString((int)(GetTickCount()-t0))+"ms");
|
| g_eval_busy = false; return;
|
| }
|
| string r_cool_exec="";
|
| if(!CircuitCooldownAllowed(r_cool_exec))
|
| {
|
| if(ShouldLog(LOG_INFO)) PrintFormat("[RISK4] blocked %s on %s/%s reason=%s (circuit_cooldown)", order.strategy_name, _Symbol, EnumToString(_Period), r_cool_exec);
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogGating(_Symbol, (int)_Period, order.strategy_name, "risk4_circuit_cooldown", false, r_cool_exec+";p6_latency="+IntegerToString((int)(GetTickCount()-t0))+"ms");
|
| g_eval_busy = false; return;
|
| }
|
|
|
| if(ShouldLog(LOG_INFO)) PrintFormat("[READY] %s on %s/%s passed gates", order.strategy_name, _Symbol, EnumToString(_Period));
|
|
|
| // Apply policy-driven scaling (if enabled and available) using a local copy of order
|
| TradeOrder req = order;
|
| double ppol_now = (UsePolicyGating && g_policy_loaded ? GetPolicyProb(req.strategy_name, _Symbol, (int)_Period) : -1.0);
|
| ApplyPolicyScaling(req, _Symbol, (int)_Period, ppol_now);
|
|
|
| // --- FR-06 Volatility Position Sizing (set req.lots) ---
|
| if(UseVolatilitySizer && CheckPointer(g_volatility_sizer)!=POINTER_INVALID)
|
| {
|
| double stop_pts = 0.0;
|
| double bid=0.0, ask=0.0; SymbolInfoDouble(_Symbol, SYMBOL_BID, bid); SymbolInfoDouble(_Symbol, SYMBOL_ASK, ask);
|
| if(req.stop_loss>0.0)
|
| {
|
| if(req.order_type==ORDER_TYPE_BUY || req.order_type==ORDER_TYPE_BUY_LIMIT || req.order_type==ORDER_TYPE_BUY_STOP)
|
| {
|
| double ref = (req.price>0.0? req.price : ask);
|
| stop_pts = MathMax(0.0, (ref - req.stop_loss)/_Point);
|
| }
|
| else
|
| {
|
| double ref = (req.price>0.0? req.price : bid);
|
| stop_pts = MathMax(0.0, (req.stop_loss - ref)/_Point);
|
| }
|
| }
|
| double atr_pct=0.0; string vs_r1=""; double mult_hint = (*g_volatility_sizer).CalculateSizeMultiplier(atr_pct, vs_r1);
|
| double mult_final=1.0; string vs_r2="";
|
| double sized = (*g_volatility_sizer).CalculatePositionSize(BaseLotSize, stop_pts, mult_final, vs_r2);
|
| if(sized>0.0) req.lots = sized;
|
| if(TelemetryEnabled && CheckPointer(g_tel_standard)!=POINTER_INVALID)
|
| (*g_tel_standard).LogVolatilitySizingEvent(_Symbol, (int)_Period, BaseLotSize, mult_final, (sized>0.0?sized:BaseLotSize), atr_pct);
|
| }
|
|
|
| // Execute the trade using TradeManager
|
| CTradeManager tm(_Symbol, 0.0 /* default to symbol min lot if order.lots==0 */, (int)MagicNumber);
|
| bool exec_ok = tm.ExecuteOrder(req);
|
| uint retcode = tm.ResultRetcode();
|
| ulong deal_id = tm.ResultDeal();
|
| ulong order_id = tm.ResultOrder();
|
|
|
| if(exec_ok)
|
| {
|
| g_last_trade_placed = TimeCurrent();
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogTradeExecuted(order.strategy_name, _Symbol, (int)_Period, (int)retcode, deal_id, order_id);
|
| // Exploration accounting: count only on successful execution
|
| if(g_explore_pending_key!="")
|
| {
|
| IncExploreCount(g_explore_pending_key);
|
| if(ShouldLog(LOG_INFO)) PrintFormat("Exploration used for %s -> %d/%d", g_explore_pending_key, GetExploreCount(g_explore_pending_key), ExploreMaxPerSlice);
|
| g_explore_pending_key = "";
|
| }
|
| }
|
| else
|
| {
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| (*g_telemetry).LogTradeFailed(order.strategy_name, _Symbol, (int)_Period, (int)retcode);
|
| // Ensure exploration pending key does not leak across failed executions
|
| if(g_explore_pending_key!="")
|
| {
|
| if(ShouldLog(LOG_INFO)) PrintFormat("Exploration not used due to failed execution for %s", g_explore_pending_key);
|
| g_explore_pending_key = "";
|
| }
|
| }
|
|
|
| g_eval_busy = false;
|
| }
|
|
|
| // --- Terminal event handlers
|
| int OnInit()
|
| {
|
| EnsureSessionRollover();
|
| EnsureTelemetry();
|
| EnsurePM();
|
| EnsureSessionManager();
|
| EnsureCorrelationManager();
|
| EnsureVolatilitySizer();
|
|
|
| // Initialize Incremental Insight Engine for real-time strategy approval
|
| g_insight_engine = new CIncrementalInsightEngine();
|
| if(CheckPointer(g_insight_engine) != POINTER_INVALID)
|
| {
|
| if(ShouldLog(LOG_INFO)) Print("[LiveEA] Incremental Insight Engine initialized");
|
| }
|
| // Load exploration counters and gating caches
|
| LoadExploreCounts();
|
| LoadExploreCountsDay();
|
| bool gate_loaded = Insights_Load();
|
| if(ShouldLog(LOG_INFO)) PrintFormat("Insights gating cache load: %s", (gate_loaded?"ok":"fail"));
|
| if(UsePolicyGating) { bool pol_ok = Policy_Load(); if(ShouldLog(LOG_INFO)) PrintFormat("Policy cache load: %s", (pol_ok?"ok":"fail")); }
|
| // Load news blackout windows
|
| if(UseNewsFilter && NewsUseFile)
|
| {
|
| // Reset and load
|
| ArrayResize(g_news_key,0); ArrayResize(g_news_from,0); ArrayResize(g_news_to,0); ArrayResize(g_news_impact,0);
|
| int h = FileOpen(NewsFileRelPath, FILE_READ|FILE_CSV|FILE_ANSI|FILE_COMMON, ',');
|
| if(h!=INVALID_HANDLE)
|
| {
|
| bool first=true;
|
| while(!FileIsEnding(h))
|
| {
|
| string k = FileReadString(h); if(k=="" && FileIsEnding(h)) break;
|
| string sfrom = FileReadString(h);
|
| string sto = FileReadString(h);
|
| string simp = FileReadString(h);
|
| if(first && (StringFind(k, "key", 0)==0)) { first=false; continue; }
|
| first=false;
|
| datetime tfrom = StringToTime(sfrom);
|
| datetime tto = StringToTime(sto);
|
| int impact = (int)StringToInteger(simp);
|
| int n = ArraySize(g_news_key);
|
| ArrayResize(g_news_key,n+1); ArrayResize(g_news_from,n+1); ArrayResize(g_news_to,n+1); ArrayResize(g_news_impact,n+1);
|
| StringToUpper(k);
|
| g_news_key[n]=k; g_news_from[n]=tfrom; g_news_to[n]=tto; g_news_impact[n]=impact;
|
| }
|
| FileClose(h);
|
| if(ShouldLog(LOG_INFO)) PrintFormat("News blackout windows loaded: %d", ArraySize(g_news_key));
|
| }
|
| else
|
| {
|
| if(ShouldLog(LOG_WARN)) PrintFormat("News filter: cannot open %s (Common). Err=%d", NewsFileRelPath, GetLastError());
|
| }
|
| }
|
| // GATE summary
|
| string gsum = StringFormat("insights=%s policy=%s(min=%.3f) explore=%s caps(slice=%d/day=%d)",
|
| (UseInsightsGating?"on":"off"), (UsePolicyGating?"on":"off"), g_policy_min_conf,
|
| (ExploreOnNoSlice?"on":"off"), ExploreMaxPerSlice, ExploreMaxPerSlicePerDay);
|
| if(ShouldLog(LOG_INFO)) PrintFormat("[GATE] %s", gsum);
|
| if(ShouldLog(LOG_INFO)) Print("LiveEA initialized");
|
| // Start watcher timer for insights ready signal
|
| if(InsightsAutoReload && InsightsReadyPollSec>0)
|
| EventSetTimer(InsightsReadyPollSec);
|
| // Bootstrap: ensure insights are fresh or request a rebuild immediately
|
| MaybeEnsureInsights();
|
| return(INIT_SUCCEEDED);
|
| }
|
|
|
| void OnDeinit(const int reason)
|
| {
|
| if(CheckPointer(g_position_manager)!=POINTER_INVALID) { delete g_position_manager; g_position_manager=NULL; }
|
| if(CheckPointer(g_telemetry)!=POINTER_INVALID) { delete g_telemetry; g_telemetry=NULL; }
|
| if(CheckPointer(g_tel_standard)!=POINTER_INVALID) { delete g_tel_standard; g_tel_standard=NULL; }
|
| if(CheckPointer(g_session_manager)!=POINTER_INVALID) { delete g_session_manager; g_session_manager=NULL; }
|
| if(CheckPointer(g_correlation_manager)!=POINTER_INVALID) { delete g_correlation_manager; g_correlation_manager=NULL; }
|
| if(CheckPointer(g_volatility_sizer)!=POINTER_INVALID) { delete g_volatility_sizer; g_volatility_sizer=NULL; }
|
| if(CheckPointer(g_insight_engine)!=POINTER_INVALID) { delete g_insight_engine; g_insight_engine=NULL; }
|
| if(InsightsAutoReload && InsightsReadyPollSec>0)
|
| EventKillTimer();
|
| }
|
|
|
| void OnTick()
|
| {
|
| // Strategy orchestration is external; gates are available via EvaluateAndMaybeExecute(order)
|
| // Early gating before any selection/insights logic
|
| if(!EvaluateAndMaybeExecute_PreSelector())
|
| {
|
| if(ShouldLog(LOG_INFO)) Print("[EARLY] gates blocked tick");
|
| return;
|
| }
|
| // If an external orchestrator performs selection and prepares orders, it should
|
| // call EvaluateAndMaybeExecute_PreExecute() just before execution. We call it here
|
| // as a placeholder to ensure telemetry is emitted even when no orders are placed.
|
| EvaluateAndMaybeExecute_PreExecute();
|
| }
|
|
|
| // --- Phase 6: Gate orchestration with shadow telemetry
|
| ulong NowMs(){ return (ulong)GetTickCount(); }
|
|
|
| void LogGate(const string tag, const bool allowed, const string phase, const ulong t0)
|
| {
|
| int latency = (int)(NowMs() - t0);
|
| if(ShouldLog(LOG_INFO))
|
| PrintFormat("[%s] %s latency_ms=%d", tag, (allowed?"allow":"block"), latency);
|
| if(TelemetryEnabled && CheckPointer(g_telemetry)!=POINTER_INVALID)
|
| {
|
| string det = StringFormat("phase=%s p6_latency_ms=%d", phase, latency);
|
| (*g_telemetry).LogEvent(_Symbol, (int)_Period, "gate", StringFormat("%s_%s", tag, (allowed?"allow":"block")), det);
|
| }
|
| // Standardized telemetry
|
| if(TelemetryEnabled && CheckPointer(g_tel_standard)!=POINTER_INVALID)
|
| {
|
| (*g_tel_standard).LogGateEvent(_Symbol, (int)_Period, tag, allowed, phase, latency, "n/a");
|
| }
|
| }
|
|
|
| // Early gates before selector/insights
|
| bool EarlyGatesAllow()
|
| {
|
| string reason;
|
| ulong t0;
|
| // NEWS
|
| t0=NowMs(); bool news_ok = NewsAllowed(reason); LogGate("NEWS", news_ok, "early", t0);
|
| if(!news_ok && !NoConstraintsMode) return false;
|
| // PROMO
|
| t0=NowMs(); bool promo_ok = PromotionAllowed(reason); LogGate("PROMO", promo_ok, "early", t0);
|
| if(!promo_ok && !NoConstraintsMode) return false;
|
| // REGIME
|
| t0=NowMs(); bool reg_ok = RegimeAllowed(reason); LogGate("REGIME", reg_ok, "early", t0);
|
| if(!reg_ok && !NoConstraintsMode) return false;
|
| // CIRCUIT cooldown
|
| t0=NowMs(); bool circ_ok = CircuitCooldownAllowed(reason); LogGate("CIRCUIT", circ_ok, "early", t0);
|
| // SESSION (diagnostic in early; block only if NoConstraintsMode is false)
|
| EnsureSessionManager();
|
| if(UseSessionManager && CheckPointer(g_session_manager)!=POINTER_INVALID)
|
| {
|
| t0=NowMs(); bool sess_ok = g_session_manager.IsSessionAllowed(reason); LogGate("SESSION", sess_ok, "early", t0);
|
| if(!sess_ok && !NoConstraintsMode) return false;
|
| }
|
| // CORRELATION (diagnostic/block depending on NoConstraintsMode)
|
| EnsureCorrelationManager();
|
| if(UseCorrelationManager && CheckPointer(g_correlation_manager)!=POINTER_INVALID)
|
| {
|
| double max_corr;
|
| t0=NowMs(); bool corr_ok = g_correlation_manager.CheckCorrelationLimits(reason, max_corr); LogGate("CORR", corr_ok, "early", t0);
|
| if(!corr_ok && !NoConstraintsMode) return false;
|
| }
|
| return true;
|
| }
|
|
|
| // Final risk4 gates just before execution
|
| bool Risk4GatesAllow()
|
| {
|
| string reason;
|
| ulong t0;
|
| t0=NowMs(); bool news_ok = NewsAllowed(reason); LogGate("NEWS", news_ok, "risk4", t0); if(!news_ok) return false;
|
| t0=NowMs(); bool promo_ok= PromotionAllowed(reason); LogGate("PROMO", promo_ok,"risk4", t0); if(!promo_ok) return false;
|
| t0=NowMs(); bool reg_ok = RegimeAllowed(reason); LogGate("REGIME", reg_ok, "risk4", t0); if(!reg_ok) return false;
|
| t0=NowMs(); bool circ_ok = CircuitCooldownAllowed(reason); LogGate("CIRCUIT", circ_ok,"risk4", t0); if(!circ_ok) return false;
|
|
|
| // FR-02 Session gate
|
| EnsureSessionManager();
|
| if(UseSessionManager && CheckPointer(g_session_manager)!=POINTER_INVALID)
|
| {
|
| t0=NowMs(); bool sess_ok = g_session_manager.IsSessionAllowed(reason); LogGate("SESSION", sess_ok, "risk4", t0); if(!sess_ok) return false;
|
| }
|
|
|
| // FR-05 Correlation gate
|
| EnsureCorrelationManager();
|
| if(UseCorrelationManager && CheckPointer(g_correlation_manager)!=POINTER_INVALID)
|
| {
|
| double max_corr;
|
| t0=NowMs(); bool corr_ok = g_correlation_manager.CheckCorrelationLimits(reason, max_corr); LogGate("CORR", corr_ok, "risk4", t0); if(!corr_ok) return false;
|
| }
|
|
|
| // FR-04 Position Manager gate
|
| EnsurePM();
|
| if(UsePositionManager && CheckPointer(g_position_manager)!=POINTER_INVALID)
|
| {
|
| int open_positions = PositionsTotal();
|
| bool pm_ok = (PMMaxOpenPositions<=0 ? true : (open_positions < PMMaxOpenPositions));
|
| t0=NowMs(); LogGate("PM", pm_ok, "risk4", t0);
|
| if(!pm_ok) return false;
|
| }
|
|
|
| return true;
|
| }
|
|
|
| // Public function to be called by orchestrator before/after selector/insights
|
| bool EvaluateAndMaybeExecute_PreSelector()
|
| {
|
| return EarlyGatesAllow();
|
| }
|
|
|
| bool EvaluateAndMaybeExecute_PreExecute()
|
| {
|
| return Risk4GatesAllow();
|
| }
|
|
|
| // --- Insights ready/reload logic ---
|
| bool IsInsightsFresh(const int fresh_minutes)
|
| {
|
| string ip = "DualEA\\insights.json";
|
| long ex_i = FileGetInteger(ip, FILE_EXISTS, true);
|
| if(ex_i==0) return false;
|
| datetime ti = (datetime)FileGetInteger(ip, FILE_MODIFY_DATE, true);
|
| if(fresh_minutes<=0) return true; // any existing is fine
|
| return ((TimeCurrent() - ti) <= (fresh_minutes*60));
|
| }
|
|
|
| void RequestInsightsReload()
|
| {
|
| string path = "DualEA\\insights.reload";
|
| if(FileIsExist(path, FILE_COMMON)) FileDelete(path, FILE_COMMON);
|
| int h = FileOpen(path, FILE_WRITE|FILE_TXT|FILE_ANSI|FILE_COMMON);
|
| if(h!=INVALID_HANDLE)
|
| {
|
| FileWrite(h, IntegerToString((int)TimeCurrent()));
|
| FileClose(h);
|
| if(ShouldLog(LOG_INFO)) Print("Insights reload requested");
|
| }
|
| else
|
| {
|
| if(ShouldLog(LOG_WARN)) PrintFormat("Insights reload: cannot create %s (err=%d)", path, GetLastError());
|
| }
|
| }
|
|
|
| void CheckInsightsReady()
|
| {
|
| string rdy = "DualEA\\insights.ready";
|
| int h = FileOpen(rdy, FILE_READ|FILE_TXT|FILE_ANSI|FILE_COMMON);
|
| if(h==INVALID_HANDLE) return;
|
| // Ready present -> consume and reload
|
| FileClose(h);
|
| if(!FileDelete(rdy, FILE_COMMON))
|
| {
|
| // still proceed, but log
|
| if(ShouldLog(LOG_WARN)) PrintFormat("Insights ready: cannot delete %s (err=%d)", rdy, GetLastError());
|
| }
|
| bool ok = Insights_Load();
|
| if(ShouldLog(LOG_INFO)) PrintFormat("Insights ready detected: reload %s", (ok?"ok":"fail"));
|
| g_waiting_insights = false;
|
| }
|
|
|
| void MaybeEnsureInsights()
|
| {
|
| if(!InsightsAutoReload) return;
|
| if(IsInsightsFresh(InsightsLiveFreshMinutes)) return;
|
| if(!g_waiting_insights)
|
| {
|
| RequestInsightsReload();
|
| g_waiting_insights = true;
|
| }
|
| }
|
|
|
| void OnTimer()
|
| {
|
| if(!InsightsAutoReload) return;
|
| MaybeEnsureInsights();
|
| CheckInsightsReady();
|
| }
|