2026-10-30 07:19:59 +07:00
//+------------------------------------------------------------------+
//| 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 |
//| |
2026-10-30 09:19:45 +07:00
//| 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(). |
//| |
2026-10-30 07:19:59 +07:00
//| 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. |
fix: enforce account scope and daily monitoring continuity
Schema 7 adds a persisted monitoring heartbeat, the last recorded gap and a gap hold, with deterministic migration from schema 6 that keeps every lock, cycle identity and reservation.
D0 is no longer invented on startup or on a rollover: a trading day is adopted only when its boundary was observed while the EA was executing, or when the equity path across it is provably constant (no position, no pending order, no account deal). Otherwise the previous D0 is preserved, a monitoring-gap hold blocks every new entry and the exact gap is reported; the hold survives restart and clears only at an observed day boundary or an explicit owner cycle. Static, target and daily thresholds keep their priority, so a breach is still locked and liquidated while a day is unverified.
Account scope is now counted and disclosed for owned and unmanaged positions and pending orders; unmanaged pending orders block entries under the external-exposure guard, and the EA states that the account-wide limits cannot be guaranteed while whole-account liquidation stays disabled.
2026-10-30 11:20:47 +07:00
//| |
//| 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. |
2026-10-30 07:19:59 +07:00
//+------------------------------------------------------------------+
# 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. "
fix: enforce account scope and daily monitoring continuity
Schema 7 adds a persisted monitoring heartbeat, the last recorded gap and a gap hold, with deterministic migration from schema 6 that keeps every lock, cycle identity and reservation.
D0 is no longer invented on startup or on a rollover: a trading day is adopted only when its boundary was observed while the EA was executing, or when the equity path across it is provably constant (no position, no pending order, no account deal). Otherwise the previous D0 is preserved, a monitoring-gap hold blocks every new entry and the exact gap is reported; the hold survives restart and clears only at an observed day boundary or an explicit owner cycle. Static, target and daily thresholds keep their priority, so a breach is still locked and liquidated while a day is unverified.
Account scope is now counted and disclosed for owned and unmanaged positions and pending orders; unmanaged pending orders block entries under the external-exposure guard, and the EA states that the account-wide limits cannot be guaranteed while whole-account liquidation stays disabled.
2026-10-30 11:20:47 +07:00
# 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. "
2026-10-30 07:19:59 +07:00
# 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)
2026-10-30 09:19:45 +07:00
input bool InpRequireAllSymbols = true ; // Fail init when a configured symbol cannot be resolved
2026-10-30 07:19:59 +07:00
input ENUM_TIMEFRAMES InpSignalTimeframe = PERIOD_H1 ; // Signal timeframe
2026-10-30 08:52:18 +07:00
//--- 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)
2026-10-30 07:19:59 +07:00
input string InpPortfolioId = " UQ_PORTFOLIO_A " ; // Portfolio identifier
fix: validate persistent risk-state integrity and fail closed on missing state
* UQ_State: checksummed schema v2 with strict typed parsing, duplicate/unknown-field rejection and semantic validation of every mandatory field; legacy v1 files are validated and migrated on load
* UQ_State: verified backup retention, staged-file verification before commit and post-commit re-verification
* UQ_State: redundant identity-scoped initialization marker reconciled against the persisted state
* UQ_Risk: a new risk cycle requires an explicit owner authorization and a deliberately declared positive B0; the current account balance is never used as a substitute
* UQ_Risk: missing, empty, corrupt, foreign, version-unsupported or schema-invalid state fails closed; backup recovery only under explicit owner authorization
* UQ_Risk: a failed state write degrades the controller to unhealthy, blocks all new entries, preserves the in-memory lock and retries at a bounded interval
* UQ_Risk: daily rollover keeps permanent locks and reports unmonitored intervals explicitly
* UQ_Defs: state schema constants, marker global-variable prefix and the STATE PERSISTENCE FAILED block reason
* Universal_Quant_EA: first-initialization and backup-restore authorization inputs, input validation and state-health status line
2026-10-30 08:07:18 +07:00
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
2026-10-30 07:19:59 +07:00
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 %
2026-10-30 08:52:18 +07:00
input double InpRiskBufferPct = 0.0 ; // Risk buffer for costs and slippage %
2026-10-30 07:19:59 +07:00
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)
Fix duplicate-instance ownership integrity (V2 guard + persistence gate)
Defect A - shared guard deletion and first-creation race:
- The shared V2 guard is now a persistent coordination object. It is never
deleted after creation; release and rollback are both a compare-and-swap
back to the exact unclaimed sentinel (0.0), so a contender that acquires
the guard between an owner's CAS and its cleanup keeps its claim.
- First creation is fenced by ballots: the contender announces its own
ballot, scans for live foreign ballots, re-reads the guard AFTER the scan
(a stale observation of absence is never used), verifies the sentinel it
wrote, claims it by CAS and re-scans afterwards. A rival found at any
point fails the claim closed and the guard is given back with a CAS to
the sentinel - never deleted, never overwritten.
- The owner's ballot stays visible for as long as it owns the guard and is
renewed by the heartbeat; only a contender's own ballot is ever removed,
and only after it no longer owns the guard.
- UQ_GUARD_UNCLAIMED is a distinct protocol state, accepted for the V2
guard namespace only. The legacy guard namespace keeps zero as corrupt
evidence, and negative, fractional, non-finite and future values stay
invalid everywhere.
- Ownership loss is latched for the lifetime of the guard object.
Defect B - ownership loss must stop persistence:
- CUQInstanceGuard implements CUQOwnershipVerifier: a read-only proof that
the guard still carries this instance's value and is still inside the TTL.
- CUQRisk binds that verifier after the guard is acquired and before Init(),
and refuses to initialize without one.
- A persistence boundary inside CUQRisk gates the primary write, the backup
write, the staging write, legacy migration, V2 marker creation, marker
restoration, marker removal and reservation reconciliation. Any failure
latches the loss: the controller is unhealthy, entries are blocked, and
no retry or reacquisition is possible in that instance.
- UQStateSave() takes the verifier and fences its three file mutations
individually, so a loss proven before the primary commit cannot write it.
EA event ordering:
- OnTick() and OnTimer() verify ownership before risk evaluation, lock
servicing, trailing and symbol processing.
- OnTradeTransaction() does not invoke persistence-capable reservation
reconciliation after the loss.
- OnDeinit() releases only a guard this instance provably still holds.
- Heartbeats are throttled; every event performs the read-only proof, so a
two second timer is not the only discovery path.
Input semantics:
- InpPreventDuplicateInstances is deprecated and reserved. Exclusive
persistence ownership is unconditional, and an ownership loss always
blocks entries, so the input has no effect. It is kept so existing .set
files keep loading, and a runtime note reports that it is ignored.
Preserved unchanged: V2 fingerprint and file locators, legacy identity hash
and key, legacy ambiguity refusal, risk limits and strategy logic.
2026-10-31 19:20:50 +07:00
//--- deprecated and reserved: kept only so existing .set files keep loading.
//--- Exclusive persistence ownership is unconditional - it can never be
//--- disabled - and an ownership loss always blocks entries regardless of this
//--- value, so the input has no effect on either. See InstanceOwnershipHeld().
input bool InpPreventDuplicateInstances = true ; // Deprecated, reserved (ownership is always exclusive)
2026-10-30 07:19:59 +07:00
input group " === Owner reset of permanent locks === "
2026-10-30 08:37:46 +07:00
//--- 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.
2026-10-30 07:19:59 +07:00
input ENUM_UQ_RESET_REQUEST InpResetRequest = UQ_RESET_NONE ; // Reset request
2026-10-30 08:37:46 +07:00
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)
2026-10-30 07:19:59 +07:00
//+------------------------------------------------------------------+
//| Globals |
//+------------------------------------------------------------------+
CUQRisk g_risk ;
CUQSymbols g_symbols ;
2026-10-31 13:47:47 +07:00
//--- 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 ;
2026-10-30 07:19:59 +07:00
bool g_instanceConflict = false ;
bool g_instanceOwned = false ;
string g_instanceGV = " " ;
Fix duplicate-instance ownership integrity (V2 guard + persistence gate)
Defect A - shared guard deletion and first-creation race:
- The shared V2 guard is now a persistent coordination object. It is never
deleted after creation; release and rollback are both a compare-and-swap
back to the exact unclaimed sentinel (0.0), so a contender that acquires
the guard between an owner's CAS and its cleanup keeps its claim.
- First creation is fenced by ballots: the contender announces its own
ballot, scans for live foreign ballots, re-reads the guard AFTER the scan
(a stale observation of absence is never used), verifies the sentinel it
wrote, claims it by CAS and re-scans afterwards. A rival found at any
point fails the claim closed and the guard is given back with a CAS to
the sentinel - never deleted, never overwritten.
- The owner's ballot stays visible for as long as it owns the guard and is
renewed by the heartbeat; only a contender's own ballot is ever removed,
and only after it no longer owns the guard.
- UQ_GUARD_UNCLAIMED is a distinct protocol state, accepted for the V2
guard namespace only. The legacy guard namespace keeps zero as corrupt
evidence, and negative, fractional, non-finite and future values stay
invalid everywhere.
- Ownership loss is latched for the lifetime of the guard object.
Defect B - ownership loss must stop persistence:
- CUQInstanceGuard implements CUQOwnershipVerifier: a read-only proof that
the guard still carries this instance's value and is still inside the TTL.
- CUQRisk binds that verifier after the guard is acquired and before Init(),
and refuses to initialize without one.
- A persistence boundary inside CUQRisk gates the primary write, the backup
write, the staging write, legacy migration, V2 marker creation, marker
restoration, marker removal and reservation reconciliation. Any failure
latches the loss: the controller is unhealthy, entries are blocked, and
no retry or reacquisition is possible in that instance.
- UQStateSave() takes the verifier and fences its three file mutations
individually, so a loss proven before the primary commit cannot write it.
EA event ordering:
- OnTick() and OnTimer() verify ownership before risk evaluation, lock
servicing, trailing and symbol processing.
- OnTradeTransaction() does not invoke persistence-capable reservation
reconciliation after the loss.
- OnDeinit() releases only a guard this instance provably still holds.
- Heartbeats are throttled; every event performs the read-only proof, so a
two second timer is not the only discovery path.
Input semantics:
- InpPreventDuplicateInstances is deprecated and reserved. Exclusive
persistence ownership is unconditional, and an ownership loss always
blocks entries, so the input has no effect. It is kept so existing .set
files keep loading, and a runtime note reports that it is ignored.
Preserved unchanged: V2 fingerprint and file locators, legacy identity hash
and key, legacy ambiguity refusal, risk limits and strategy logic.
2026-10-31 19:20:50 +07:00
//--- the heartbeat is throttled because a compare-and-swap over a terminal
//--- global variable is not free, while the read-only proof of ownership is
//--- cheap and therefore runs on every event, not only on the timer
# define UQ_HEARTBEAT_INTERVAL_MS 1000
uint g_lastHeartbeatTick = 0 ;
2026-10-30 08:18:19 +07:00
CUQLiquidator g_liq ;
2026-10-30 07:19:59 +07:00
uint g_lastStatusTick = 0 ;
uint g_lastTrailTick = 0 ;
2026-10-30 09:19:45 +07:00
uint g_lastScheduleTick = 0 ;
uint g_schedulePasses = 0 ;
datetime g_lastScheduleTime = 0 ;
2026-10-30 07:19:59 +07:00
//+------------------------------------------------------------------+
//| 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 ) ;
}
fix: validate persistent risk-state integrity and fail closed on missing state
* UQ_State: checksummed schema v2 with strict typed parsing, duplicate/unknown-field rejection and semantic validation of every mandatory field; legacy v1 files are validated and migrated on load
* UQ_State: verified backup retention, staged-file verification before commit and post-commit re-verification
* UQ_State: redundant identity-scoped initialization marker reconciled against the persisted state
* UQ_Risk: a new risk cycle requires an explicit owner authorization and a deliberately declared positive B0; the current account balance is never used as a substitute
* UQ_Risk: missing, empty, corrupt, foreign, version-unsupported or schema-invalid state fails closed; backup recovery only under explicit owner authorization
* UQ_Risk: a failed state write degrades the controller to unhealthy, blocks all new entries, preserves the in-memory lock and retries at a bounded interval
* UQ_Risk: daily rollover keeps permanent locks and reports unmonitored intervals explicitly
* UQ_Defs: state schema constants, marker global-variable prefix and the STATE PERSISTENCE FAILED block reason
* Universal_Quant_EA: first-initialization and backup-restore authorization inputs, input validation and state-health status line
2026-10-30 08:07:18 +07:00
if ( InpAuthorizeFirstInit & & ! ( InpInitialBalance > 0.0 ) )
{
err = " Authorizing a new risk cycle requires an explicit positive initial balance B0 " ;
return ( false ) ;
}
2026-10-30 07:19:59 +07:00
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 ) ;
}
2026-10-30 08:52:18 +07:00
if ( ! ( InpRiskBufferPct > = 0.0 & & InpRiskBufferPct < = 50.0 ) )
{
err = " Risk buffer must be inside [0, 50] percent " ;
return ( false ) ;
}
2026-10-30 07:19:59 +07:00
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 ) ;
}
2026-10-30 08:37:46 +07:00
if ( InpResetRequest ! = UQ_RESET_NONE )
2026-10-30 07:19:59 +07:00
{
2026-10-30 08:37:46 +07:00
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 ) ;
}
2026-10-30 07:19:59 +07:00
}
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 |
fix: enforce account scope and daily monitoring continuity
Schema 7 adds a persisted monitoring heartbeat, the last recorded gap and a gap hold, with deterministic migration from schema 6 that keeps every lock, cycle identity and reservation.
D0 is no longer invented on startup or on a rollover: a trading day is adopted only when its boundary was observed while the EA was executing, or when the equity path across it is provably constant (no position, no pending order, no account deal). Otherwise the previous D0 is preserved, a monitoring-gap hold blocks every new entry and the exact gap is reported; the hold survives restart and clears only at an observed day boundary or an explicit owner cycle. Static, target and daily thresholds keep their priority, so a breach is still locked and liquidated while a day is unverified.
Account scope is now counted and disclosed for owned and unmanaged positions and pending orders; unmanaged pending orders block entries under the external-exposure guard, and the EA states that the account-wide limits cannot be guaranteed while whole-account liquidation stays disabled.
2026-10-30 11:20:47 +07:00
//| |
//| 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. |
2026-10-30 07:19:59 +07:00
//+------------------------------------------------------------------+
fix: enforce account scope and daily monitoring continuity
Schema 7 adds a persisted monitoring heartbeat, the last recorded gap and a gap hold, with deterministic migration from schema 6 that keeps every lock, cycle identity and reservation.
D0 is no longer invented on startup or on a rollover: a trading day is adopted only when its boundary was observed while the EA was executing, or when the equity path across it is provably constant (no position, no pending order, no account deal). Otherwise the previous D0 is preserved, a monitoring-gap hold blocks every new entry and the exact gap is reported; the hold survives restart and clears only at an observed day boundary or an explicit owner cycle. Static, target and daily thresholds keep their priority, so a breach is still locked and liquidated while a day is unverified.
Account scope is now counted and disclosed for owned and unmanaged positions and pending orders; unmanaged pending orders block entries under the external-exposure guard, and the EA states that the account-wide limits cannot be guaranteed while whole-account liquidation stays disabled.
2026-10-30 11:20:47 +07:00
int g_lastExternalExposure = -1 ;
2026-10-30 07:19:59 +07:00
bool ExternalExposurePresent ( void )
{
fix: enforce account scope and daily monitoring continuity
Schema 7 adds a persisted monitoring heartbeat, the last recorded gap and a gap hold, with deterministic migration from schema 6 that keeps every lock, cycle identity and reservation.
D0 is no longer invented on startup or on a rollover: a trading day is adopted only when its boundary was observed while the EA was executing, or when the equity path across it is provably constant (no position, no pending order, no account deal). Otherwise the previous D0 is preserved, a monitoring-gap hold blocks every new entry and the exact gap is reported; the hold survives restart and clears only at an observed day boundary or an explicit owner cycle. Static, target and daily thresholds keep their priority, so a breach is still locked and liquidated while a day is unverified.
Account scope is now counted and disclosed for owned and unmanaged positions and pending orders; unmanaged pending orders block entries under the external-exposure guard, and the EA states that the account-wide limits cannot be guaranteed while whole-account liquidation stays disabled.
2026-10-30 11:20:47 +07:00
UQAccountScope scope ;
if ( ! UQAccountScopeScan ( ( ulong ) InpMagic , scope ) )
return ( true ) ; // fail closed: the scope cannot be verified
2026-10-30 07:19:59 +07:00
fix: enforce account scope and daily monitoring continuity
Schema 7 adds a persisted monitoring heartbeat, the last recorded gap and a gap hold, with deterministic migration from schema 6 that keeps every lock, cycle identity and reservation.
D0 is no longer invented on startup or on a rollover: a trading day is adopted only when its boundary was observed while the EA was executing, or when the equity path across it is provably constant (no position, no pending order, no account deal). Otherwise the previous D0 is preserved, a monitoring-gap hold blocks every new entry and the exact gap is reported; the hold survives restart and clears only at an observed day boundary or an explicit owner cycle. Static, target and daily thresholds keep their priority, so a breach is still locked and liquidated while a day is unverified.
Account scope is now counted and disclosed for owned and unmanaged positions and pending orders; unmanaged pending orders block entries under the external-exposure guard, and the EA states that the account-wide limits cannot be guaranteed while whole-account liquidation stays disabled.
2026-10-30 11:20:47 +07:00
const int unmanaged = scope . externalPositions + scope . externalPending ;
if ( unmanaged ! = g_lastExternalExposure )
2026-10-30 07:19:59 +07:00
{
fix: enforce account scope and daily monitoring continuity
Schema 7 adds a persisted monitoring heartbeat, the last recorded gap and a gap hold, with deterministic migration from schema 6 that keeps every lock, cycle identity and reservation.
D0 is no longer invented on startup or on a rollover: a trading day is adopted only when its boundary was observed while the EA was executing, or when the equity path across it is provably constant (no position, no pending order, no account deal). Otherwise the previous D0 is preserved, a monitoring-gap hold blocks every new entry and the exact gap is reported; the hold survives restart and clears only at an observed day boundary or an explicit owner cycle. Static, target and daily thresholds keep their priority, so a breach is still locked and liquidated while a day is unverified.
Account scope is now counted and disclosed for owned and unmanaged positions and pending orders; unmanaged pending orders block entries under the external-exposure guard, and the EA states that the account-wide limits cannot be guaranteed while whole-account liquidation stays disabled.
2026-10-30 11:20:47 +07:00
g_lastExternalExposure = unmanaged ;
PrintFormat ( " [UQ] SCOPE: %s " ,
UQScopeDisclosure ( scope , InpBlockOnExternalPositions , InpAllowWholeAccountLiquidation ) ) ;
2026-10-30 07:19:59 +07:00
}
fix: enforce account scope and daily monitoring continuity
Schema 7 adds a persisted monitoring heartbeat, the last recorded gap and a gap hold, with deterministic migration from schema 6 that keeps every lock, cycle identity and reservation.
D0 is no longer invented on startup or on a rollover: a trading day is adopted only when its boundary was observed while the EA was executing, or when the equity path across it is provably constant (no position, no pending order, no account deal). Otherwise the previous D0 is preserved, a monitoring-gap hold blocks every new entry and the exact gap is reported; the hold survives restart and clears only at an observed day boundary or an explicit owner cycle. Static, target and daily thresholds keep their priority, so a breach is still locked and liquidated while a day is unverified.
Account scope is now counted and disclosed for owned and unmanaged positions and pending orders; unmanaged pending orders block entries under the external-exposure guard, and the EA states that the account-wide limits cannot be guaranteed while whole-account liquidation stays disabled.
2026-10-30 11:20:47 +07:00
return ( InpBlockOnExternalPositions & & unmanaged > 0 ) ;
2026-10-30 07:19:59 +07:00
}
//+------------------------------------------------------------------+
2026-10-30 08:18:19 +07:00
//| 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. |
2026-10-30 07:19:59 +07:00
//+------------------------------------------------------------------+
void ServiceRiskLocks ( void )
{
if ( ! g_risk . LiquidationRequired ( ) )
{
2026-10-30 08:18:19 +07:00
if ( ! g_liq . Idle ( ) )
2026-10-30 07:19:59 +07:00
{
2026-10-30 08:18:19 +07:00
PrintFormat ( " [UQ] Risk lock cleared; liquidation management idle again (last state %s). " ,
g_liq . StateText ( ) ) ;
g_liq . Reset ( ) ;
2026-10-30 07:19:59 +07:00
}
return ;
}
2026-10-30 08:18:19 +07:00
//--- a lock arms the machine, including after an EA restart
if ( g_liq . Idle ( ) )
2026-10-30 07:19:59 +07:00
{
2026-10-30 08:18:19 +07:00
g_liq . Arm ( ) ;
PrintFormat ( " [UQ] Liquidation armed by risk control (%s). " , g_risk . BlockText ( ) ) ;
2026-10-30 07:19:59 +07:00
}
2026-10-30 08:18:19 +07:00
g_liq . Service ( ) ;
2026-10-30 07:19:59 +07:00
}
//+------------------------------------------------------------------+
//| 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 ;
2026-10-30 09:19:45 +07:00
//--- 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 ;
}
2026-10-30 07:19:59 +07:00
UQExecConfig ec ;
ec . magic = ( ulong ) InpMagic ;
ec . deviation = InpDeviationPoints ;
ec . portfolioTag = InpPortfolioId ;
ec . riskPct = InpRiskPerTradePct ;
ec . maxAggRiskPct = InpMaxAggRiskPct ;
2026-10-30 08:52:18 +07:00
ec . riskBufferPct = InpRiskBufferPct ;
2026-10-30 07:19:59 +07:00
ec . slAtrMultiplier = InpSlAtrMultiplier ;
ec . maxSpreadPoints = InpMaxSpreadPoints ;
double lots = 0.0 ;
string err = " " ;
2026-10-30 08:52:18 +07:00
if ( ! UQOpenSignalPosition ( symbol , direction , atr , ec , g_risk , lots , err ) )
2026-10-30 07:19:59 +07:00
{
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 ) ;
}
2026-10-30 09:19:45 +07:00
//+------------------------------------------------------------------+
//| 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 ) ;
}
2026-10-30 07:19:59 +07:00
//+------------------------------------------------------------------+
//| Compact on-chart status display |
//+------------------------------------------------------------------+
void UpdateStatus ( void )
{
2026-10-30 08:52:18 +07:00
UQExposure exp ;
g_risk . Exposure ( ( ulong ) InpMagic , exp ) ;
2026-10-30 07:19:59 +07:00
const double equity = g_risk . Equity ( ) ;
2026-10-30 08:52:18 +07:00
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 ;
2026-10-30 07:19:59 +07:00
fix: enforce account scope and daily monitoring continuity
Schema 7 adds a persisted monitoring heartbeat, the last recorded gap and a gap hold, with deterministic migration from schema 6 that keeps every lock, cycle identity and reservation.
D0 is no longer invented on startup or on a rollover: a trading day is adopted only when its boundary was observed while the EA was executing, or when the equity path across it is provably constant (no position, no pending order, no account deal). Otherwise the previous D0 is preserved, a monitoring-gap hold blocks every new entry and the exact gap is reported; the hold survives restart and clears only at an observed day boundary or an explicit owner cycle. Static, target and daily thresholds keep their priority, so a breach is still locked and liquidated while a day is unverified.
Account scope is now counted and disclosed for owned and unmanaged positions and pending orders; unmanaged pending orders block entries under the external-exposure guard, and the EA states that the account-wide limits cannot be guaranteed while whole-account liquidation stays disabled.
2026-10-30 11:20:47 +07:00
UQAccountScope scope ;
const bool scopeKnown = UQAccountScopeScan ( ( ulong ) InpMagic , scope ) ;
2026-10-30 07:19:59 +07:00
2026-10-30 09:19:45 +07:00
string scheduleText = " idle " ;
if ( g_lastScheduleTime > 0 )
scheduleText = TimeToString ( g_lastScheduleTime , TIME_DATE | TIME_SECONDS ) ;
2026-10-30 07:19:59 +07:00
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 ( ) ) ;
2026-10-30 08:37:46 +07:00
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 " ) ) ) ;
2026-10-30 10:20:54 +07:00
//--- 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 ( ) ) ;
fix: recover the latest verified persistent state
The backup was built from the previous primary before the new primary was installed, so it could lag the newest successful update, and a missing primary was still treated as a candidate fresh installation. Neither an interrupted commit nor a stale mirror was detectable.
B1 state schema 6 (defines UQ_STATE_FORMAT=6, UQ_STATE_IDENTITY_FORMAT=5) adds a monotonic stateRevision plus recoveryHold/recoveryHoldTime. Formats 1..5 keep their own field block, so every published checksum still verifies over exactly the block that produced it, and a migration sets generation 1 without inventing a lock event.
B2 UQStateSave is write-ahead and generation-aware: the next generation is derived from what is proven on disk (primary and backup), the staged file is written and read back, the same verified bytes go to the backup and are read back, the primary is replaced and read back, and primary and backup are finally reconciled on generation and content. A newer verified state is never overwritten by an older instance; a failure at any step returns false, which blocks every new entry.
B3 LoadPersistentState orders primary and backup by generation: newer primary wins, equal generations must agree on content (otherwise fail closed), and a backup exactly one generation ahead is the interrupted-commit window and is adopted. Anything else is refused.
B4 a backup is evidence of a previous initialization, so a missing primary is never a fresh installation and never creates a replacement B0; a verified backup is restored only under explicit owner authorization.
B5 a recovered, reordered or superseded state always starts behind a persisted recovery hold (UQ_BLOCK_RECOVERY) that survives restart and only an explicit owner-authorized recovery risk cycle (UQ_RESET_RECOVERY, fresh nonce, explicit B0, flat scope) can clear. A recovery never clears a permanent lock the recovered state carries, and a cycle without a prior permanent lock records no prior-cycle reference instead of an inconsistent one.
B6 the initialization marker is written and then verified; a marker that cannot be persisted or verified fails the load closed.
B7 the status panel reports the persistence generation and an active recovery hold.
2026-10-30 11:09:43 +07:00
s + = StringFormat ( " State : %s | persistence %s | generation %I64d \n " ,
fix: validate persistent risk-state integrity and fail closed on missing state
* UQ_State: checksummed schema v2 with strict typed parsing, duplicate/unknown-field rejection and semantic validation of every mandatory field; legacy v1 files are validated and migrated on load
* UQ_State: verified backup retention, staged-file verification before commit and post-commit re-verification
* UQ_State: redundant identity-scoped initialization marker reconciled against the persisted state
* UQ_Risk: a new risk cycle requires an explicit owner authorization and a deliberately declared positive B0; the current account balance is never used as a substitute
* UQ_Risk: missing, empty, corrupt, foreign, version-unsupported or schema-invalid state fails closed; backup recovery only under explicit owner authorization
* UQ_Risk: a failed state write degrades the controller to unhealthy, blocks all new entries, preserves the in-memory lock and retries at a bounded interval
* UQ_Risk: daily rollover keeps permanent locks and reports unmonitored intervals explicitly
* UQ_Defs: state schema constants, marker global-variable prefix and the STATE PERSISTENCE FAILED block reason
* Universal_Quant_EA: first-initialization and backup-restore authorization inputs, input validation and state-health status line
2026-10-30 08:07:18 +07:00
( g_risk . StateFromBackup ( ) ? " RESTORED-FROM-BACKUP " : ( g_risk . StateMigrated ( ) ? " MIGRATED " : " VERIFIED " ) ) ,
fix: recover the latest verified persistent state
The backup was built from the previous primary before the new primary was installed, so it could lag the newest successful update, and a missing primary was still treated as a candidate fresh installation. Neither an interrupted commit nor a stale mirror was detectable.
B1 state schema 6 (defines UQ_STATE_FORMAT=6, UQ_STATE_IDENTITY_FORMAT=5) adds a monotonic stateRevision plus recoveryHold/recoveryHoldTime. Formats 1..5 keep their own field block, so every published checksum still verifies over exactly the block that produced it, and a migration sets generation 1 without inventing a lock event.
B2 UQStateSave is write-ahead and generation-aware: the next generation is derived from what is proven on disk (primary and backup), the staged file is written and read back, the same verified bytes go to the backup and are read back, the primary is replaced and read back, and primary and backup are finally reconciled on generation and content. A newer verified state is never overwritten by an older instance; a failure at any step returns false, which blocks every new entry.
B3 LoadPersistentState orders primary and backup by generation: newer primary wins, equal generations must agree on content (otherwise fail closed), and a backup exactly one generation ahead is the interrupted-commit window and is adopted. Anything else is refused.
B4 a backup is evidence of a previous initialization, so a missing primary is never a fresh installation and never creates a replacement B0; a verified backup is restored only under explicit owner authorization.
B5 a recovered, reordered or superseded state always starts behind a persisted recovery hold (UQ_BLOCK_RECOVERY) that survives restart and only an explicit owner-authorized recovery risk cycle (UQ_RESET_RECOVERY, fresh nonce, explicit B0, flat scope) can clear. A recovery never clears a permanent lock the recovered state carries, and a cycle without a prior permanent lock records no prior-cycle reference instead of an inconsistent one.
B6 the initialization marker is written and then verified; a marker that cannot be persisted or verified fails the load closed.
B7 the status panel reports the persistence generation and an active recovery hold.
2026-10-30 11:09:43 +07:00
( g_risk . Healthy ( ) ? " OK " : " FAILED " ) , g_risk . StateRevision ( ) ) ;
Fix duplicate-instance ownership integrity (V2 guard + persistence gate)
Defect A - shared guard deletion and first-creation race:
- The shared V2 guard is now a persistent coordination object. It is never
deleted after creation; release and rollback are both a compare-and-swap
back to the exact unclaimed sentinel (0.0), so a contender that acquires
the guard between an owner's CAS and its cleanup keeps its claim.
- First creation is fenced by ballots: the contender announces its own
ballot, scans for live foreign ballots, re-reads the guard AFTER the scan
(a stale observation of absence is never used), verifies the sentinel it
wrote, claims it by CAS and re-scans afterwards. A rival found at any
point fails the claim closed and the guard is given back with a CAS to
the sentinel - never deleted, never overwritten.
- The owner's ballot stays visible for as long as it owns the guard and is
renewed by the heartbeat; only a contender's own ballot is ever removed,
and only after it no longer owns the guard.
- UQ_GUARD_UNCLAIMED is a distinct protocol state, accepted for the V2
guard namespace only. The legacy guard namespace keeps zero as corrupt
evidence, and negative, fractional, non-finite and future values stay
invalid everywhere.
- Ownership loss is latched for the lifetime of the guard object.
Defect B - ownership loss must stop persistence:
- CUQInstanceGuard implements CUQOwnershipVerifier: a read-only proof that
the guard still carries this instance's value and is still inside the TTL.
- CUQRisk binds that verifier after the guard is acquired and before Init(),
and refuses to initialize without one.
- A persistence boundary inside CUQRisk gates the primary write, the backup
write, the staging write, legacy migration, V2 marker creation, marker
restoration, marker removal and reservation reconciliation. Any failure
latches the loss: the controller is unhealthy, entries are blocked, and
no retry or reacquisition is possible in that instance.
- UQStateSave() takes the verifier and fences its three file mutations
individually, so a loss proven before the primary commit cannot write it.
EA event ordering:
- OnTick() and OnTimer() verify ownership before risk evaluation, lock
servicing, trailing and symbol processing.
- OnTradeTransaction() does not invoke persistence-capable reservation
reconciliation after the loss.
- OnDeinit() releases only a guard this instance provably still holds.
- Heartbeats are throttled; every event performs the read-only proof, so a
two second timer is not the only discovery path.
Input semantics:
- InpPreventDuplicateInstances is deprecated and reserved. Exclusive
persistence ownership is unconditional, and an ownership loss always
blocks entries, so the input has no effect. It is kept so existing .set
files keep loading, and a runtime note reports that it is ignored.
Preserved unchanged: V2 fingerprint and file locators, legacy identity hash
and key, legacy ambiguity refusal, risk limits and strategy logic.
2026-10-31 19:20:50 +07:00
s + = StringFormat ( " Ownership : %s \n " ,
( ! g_instanceOwned | | g_instanceConflict
? ( g_risk . OwnershipLost ( ) ? " LOST - persistence refused, entries blocked " : " NOT PROVEN - entries blocked " )
: " HELD (exclusive) " ) ) ;
fix: recover the latest verified persistent state
The backup was built from the previous primary before the new primary was installed, so it could lag the newest successful update, and a missing primary was still treated as a candidate fresh installation. Neither an interrupted commit nor a stale mirror was detectable.
B1 state schema 6 (defines UQ_STATE_FORMAT=6, UQ_STATE_IDENTITY_FORMAT=5) adds a monotonic stateRevision plus recoveryHold/recoveryHoldTime. Formats 1..5 keep their own field block, so every published checksum still verifies over exactly the block that produced it, and a migration sets generation 1 without inventing a lock event.
B2 UQStateSave is write-ahead and generation-aware: the next generation is derived from what is proven on disk (primary and backup), the staged file is written and read back, the same verified bytes go to the backup and are read back, the primary is replaced and read back, and primary and backup are finally reconciled on generation and content. A newer verified state is never overwritten by an older instance; a failure at any step returns false, which blocks every new entry.
B3 LoadPersistentState orders primary and backup by generation: newer primary wins, equal generations must agree on content (otherwise fail closed), and a backup exactly one generation ahead is the interrupted-commit window and is adopted. Anything else is refused.
B4 a backup is evidence of a previous initialization, so a missing primary is never a fresh installation and never creates a replacement B0; a verified backup is restored only under explicit owner authorization.
B5 a recovered, reordered or superseded state always starts behind a persisted recovery hold (UQ_BLOCK_RECOVERY) that survives restart and only an explicit owner-authorized recovery risk cycle (UQ_RESET_RECOVERY, fresh nonce, explicit B0, flat scope) can clear. A recovery never clears a permanent lock the recovered state carries, and a cycle without a prior permanent lock records no prior-cycle reference instead of an inconsistent one.
B6 the initialization marker is written and then verified; a marker that cannot be persisted or verified fails the load closed.
B7 the status panel reports the persistence generation and an active recovery hold.
2026-10-30 11:09:43 +07:00
//--- 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 ) ) ;
2026-10-30 07:19:59 +07:00
s + = StringFormat ( " B0 %.2f | Equity %.2f | Target %.2f \n " ,
g_risk . Baseline ( ) , equity , g_risk . TargetEquity ( ) ) ;
fix: enforce account scope and daily monitoring continuity
Schema 7 adds a persisted monitoring heartbeat, the last recorded gap and a gap hold, with deterministic migration from schema 6 that keeps every lock, cycle identity and reservation.
D0 is no longer invented on startup or on a rollover: a trading day is adopted only when its boundary was observed while the EA was executing, or when the equity path across it is provably constant (no position, no pending order, no account deal). Otherwise the previous D0 is preserved, a monitoring-gap hold blocks every new entry and the exact gap is reported; the hold survives restart and clears only at an observed day boundary or an explicit owner cycle. Static, target and daily thresholds keep their priority, so a breach is still locked and liquidated while a day is unverified.
Account scope is now counted and disclosed for owned and unmanaged positions and pending orders; unmanaged pending orders block entries under the external-exposure guard, and the EA states that the account-wide limits cannot be guaranteed while whole-account liquidation stays disabled.
2026-10-30 11:20:47 +07:00
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 ) ) ;
2026-10-30 07:19:59 +07:00
s + = StringFormat ( " Static DD %.2f%% / %.2f%% | Floors %.2f / %.2f \n " ,
g_risk . StaticDdPct ( ) , g_risk . StaticLimitPct ( ) ,
g_risk . DailyFloor ( ) , g_risk . StaticFloor ( ) ) ;
2026-10-30 08:52:18 +07:00
s + = StringFormat ( " EA exposure %.2f%% / %.2f%%%s \n " ,
2026-10-30 07:19:59 +07:00
riskPct , InpMaxAggRiskPct ,
fix: fail closed on invalid aggregate exposure
UQExposureFromPools() no longer converts an unusable OrderCalcProfit() result into a plausible risk with MathAbs().
Every owned position and pending order is now validated: direction, instrument pricing, volume, entry/reference price, protective stop, stop side, OrderCalcProfit success, finiteness and a strictly adverse monetary result. A zero, positive, non-finite or inconsistent result marks the aggregate exposure unbounded, records the symbol, ticket and reason, and refuses new entries instead of counting the item as zero risk.
The conservative worse-of opening/current reference rule is unchanged, the aggregate formula (1.0% of live equity) is unchanged, and unquantifiable exposure is never represented as zero. The volume sizing and the reserved entry risk in UQ_Exec.mqh require the same adverse result, so a fabricated amount can never enter the reservation or the ceiling.
2026-10-30 10:33:44 +07:00
( exp . bounded ? " " : " UNBOUNDED: " + UQExposureBrief ( exp . detail ) ) ) ;
2026-10-30 08:52:18 +07:00
s + = StringFormat ( " positions %.2f%% (%d) | pending %d | reserved %.2f%% %s \n " ,
posPct , exp . positionCount , exp . pendingCount , resPct ,
UQReserveStateText ( g_risk . ReserveState ( ) ) ) ;
fix: correlate outstanding entry risk reservations
A reservation could be released merely because an unrelated position or pending order existed under the same Magic Number, and an order ticket was treated as a position ticket. An entry whose submission result was lost also released its reserved risk.
- UQ_Defs: state schema 5 adds the reservation identity; ENUM_UQ_SUBMIT_RESULT and a retcode classifier that only accepts proven non-execution.
- UQ_State: reservation id, magic, scope, direction, volume, entry, stop, order/deal/position tickets are persisted, checksummed and strictly validated. A previous-schema reservation migrates to UNRESOLVED with its money preserved, never to NONE.
- UQ_Risk: UQReserveCorrelate links a reservation to the broker only through its own order ticket, a deal whose DEAL_ORDER is that ticket, or the traced POSITION_IDENTIFIER. Ambiguity, a disconnected terminal and a missing record all keep the money reserved as UNRESOLVED and block entries.
- UQ_Exec: UQSendMarket reports the submission outcome; the caller withdraws reserved risk only on a local preflight failure or a proven broker rejection.
- Universal_Quant_EA.mq5: deals and order transactions reconcile the reservation immediately; status and startup diagnostics expose the reservation identity.
2026-10-30 10:43:15 +07:00
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 ( ) ) ) ;
fix: enforce account scope and daily monitoring continuity
Schema 7 adds a persisted monitoring heartbeat, the last recorded gap and a gap hold, with deterministic migration from schema 6 that keeps every lock, cycle identity and reservation.
D0 is no longer invented on startup or on a rollover: a trading day is adopted only when its boundary was observed while the EA was executing, or when the equity path across it is provably constant (no position, no pending order, no account deal). Otherwise the previous D0 is preserved, a monitoring-gap hold blocks every new entry and the exact gap is reported; the hold survives restart and clears only at an observed day boundary or an explicit owner cycle. Static, target and daily thresholds keep their priority, so a breach is still locked and liquidated while a day is unverified.
Account scope is now counted and disclosed for owned and unmanaged positions and pending orders; unmanaged pending orders block entries under the external-exposure guard, and the EA states that the account-wide limits cannot be guaranteed while whole-account liquidation stays disabled.
2026-10-30 11:20:47 +07:00
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 ) ) ;
2026-10-30 08:18:19 +07:00
s + = StringFormat ( " Liquidation : %s | cycles %d \n " , g_liq . StateText ( ) , g_liq . Cycles ( ) ) ;
2026-10-30 09:19:45 +07:00
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 ( ) ) ;
2026-10-30 07:19:59 +07:00
Comment ( s ) ;
}
//+------------------------------------------------------------------+
void UpdateStatusThrottled ( void )
{
const uint now = GetTickCount ( ) ;
if ( ( now - g_lastStatusTick ) < 1000 )
return ;
g_lastStatusTick = now ;
UpdateStatus ( ) ;
}
2026-10-31 13:02:46 +07:00
//+------------------------------------------------------------------+
2026-10-31 13:28:11 +07:00
//| 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. |
//+------------------------------------------------------------------+
Fix duplicate-instance ownership integrity (V2 guard + persistence gate)
Defect A - shared guard deletion and first-creation race:
- The shared V2 guard is now a persistent coordination object. It is never
deleted after creation; release and rollback are both a compare-and-swap
back to the exact unclaimed sentinel (0.0), so a contender that acquires
the guard between an owner's CAS and its cleanup keeps its claim.
- First creation is fenced by ballots: the contender announces its own
ballot, scans for live foreign ballots, re-reads the guard AFTER the scan
(a stale observation of absence is never used), verifies the sentinel it
wrote, claims it by CAS and re-scans afterwards. A rival found at any
point fails the claim closed and the guard is given back with a CAS to
the sentinel - never deleted, never overwritten.
- The owner's ballot stays visible for as long as it owns the guard and is
renewed by the heartbeat; only a contender's own ballot is ever removed,
and only after it no longer owns the guard.
- UQ_GUARD_UNCLAIMED is a distinct protocol state, accepted for the V2
guard namespace only. The legacy guard namespace keeps zero as corrupt
evidence, and negative, fractional, non-finite and future values stay
invalid everywhere.
- Ownership loss is latched for the lifetime of the guard object.
Defect B - ownership loss must stop persistence:
- CUQInstanceGuard implements CUQOwnershipVerifier: a read-only proof that
the guard still carries this instance's value and is still inside the TTL.
- CUQRisk binds that verifier after the guard is acquired and before Init(),
and refuses to initialize without one.
- A persistence boundary inside CUQRisk gates the primary write, the backup
write, the staging write, legacy migration, V2 marker creation, marker
restoration, marker removal and reservation reconciliation. Any failure
latches the loss: the controller is unhealthy, entries are blocked, and
no retry or reacquisition is possible in that instance.
- UQStateSave() takes the verifier and fences its three file mutations
individually, so a loss proven before the primary commit cannot write it.
EA event ordering:
- OnTick() and OnTimer() verify ownership before risk evaluation, lock
servicing, trailing and symbol processing.
- OnTradeTransaction() does not invoke persistence-capable reservation
reconciliation after the loss.
- OnDeinit() releases only a guard this instance provably still holds.
- Heartbeats are throttled; every event performs the read-only proof, so a
two second timer is not the only discovery path.
Input semantics:
- InpPreventDuplicateInstances is deprecated and reserved. Exclusive
persistence ownership is unconditional, and an ownership loss always
blocks entries, so the input has no effect. It is kept so existing .set
files keep loading, and a runtime note reports that it is ignored.
Preserved unchanged: V2 fingerprint and file locators, legacy identity hash
and key, legacy ambiguity refusal, risk limits and strategy logic.
2026-10-31 19:20:50 +07:00
//+------------------------------------------------------------------+
//| Latch the loss of exclusive instance ownership |
//| |
//| The loss is permanent for this run: the entry gate closes, the |
//| risk controller is told to refuse every persistent mutation and |
//| nothing reacquires the guard inside this instance. |
//+------------------------------------------------------------------+
void InstanceOwnershipLost ( const string why )
{
if ( ! g_instanceConflict )
{
PrintFormat ( " [UQ] ERROR: this instance no longer owns the duplicate-instance guard '%s' (%s). New entries are blocked and no persistent risk state is read, written, migrated or marked from this point on. " ,
g_instanceGV , why ) ;
}
g_instanceConflict = true ; // latched for the lifetime of this instance
g_instanceOwned = false ;
g_risk . DeclareOwnershipLost ( why ) ;
}
//+------------------------------------------------------------------+
//| Proof of ownership before anything that may mutate risk state |
//| |
//| The heartbeat is throttled, the read-only verification is not: |
//| every persistence-capable event verifies the guard instead of |
//| trusting the two second timer. The first failure latches the loss. |
//+------------------------------------------------------------------+
bool InstanceOwnershipHeld ( const bool allowHeartbeat )
{
if ( ! g_instanceOwned | | g_instanceConflict )
return ( false ) ;
if ( g_instanceGuard . Lost ( ) )
{
InstanceOwnershipLost ( g_instanceGuard . Detail ( ) ) ;
return ( false ) ;
}
if ( allowHeartbeat )
{
const uint now = GetTickCount ( ) ;
if ( g_lastHeartbeatTick = = 0 | | ( now - g_lastHeartbeatTick ) > = UQ_HEARTBEAT_INTERVAL_MS )
{
g_lastHeartbeatTick = now ;
//--- a compare-and-swap against the value this instance holds, so it
//--- fails as soon as another instance owns the guard
if ( ! g_instanceGuard . Heartbeat ( TimeLocal ( ) ) )
{
InstanceOwnershipLost ( g_instanceGuard . Detail ( ) ) ;
return ( false ) ;
}
}
}
string detail = " " ;
if ( ! g_instanceGuard . OwnershipVerified ( detail ) )
{
InstanceOwnershipLost ( detail ) ;
return ( false ) ;
}
return ( true ) ;
}
2026-10-30 07:19:59 +07:00
//+------------------------------------------------------------------+
//| 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 ;
2026-10-30 08:52:18 +07:00
rc . magic = ( ulong ) InpMagic ;
2026-10-30 07:19:59 +07:00
rc . portfolio = InpPortfolioId ;
rc . balanceOverride = InpInitialBalance ;
rc . dailyLimitPct = InpDailyLossPct ;
rc . staticLimitPct = InpStaticDdPct ;
rc . targetPct = InpTargetPct ;
rc . resetRequest = InpResetRequest ;
rc . resetToken = InpResetToken ;
2026-10-30 08:37:46 +07:00
rc . resetBaseline = InpResetBaseline ;
rc . resetNonce = InpResetNonce ;
fix: validate persistent risk-state integrity and fail closed on missing state
* UQ_State: checksummed schema v2 with strict typed parsing, duplicate/unknown-field rejection and semantic validation of every mandatory field; legacy v1 files are validated and migrated on load
* UQ_State: verified backup retention, staged-file verification before commit and post-commit re-verification
* UQ_State: redundant identity-scoped initialization marker reconciled against the persisted state
* UQ_Risk: a new risk cycle requires an explicit owner authorization and a deliberately declared positive B0; the current account balance is never used as a substitute
* UQ_Risk: missing, empty, corrupt, foreign, version-unsupported or schema-invalid state fails closed; backup recovery only under explicit owner authorization
* UQ_Risk: a failed state write degrades the controller to unhealthy, blocks all new entries, preserves the in-memory lock and retries at a bounded interval
* UQ_Risk: daily rollover keeps permanent locks and reports unmonitored intervals explicitly
* UQ_Defs: state schema constants, marker global-variable prefix and the STATE PERSISTENCE FAILED block reason
* Universal_Quant_EA: first-initialization and backup-restore authorization inputs, input validation and state-health status line
2026-10-30 08:07:18 +07:00
rc . allowFirstInit = InpAuthorizeFirstInit ;
rc . allowBackupRestore = InpAllowBackupRestore ;
2026-10-30 07:19:59 +07:00
2026-10-30 08:37:46 +07:00
//--- 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 ;
2026-10-31 13:47:47 +07:00
//--- 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
Fix duplicate-instance ownership integrity (V2 guard + persistence gate)
Defect A - shared guard deletion and first-creation race:
- The shared V2 guard is now a persistent coordination object. It is never
deleted after creation; release and rollback are both a compare-and-swap
back to the exact unclaimed sentinel (0.0), so a contender that acquires
the guard between an owner's CAS and its cleanup keeps its claim.
- First creation is fenced by ballots: the contender announces its own
ballot, scans for live foreign ballots, re-reads the guard AFTER the scan
(a stale observation of absence is never used), verifies the sentinel it
wrote, claims it by CAS and re-scans afterwards. A rival found at any
point fails the claim closed and the guard is given back with a CAS to
the sentinel - never deleted, never overwritten.
- The owner's ballot stays visible for as long as it owns the guard and is
renewed by the heartbeat; only a contender's own ballot is ever removed,
and only after it no longer owns the guard.
- UQ_GUARD_UNCLAIMED is a distinct protocol state, accepted for the V2
guard namespace only. The legacy guard namespace keeps zero as corrupt
evidence, and negative, fractional, non-finite and future values stay
invalid everywhere.
- Ownership loss is latched for the lifetime of the guard object.
Defect B - ownership loss must stop persistence:
- CUQInstanceGuard implements CUQOwnershipVerifier: a read-only proof that
the guard still carries this instance's value and is still inside the TTL.
- CUQRisk binds that verifier after the guard is acquired and before Init(),
and refuses to initialize without one.
- A persistence boundary inside CUQRisk gates the primary write, the backup
write, the staging write, legacy migration, V2 marker creation, marker
restoration, marker removal and reservation reconciliation. Any failure
latches the loss: the controller is unhealthy, entries are blocked, and
no retry or reacquisition is possible in that instance.
- UQStateSave() takes the verifier and fences its three file mutations
individually, so a loss proven before the primary commit cannot write it.
EA event ordering:
- OnTick() and OnTimer() verify ownership before risk evaluation, lock
servicing, trailing and symbol processing.
- OnTradeTransaction() does not invoke persistence-capable reservation
reconciliation after the loss.
- OnDeinit() releases only a guard this instance provably still holds.
- Heartbeats are throttled; every event performs the read-only proof, so a
two second timer is not the only discovery path.
Input semantics:
- InpPreventDuplicateInstances is deprecated and reserved. Exclusive
persistence ownership is unconditional, and an ownership loss always
blocks entries, so the input has no effect. It is kept so existing .set
files keep loading, and a runtime note reports that it is ignored.
Preserved unchanged: V2 fingerprint and file locators, legacy identity hash
and key, legacy ambiguity refusal, risk limits and strategy logic.
2026-10-31 19:20:50 +07:00
//--- g_risk.Init(). Exclusive ownership is unconditional and cannot be
//--- disabled by any input; InpPreventDuplicateInstances is deprecated and
//--- reserved.
2026-10-31 13:47:47 +07:00
//--- 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 ) ;
Fix duplicate-instance ownership integrity (V2 guard + persistence gate)
Defect A - shared guard deletion and first-creation race:
- The shared V2 guard is now a persistent coordination object. It is never
deleted after creation; release and rollback are both a compare-and-swap
back to the exact unclaimed sentinel (0.0), so a contender that acquires
the guard between an owner's CAS and its cleanup keeps its claim.
- First creation is fenced by ballots: the contender announces its own
ballot, scans for live foreign ballots, re-reads the guard AFTER the scan
(a stale observation of absence is never used), verifies the sentinel it
wrote, claims it by CAS and re-scans afterwards. A rival found at any
point fails the claim closed and the guard is given back with a CAS to
the sentinel - never deleted, never overwritten.
- The owner's ballot stays visible for as long as it owns the guard and is
renewed by the heartbeat; only a contender's own ballot is ever removed,
and only after it no longer owns the guard.
- UQ_GUARD_UNCLAIMED is a distinct protocol state, accepted for the V2
guard namespace only. The legacy guard namespace keeps zero as corrupt
evidence, and negative, fractional, non-finite and future values stay
invalid everywhere.
- Ownership loss is latched for the lifetime of the guard object.
Defect B - ownership loss must stop persistence:
- CUQInstanceGuard implements CUQOwnershipVerifier: a read-only proof that
the guard still carries this instance's value and is still inside the TTL.
- CUQRisk binds that verifier after the guard is acquired and before Init(),
and refuses to initialize without one.
- A persistence boundary inside CUQRisk gates the primary write, the backup
write, the staging write, legacy migration, V2 marker creation, marker
restoration, marker removal and reservation reconciliation. Any failure
latches the loss: the controller is unhealthy, entries are blocked, and
no retry or reacquisition is possible in that instance.
- UQStateSave() takes the verifier and fences its three file mutations
individually, so a loss proven before the primary commit cannot write it.
EA event ordering:
- OnTick() and OnTimer() verify ownership before risk evaluation, lock
servicing, trailing and symbol processing.
- OnTradeTransaction() does not invoke persistence-capable reservation
reconciliation after the loss.
- OnDeinit() releases only a guard this instance provably still holds.
- Heartbeats are throttled; every event performs the read-only proof, so a
two second timer is not the only discovery path.
Input semantics:
- InpPreventDuplicateInstances is deprecated and reserved. Exclusive
persistence ownership is unconditional, and an ownership loss always
blocks entries, so the input has no effect. It is kept so existing .set
files keep loading, and a runtime note reports that it is ignored.
Preserved unchanged: V2 fingerprint and file locators, legacy identity hash
and key, legacy ambiguity refusal, risk limits and strategy logic.
2026-10-31 19:20:50 +07:00
//--- the deprecated input is reported and never obeyed: exclusive ownership
//--- is unconditional, so its value can neither widen access to the risk
//--- scope nor keep persistence alive after an ownership loss
if ( ! InpPreventDuplicateInstances )
PrintFormat ( " [UQ] NOTE: InpPreventDuplicateInstances=false is deprecated and has no effect. Exclusive persistence ownership is mandatory and an ownership loss always blocks entries. " ) ;
//--- the risk controller receives the ownership verifier of the guard this
//--- instance holds BEFORE it may read, migrate, create or save anything
g_risk . BindOwnership ( & g_instanceGuard ) ;
2026-10-31 13:47:47 +07:00
//--- only now may the persistent risk scope be touched
2026-10-30 07:19:59 +07:00
if ( ! g_risk .Init ( rc ) )
{
2026-10-31 13:47:47 +07:00
PrintFormat ( " [UQ] INIT FAILED: risk controller: %s. The instance guard is released. " , g_risk . LastError ( ) ) ;
g_instanceGuard . Release ( ) ;
g_instanceOwned = false ;
g_instanceConflict = true ;
2026-10-30 07:19:59 +07:00
return ( INIT_FAILED ) ;
}
2026-10-30 08:37:46 +07:00
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 ( ) ) ;
2026-10-30 08:52:18 +07:00
PrintFormat ( " [UQ] Ownership scope: magic=%s portfolio=%s. One Magic Number must belong to exactly one deployment. " ,
IntegerToString ( InpMagic ) , InpPortfolioId ) ;
fix: enforce account scope and daily monitoring continuity
Schema 7 adds a persisted monitoring heartbeat, the last recorded gap and a gap hold, with deterministic migration from schema 6 that keeps every lock, cycle identity and reservation.
D0 is no longer invented on startup or on a rollover: a trading day is adopted only when its boundary was observed while the EA was executing, or when the equity path across it is provably constant (no position, no pending order, no account deal). Otherwise the previous D0 is preserved, a monitoring-gap hold blocks every new entry and the exact gap is reported; the hold survives restart and clears only at an observed day boundary or an explicit owner cycle. Static, target and daily thresholds keep their priority, so a breach is still locked and liquidated while a day is unverified.
Account scope is now counted and disclosed for owned and unmanaged positions and pending orders; unmanaged pending orders block entries under the external-exposure guard, and the EA states that the account-wide limits cannot be guaranteed while whole-account liquidation stays disabled.
2026-10-30 11:20:47 +07:00
//--- 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. " ) ;
}
2026-10-30 08:52:18 +07:00
if ( g_risk . ReserveState ( ) ! = UQ_RESERVE_NONE )
fix: correlate outstanding entry risk reservations
A reservation could be released merely because an unrelated position or pending order existed under the same Magic Number, and an order ticket was treated as a position ticket. An entry whose submission result was lost also released its reserved risk.
- UQ_Defs: state schema 5 adds the reservation identity; ENUM_UQ_SUBMIT_RESULT and a retcode classifier that only accepts proven non-execution.
- UQ_State: reservation id, magic, scope, direction, volume, entry, stop, order/deal/position tickets are persisted, checksummed and strictly validated. A previous-schema reservation migrates to UNRESOLVED with its money preserved, never to NONE.
- UQ_Risk: UQReserveCorrelate links a reservation to the broker only through its own order ticket, a deal whose DEAL_ORDER is that ticket, or the traced POSITION_IDENTIFIER. Ambiguity, a disconnected terminal and a missing record all keep the money reserved as UNRESOLVED and block entries.
- UQ_Exec: UQSendMarket reports the submission outcome; the caller withdraws reserved risk only on a local preflight failure or a proven broker rejection.
- Universal_Quant_EA.mq5: deals and order transactions reconcile the reservation immediately; status and startup diagnostics expose the reservation identity.
2026-10-30 10:43:15 +07:00
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. " ,
2026-10-30 08:52:18 +07:00
g_risk . ReservedRisk ( ) , g_risk . ReserveSymbol ( ) ,
fix: correlate outstanding entry risk reservations
A reservation could be released merely because an unrelated position or pending order existed under the same Magic Number, and an order ticket was treated as a position ticket. An entry whose submission result was lost also released its reserved risk.
- UQ_Defs: state schema 5 adds the reservation identity; ENUM_UQ_SUBMIT_RESULT and a retcode classifier that only accepts proven non-execution.
- UQ_State: reservation id, magic, scope, direction, volume, entry, stop, order/deal/position tickets are persisted, checksummed and strictly validated. A previous-schema reservation migrates to UNRESOLVED with its money preserved, never to NONE.
- UQ_Risk: UQReserveCorrelate links a reservation to the broker only through its own order ticket, a deal whose DEAL_ORDER is that ticket, or the traced POSITION_IDENTIFIER. Ambiguity, a disconnected terminal and a missing record all keep the money reserved as UNRESOLVED and block entries.
- UQ_Exec: UQSendMarket reports the submission outcome; the caller withdraws reserved risk only on a local preflight failure or a proven broker rejection.
- Universal_Quant_EA.mq5: deals and order transactions reconcile the reservation immediately; status and startup diagnostics expose the reservation identity.
2026-10-30 10:43:15 +07:00
UQReserveStateText ( g_risk . ReserveState ( ) ) , g_risk . ReserveId ( ) ,
2026-10-30 08:52:18 +07:00
IntegerToString ( ( long ) g_risk . ReserveTicket ( ) ) ,
fix: correlate outstanding entry risk reservations
A reservation could be released merely because an unrelated position or pending order existed under the same Magic Number, and an order ticket was treated as a position ticket. An entry whose submission result was lost also released its reserved risk.
- UQ_Defs: state schema 5 adds the reservation identity; ENUM_UQ_SUBMIT_RESULT and a retcode classifier that only accepts proven non-execution.
- UQ_State: reservation id, magic, scope, direction, volume, entry, stop, order/deal/position tickets are persisted, checksummed and strictly validated. A previous-schema reservation migrates to UNRESOLVED with its money preserved, never to NONE.
- UQ_Risk: UQReserveCorrelate links a reservation to the broker only through its own order ticket, a deal whose DEAL_ORDER is that ticket, or the traced POSITION_IDENTIFIER. Ambiguity, a disconnected terminal and a missing record all keep the money reserved as UNRESOLVED and block entries.
- UQ_Exec: UQSendMarket reports the submission outcome; the caller withdraws reserved risk only on a local preflight failure or a proven broker rejection.
- Universal_Quant_EA.mq5: deals and order transactions reconcile the reservation immediately; status and startup diagnostics expose the reservation identity.
2026-10-30 10:43:15 +07:00
IntegerToString ( ( long ) g_risk . ReserveDeal ( ) ) ,
IntegerToString ( ( long ) g_risk . ReservePosition ( ) ) ) ;
2026-10-30 08:52:18 +07:00
2026-10-30 07:19:59 +07:00
//--- symbol registry and indicator handles
if ( ! g_symbols .Init ( InpSymbolMode , InpSymbolList , InpSignalTimeframe ,
2026-10-30 09:19:45 +07:00
InpChannelLookback , InpEmaPeriod , InpAtrPeriod ,
InpRequireAllSymbols ) )
2026-10-30 07:19:59 +07:00
{
PrintFormat ( " [UQ] INIT FAILED: %s " , g_symbols . LastError ( ) ) ;
2026-10-31 13:47:47 +07:00
g_instanceGuard . Release ( ) ;
g_instanceOwned = false ;
2026-10-30 07:19:59 +07:00
return ( INIT_PARAMETERS_INCORRECT ) ;
}
2026-10-30 09:19:45 +07:00
PrintFormat ( " [UQ] %d symbol(s) managed | broker tag %s | %d rejected. " ,
g_symbols . Count ( ) , g_symbols . TagText ( ) , g_symbols . Skipped ( ) ) ;
2026-10-30 08:18:19 +07:00
g_liq .Init ( ( ulong ) InpMagic , InpDeviationPoints , InpAllowWholeAccountLiquidation ) ;
2026-10-30 07:19:59 +07:00
EventSetTimer ( 2 ) ;
UpdateStatus ( ) ;
return ( INIT_SUCCEEDED ) ;
}
//+------------------------------------------------------------------+
//| Deinitialisation |
//+------------------------------------------------------------------+
void OnDeinit ( const int reason )
{
EventKillTimer ( ) ;
g_symbols . Deinit ( ) ;
2026-10-31 13:47:47 +07:00
//--- 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 )
2026-10-30 07:19:59 +07:00
{
2026-10-31 13:47:47 +07:00
if ( ! g_instanceGuard . Release ( ) )
PrintFormat ( " [UQ] Instance guard '%s' was not released: %s " ,
g_instanceGV , g_instanceGuard . Detail ( ) ) ;
2026-10-30 07:19:59 +07:00
g_instanceOwned = false ;
}
Comment ( " " ) ;
PrintFormat ( " [UQ] Stopped (reason=%d). Final lock state: %s " , reason , g_risk . BlockText ( ) ) ;
}
//+------------------------------------------------------------------+
//| Main tick handler |
//+------------------------------------------------------------------+
void OnTick ( void )
{
Fix duplicate-instance ownership integrity (V2 guard + persistence gate)
Defect A - shared guard deletion and first-creation race:
- The shared V2 guard is now a persistent coordination object. It is never
deleted after creation; release and rollback are both a compare-and-swap
back to the exact unclaimed sentinel (0.0), so a contender that acquires
the guard between an owner's CAS and its cleanup keeps its claim.
- First creation is fenced by ballots: the contender announces its own
ballot, scans for live foreign ballots, re-reads the guard AFTER the scan
(a stale observation of absence is never used), verifies the sentinel it
wrote, claims it by CAS and re-scans afterwards. A rival found at any
point fails the claim closed and the guard is given back with a CAS to
the sentinel - never deleted, never overwritten.
- The owner's ballot stays visible for as long as it owns the guard and is
renewed by the heartbeat; only a contender's own ballot is ever removed,
and only after it no longer owns the guard.
- UQ_GUARD_UNCLAIMED is a distinct protocol state, accepted for the V2
guard namespace only. The legacy guard namespace keeps zero as corrupt
evidence, and negative, fractional, non-finite and future values stay
invalid everywhere.
- Ownership loss is latched for the lifetime of the guard object.
Defect B - ownership loss must stop persistence:
- CUQInstanceGuard implements CUQOwnershipVerifier: a read-only proof that
the guard still carries this instance's value and is still inside the TTL.
- CUQRisk binds that verifier after the guard is acquired and before Init(),
and refuses to initialize without one.
- A persistence boundary inside CUQRisk gates the primary write, the backup
write, the staging write, legacy migration, V2 marker creation, marker
restoration, marker removal and reservation reconciliation. Any failure
latches the loss: the controller is unhealthy, entries are blocked, and
no retry or reacquisition is possible in that instance.
- UQStateSave() takes the verifier and fences its three file mutations
individually, so a loss proven before the primary commit cannot write it.
EA event ordering:
- OnTick() and OnTimer() verify ownership before risk evaluation, lock
servicing, trailing and symbol processing.
- OnTradeTransaction() does not invoke persistence-capable reservation
reconciliation after the loss.
- OnDeinit() releases only a guard this instance provably still holds.
- Heartbeats are throttled; every event performs the read-only proof, so a
two second timer is not the only discovery path.
Input semantics:
- InpPreventDuplicateInstances is deprecated and reserved. Exclusive
persistence ownership is unconditional, and an ownership loss always
blocks entries, so the input has no effect. It is kept so existing .set
files keep loading, and a runtime note reports that it is ignored.
Preserved unchanged: V2 fingerprint and file locators, legacy identity hash
and key, legacy ambiguity refusal, risk limits and strategy logic.
2026-10-31 19:20:50 +07:00
//--- ownership is proven before any persistence-capable path runs: risk
//--- evaluation, lock servicing, trailing management and symbol processing
//--- can all mutate persistent risk state
if ( ! InstanceOwnershipHeld ( true ) )
{
UpdateStatusThrottled ( ) ;
return ;
}
2026-10-30 07:19:59 +07:00
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
2026-10-30 09:19:45 +07:00
ServiceTrailing ( ) ;
2026-10-30 07:19:59 +07:00
2026-10-30 09:19:45 +07:00
//--- the tick path only wakes the scheduler; all symbol processing,
//--- including the account risk gate, lives in one place
ProcessManagedSymbols ( ) ;
2026-10-30 07:19:59 +07:00
UpdateStatusThrottled ( ) ;
}
//+------------------------------------------------------------------+
2026-10-30 09:19:45 +07:00
//| 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. |
2026-10-30 07:19:59 +07:00
//+------------------------------------------------------------------+
void OnTimer ( void )
{
Fix duplicate-instance ownership integrity (V2 guard + persistence gate)
Defect A - shared guard deletion and first-creation race:
- The shared V2 guard is now a persistent coordination object. It is never
deleted after creation; release and rollback are both a compare-and-swap
back to the exact unclaimed sentinel (0.0), so a contender that acquires
the guard between an owner's CAS and its cleanup keeps its claim.
- First creation is fenced by ballots: the contender announces its own
ballot, scans for live foreign ballots, re-reads the guard AFTER the scan
(a stale observation of absence is never used), verifies the sentinel it
wrote, claims it by CAS and re-scans afterwards. A rival found at any
point fails the claim closed and the guard is given back with a CAS to
the sentinel - never deleted, never overwritten.
- The owner's ballot stays visible for as long as it owns the guard and is
renewed by the heartbeat; only a contender's own ballot is ever removed,
and only after it no longer owns the guard.
- UQ_GUARD_UNCLAIMED is a distinct protocol state, accepted for the V2
guard namespace only. The legacy guard namespace keeps zero as corrupt
evidence, and negative, fractional, non-finite and future values stay
invalid everywhere.
- Ownership loss is latched for the lifetime of the guard object.
Defect B - ownership loss must stop persistence:
- CUQInstanceGuard implements CUQOwnershipVerifier: a read-only proof that
the guard still carries this instance's value and is still inside the TTL.
- CUQRisk binds that verifier after the guard is acquired and before Init(),
and refuses to initialize without one.
- A persistence boundary inside CUQRisk gates the primary write, the backup
write, the staging write, legacy migration, V2 marker creation, marker
restoration, marker removal and reservation reconciliation. Any failure
latches the loss: the controller is unhealthy, entries are blocked, and
no retry or reacquisition is possible in that instance.
- UQStateSave() takes the verifier and fences its three file mutations
individually, so a loss proven before the primary commit cannot write it.
EA event ordering:
- OnTick() and OnTimer() verify ownership before risk evaluation, lock
servicing, trailing and symbol processing.
- OnTradeTransaction() does not invoke persistence-capable reservation
reconciliation after the loss.
- OnDeinit() releases only a guard this instance provably still holds.
- Heartbeats are throttled; every event performs the read-only proof, so a
two second timer is not the only discovery path.
Input semantics:
- InpPreventDuplicateInstances is deprecated and reserved. Exclusive
persistence ownership is unconditional, and an ownership loss always
blocks entries, so the input has no effect. It is kept so existing .set
files keep loading, and a runtime note reports that it is ignored.
Preserved unchanged: V2 fingerprint and file locators, legacy identity hash
and key, legacy ambiguity refusal, risk limits and strategy logic.
2026-10-31 19:20:50 +07:00
//--- the timer is this instance's heartbeat, so ownership is refreshed and
//--- verified BEFORE anything that may mutate persistent risk state runs
if ( ! InstanceOwnershipHeld ( true ) )
2026-10-31 13:02:46 +07:00
{
Fix duplicate-instance ownership integrity (V2 guard + persistence gate)
Defect A - shared guard deletion and first-creation race:
- The shared V2 guard is now a persistent coordination object. It is never
deleted after creation; release and rollback are both a compare-and-swap
back to the exact unclaimed sentinel (0.0), so a contender that acquires
the guard between an owner's CAS and its cleanup keeps its claim.
- First creation is fenced by ballots: the contender announces its own
ballot, scans for live foreign ballots, re-reads the guard AFTER the scan
(a stale observation of absence is never used), verifies the sentinel it
wrote, claims it by CAS and re-scans afterwards. A rival found at any
point fails the claim closed and the guard is given back with a CAS to
the sentinel - never deleted, never overwritten.
- The owner's ballot stays visible for as long as it owns the guard and is
renewed by the heartbeat; only a contender's own ballot is ever removed,
and only after it no longer owns the guard.
- UQ_GUARD_UNCLAIMED is a distinct protocol state, accepted for the V2
guard namespace only. The legacy guard namespace keeps zero as corrupt
evidence, and negative, fractional, non-finite and future values stay
invalid everywhere.
- Ownership loss is latched for the lifetime of the guard object.
Defect B - ownership loss must stop persistence:
- CUQInstanceGuard implements CUQOwnershipVerifier: a read-only proof that
the guard still carries this instance's value and is still inside the TTL.
- CUQRisk binds that verifier after the guard is acquired and before Init(),
and refuses to initialize without one.
- A persistence boundary inside CUQRisk gates the primary write, the backup
write, the staging write, legacy migration, V2 marker creation, marker
restoration, marker removal and reservation reconciliation. Any failure
latches the loss: the controller is unhealthy, entries are blocked, and
no retry or reacquisition is possible in that instance.
- UQStateSave() takes the verifier and fences its three file mutations
individually, so a loss proven before the primary commit cannot write it.
EA event ordering:
- OnTick() and OnTimer() verify ownership before risk evaluation, lock
servicing, trailing and symbol processing.
- OnTradeTransaction() does not invoke persistence-capable reservation
reconciliation after the loss.
- OnDeinit() releases only a guard this instance provably still holds.
- Heartbeats are throttled; every event performs the read-only proof, so a
two second timer is not the only discovery path.
Input semantics:
- InpPreventDuplicateInstances is deprecated and reserved. Exclusive
persistence ownership is unconditional, and an ownership loss always
blocks entries, so the input has no effect. It is kept so existing .set
files keep loading, and a runtime note reports that it is ignored.
Preserved unchanged: V2 fingerprint and file locators, legacy identity hash
and key, legacy ambiguity refusal, risk limits and strategy logic.
2026-10-31 19:20:50 +07:00
UpdateStatus ( ) ;
return ;
2026-10-31 13:02:46 +07:00
}
2026-10-30 07:19:59 +07:00
Fix duplicate-instance ownership integrity (V2 guard + persistence gate)
Defect A - shared guard deletion and first-creation race:
- The shared V2 guard is now a persistent coordination object. It is never
deleted after creation; release and rollback are both a compare-and-swap
back to the exact unclaimed sentinel (0.0), so a contender that acquires
the guard between an owner's CAS and its cleanup keeps its claim.
- First creation is fenced by ballots: the contender announces its own
ballot, scans for live foreign ballots, re-reads the guard AFTER the scan
(a stale observation of absence is never used), verifies the sentinel it
wrote, claims it by CAS and re-scans afterwards. A rival found at any
point fails the claim closed and the guard is given back with a CAS to
the sentinel - never deleted, never overwritten.
- The owner's ballot stays visible for as long as it owns the guard and is
renewed by the heartbeat; only a contender's own ballot is ever removed,
and only after it no longer owns the guard.
- UQ_GUARD_UNCLAIMED is a distinct protocol state, accepted for the V2
guard namespace only. The legacy guard namespace keeps zero as corrupt
evidence, and negative, fractional, non-finite and future values stay
invalid everywhere.
- Ownership loss is latched for the lifetime of the guard object.
Defect B - ownership loss must stop persistence:
- CUQInstanceGuard implements CUQOwnershipVerifier: a read-only proof that
the guard still carries this instance's value and is still inside the TTL.
- CUQRisk binds that verifier after the guard is acquired and before Init(),
and refuses to initialize without one.
- A persistence boundary inside CUQRisk gates the primary write, the backup
write, the staging write, legacy migration, V2 marker creation, marker
restoration, marker removal and reservation reconciliation. Any failure
latches the loss: the controller is unhealthy, entries are blocked, and
no retry or reacquisition is possible in that instance.
- UQStateSave() takes the verifier and fences its three file mutations
individually, so a loss proven before the primary commit cannot write it.
EA event ordering:
- OnTick() and OnTimer() verify ownership before risk evaluation, lock
servicing, trailing and symbol processing.
- OnTradeTransaction() does not invoke persistence-capable reservation
reconciliation after the loss.
- OnDeinit() releases only a guard this instance provably still holds.
- Heartbeats are throttled; every event performs the read-only proof, so a
two second timer is not the only discovery path.
Input semantics:
- InpPreventDuplicateInstances is deprecated and reserved. Exclusive
persistence ownership is unconditional, and an ownership loss always
blocks entries, so the input has no effect. It is kept so existing .set
files keep loading, and a runtime note reports that it is ignored.
Preserved unchanged: V2 fingerprint and file locators, legacy identity hash
and key, legacy ambiguity refusal, risk limits and strategy logic.
2026-10-31 19:20:50 +07:00
g_risk . Evaluate ( ) ;
ServiceRiskLocks ( ) ;
2026-10-30 09:19:45 +07:00
if ( InpEnableTrading & & ! g_risk . LiquidationRequired ( ) & & TradingPermissionsOk ( ) )
{
ServiceTrailing ( ) ;
ProcessManagedSymbols ( ) ;
}
2026-10-30 07:19:59 +07:00
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 )
fix: correlate outstanding entry risk reservations
A reservation could be released merely because an unrelated position or pending order existed under the same Magic Number, and an order ticket was treated as a position ticket. An entry whose submission result was lost also released its reserved risk.
- UQ_Defs: state schema 5 adds the reservation identity; ENUM_UQ_SUBMIT_RESULT and a retcode classifier that only accepts proven non-execution.
- UQ_State: reservation id, magic, scope, direction, volume, entry, stop, order/deal/position tickets are persisted, checksummed and strictly validated. A previous-schema reservation migrates to UNRESOLVED with its money preserved, never to NONE.
- UQ_Risk: UQReserveCorrelate links a reservation to the broker only through its own order ticket, a deal whose DEAL_ORDER is that ticket, or the traced POSITION_IDENTIFIER. Ambiguity, a disconnected terminal and a missing record all keep the money reserved as UNRESOLVED and block entries.
- UQ_Exec: UQSendMarket reports the submission outcome; the caller withdraws reserved risk only on a local preflight failure or a proven broker rejection.
- Universal_Quant_EA.mq5: deals and order transactions reconcile the reservation immediately; status and startup diagnostics expose the reservation identity.
2026-10-30 10:43:15 +07:00
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
Fix duplicate-instance ownership integrity (V2 guard + persistence gate)
Defect A - shared guard deletion and first-creation race:
- The shared V2 guard is now a persistent coordination object. It is never
deleted after creation; release and rollback are both a compare-and-swap
back to the exact unclaimed sentinel (0.0), so a contender that acquires
the guard between an owner's CAS and its cleanup keeps its claim.
- First creation is fenced by ballots: the contender announces its own
ballot, scans for live foreign ballots, re-reads the guard AFTER the scan
(a stale observation of absence is never used), verifies the sentinel it
wrote, claims it by CAS and re-scans afterwards. A rival found at any
point fails the claim closed and the guard is given back with a CAS to
the sentinel - never deleted, never overwritten.
- The owner's ballot stays visible for as long as it owns the guard and is
renewed by the heartbeat; only a contender's own ballot is ever removed,
and only after it no longer owns the guard.
- UQ_GUARD_UNCLAIMED is a distinct protocol state, accepted for the V2
guard namespace only. The legacy guard namespace keeps zero as corrupt
evidence, and negative, fractional, non-finite and future values stay
invalid everywhere.
- Ownership loss is latched for the lifetime of the guard object.
Defect B - ownership loss must stop persistence:
- CUQInstanceGuard implements CUQOwnershipVerifier: a read-only proof that
the guard still carries this instance's value and is still inside the TTL.
- CUQRisk binds that verifier after the guard is acquired and before Init(),
and refuses to initialize without one.
- A persistence boundary inside CUQRisk gates the primary write, the backup
write, the staging write, legacy migration, V2 marker creation, marker
restoration, marker removal and reservation reconciliation. Any failure
latches the loss: the controller is unhealthy, entries are blocked, and
no retry or reacquisition is possible in that instance.
- UQStateSave() takes the verifier and fences its three file mutations
individually, so a loss proven before the primary commit cannot write it.
EA event ordering:
- OnTick() and OnTimer() verify ownership before risk evaluation, lock
servicing, trailing and symbol processing.
- OnTradeTransaction() does not invoke persistence-capable reservation
reconciliation after the loss.
- OnDeinit() releases only a guard this instance provably still holds.
- Heartbeats are throttled; every event performs the read-only proof, so a
two second timer is not the only discovery path.
Input semantics:
- InpPreventDuplicateInstances is deprecated and reserved. Exclusive
persistence ownership is unconditional, and an ownership loss always
blocks entries, so the input has no effect. It is kept so existing .set
files keep loading, and a runtime note reports that it is ignored.
Preserved unchanged: V2 fingerprint and file locators, legacy identity hash
and key, legacy ambiguity refusal, risk limits and strategy logic.
2026-10-31 19:20:50 +07:00
//--- for, so it is reconciled at once instead of on the next poll - but
//--- never after this instance lost the guard, because reconciliation
//--- persists the risk state it reconciles
if ( g_risk . ReserveState ( ) ! = UQ_RESERVE_NONE & & InstanceOwnershipHeld ( true ) )
fix: correlate outstanding entry risk reservations
A reservation could be released merely because an unrelated position or pending order existed under the same Magic Number, and an order ticket was treated as a position ticket. An entry whose submission result was lost also released its reserved risk.
- UQ_Defs: state schema 5 adds the reservation identity; ENUM_UQ_SUBMIT_RESULT and a retcode classifier that only accepts proven non-execution.
- UQ_State: reservation id, magic, scope, direction, volume, entry, stop, order/deal/position tickets are persisted, checksummed and strictly validated. A previous-schema reservation migrates to UNRESOLVED with its money preserved, never to NONE.
- UQ_Risk: UQReserveCorrelate links a reservation to the broker only through its own order ticket, a deal whose DEAL_ORDER is that ticket, or the traced POSITION_IDENTIFIER. Ambiguity, a disconnected terminal and a missing record all keep the money reserved as UNRESOLVED and block entries.
- UQ_Exec: UQSendMarket reports the submission outcome; the caller withdraws reserved risk only on a local preflight failure or a proven broker rejection.
- Universal_Quant_EA.mq5: deals and order transactions reconcile the reservation immediately; status and startup diagnostics expose the reservation identity.
2026-10-30 10:43:15 +07:00
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 )
{
Fix duplicate-instance ownership integrity (V2 guard + persistence gate)
Defect A - shared guard deletion and first-creation race:
- The shared V2 guard is now a persistent coordination object. It is never
deleted after creation; release and rollback are both a compare-and-swap
back to the exact unclaimed sentinel (0.0), so a contender that acquires
the guard between an owner's CAS and its cleanup keeps its claim.
- First creation is fenced by ballots: the contender announces its own
ballot, scans for live foreign ballots, re-reads the guard AFTER the scan
(a stale observation of absence is never used), verifies the sentinel it
wrote, claims it by CAS and re-scans afterwards. A rival found at any
point fails the claim closed and the guard is given back with a CAS to
the sentinel - never deleted, never overwritten.
- The owner's ballot stays visible for as long as it owns the guard and is
renewed by the heartbeat; only a contender's own ballot is ever removed,
and only after it no longer owns the guard.
- UQ_GUARD_UNCLAIMED is a distinct protocol state, accepted for the V2
guard namespace only. The legacy guard namespace keeps zero as corrupt
evidence, and negative, fractional, non-finite and future values stay
invalid everywhere.
- Ownership loss is latched for the lifetime of the guard object.
Defect B - ownership loss must stop persistence:
- CUQInstanceGuard implements CUQOwnershipVerifier: a read-only proof that
the guard still carries this instance's value and is still inside the TTL.
- CUQRisk binds that verifier after the guard is acquired and before Init(),
and refuses to initialize without one.
- A persistence boundary inside CUQRisk gates the primary write, the backup
write, the staging write, legacy migration, V2 marker creation, marker
restoration, marker removal and reservation reconciliation. Any failure
latches the loss: the controller is unhealthy, entries are blocked, and
no retry or reacquisition is possible in that instance.
- UQStateSave() takes the verifier and fences its three file mutations
individually, so a loss proven before the primary commit cannot write it.
EA event ordering:
- OnTick() and OnTimer() verify ownership before risk evaluation, lock
servicing, trailing and symbol processing.
- OnTradeTransaction() does not invoke persistence-capable reservation
reconciliation after the loss.
- OnDeinit() releases only a guard this instance provably still holds.
- Heartbeats are throttled; every event performs the read-only proof, so a
two second timer is not the only discovery path.
Input semantics:
- InpPreventDuplicateInstances is deprecated and reserved. Exclusive
persistence ownership is unconditional, and an ownership loss always
blocks entries, so the input has no effect. It is kept so existing .set
files keep loading, and a runtime note reports that it is ignored.
Preserved unchanged: V2 fingerprint and file locators, legacy identity hash
and key, legacy ambiguity refusal, risk limits and strategy logic.
2026-10-31 19:20:50 +07:00
if ( g_risk . ReserveState ( ) ! = UQ_RESERVE_NONE & & InstanceOwnershipHeld ( true ) )
fix: correlate outstanding entry risk reservations
A reservation could be released merely because an unrelated position or pending order existed under the same Magic Number, and an order ticket was treated as a position ticket. An entry whose submission result was lost also released its reserved risk.
- UQ_Defs: state schema 5 adds the reservation identity; ENUM_UQ_SUBMIT_RESULT and a retcode classifier that only accepts proven non-execution.
- UQ_State: reservation id, magic, scope, direction, volume, entry, stop, order/deal/position tickets are persisted, checksummed and strictly validated. A previous-schema reservation migrates to UNRESOLVED with its money preserved, never to NONE.
- UQ_Risk: UQReserveCorrelate links a reservation to the broker only through its own order ticket, a deal whose DEAL_ORDER is that ticket, or the traced POSITION_IDENTIFIER. Ambiguity, a disconnected terminal and a missing record all keep the money reserved as UNRESOLVED and block entries.
- UQ_Exec: UQSendMarket reports the submission outcome; the caller withdraws reserved risk only on a local preflight failure or a proven broker rejection.
- Universal_Quant_EA.mq5: deals and order transactions reconcile the reservation immediately; status and startup diagnostics expose the reservation identity.
2026-10-30 10:43:15 +07:00
g_risk . ReconcileReservationDepth ( true ) ;
2026-10-30 07:19:59 +07:00
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 ) ;
}
}
}
//+------------------------------------------------------------------+