//+------------------------------------------------------------------+ //| Universal_Quant_EA.mq5 | //| | //| Configurable quantitative EA for eligible broker-listed FX | //| pairs and selectable MetaTrader 5 timeframes. | //| | //| Baseline model: volatility-normalised channel breakout on | //| completed candles, EMA trend filter, ATR initial stop and ATR | //| trailing stop. | //| | //| Centralised account risk control, evaluated in strict priority | //| order before any signal: | //| 1. static drawdown (default 8%) - permanent lock | //| 2. equity target (default 10%) - permanent lock | //| 3. daily loss (default 5%) - lock until next day | //| 4. baseline / persistent-state validity | //| 5. account, symbol and exposure permissions | //| 6. signal generation and order submission | //| | //| Symbol resolution is fail-closed: only a unique eligible FX | //| instrument is ever traded, and one centralised scheduler walks | //| every managed symbol from both OnTick() and OnTimer(). | //| | //| No martingale, no grid recovery, no loss-recovery lot scaling | //| and no removal of protective stops. Defaults are engineering | //| parameters, not proven optimal values, and nothing here claims | //| universal profitability. | //| | //| ACCOUNT SCOPE. The three limits are account-equity limits, but | //| liquidation is restricted to this EA's own Magic Number unless | //| whole-account liquidation is explicitly authorized. Unmanaged | //| positions and pending orders are counted, disclosed and, while | //| the guard is enabled, they block every new entry. Strict | //| account-wide enforcement therefore requires a dedicated account | //| or an explicit authorization for whole-account liquidation; it | //| is never enabled silently and is never claimed while unmanaged | //| exposure remains open. | //| | //| MONITORING CONTINUITY. The limits can only be enforced while the | //| EA is executing. A persisted heartbeat proves which broker- | //| server trading day was observed; an unobserved day boundary is | //| reconstructed only from a provably constant equity path, and | //| otherwise preserves D0 and blocks entries. Equity thresholds are | //| protective triggers, not guaranteed maximum realized losses: | //| slippage, gaps, broker execution and outages can exceed them. | //+------------------------------------------------------------------+ #property copyright "Universal_Quant_EA" #property version "1.00" #property description "Volatility-normalised channel-breakout EA for FX symbols on selectable timeframes." #property description "Centralised risk control: daily loss 5%, static drawdown 8%, equity target 10%." #property description "No martingale, no grid recovery, no removal of protective stops." #property description "Liquidation is EA-scope by default; unmanaged exposure is disclosed and blocks entries." #property description "A monitoring-gap hold blocks entries when a trading day could not be observed." #include "Include/UQ_Defs.mqh" #include "Include/UQ_State.mqh" #include "Include/UQ_Risk.mqh" #include "Include/UQ_Symbols.mqh" #include "Include/UQ_Exec.mqh" //+------------------------------------------------------------------+ //| Input parameters | //+------------------------------------------------------------------+ input group "=== Operational control ===" input bool InpEnableTrading = false; // Trading enabled input ENUM_UQ_SYMBOL_MODE InpSymbolMode = UQ_SYM_EXPLICIT_LIST; // Symbol selection mode input string InpSymbolList = "EURUSD,GBPUSD,USDJPY,AUDUSD,USDCAD"; // Symbol list (comma separated) input bool InpRequireAllSymbols = true; // Fail init when a configured symbol cannot be resolved input ENUM_TIMEFRAMES InpSignalTimeframe = PERIOD_H1; // Signal timeframe //--- Ownership of positions and orders is decided by Magic Number only, so one //--- Magic Number belongs to exactly one deployment: two portfolios sharing a //--- Magic Number on one account would each manage the other's exposure. Order //--- comments are diagnostics and never an ownership criterion. input long InpMagic = 20261030; // Magic Number (unique per deployment) input string InpPortfolioId = "UQ_PORTFOLIO_A"; // Portfolio identifier input double InpInitialBalance = 0.0; // Initial balance B0 (required to create a risk cycle) input bool InpAuthorizeFirstInit = false; // Authorize a new risk cycle when no state exists input bool InpAllowBackupRestore = false; // Allow restore from verified state backup input group "=== Account risk limits ===" input double InpDailyLossPct = 5.0; // Daily maximum loss % input double InpStaticDdPct = 8.0; // Static maximum drawdown % input double InpTargetPct = 10.0; // Equity growth target % input double InpRiskPerTradePct = 0.25; // Risk per trade % input double InpMaxAggRiskPct = 1.0; // Maximum aggregate open risk % input double InpRiskBufferPct = 0.0; // Risk buffer for costs and slippage % input int InpMaxPositions = 5; // Maximum simultaneous EA positions input group "=== Strategy ===" input int InpChannelLookback = 20; // Channel lookback (bars) input int InpEmaPeriod = 100; // EMA trend filter period input int InpAtrPeriod = 14; // ATR period input double InpSlAtrMultiplier = 2.0; // Initial stop loss (x ATR) input double InpTrailAtrMultiplier = 3.0; // ATR trailing stop (x ATR) input group "=== Execution filters ===" input double InpMaxSpreadPoints = 30.0; // Maximum spread (points, 0 = disabled) input ulong InpDeviationPoints = 20; // Maximum price deviation (points) input group "=== External exposure handling ===" input bool InpBlockOnExternalPositions = true; // Block new entries when external positions exist input bool InpAllowWholeAccountLiquidation = false; // Allow whole-account liquidation (dangerous) input bool InpPreventDuplicateInstances = true; // Block entries when a duplicate instance is active input group "=== Owner reset of permanent locks ===" //--- A reset never clears a lock in place: it opens a new risk cycle with the //--- baseline supplied below. Every reset needs a unique nonce, the operation is //--- persisted together with the consumed nonce, and the reset inputs must be //--- cleared afterwards so a restart cannot repeat it. input ENUM_UQ_RESET_REQUEST InpResetRequest = UQ_RESET_NONE; // Reset request input string InpResetToken = ""; // Reset confirmation phrase input string InpResetNonce = ""; // Reset nonce (unique, >= 12 chars) input double InpResetBaseline = 0.0; // New cycle baseline B0 (reset only) //+------------------------------------------------------------------+ //| Globals | //+------------------------------------------------------------------+ CUQRisk g_risk; CUQSymbols g_symbols; //--- duplicate-instance ownership: the guard is acquired through a documented //--- atomic compare-and-swap on the terminal global variable, never by //--- "check for a value and then publish one" CUQTerminalGuardStore g_guardStore; CUQInstanceGuard g_instanceGuard; bool g_instanceConflict = false; bool g_instanceOwned = false; string g_instanceGV = ""; CUQLiquidator g_liq; uint g_lastStatusTick = 0; uint g_lastTrailTick = 0; uint g_lastScheduleTick = 0; uint g_schedulePasses = 0; datetime g_lastScheduleTime = 0; //+------------------------------------------------------------------+ //| Input validation. Invalid combinations are rejected, never | //| silently replaced by unrelated values. | //+------------------------------------------------------------------+ bool ValidateInputs(string &err) { err = ""; if(InpMagic <= 0) { err = "Magic Number must be positive"; return(false); } if(StringLen(InpPortfolioId) <= 0) { err = "Portfolio identifier must not be empty"; return(false); } if(StringLen(InpPortfolioId) > 24) { err = "Portfolio identifier must be at most 24 characters"; return(false); } if(InpInitialBalance < 0.0) { err = "Initial balance override must not be negative"; return(false); } if(InpAuthorizeFirstInit && !(InpInitialBalance > 0.0)) { err = "Authorizing a new risk cycle requires an explicit positive initial balance B0"; return(false); } if(!(InpDailyLossPct > 0.0 && InpDailyLossPct < 100.0)) { err = "Daily maximum loss percentage must be inside (0, 100)"; return(false); } if(!(InpStaticDdPct > 0.0 && InpStaticDdPct < 100.0)) { err = "Static maximum drawdown percentage must be inside (0, 100)"; return(false); } if(!(InpTargetPct > 0.0 && InpTargetPct <= 1000.0)) { err = "Equity growth target percentage must be inside (0, 1000]"; return(false); } if(!(InpRiskPerTradePct > 0.0 && InpRiskPerTradePct <= 100.0)) { err = "Risk per trade percentage must be inside (0, 100]"; return(false); } if(!(InpMaxAggRiskPct > 0.0 && InpMaxAggRiskPct <= 100.0)) { err = "Maximum aggregate open risk percentage must be inside (0, 100]"; return(false); } if(InpRiskPerTradePct > InpMaxAggRiskPct) { err = "Risk per trade must not exceed the maximum aggregate open risk"; return(false); } if(!(InpRiskBufferPct >= 0.0 && InpRiskBufferPct <= 50.0)) { err = "Risk buffer must be inside [0, 50] percent"; return(false); } if(InpMaxPositions < 1) { err = "Maximum simultaneous positions must be at least 1"; return(false); } if(InpChannelLookback < 2) { err = "Channel lookback must be at least 2 bars"; return(false); } if(InpEmaPeriod < 2) { err = "EMA period must be at least 2 bars"; return(false); } if(InpAtrPeriod < 2) { err = "ATR period must be at least 2 bars"; return(false); } if(!(InpSlAtrMultiplier > 0.0)) { err = "Initial stop-loss ATR multiplier must be positive"; return(false); } if(!(InpTrailAtrMultiplier > 0.0)) { err = "ATR trailing multiplier must be positive"; return(false); } if(InpMaxSpreadPoints < 0.0) { err = "Maximum spread filter must not be negative"; return(false); } if(InpDeviationPoints > 1000) { err = "Deviation must not exceed 1000 points"; return(false); } if(InpSymbolMode == UQ_SYM_EXPLICIT_LIST && StringLen(InpSymbolList) == 0) { err = "Explicit symbol list mode requires at least one symbol"; return(false); } if(InpResetRequest != UQ_RESET_NONE) { if(StringLen(InpResetToken) == 0) { err = "An owner reset request requires the confirmation phrase"; return(false); } if(!UQResetNonceValid(InpResetNonce)) { err = StringFormat("An owner reset request requires a unique nonce of %d..%d printable characters without '=' or '|'", UQ_RESET_NONCE_MIN_LEN, UQ_RESET_NONCE_MAX_LEN); return(false); } if(!(InpResetBaseline > 0.0)) { err = "An owner reset request requires the baseline B0 of the new risk cycle"; return(false); } } return(true); } //+------------------------------------------------------------------+ //| Terminal, account and program trading permissions | //+------------------------------------------------------------------+ bool TradingPermissionsOk(void) { if(!(bool)TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) return(false); if(!(bool)MQLInfoInteger(MQL_TRADE_ALLOWED)) return(false); if(!(bool)AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)) return(false); if(!(bool)AccountInfoInteger(ACCOUNT_TRADE_EXPERT)) return(false); return(true); } //+------------------------------------------------------------------+ //| Netting accounts allow only one position per symbol | //+------------------------------------------------------------------+ bool IsNettingAccount(void) { const long mode = (long)AccountInfoInteger(ACCOUNT_MARGIN_MODE); return(mode != (long)ACCOUNT_MARGIN_MODE_RETAIL_HEDGING); } //+------------------------------------------------------------------+ //| Own positions, optionally restricted to one symbol | //+------------------------------------------------------------------+ int CountOwnPositions(const string symbol) { int count = 0; const int total = PositionsTotal(); for(int i = 0; i < total; i++) { const ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if((ulong)PositionGetInteger(POSITION_MAGIC) != (ulong)InpMagic) continue; if(StringLen(symbol) > 0 && PositionGetString(POSITION_SYMBOL) != symbol) continue; count++; } return(count); } //+------------------------------------------------------------------+ //| Any position on a symbol, regardless of ownership | //+------------------------------------------------------------------+ int CountPositionsOnSymbol(const string symbol) { int count = 0; const int total = PositionsTotal(); for(int i = 0; i < total; i++) { const ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(PositionGetString(POSITION_SYMBOL) == symbol) count++; } return(count); } //+------------------------------------------------------------------+ //| Detect account exposure that this EA does not own | //| | //| The three limits are account-equity limits, so unmanaged positions| //| and pending orders are counted and disclosed. While the guard is | //| enabled they block every new entry; they are never modified, and | //| the EA never claims account-wide protection over them. | //+------------------------------------------------------------------+ int g_lastExternalExposure = -1; bool ExternalExposurePresent(void) { UQAccountScope scope; if(!UQAccountScopeScan((ulong)InpMagic, scope)) return(true); // fail closed: the scope cannot be verified const int unmanaged = scope.externalPositions + scope.externalPending; if(unmanaged != g_lastExternalExposure) { g_lastExternalExposure = unmanaged; PrintFormat("[UQ] SCOPE: %s", UQScopeDisclosure(scope, InpBlockOnExternalPositions, InpAllowWholeAccountLiquidation)); } return(InpBlockOnExternalPositions && unmanaged > 0); } //+------------------------------------------------------------------+ //| Drive liquidation from the active risk lock. The lock is the | //| authority: new entries stay blocked for as long as it is active, | //| independently of liquidation progress. The machine is a state | //| machine that only reports completion after verifying both pools. | //+------------------------------------------------------------------+ void ServiceRiskLocks(void) { if(!g_risk.LiquidationRequired()) { if(!g_liq.Idle()) { PrintFormat("[UQ] Risk lock cleared; liquidation management idle again (last state %s).", g_liq.StateText()); g_liq.Reset(); } return; } //--- a lock arms the machine, including after an EA restart if(g_liq.Idle()) { g_liq.Arm(); PrintFormat("[UQ] Liquidation armed by risk control (%s).", g_risk.BlockText()); } g_liq.Service(); } //+------------------------------------------------------------------+ //| Evaluate one new signal bar for one symbol | //+------------------------------------------------------------------+ void ProcessSymbolBar(const int idx, const datetime barTime) { const string symbol = g_symbols.Name(idx); if(StringLen(symbol) == 0) return; if(g_symbols.LastEntryBar(idx) == barTime) return; if(CountOwnPositions(symbol) > 0) return; // one EA position per symbol if(CountOwnPositions("") >= InpMaxPositions) { g_symbols.Note(idx, "maximum simultaneous EA positions reached", true); return; } if(IsNettingAccount() && CountPositionsOnSymbol(symbol) > 0) { g_symbols.Note(idx, "netting account already holds a position on this symbol", true); return; } int direction = 0; double atr = 0.0; string reason = ""; if(!g_symbols.EvaluateSignal(idx, direction, atr, reason)) { g_symbols.Note(idx, "signal unavailable: " + reason, true); return; } if(direction == 0) return; //--- the broker trade mode must allow this side of the market string dirReason = ""; if(!UQSymbolAllowsDirection(symbol, direction, dirReason)) { g_symbols.Note(idx, "signal direction is not permitted: " + dirReason, true); return; } UQExecConfig ec; ec.magic = (ulong)InpMagic; ec.deviation = InpDeviationPoints; ec.portfolioTag = InpPortfolioId; ec.riskPct = InpRiskPerTradePct; ec.maxAggRiskPct = InpMaxAggRiskPct; ec.riskBufferPct = InpRiskBufferPct; ec.slAtrMultiplier = InpSlAtrMultiplier; ec.maxSpreadPoints = InpMaxSpreadPoints; double lots = 0.0; string err = ""; if(!UQOpenSignalPosition(symbol, direction, atr, ec, g_risk, lots, err)) { PrintFormat("[UQ] SKIP %s %s: %s", symbol, (direction > 0 ? "BUY" : "SELL"), err); return; } g_symbols.SetLastEntryBar(idx, barTime); PrintFormat("[UQ] SIGNAL %s %s bar=%s | %s", symbol, (direction > 0 ? "BUY" : "SELL"), TimeToString(barTime, TIME_DATE | TIME_MINUTES), reason); } //+------------------------------------------------------------------+ //| Centralised multi-symbol scheduler | //| | //| Every managed symbol is examined for its own completed signal | //| bar, so a multi-symbol portfolio never depends on ticks arriving | //| on the attached chart symbol alone. The same body serves | //| OnTick() and OnTimer(), which keeps one risk order, one account | //| risk check per submission and one duplicate-entry guard no | //| matter which event source woke it up. | //+------------------------------------------------------------------+ void ProcessManagedSymbols(void) { if(g_symbols.Count() <= 0) return; if(!InpEnableTrading || g_instanceConflict) return; if(g_risk.LiquidationRequired() || !g_risk.CanOpenNew()) return; if(!TradingPermissionsOk()) return; if(ExternalExposurePresent()) return; g_lastScheduleTick = GetTickCount(); g_lastScheduleTime = UQServerNow(); g_schedulePasses++; for(int i = 0; i < g_symbols.Count(); i++) { //--- the gate is re-checked for every symbol: a lock, an unresolved //--- reservation or an exhausted budget stops the remaining symbols if(!g_risk.CanOpenNew()) break; datetime barTime = 0; if(!g_symbols.IsNewBar(i, barTime)) continue; //--- the risk state may have changed while an earlier symbol was //--- submitted, so the account gate is never assumed to still hold if(!g_risk.CanOpenNew()) break; ProcessSymbolBar(i, barTime); } } //+------------------------------------------------------------------+ //| Protective trailing, throttled to once per second | //+------------------------------------------------------------------+ void ServiceTrailing(void) { const uint now = GetTickCount(); if((now - g_lastTrailTick) < 1000) return; g_lastTrailTick = now; UQApplyAtrTrailing((ulong)InpMagic, g_symbols, InpTrailAtrMultiplier, InpDeviationPoints); } //+------------------------------------------------------------------+ //| Compact on-chart status display | //+------------------------------------------------------------------+ void UpdateStatus(void) { UQExposure exp; g_risk.Exposure((ulong)InpMagic, exp); const double equity = g_risk.Equity(); const double riskPct = (equity > 0.0) ? (exp.Total() / equity) * 100.0 : 0.0; const double posPct = (equity > 0.0) ? (exp.positions / equity) * 100.0 : 0.0; const double resPct = (equity > 0.0) ? (exp.reserved / equity) * 100.0 : 0.0; UQAccountScope scope; const bool scopeKnown = UQAccountScopeScan((ulong)InpMagic, scope); string scheduleText = "idle"; if(g_lastScheduleTime > 0) scheduleText = TimeToString(g_lastScheduleTime, TIME_DATE | TIME_SECONDS); string s = ""; s += StringFormat("%s v%s | %s | %s\n", UQ_COMMENT_PREFIX, UQ_VERSION, g_risk.IdentityKey(), EnumToString(InpSignalTimeframe)); s += StringFormat("Lock state : %s\n", g_risk.BlockText()); s += StringFormat("Cycle : %s seq %d | %s\n", g_risk.CycleId(), g_risk.CycleSequence(), (g_risk.ResetRejected() ? "RESET REJECTED" : (g_risk.ResetApplied() ? "RESET APPLIED" : "reset idle"))); //--- recorded lock history of the closed cycle: kind 3 means both //--- permanent locks were carried, and it is preserved across restarts s += StringFormat("Prior locks : %s (kind %d)\n", g_risk.PriorLockText(), g_risk.PriorLockKind()); s += StringFormat("State : %s | persistence %s | generation %I64d\n", (g_risk.StateFromBackup() ? "RESTORED-FROM-BACKUP" : (g_risk.StateMigrated() ? "MIGRATED" : "VERIFIED")), (g_risk.Healthy() ? "OK" : "FAILED"), g_risk.StateRevision()); //--- a recovery hold is shown explicitly: entries stay blocked until the //--- owner authorizes a recovery risk cycle, even after a restart if(g_risk.RecoveryHold()) s += StringFormat("Recovery : HOLD since %s | owner authorization required\n", TimeToString(g_risk.RecoveryHoldTime(), TIME_DATE | TIME_SECONDS)); s += StringFormat("B0 %.2f | Equity %.2f | Target %.2f\n", g_risk.Baseline(), equity, g_risk.TargetEquity()); s += StringFormat("D0 %.2f | Daily loss %.2f%% / %.2f%%%s\n", g_risk.DailyStart(), g_risk.DailyLossPct(), g_risk.DailyLimitPct(), (g_risk.DailyMonitored() ? "" : " UNVERIFIED DAY (monitoring gap)")); s += StringFormat("Monitoring : %s | gap %d s | heartbeat %s\n", (g_risk.MonitorGapHold() ? "GAP HOLD" : "continuous"), g_risk.MonitorGapSeconds(), (g_risk.LastMonitorTime() > 0 ? TimeToString(g_risk.LastMonitorTime(), TIME_DATE | TIME_SECONDS) : "none")); if(g_risk.MonitorGapHold()) s += StringFormat("Gap hold : since %s | entries blocked until an observed day boundary\n", TimeToString(g_risk.MonitorGapHoldTime(), TIME_DATE | TIME_SECONDS)); s += StringFormat("Static DD %.2f%% / %.2f%% | Floors %.2f / %.2f\n", g_risk.StaticDdPct(), g_risk.StaticLimitPct(), g_risk.DailyFloor(), g_risk.StaticFloor()); s += StringFormat("EA exposure %.2f%% / %.2f%%%s\n", riskPct, InpMaxAggRiskPct, (exp.bounded ? "" : " UNBOUNDED: " + UQExposureBrief(exp.detail))); s += StringFormat(" positions %.2f%% (%d) | pending %d | reserved %.2f%% %s\n", posPct, exp.positionCount, exp.pendingCount, resPct, UQReserveStateText(g_risk.ReserveState())); if(g_risk.ReserveState() != UQ_RESERVE_NONE) s += StringFormat(" reservation id %s | order %s | deal %s | position %s\n", g_risk.ReserveId(), IntegerToString((long)g_risk.ReserveTicket()), IntegerToString((long)g_risk.ReserveDeal()), IntegerToString((long)g_risk.ReservePosition())); s += StringFormat("EA positions %d / %d | External positions %d | External orders %d\n", scope.ownPositions, InpMaxPositions, scope.externalPositions, scope.externalPending); if(!scopeKnown) s += "Scope : UNVERIFIED, new entries are blocked\n"; else if(scope.externalPositions > 0 || scope.externalPending > 0) s += StringFormat("Scope : %s\n", UQScopeDisclosure(scope, InpBlockOnExternalPositions, InpAllowWholeAccountLiquidation)); s += StringFormat("Liquidation : %s | cycles %d\n", g_liq.StateText(), g_liq.Cycles()); s += StringFormat("Scheduler : %s | passes %d | idle %u ms\n", scheduleText, (int)g_schedulePasses, (uint)(g_lastScheduleTick > 0 ? (GetTickCount() - g_lastScheduleTick) : 0)); s += StringFormat("Trading %s | Symbols %d | tag %s | rejected %d", (InpEnableTrading ? "ENABLED" : "DISABLED"), g_symbols.Count(), g_symbols.TagText(), g_symbols.Skipped()); Comment(s); } //+------------------------------------------------------------------+ void UpdateStatusThrottled(void) { const uint now = GetTickCount(); if((now - g_lastStatusTick) < 1000) return; g_lastStatusTick = now; UpdateStatus(); } //+------------------------------------------------------------------+ //| Duplicate-instance guard names live in UQ_State.mqh together with | //| the other persistence locators, so the V2 guard, the V2 marker and | //| the V2 state files are derived from one fingerprint by one | //| definition and one test path. | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Initialisation | //+------------------------------------------------------------------+ int OnInit(void) { string cfgErr = ""; if(!ValidateInputs(cfgErr)) { PrintFormat("[UQ] INIT FAILED: %s", cfgErr); return(INIT_PARAMETERS_INCORRECT); } PrintFormat("[UQ] Universal_Quant_EA v%s starting | trading=%s | timeframe=%s | symbols=%s", UQ_VERSION, (InpEnableTrading ? "ENABLED" : "DISABLED"), EnumToString(InpSignalTimeframe), (InpSymbolMode == UQ_SYM_CURRENT_CHART ? _Symbol : InpSymbolList)); //--- centralized risk controller UQRiskConfig rc; rc.magic = (ulong)InpMagic; rc.portfolio = InpPortfolioId; rc.balanceOverride = InpInitialBalance; rc.dailyLimitPct = InpDailyLossPct; rc.staticLimitPct = InpStaticDdPct; rc.targetPct = InpTargetPct; rc.resetRequest = InpResetRequest; rc.resetToken = InpResetToken; rc.resetBaseline = InpResetBaseline; rc.resetNonce = InpResetNonce; rc.allowFirstInit = InpAuthorizeFirstInit; rc.allowBackupRestore = InpAllowBackupRestore; //--- an owner reset is only accepted on a verified flat account scope UQResetScope resetScope; resetScope.ok = false; resetScope.detail = ""; resetScope.ownPositions = 0; resetScope.ownPending = 0; resetScope.externalPositions = 0; resetScope.externalPending = 0; resetScope.liquidationComplete = (g_liq.Idle() || g_liq.Complete()); if(InpResetRequest != UQ_RESET_NONE) { UQCheckResetScope((ulong)InpMagic, InpBlockOnExternalPositions, resetScope.liquidationComplete, resetScope); if(!resetScope.ok) PrintFormat("[UQ] RESET PRECONDITION NOT MET: %s", resetScope.detail); } rc.resetPreconditionsOk = resetScope.ok; rc.resetPreconditionDetail = resetScope.detail; //--- duplicate-instance ownership is established BEFORE any function that can //--- read, migrate, create or save a persistent risk scope is called. The //--- requirement is unconditional: the ownership record is what makes one //--- persistent scope exclusive, so a duplicate deployment must never reach //--- g_risk.Init(). InpPreventDuplicateInstances only decides whether a guard //--- that is lost later also blocks entries; it cannot disable ownership. //--- //--- Order enforced here: //--- 1. account identity (server, login, portfolio) //--- 2. legacy-instance ownership //--- 3. V2-instance ownership, acquired atomically //--- 4. g_risk.Init(), i.e. risk-state load / migration / creation / save string ownershipDetail = ""; UQIdentity ownershipId; string identityErr = ""; if(!UQBuildIdentity(InpPortfolioId, ownershipId, identityErr)) { PrintFormat("[UQ] INIT FAILED: %s. No persistent risk state was read, migrated, created or saved.", identityErr); return(INIT_FAILED); } const ENUM_UQ_OWNERSHIP ownership = UQInstanceOwnershipAcquire(g_instanceGuard, &g_guardStore, ownershipId, TimeLocal(), UQ_INSTANCE_TTL_SECONDS, ownershipDetail); g_instanceGV = g_instanceGuard.Name(); g_instanceOwned = false; g_instanceConflict = true; if(ownership != UQ_OWNERSHIP_READY) { PrintFormat("[UQ] INIT FAILED: duplicate-instance ownership was not established (%s): %s", UQOwnershipText(ownership), ownershipDetail); PrintFormat("[UQ] Fail closed: no persistent risk state was read, migrated, created or saved, and no state marker was written."); return(INIT_FAILED); } g_instanceOwned = true; g_instanceConflict = false; PrintFormat("[UQ] Instance guard '%s' acquired atomically (%d chars, portfolio '%s').", g_instanceGV, StringLen(g_instanceGV), InpPortfolioId); //--- only now may the persistent risk scope be touched if(!g_risk.Init(rc)) { PrintFormat("[UQ] INIT FAILED: risk controller: %s. The instance guard is released.", g_risk.LastError()); g_instanceGuard.Release(); g_instanceOwned = false; g_instanceConflict = true; return(INIT_FAILED); } if(g_risk.ResetRejected()) PrintFormat("[UQ] RISK CYCLE UNCHANGED: the owner reset request was rejected; new entries stay blocked until it is corrected or removed."); if(g_risk.ResetApplied()) PrintFormat("[UQ] OWNER RESET APPLIED: risk cycle %s is active. Remove the reset inputs now so a restart cannot repeat the request.", g_risk.CycleId()); PrintFormat("[UQ] Ownership scope: magic=%s portfolio=%s. One Magic Number must belong to exactly one deployment.", IntegerToString(InpMagic), InpPortfolioId); //--- the enforced liquidation scope is disclosed at start: the limits are //--- account-equity limits, but this EA only controls its own magic scope //--- unless whole-account liquidation is explicitly authorized { UQAccountScope scope; if(UQAccountScopeScan((ulong)InpMagic, scope)) PrintFormat("[UQ] Scope: %s", UQScopeDisclosure(scope, InpBlockOnExternalPositions, InpAllowWholeAccountLiquidation)); else PrintFormat("[UQ] WARNING: the account scope cannot be verified; new entries are blocked until the broker state is readable."); } if(g_risk.ReserveState() != UQ_RESERVE_NONE) PrintFormat("[UQ] WARNING: an entry reservation of %.2f %s survived a restart in state %s (id=%s, order=%s, deal=%s, position=%s). It keeps counting as exposure and new entries stay blocked until a broker transaction proves what the request produced.", g_risk.ReservedRisk(), g_risk.ReserveSymbol(), UQReserveStateText(g_risk.ReserveState()), g_risk.ReserveId(), IntegerToString((long)g_risk.ReserveTicket()), IntegerToString((long)g_risk.ReserveDeal()), IntegerToString((long)g_risk.ReservePosition())); //--- symbol registry and indicator handles if(!g_symbols.Init(InpSymbolMode, InpSymbolList, InpSignalTimeframe, InpChannelLookback, InpEmaPeriod, InpAtrPeriod, InpRequireAllSymbols)) { PrintFormat("[UQ] INIT FAILED: %s", g_symbols.LastError()); g_instanceGuard.Release(); g_instanceOwned = false; return(INIT_PARAMETERS_INCORRECT); } PrintFormat("[UQ] %d symbol(s) managed | broker tag %s | %d rejected.", g_symbols.Count(), g_symbols.TagText(), g_symbols.Skipped()); g_liq.Init((ulong)InpMagic, InpDeviationPoints, InpAllowWholeAccountLiquidation); EventSetTimer(2); UpdateStatus(); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Deinitialisation | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); g_symbols.Deinit(); //--- release only a guard this instance demonstrably still owns. The release //--- is a compare-and-swap against the value this instance holds, so a claim //--- a successor acquired is never deleted or overwritten here. if(g_instanceOwned) { if(!g_instanceGuard.Release()) PrintFormat("[UQ] Instance guard '%s' was not released: %s", g_instanceGV, g_instanceGuard.Detail()); g_instanceOwned = false; } Comment(""); PrintFormat("[UQ] Stopped (reason=%d). Final lock state: %s", reason, g_risk.BlockText()); } //+------------------------------------------------------------------+ //| Main tick handler | //+------------------------------------------------------------------+ void OnTick(void) { g_risk.Evaluate(); //--- a risk-control event always outranks a trading signal ServiceRiskLocks(); if(g_risk.LiquidationRequired()) { UpdateStatusThrottled(); return; } if(!InpEnableTrading) { UpdateStatusThrottled(); return; } if(!TradingPermissionsOk()) { UpdateStatusThrottled(); return; } //--- protective trailing runs even when the entry gate is closed ServiceTrailing(); //--- the tick path only wakes the scheduler; all symbol processing, //--- including the account risk gate, lives in one place ProcessManagedSymbols(); UpdateStatusThrottled(); } //+------------------------------------------------------------------+ //| Timer: risk monitoring, trailing, scheduling and status refresh | //| | //| A managed symbol that never ticks on the attached chart must | //| still be processed, so the timer runs the same scheduled body as | //| the tick handler. The duplicate-entry guard keeps the two paths | //| from acting twice on one completed bar. | //+------------------------------------------------------------------+ void OnTimer(void) { g_risk.Evaluate(); ServiceRiskLocks(); //--- the heartbeat is the evidence a duplicate instance relies on, so a //--- heartbeat that cannot be written and verified closes the entry gate //--- instead of leaving this instance silently unprotected if(g_instanceOwned) { //--- the heartbeat is a compare-and-swap against the value this instance //--- holds, so it fails as soon as another instance owns the guard. A //--- guard that can no longer be proven closes the entry gate here. if(!g_instanceGuard.Heartbeat(TimeLocal())) { if(!g_instanceConflict) PrintFormat("[UQ] ERROR: the duplicate-instance guard '%s' is no longer owned by this instance (%s). New entries are blocked.", g_instanceGV, g_instanceGuard.Detail()); g_instanceConflict = true; g_instanceOwned = false; } } if(InpEnableTrading && !g_risk.LiquidationRequired() && TradingPermissionsOk()) { ServiceTrailing(); ProcessManagedSymbols(); } UpdateStatus(); } //+------------------------------------------------------------------+ //| Execution feedback | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result) { if(trans.type == TRADE_TRANSACTION_DEAL_ADD) { if(StringLen(trans.symbol) > 0) PrintFormat("[UQ] Deal recorded: %s deal=%s order=%s position=%s", trans.symbol, IntegerToString((long)trans.deal), IntegerToString((long)trans.order), IntegerToString((long)trans.position)); //--- a deal is exactly the evidence an outstanding reservation waits //--- for, so it is reconciled at once instead of on the next poll if(g_risk.ReserveState() != UQ_RESERVE_NONE) g_risk.ReconcileReservationDepth(true); return; } //--- a pending order becoming active or disappearing is the other half of //--- the evidence set: reconcile it as well, never assume it was rejected if(trans.type == TRADE_TRANSACTION_ORDER_ADD || trans.type == TRADE_TRANSACTION_ORDER_UPDATE || trans.type == TRADE_TRANSACTION_ORDER_DELETE) { if(g_risk.ReserveState() != UQ_RESERVE_NONE) g_risk.ReconcileReservationDepth(true); return; } if(trans.type == TRADE_TRANSACTION_REQUEST) { if(result.retcode != TRADE_RETCODE_DONE && result.retcode != TRADE_RETCODE_PLACED && result.retcode != 0) { PrintFormat("[UQ] Request rejected: retcode=%d comment='%s'", result.retcode, result.comment); } } } //+------------------------------------------------------------------+