Universal_Quant_EA/Include/UQ_State.mqh
1830 lines
67 KiB
MQL5
//+------------------------------------------------------------------+
//| UQ_State.mqh |
//| Universal Quant EA - identity and persistent risk state |
//| |
//| State is stored as an ASCII key=value block in the terminal |
//| common folder. The file name and the embedded identity hash are |
//| both derived from broker server + account login + portfolio id, |
//| so one account can never silently inherit another's baselines. |
//| |
//| Lifecycle policy - fail closed: |
//| * a state file that is absent, empty, truncated, corrupt, |
//| version-unsupported, identity-mismatched or schema-invalid |
//| is NEVER treated as a fresh installation; |
//| * only an explicit owner-authorized first initialization with |
//| a deliberately declared positive baseline may create a new |
//| risk cycle; |
//| * a redundant marker global variable records that the scope |
//| has already been initialized, so deleting the state file |
//| cannot silently restart a risk cycle; |
//| * every serialized state carries a checksum over a canonical |
//| fixed-order field block, verified on load and again after |
//| every write; |
Universal Quant EA
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
//| * every state carries a strictly increasing generation that is |
//| written ahead of the primary, read back and reconciled, so a |
//| stale copy can never overwrite a newer verified state; |
//| * the previous verified state is retained as a backup and is |
//| only ever used under an explicit owner authorization, and any |
//| recovered state always starts behind a recovery hold. |
//| |
//| No software can prove that a previous installation never |
//| existed if every evidence source was deliberately erased. In |
//| that case the owner must authorize a new risk cycle explicitly. |
//+------------------------------------------------------------------+
#ifndef UQ_STATE_MQH
#define UQ_STATE_MQH
#include "UQ_Defs.mqh"
#define UQ_STATE_TEMP_EXT ".tmp"
#define UQ_STATE_BACKUP_EXT ".bak"
#define UQ_STATE_CHECKSUM_KEY "checksum"
#define UQ_STATE_CHECKSUM_LENGTH 16
#define UQ_STATE_RETRY_SECONDS 5
#define UQ_STATE_MAX_CLOCK_SKEW 86400
#define UQ_STATE_MAX_RESET_SIG 64
//+------------------------------------------------------------------+
//| Identity of the persistence scope |
//+------------------------------------------------------------------+
struct UQIdentity
{
string server; // broker server name (never written to disk)
long login; // account login (never written to disk)
string portfolio; // EA portfolio identifier
string key; // file-system safe scope key
string hash; // integrity hash of the scope triple
};
//+------------------------------------------------------------------+
//| Persisted risk state |
//+------------------------------------------------------------------+
struct UQRiskState
{
int format;
string identity; // integrity hash of the owning scope
double baselineBalance; // B0 - fixed initial balance baseline
bool baselineValid;
datetime baselineTime;
double dailyStartEquity; // D0 - starting equity of the trading day
bool dailyValid;
datetime dailyDate; // broker-server calendar day of D0
datetime dailyStartTime;
bool dailyLock;
datetime dailyLockTime;
bool staticLock;
datetime staticLockTime;
bool targetLock;
datetime targetLockTime;
string resetSignature; // legacy pre-cycle reset signature, migrated and unused
datetime createdAt;
datetime updatedAt;
int cycleSequence; // risk-cycle counter, 1 for the first cycle
string cycleId; // unique identifier of the current risk cycle
string resetNonceHistory; // consumed reset nonces, '|' separated
double priorBaselineBalance; // B0 of the cycle closed by the most recent reset
string priorCycleId; // identifier of that previous cycle
int priorLockKind; // 0 none, 1 static-drawdown, 2 equity-target, 3 both
datetime priorLockTime; // lock event that authorized the most recent reset
//--- outstanding entry risk. A submitted entry is reserved before the
//--- request leaves the terminal and released only after the broker state
//--- proves where the order went, so a restart cannot forget it.
ENUM_UQ_RESERVE_STATE reserveState;
double reserveMoney;
ulong reserveTicket; // order ticket returned by the server
string reserveSymbol;
datetime reserveTime;
string reserveId; // identity of this request, unique per scope
ulong reserveMagic; // Magic Number of the request
string reserveScope; // portfolio scope the request belongs to
int reserveDirection; // +1 buy, -1 sell, 0 when it cannot be proven
double reserveVolume; // requested volume
double reserveEntry; // expected entry price of the request
double reserveStop; // submitted protective stop
ulong reserveDeal; // deal ticket reported by the server
ulong reservePosition; // position identifier reported for the deal
Universal Quant EA
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
//--- persistence generation. Every authoritative state carries a strictly
//--- increasing revision, so an interrupted commit is always detectable and
//--- a stale copy can never overwrite a newer verified state.
long stateRevision;
bool recoveryHold; // entries blocked until the owner authorizes recovery
datetime recoveryHoldTime; // when the recovery hold was recorded
//--- monitoring continuity. The heartbeat records the last verified
//--- execution moment, so a restart can distinguish a trading day that was
//--- observed from its boundary from one whose equity path was never seen.
datetime monitorTime; // last verified monitoring heartbeat (server time)
int monitorGapSeconds; // most recent unobserved interval, 0 = none
bool gapHold; // entries blocked until an observed day boundary
datetime gapHoldTime; // when the monitoring-gap hold was recorded
};
//+------------------------------------------------------------------+
//| Classification of one load attempt |
//+------------------------------------------------------------------+
enum ENUM_UQ_STATE_LOAD
{
UQ_LOAD_ERROR = -1, // file exists but is unusable
UQ_LOAD_ABSENT = 0, // no file at this location
UQ_LOAD_VALID = 1, // current format, checksum verified
UQ_LOAD_LEGACY = 2 // previous published format, schema verified
};
//+------------------------------------------------------------------+
//| Reconciliation state of the redundant initialization marker |
//+------------------------------------------------------------------+
enum ENUM_UQ_MARKER_STATE
{
UQ_MARKER_ABSENT = 0, // marker missing (or unusable value)
UQ_MARKER_MATCH = 1, // marker agrees with the persisted state
UQ_MARKER_MISMATCH = 2 // marker disagrees with the persisted state
};
//+------------------------------------------------------------------+
//| Canonical persisted field table. The order of this enumeration |
//| defines the order of the canonical block that is hashed, so the |
//| checksum is independent of the physical line order in the file. |
//+------------------------------------------------------------------+
enum ENUM_UQ_STATE_FIELD
{
UQ_F_FORMAT = 0,
UQ_F_IDENTITY,
UQ_F_BASELINE_BALANCE,
UQ_F_BASELINE_VALID,
UQ_F_BASELINE_TIME,
UQ_F_DAILY_EQUITY,
UQ_F_DAILY_VALID,
UQ_F_DAILY_DATE,
UQ_F_DAILY_START_TIME,
UQ_F_DAILY_LOCK,
UQ_F_DAILY_LOCK_TIME,
UQ_F_STATIC_LOCK,
UQ_F_STATIC_LOCK_TIME,
UQ_F_TARGET_LOCK,
UQ_F_TARGET_LOCK_TIME,
UQ_F_RESET_SIGNATURE,
UQ_F_CREATED_AT,
UQ_F_UPDATED_AT,
UQ_F_CYCLE_SEQ,
UQ_F_CYCLE_ID,
UQ_F_RESET_NONCE_HISTORY,
UQ_F_PRIOR_B0,
UQ_F_PRIOR_CYCLE_ID,
UQ_F_PRIOR_LOCK_KIND,
UQ_F_PRIOR_LOCK_TIME,
UQ_F_RESERVE_STATE,
UQ_F_RESERVE_MONEY,
UQ_F_RESERVE_TICKET,
UQ_F_RESERVE_SYMBOL,
UQ_F_RESERVE_TIME,
UQ_F_RESERVE_ID,
UQ_F_RESERVE_MAGIC,
UQ_F_RESERVE_SCOPE,
UQ_F_RESERVE_DIRECTION,
UQ_F_RESERVE_VOLUME,
UQ_F_RESERVE_ENTRY,
UQ_F_RESERVE_STOP,
UQ_F_RESERVE_DEAL,
UQ_F_RESERVE_POSITION,
Universal Quant EA
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
UQ_F_STATE_REVISION,
UQ_F_RECOVERY_HOLD,
UQ_F_RECOVERY_TIME,
UQ_F_MONITOR_TIME,
UQ_F_MONITOR_GAP,
UQ_F_GAP_HOLD,
UQ_F_GAP_HOLD_TIME,
UQ_F_COUNT
};
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Number of fields written by the schemas that predate the risk- |
//| cycle schema. They lead the canonical table, so a legacy checksum |
//| is recomputed over exactly the block that produced it. |
//+------------------------------------------------------------------+
#define UQ_STATE_BASE_FIELDS 18
#define UQ_STATE_CYCLE_FIELDS 25
#define UQ_STATE_RESERVE_FIELDS 30
Universal Quant EA
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
#define UQ_STATE_IDENTITY_FIELDS 39
#define UQ_STATE_GENERATION_FIELDS 42
#define UQ_STATE_MONITOR_FIELDS 46
//+------------------------------------------------------------------+
//| Number of fields that a given schema serialises. A checksum is |
//| only valid over the exact block that produced it, so the field |
//| count is derived from the schema version and never from the |
//| current build. |
//+------------------------------------------------------------------+
int UQFieldCountForFormat(const int fmt)
{
if(fmt >= UQ_STATE_FORMAT)
return((int)UQ_F_COUNT);
if(fmt >= UQ_STATE_GENERATION_FORMAT)
return((int)UQ_STATE_GENERATION_FIELDS);
Universal Quant EA
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
if(fmt >= UQ_STATE_IDENTITY_FORMAT)
return((int)UQ_STATE_IDENTITY_FIELDS);
if(fmt == UQ_STATE_RESERVE_FORMAT)
return((int)UQ_STATE_RESERVE_FIELDS);
if(fmt == UQ_STATE_CYCLE_FORMAT)
return((int)UQ_STATE_CYCLE_FIELDS);
return((int)UQ_STATE_BASE_FIELDS);
}
//+------------------------------------------------------------------+
//| Reset a state structure to defaults |
//+------------------------------------------------------------------+
void UQStateReset(UQRiskState &st)
{
st.format = UQ_STATE_FORMAT;
st.identity = "";
st.baselineBalance = 0.0;
st.baselineValid = false;
st.baselineTime = 0;
st.dailyStartEquity = 0.0;
st.dailyValid = false;
st.dailyDate = 0;
st.dailyStartTime = 0;
st.dailyLock = false;
st.dailyLockTime = 0;
st.staticLock = false;
st.staticLockTime = 0;
st.targetLock = false;
st.targetLockTime = 0;
st.resetSignature = "";
st.createdAt = 0;
st.updatedAt = 0;
st.cycleSequence = 0;
st.cycleId = "";
st.resetNonceHistory = "";
st.priorBaselineBalance = 0.0;
st.priorCycleId = "";
st.priorLockKind = 0;
st.priorLockTime = 0;
st.reserveState = UQ_RESERVE_NONE;
st.reserveMoney = 0.0;
st.reserveTicket = 0;
st.reserveSymbol = "";
st.reserveTime = 0;
st.reserveId = "";
st.reserveMagic = 0;
st.reserveScope = "";
st.reserveDirection = 0;
st.reserveVolume = 0.0;
st.reserveEntry = 0.0;
st.reserveStop = 0.0;
st.reserveDeal = 0;
st.reservePosition = 0;
Universal Quant EA
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
st.stateRevision = 0;
st.recoveryHold = false;
st.recoveryHoldTime = 0;
st.monitorTime = 0;
st.monitorGapSeconds = 0;
st.gapHold = false;
st.gapHoldTime = 0;
}
//+------------------------------------------------------------------+
//| Canonical key of a persisted field |
//+------------------------------------------------------------------+
string UQFieldKey(const int idx)
{
switch(idx)
{
case UQ_F_FORMAT: return("format");
case UQ_F_IDENTITY: return("identity");
case UQ_F_BASELINE_BALANCE: return("baselineBalance");
case UQ_F_BASELINE_VALID: return("baselineValid");
case UQ_F_BASELINE_TIME: return("baselineTime");
case UQ_F_DAILY_EQUITY: return("dailyStartEquity");
case UQ_F_DAILY_VALID: return("dailyValid");
case UQ_F_DAILY_DATE: return("dailyDate");
case UQ_F_DAILY_START_TIME: return("dailyStartTime");
case UQ_F_DAILY_LOCK: return("dailyLock");
case UQ_F_DAILY_LOCK_TIME: return("dailyLockTime");
case UQ_F_STATIC_LOCK: return("staticLock");
case UQ_F_STATIC_LOCK_TIME: return("staticLockTime");
case UQ_F_TARGET_LOCK: return("targetLock");
case UQ_F_TARGET_LOCK_TIME: return("targetLockTime");
case UQ_F_RESET_SIGNATURE: return("resetSignature");
case UQ_F_CREATED_AT: return("createdAt");
case UQ_F_UPDATED_AT: return("updatedAt");
case UQ_F_CYCLE_SEQ: return("cycleSequence");
case UQ_F_CYCLE_ID: return("cycleId");
case UQ_F_RESET_NONCE_HISTORY: return("resetNonceHistory");
case UQ_F_PRIOR_B0: return("priorBaselineBalance");
case UQ_F_PRIOR_CYCLE_ID: return("priorCycleId");
case UQ_F_PRIOR_LOCK_KIND: return("priorLockKind");
case UQ_F_PRIOR_LOCK_TIME: return("priorLockTime");
case UQ_F_RESERVE_STATE: return("reserveState");
case UQ_F_RESERVE_MONEY: return("reserveMoney");
case UQ_F_RESERVE_TICKET: return("reserveTicket");
case UQ_F_RESERVE_SYMBOL: return("reserveSymbol");
case UQ_F_RESERVE_TIME: return("reserveTime");
case UQ_F_RESERVE_ID: return("reserveId");
case UQ_F_RESERVE_MAGIC: return("reserveMagic");
case UQ_F_RESERVE_SCOPE: return("reserveScope");
case UQ_F_RESERVE_DIRECTION: return("reserveDirection");
case UQ_F_RESERVE_VOLUME: return("reserveVolume");
case UQ_F_RESERVE_ENTRY: return("reserveEntry");
case UQ_F_RESERVE_STOP: return("reserveStop");
case UQ_F_RESERVE_DEAL: return("reserveDeal");
case UQ_F_RESERVE_POSITION: return("reservePosition");
Universal Quant EA
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
case UQ_F_STATE_REVISION: return("stateRevision");
case UQ_F_RECOVERY_HOLD: return("recoveryHold");
case UQ_F_RECOVERY_TIME: return("recoveryHoldTime");
case UQ_F_MONITOR_TIME: return("monitorTime");
case UQ_F_MONITOR_GAP: return("monitorGapSeconds");
case UQ_F_GAP_HOLD: return("gapHold");
case UQ_F_GAP_HOLD_TIME: return("gapHoldTime");
}
return("");
}
//+------------------------------------------------------------------+
//| Index of a persisted field, or -1 when unknown |
//+------------------------------------------------------------------+
int UQFieldIndex(const string key)
{
for(int i = 0; i < (int)UQ_F_COUNT; i++)
if(UQFieldKey(i) == key)
return(i);
return(-1);
}
//+------------------------------------------------------------------+
//| Canonical serialised value of one persisted field |
//+------------------------------------------------------------------+
string UQStateFieldValue(UQRiskState &st, const int idx)
{
switch(idx)
{
case UQ_F_FORMAT: return(IntegerToString(st.format));
case UQ_F_IDENTITY: return(st.identity);
case UQ_F_BASELINE_BALANCE: return(DoubleToString(st.baselineBalance, 8));
case UQ_F_BASELINE_VALID: return(st.baselineValid ? "1" : "0");
case UQ_F_BASELINE_TIME: return(IntegerToString((long)st.baselineTime));
case UQ_F_DAILY_EQUITY: return(DoubleToString(st.dailyStartEquity, 8));
case UQ_F_DAILY_VALID: return(st.dailyValid ? "1" : "0");
case UQ_F_DAILY_DATE: return(IntegerToString((long)st.dailyDate));
case UQ_F_DAILY_START_TIME: return(IntegerToString((long)st.dailyStartTime));
case UQ_F_DAILY_LOCK: return(st.dailyLock ? "1" : "0");
case UQ_F_DAILY_LOCK_TIME: return(IntegerToString((long)st.dailyLockTime));
case UQ_F_STATIC_LOCK: return(st.staticLock ? "1" : "0");
case UQ_F_STATIC_LOCK_TIME: return(IntegerToString((long)st.staticLockTime));
case UQ_F_TARGET_LOCK: return(st.targetLock ? "1" : "0");
case UQ_F_TARGET_LOCK_TIME: return(IntegerToString((long)st.targetLockTime));
case UQ_F_RESET_SIGNATURE: return(st.resetSignature);
case UQ_F_CREATED_AT: return(IntegerToString((long)st.createdAt));
case UQ_F_UPDATED_AT: return(IntegerToString((long)st.updatedAt));
case UQ_F_CYCLE_SEQ: return(IntegerToString((long)st.cycleSequence));
case UQ_F_CYCLE_ID: return(st.cycleId);
case UQ_F_RESET_NONCE_HISTORY: return(st.resetNonceHistory);
case UQ_F_PRIOR_B0: return(DoubleToString(st.priorBaselineBalance, 8));
case UQ_F_PRIOR_CYCLE_ID: return(st.priorCycleId);
case UQ_F_PRIOR_LOCK_KIND: return(IntegerToString((long)st.priorLockKind));
case UQ_F_PRIOR_LOCK_TIME: return(IntegerToString((long)st.priorLockTime));
case UQ_F_RESERVE_STATE: return(IntegerToString((long)st.reserveState));
case UQ_F_RESERVE_MONEY: return(DoubleToString(st.reserveMoney, 8));
case UQ_F_RESERVE_TICKET: return(IntegerToString((long)st.reserveTicket));
case UQ_F_RESERVE_SYMBOL: return(st.reserveSymbol);
case UQ_F_RESERVE_TIME: return(IntegerToString((long)st.reserveTime));
case UQ_F_RESERVE_ID: return(st.reserveId);
case UQ_F_RESERVE_MAGIC: return(IntegerToString((long)st.reserveMagic));
case UQ_F_RESERVE_SCOPE: return(st.reserveScope);
case UQ_F_RESERVE_DIRECTION: return(IntegerToString((long)st.reserveDirection));
case UQ_F_RESERVE_VOLUME: return(DoubleToString(st.reserveVolume, 8));
case UQ_F_RESERVE_ENTRY: return(DoubleToString(st.reserveEntry, 8));
case UQ_F_RESERVE_STOP: return(DoubleToString(st.reserveStop, 8));
case UQ_F_RESERVE_DEAL: return(IntegerToString((long)st.reserveDeal));
case UQ_F_RESERVE_POSITION: return(IntegerToString((long)st.reservePosition));
Universal Quant EA
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
case UQ_F_STATE_REVISION: return(IntegerToString(st.stateRevision));
case UQ_F_RECOVERY_HOLD: return(st.recoveryHold ? "1" : "0");
case UQ_F_RECOVERY_TIME: return(IntegerToString((long)st.recoveryHoldTime));
case UQ_F_MONITOR_TIME: return(IntegerToString((long)st.monitorTime));
case UQ_F_MONITOR_GAP: return(IntegerToString((long)st.monitorGapSeconds));
case UQ_F_GAP_HOLD: return(st.gapHold ? "1" : "0");
case UQ_F_GAP_HOLD_TIME: return(IntegerToString((long)st.gapHoldTime));
}
return("");
}
//+------------------------------------------------------------------+
//| Canonical block that is covered by the integrity checksum |
//+------------------------------------------------------------------+
string UQStateCanonicalBody(UQRiskState &st)
{
//--- every published schema is hashed over its own field set only, so an
//--- older checksum still verifies and can be migrated deterministically
const int fields = UQFieldCountForFormat(st.format);
string body = "";
for(int i = 0; i < fields; i++)
body += UQFieldKey(i) + "=" + UQStateFieldValue(st, i) + "\r\n";
return(body);
}
//+------------------------------------------------------------------+
//| Strict integer parser: rejects empty, blank and non-numeric text |
//+------------------------------------------------------------------+
bool UQParseLong(const string v, long &out)
{
out = 0;
const int n = StringLen(v);
if(n <= 0 || n > 20)
return(false);
int start = 0;
const ushort first = StringGetCharacter(v, 0);
if(first == '-' || first == '+')
start = 1;
if(start >= n)
return(false);
int digits = 0;
for(int i = start; i < n; i++)
{
const ushort c = StringGetCharacter(v, i);
if(c < '0' || c > '9')
return(false);
digits++;
}
if(digits == 0)
return(false);
out = StringToInteger(v);
return(true);
}
//+------------------------------------------------------------------+
//| Strict double parser: one optional leading sign, one decimal |
//| point, at least one digit and no trailing garbage |
//+------------------------------------------------------------------+
bool UQParseDoubleStrict(const string v, double &out)
{
out = 0.0;
const int n = StringLen(v);
if(n <= 0 || n > 40)
return(false);
int start = 0;
const ushort first = StringGetCharacter(v, 0);
if(first == '-' || first == '+')
start = 1;
if(start >= n)
return(false);
int digits = 0;
int points = 0;
for(int i = start; i < n; i++)
{
const ushort c = StringGetCharacter(v, i);
if(c == '.')
{
points++;
if(points > 1)
return(false);
continue;
}
if(c < '0' || c > '9')
return(false);
digits++;
}
if(digits == 0)
return(false);
out = StringToDouble(v);
return(MathIsValidNumber(out));
}
//+------------------------------------------------------------------+
//| Strict boolean parser: only the literals 0 and 1 are accepted |
//+------------------------------------------------------------------+
bool UQParseBoolStrict(const string v, bool &out)
{
out = false;
if(v == "0")
return(true);
if(v == "1")
{
out = true;
return(true);
}
return(false);
}
//+------------------------------------------------------------------+
//| Parse a state block into a structure. Every required field must |
//| be present exactly once; unknown fields are rejected. |
//+------------------------------------------------------------------+
bool UQStateParseBody(const string body, UQRiskState &st,
bool &hasChecksum, string &checksumStored, string &err)
{
hasChecksum = false;
checksumStored = "";
err = "";
UQStateReset(st);
char seen[];
ArrayResize(seen, (int)UQ_F_COUNT);
ArrayInitialize(seen, (char)0);
string lines[];
const int n = StringSplit(body, '\n', lines);
for(int i = 0; i < n; i++)
{
string line = lines[i];
StringTrimRight(line);
StringTrimLeft(line);
if(StringLen(line) == 0)
continue;
const int eq = StringFind(line, "=");
if(eq <= 0)
{
err = StringFormat("malformed line %d", i + 1);
return(false);
}
const string key = StringSubstr(line, 0, eq);
const string val = StringSubstr(line, eq + 1);
if(key == UQ_STATE_CHECKSUM_KEY)
{
if(hasChecksum)
{
err = "duplicate checksum field";
return(false);
}
if(StringLen(val) != UQ_STATE_CHECKSUM_LENGTH)
{
err = "malformed checksum field";
return(false);
}
hasChecksum = true;
checksumStored = val;
continue;
}
const int idx = UQFieldIndex(key);
if(idx < 0)
{
err = StringFormat("unknown field '%s'", key);
return(false);
}
if(seen[idx] != 0)
{
err = StringFormat("duplicate field '%s'", key);
return(false);
}
seen[idx] = (char)1;
long iv = 0;
double dv = 0.0;
bool bv = false;
switch(idx)
{
case UQ_F_FORMAT:
if(!UQParseLong(val, iv))
{ err = "field 'format' is not an integer"; return(false); }
st.format = (int)iv;
break;
case UQ_F_IDENTITY:
st.identity = val;
break;
case UQ_F_BASELINE_BALANCE:
if(!UQParseDoubleStrict(val, dv))
{ err = "field 'baselineBalance' is not numeric"; return(false); }
st.baselineBalance = dv;
break;
case UQ_F_BASELINE_VALID:
if(!UQParseBoolStrict(val, bv))
{ err = "field 'baselineValid' is not a 0/1 flag"; return(false); }
st.baselineValid = bv;
break;
case UQ_F_BASELINE_TIME:
if(!UQParseLong(val, iv))
{ err = "field 'baselineTime' is not an integer"; return(false); }
st.baselineTime = (datetime)iv;
break;
case UQ_F_DAILY_EQUITY:
if(!UQParseDoubleStrict(val, dv))
{ err = "field 'dailyStartEquity' is not numeric"; return(false); }
st.dailyStartEquity = dv;
break;
case UQ_F_DAILY_VALID:
if(!UQParseBoolStrict(val, bv))
{ err = "field 'dailyValid' is not a 0/1 flag"; return(false); }
st.dailyValid = bv;
break;
case UQ_F_DAILY_DATE:
if(!UQParseLong(val, iv))
{ err = "field 'dailyDate' is not an integer"; return(false); }
st.dailyDate = (datetime)iv;
break;
case UQ_F_DAILY_START_TIME:
if(!UQParseLong(val, iv))
{ err = "field 'dailyStartTime' is not an integer"; return(false); }
st.dailyStartTime = (datetime)iv;
break;
case UQ_F_DAILY_LOCK:
if(!UQParseBoolStrict(val, bv))
{ err = "field 'dailyLock' is not a 0/1 flag"; return(false); }
st.dailyLock = bv;
break;
case UQ_F_DAILY_LOCK_TIME:
if(!UQParseLong(val, iv))
{ err = "field 'dailyLockTime' is not an integer"; return(false); }
st.dailyLockTime = (datetime)iv;
break;
case UQ_F_STATIC_LOCK:
if(!UQParseBoolStrict(val, bv))
{ err = "field 'staticLock' is not a 0/1 flag"; return(false); }
st.staticLock = bv;
break;
case UQ_F_STATIC_LOCK_TIME:
if(!UQParseLong(val, iv))
{ err = "field 'staticLockTime' is not an integer"; return(false); }
st.staticLockTime = (datetime)iv;
break;
case UQ_F_TARGET_LOCK:
if(!UQParseBoolStrict(val, bv))
{ err = "field 'targetLock' is not a 0/1 flag"; return(false); }
st.targetLock = bv;
break;
case UQ_F_TARGET_LOCK_TIME:
if(!UQParseLong(val, iv))
{ err = "field 'targetLockTime' is not an integer"; return(false); }
st.targetLockTime = (datetime)iv;
break;
case UQ_F_RESET_SIGNATURE:
st.resetSignature = val;
break;
case UQ_F_CREATED_AT:
if(!UQParseLong(val, iv))
{ err = "field 'createdAt' is not an integer"; return(false); }
st.createdAt = (datetime)iv;
break;
case UQ_F_UPDATED_AT:
if(!UQParseLong(val, iv))
{ err = "field 'updatedAt' is not an integer"; return(false); }
st.updatedAt = (datetime)iv;
break;
case UQ_F_CYCLE_SEQ:
if(!UQParseLong(val, iv))
{ err = "field 'cycleSequence' is not an integer"; return(false); }
st.cycleSequence = (int)iv;
break;
case UQ_F_CYCLE_ID:
st.cycleId = val;
break;
case UQ_F_RESET_NONCE_HISTORY:
st.resetNonceHistory = val;
break;
case UQ_F_PRIOR_B0:
if(!UQParseDoubleStrict(val, dv))
{ err = "field 'priorBaselineBalance' is not numeric"; return(false); }
st.priorBaselineBalance = dv;
break;
case UQ_F_PRIOR_CYCLE_ID:
st.priorCycleId = val;
break;
case UQ_F_PRIOR_LOCK_KIND:
if(!UQParseLong(val, iv))
{ err = "field 'priorLockKind' is not an integer"; return(false); }
st.priorLockKind = (int)iv;
break;
case UQ_F_PRIOR_LOCK_TIME:
if(!UQParseLong(val, iv))
{ err = "field 'priorLockTime' is not an integer"; return(false); }
st.priorLockTime = (datetime)iv;
break;
case UQ_F_RESERVE_STATE:
if(!UQParseLong(val, iv))
{ err = "field 'reserveState' is not an integer"; return(false); }
st.reserveState = (ENUM_UQ_RESERVE_STATE)iv;
break;
case UQ_F_RESERVE_MONEY:
if(!UQParseDoubleStrict(val, dv))
{ err = "field 'reserveMoney' is not numeric"; return(false); }
st.reserveMoney = dv;
break;
case UQ_F_RESERVE_TICKET:
if(!UQParseLong(val, iv))
{ err = "field 'reserveTicket' is not an integer"; return(false); }
st.reserveTicket = (ulong)iv;
break;
case UQ_F_RESERVE_SYMBOL:
st.reserveSymbol = val;
break;
case UQ_F_RESERVE_TIME:
if(!UQParseLong(val, iv))
{ err = "field 'reserveTime' is not an integer"; return(false); }
st.reserveTime = (datetime)iv;
break;
case UQ_F_RESERVE_ID:
st.reserveId = val;
break;
case UQ_F_RESERVE_MAGIC:
if(!UQParseLong(val, iv) || iv < 0)
{ err = "field 'reserveMagic' is not an integer"; return(false); }
st.reserveMagic = (ulong)iv;
break;
case UQ_F_RESERVE_SCOPE:
st.reserveScope = val;
break;
case UQ_F_RESERVE_DIRECTION:
if(!UQParseLong(val, iv))
{ err = "field 'reserveDirection' is not an integer"; return(false); }
st.reserveDirection = (int)iv;
break;
case UQ_F_RESERVE_VOLUME:
if(!UQParseDoubleStrict(val, dv))
{ err = "field 'reserveVolume' is not numeric"; return(false); }
st.reserveVolume = dv;
break;
case UQ_F_RESERVE_ENTRY:
if(!UQParseDoubleStrict(val, dv))
{ err = "field 'reserveEntry' is not numeric"; return(false); }
st.reserveEntry = dv;
break;
case UQ_F_RESERVE_STOP:
if(!UQParseDoubleStrict(val, dv))
{ err = "field 'reserveStop' is not numeric"; return(false); }
st.reserveStop = dv;
break;
case UQ_F_RESERVE_DEAL:
if(!UQParseLong(val, iv) || iv < 0)
{ err = "field 'reserveDeal' is not an integer"; return(false); }
st.reserveDeal = (ulong)iv;
break;
case UQ_F_RESERVE_POSITION:
if(!UQParseLong(val, iv) || iv < 0)
{ err = "field 'reservePosition' is not an integer"; return(false); }
st.reservePosition = (ulong)iv;
break;
Universal Quant EA
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
case UQ_F_STATE_REVISION:
if(!UQParseLong(val, iv) || iv < 0)
{ err = "field 'stateRevision' is not an integer"; return(false); }
st.stateRevision = (long)iv;
break;
case UQ_F_RECOVERY_HOLD:
if(!UQParseBoolStrict(val, bv))
{ err = "field 'recoveryHold' is not a 0/1 flag"; return(false); }
st.recoveryHold = bv;
break;
case UQ_F_RECOVERY_TIME:
if(!UQParseLong(val, iv) || iv < 0)
{ err = "field 'recoveryHoldTime' is not an integer"; return(false); }
st.recoveryHoldTime = (datetime)iv;
break;
case UQ_F_MONITOR_TIME:
if(!UQParseLong(val, iv) || iv < 0)
{ err = "field 'monitorTime' is not an integer"; return(false); }
st.monitorTime = (datetime)iv;
break;
case UQ_F_MONITOR_GAP:
if(!UQParseLong(val, iv) || iv < 0 || iv > (long)UQ_MONITOR_GAP_MAX_SECONDS)
{ err = "field 'monitorGapSeconds' is not a bounded integer"; return(false); }
st.monitorGapSeconds = (int)iv;
break;
case UQ_F_GAP_HOLD:
if(!UQParseBoolStrict(val, bv))
{ err = "field 'gapHold' is not a 0/1 flag"; return(false); }
st.gapHold = bv;
break;
case UQ_F_GAP_HOLD_TIME:
if(!UQParseLong(val, iv) || iv < 0)
{ err = "field 'gapHoldTime' is not an integer"; return(false); }
st.gapHoldTime = (datetime)iv;
break;
default:
err = "unexpected field index";
return(false);
}
}
//--- a schema published before the risk-cycle fields existed carries no
//--- cycle block: only its own field set is mandatory
const int required = UQFieldCountForFormat(st.format);
for(int f = 0; f < required; f++)
if(seen[f] == 0)
{
err = StringFormat("required field '%s' is missing", UQFieldKey(f));
return(false);
}
return(true);
}
//+------------------------------------------------------------------+
//| Schema and semantics validation of a parsed state |
//+------------------------------------------------------------------+
bool UQStateValidate(UQIdentity &id, UQRiskState &st, string &err)
{
err = "";
if(st.format < UQ_STATE_LEGACY_FORMAT || st.format > UQ_STATE_FORMAT)
{
err = StringFormat("unsupported state format %d", st.format);
return(false);
}
if(st.identity != id.hash)
{
err = "state belongs to a different identity scope";
return(false);
}
if(st.createdAt <= 0)
{
err = "state creation timestamp is missing";
return(false);
}
if(st.updatedAt < st.createdAt)
{
err = "state update timestamp precedes the creation timestamp";
return(false);
}
const datetime now = UQServerNow();
if(now > 0 && st.updatedAt > now + (datetime)UQ_STATE_MAX_CLOCK_SKEW)
{
err = "state update timestamp lies implausibly far in the future";
return(false);
}
//--- fixed baseline
if(!st.baselineValid)
{
err = "baseline validity flag is not set";
return(false);
}
if(!(st.baselineBalance > 0.0))
{
err = "baseline B0 is not positive";
return(false);
}
if(st.baselineTime <= 0)
{
err = "baseline timestamp is missing";
return(false);
}
//--- daily baseline
if(!st.dailyValid)
{
err = "daily validity flag is not set";
return(false);
}
if(!(st.dailyStartEquity > 0.0))
{
err = "daily baseline D0 is not positive";
return(false);
}
if(st.dailyDate <= 0)
{
err = "broker-server trading date is missing";
return(false);
}
if(UQDayStart(st.dailyDate) != st.dailyDate)
{
err = "broker-server trading date is not day-aligned";
return(false);
}
if(st.dailyStartTime <= 0)
{
err = "daily baseline timestamp is missing";
return(false);
}
//--- lock consistency
if(st.dailyLock && st.dailyLockTime <= 0)
{
err = "daily lock is active without a consistent lock event";
return(false);
}
if(!st.dailyLock && st.dailyLockTime != 0)
{
err = "daily lock time is set while the daily lock is inactive";
return(false);
}
if(st.staticLock && st.staticLockTime <= 0)
{
err = "static-drawdown lock is active without a consistent lock event";
return(false);
}
if(!st.staticLock && st.staticLockTime != 0)
{
err = "static-drawdown lock time is set while the lock is inactive";
return(false);
}
if(st.targetLock && st.targetLockTime <= 0)
{
err = "equity-target lock is active without a consistent lock event";
return(false);
}
if(!st.targetLock && st.targetLockTime != 0)
{
err = "equity-target lock time is set while the lock is inactive";
return(false);
}
if(st.staticLock && st.targetLock)
{
//--- the two permanent locks are independent: an equity target can be
//--- reached before a drawdown (or the reverse), and neither may be lost
//--- just because the other is also armed. The entry gate reports the
//--- static-drawdown lock first, and liquidation is required either way.
if(!(st.staticLockTime > 0 && st.targetLockTime > 0))
{
err = "both permanent locks are active without consistent lock events";
return(false);
}
}
//--- reset identifier must stay a single serialisable token
if(StringLen(st.resetSignature) > UQ_STATE_MAX_RESET_SIG)
{
err = "reset-operation identifier is too long";
return(false);
}
if(StringFind(st.resetSignature, "=") >= 0 ||
StringFind(st.resetSignature, "\r") >= 0 ||
StringFind(st.resetSignature, "\n") >= 0)
{
err = "reset-operation identifier contains a forbidden character";
return(false);
}
//--- risk-cycle identity, only present from the cycle schema onwards
if(st.format >= UQ_STATE_CYCLE_FORMAT)
{
if(st.cycleSequence < 1)
{
err = "risk-cycle sequence is not positive";
return(false);
}
if(StringLen(st.cycleId) <= 0 || StringLen(st.cycleId) > UQ_RESET_CYCLE_ID_MAX_LEN)
{
err = "risk-cycle identifier is missing or too long";
return(false);
}
if(StringFind(st.cycleId, "=") >= 0 ||
StringFind(st.cycleId, "|") >= 0 ||
StringFind(st.cycleId, "\r") >= 0 ||
StringFind(st.cycleId, "\n") >= 0)
{
err = "risk-cycle identifier contains a forbidden character";
return(false);
}
//--- consumed reset nonces
if(StringLen(st.resetNonceHistory) > UQ_RESET_HISTORY_MAX_CHARS)
{
err = "consumed reset-nonce history is too long";
return(false);
}
if(StringLen(st.resetNonceHistory) > 0)
{
if(UQResetNonceHistoryCount(st.resetNonceHistory) > UQ_RESET_HISTORY_MAX)
{
err = "consumed reset-nonce history holds too many entries";
return(false);
}
string nonces[];
const int nn = StringSplit(st.resetNonceHistory, (ushort)'|', nonces);
for(int k = 0; k < nn; k++)
{
if(StringLen(nonces[k]) == 0)
continue;
if(!UQResetNonceValid(nonces[k]))
{
err = "consumed reset-nonce history holds an invalid nonce";
return(false);
}
}
}
//--- reference to the cycle that the last reset closed.
//--- The recorded kind is a bit field: 1 = static-drawdown, 2 =
//--- equity-target, 3 = both. A cycle can legitimately end with both
//--- permanent locks armed, so value 3 is accepted and preserved instead
//--- of rejecting the whole state and losing the surviving lock.
if(!UQPriorLockKindValid(st.priorLockKind))
{
err = "recorded prior lock kind is not recognized";
return(false);
}
if(st.priorLockKind == UQ_PRIOR_LOCK_NONE)
{
if(st.priorLockTime != 0 || StringLen(st.priorCycleId) > 0 ||
st.priorBaselineBalance != 0.0)
{
err = "prior-cycle reference is recorded without a prior lock event";
return(false);
}
}
else
{
if(st.priorLockTime <= 0)
{
err = "prior lock event timestamp is missing";
return(false);
}
if(StringLen(st.priorCycleId) <= 0 ||
StringLen(st.priorCycleId) > UQ_RESET_CYCLE_ID_MAX_LEN)
{
err = "prior cycle identifier is missing or too long";
return(false);
}
if(!(st.priorBaselineBalance > 0.0))
{
err = "prior cycle baseline is not positive";
return(false);
}
//--- a permanent lock of a closed cycle can only exist because an
//--- owner reset closed that cycle, and every applied reset consumes
//--- exactly one nonce: the recorded history must prove it
if(UQResetNonceHistoryCount(st.resetNonceHistory) < 1)
{
err = "recorded prior lock kind has no consumed reset nonce";
return(false);
}
if(st.priorCycleId == st.cycleId || st.priorLockTime > st.createdAt)
{
err = "prior-cycle reference is inconsistent with the current cycle";
return(false);
}
}
if(st.cycleSequence == 1 && st.priorLockKind != UQ_PRIOR_LOCK_NONE)
{
err = "the first risk cycle cannot reference a previous cycle";
return(false);
}
}
//--- entry-risk reservation, only present from the reservation schema onwards
if(st.format >= UQ_STATE_RESERVE_FORMAT)
{
if(st.reserveState != UQ_RESERVE_NONE &&
st.reserveState != UQ_RESERVE_SENT &&
st.reserveState != UQ_RESERVE_UNRESOLVED)
{
err = "entry-risk reservation state is not recognized";
return(false);
}
if(st.reserveState == UQ_RESERVE_NONE)
{
if(st.reserveMoney != 0.0 || st.reserveTicket != 0 ||
StringLen(st.reserveSymbol) > 0 || st.reserveTime != 0)
{
err = "entry-risk reservation is recorded while its state is empty";
return(false);
}
}
else
{
if(!(st.reserveMoney > 0.0))
{
err = "reserved entry risk is not positive";
return(false);
}
if(st.reserveTime <= 0)
{
err = "reserved entry risk has no timestamp";
return(false);
}
if(StringLen(st.reserveSymbol) <= 0 ||
StringLen(st.reserveSymbol) > UQ_RESERVE_SYMBOL_MAX_LEN)
{
err = "reserved entry symbol is missing or too long";
return(false);
}
if(StringFind(st.reserveSymbol, "=") >= 0 ||
StringFind(st.reserveSymbol, "|") >= 0 ||
StringFind(st.reserveSymbol, "\r") >= 0 ||
StringFind(st.reserveSymbol, "\n") >= 0)
{
err = "reserved entry symbol contains a forbidden character";
return(false);
}
//--- a reservation is persisted before the order is submitted, so it
//--- legitimately carries no ticket until the server has answered
}
}
//--- reservation identity, only present from the identity schema onwards.
//--- A reserved request that cannot be correlated with a broker record
//--- must be recorded as UNRESOLVED; it may never look like a normal
//--- submitted request that the reconciler could quietly release.
Universal Quant EA
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
if(st.format >= UQ_STATE_IDENTITY_FORMAT)
{
if(st.reserveState == UQ_RESERVE_NONE)
{
if(StringLen(st.reserveId) > 0 || st.reserveMagic != 0 ||
StringLen(st.reserveScope) > 0 || st.reserveDirection != 0 ||
st.reserveVolume != 0.0 || st.reserveEntry != 0.0 ||
st.reserveStop != 0.0 || st.reserveDeal != 0 ||
st.reservePosition != 0)
{
err = "reservation identity is recorded while its state is empty";
return(false);
}
}
else
{
if(!UQReserveTextSafe(st.reserveId, UQ_RESERVE_ID_MAX_LEN))
{
err = "reservation identifier is missing or malformed";
return(false);
}
if(StringLen(st.reserveScope) > UQ_RESERVE_SCOPE_MAX_LEN ||
(StringLen(st.reserveScope) > 0 && !UQReserveTextSafe(st.reserveScope, UQ_RESERVE_SCOPE_MAX_LEN)))
{
err = "reservation scope is malformed";
return(false);
}
if(st.reserveDirection != -1 && st.reserveDirection != 0 &&
st.reserveDirection != 1)
{
err = "reservation direction is not buy, sell or unknown";
return(false);
}
if(!MathIsValidNumber(st.reserveVolume) || st.reserveVolume < 0.0 ||
!MathIsValidNumber(st.reserveEntry) || st.reserveEntry < 0.0 ||
!MathIsValidNumber(st.reserveStop) || st.reserveStop < 0.0)
{
err = "reservation price or volume is not a finite non-negative number";
return(false);
}
//--- a request that is still expected to produce exposure must carry
//--- the identity of that exposure, otherwise it is UNRESOLVED
if(st.reserveState == UQ_RESERVE_SENT)
{
if(st.reserveDirection == 0 || !(st.reserveVolume > 0.0) ||
!(st.reserveEntry > 0.0) || !(st.reserveStop > 0.0))
{
err = "a submitted reservation carries no correlatable identity";
return(false);
}
if(st.reserveDirection > 0 && !(st.reserveStop < st.reserveEntry))
{
err = "a buy reservation carries a stop that is not below its entry";
return(false);
}
if(st.reserveDirection < 0 && !(st.reserveStop > st.reserveEntry))
{
err = "a sell reservation carries a stop that is not above its entry";
return(false);
}
}
}
}
Universal Quant EA
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
//--- persistence generation, only present from the generation schema
//--- onwards. A state without a positive generation can neither prove that
//--- it is the newest one nor be ordered against its backup, so it is
//--- refused instead of being trusted.
if(st.format >= UQ_STATE_GENERATION_FORMAT)
Universal Quant EA
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
{
if(st.stateRevision < 1)
{
err = "state generation revision is not positive";
return(false);
}
if(st.recoveryHold && st.recoveryHoldTime <= 0)
{
err = "recovery hold is active without a consistent hold event";
return(false);
}
if(!st.recoveryHold && st.recoveryHoldTime != 0)
{
err = "recovery hold time is set while no recovery hold is active";
return(false);
}
}
//--- monitoring continuity, only present from the monitoring schema
//--- onwards. A heartbeat in the future, a negative gap or a hold without
//--- an event would make the daily-loss history unverifiable, so the state
//--- is refused instead of being trusted.
if(st.format >= UQ_STATE_FORMAT)
{
if(st.monitorTime > 0 && st.monitorTime > UQServerNow() + (datetime)UQ_STATE_MAX_CLOCK_SKEW)
{
err = "monitoring heartbeat lies implausibly far in the future";
return(false);
}
if(st.monitorGapSeconds < 0 || st.monitorGapSeconds > UQ_MONITOR_GAP_MAX_SECONDS)
{
err = "recorded monitoring gap is out of range";
return(false);
}
if(st.gapHold && st.gapHoldTime <= 0)
{
err = "monitoring-gap hold is active without a consistent hold event";
return(false);
}
if(!st.gapHold && st.gapHoldTime != 0)
{
err = "monitoring-gap hold time is set while no gap hold is active";
return(false);
}
}
return(true);
}
//+------------------------------------------------------------------+
//| State file names for a given identity scope |
//+------------------------------------------------------------------+
string UQStateFileName(UQIdentity &id)
{
return("UQ_EA_STATE_" + id.key + ".cfg");
}
string UQStateBackupFileName(UQIdentity &id)
{
return("UQ_EA_STATE_" + id.key + UQ_STATE_BACKUP_EXT);
}
//+------------------------------------------------------------------+
//| Build the persistence identity from the live account |
//+------------------------------------------------------------------+
bool UQBuildIdentity(const string portfolio, UQIdentity &id, string &err)
{
err = "";
id.server = AccountInfoString(ACCOUNT_SERVER);
id.login = (long)AccountInfoInteger(ACCOUNT_LOGIN);
id.portfolio = portfolio;
if(StringLen(id.server) <= 0)
{
err = "account server name is unavailable";
return(false);
}
if(id.login <= 0)
{
err = "account login is unavailable";
return(false);
}
if(StringLen(id.portfolio) <= 0)
{
err = "portfolio identifier is empty";
return(false);
}
id.key = UQSanitizeKey(id.server) + "_" + IntegerToString(id.login) + "_" + UQSanitizeKey(id.portfolio);
id.hash = UQHashString(id.server + "|" + IntegerToString(id.login) + "|" + id.portfolio);
return(true);
}
//+------------------------------------------------------------------+
//| Risk-cycle identifier: sequence number plus a nonce/creation hash |
//+------------------------------------------------------------------+
string UQBuildCycleId(const int sequence, const string seed)
{
return(StringFormat("C%04d-%s", sequence,
StringSubstr(UQHashString(seed + "|" + IntegerToString(sequence)), 0, 8)));
}
//+------------------------------------------------------------------+
//| Upgrade a state written by an earlier published build to the |
//| current risk-cycle schema. |
//| |
//| The upgrade is deterministic and never invents a new risk cycle: |
//| the cycle the file already describes becomes cycle 1, no reset |
//| nonce has been consumed yet and no prior cycle is referenced. |
//| A pre-cycle reset signature cannot prove which nonce was used, so |
//| it is dropped: a subsequent reset always needs a fresh nonce. |
//+------------------------------------------------------------------+
void UQStateMigrateToCurrent(UQRiskState &st)
{
if(st.format >= UQ_STATE_FORMAT)
return;
//--- 6 -> 7: the monitoring fields are the only addition. The generation,
//--- every lock, the cycle identity and an outstanding reservation are
//--- carried over unchanged; no heartbeat is invented, so the first
//--- monitored moment of the migrated state establishes it.
if(st.format >= UQ_STATE_GENERATION_FORMAT)
{
st.monitorTime = 0;
st.monitorGapSeconds = 0;
st.gapHold = false;
st.gapHoldTime = 0;
st.format = UQ_STATE_FORMAT;
return;
}
Universal Quant EA
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
//--- the upgraded state becomes generation 1 of the current schema. An
//--- upgrade is not an event that may clear a lock, so no recovery hold is
//--- invented here, and the migration never discards a lock, a baseline,
//--- a consumed nonce or an outstanding reservation.
st.stateRevision = 1;
st.recoveryHold = false;
st.recoveryHoldTime = 0;
if(st.format < UQ_STATE_CYCLE_FORMAT)
{
st.cycleSequence = 1;
st.cycleId = UQBuildCycleId(1, st.identity + "|" + IntegerToString((long)st.createdAt));
st.resetNonceHistory = "";
st.priorBaselineBalance = 0.0;
st.priorCycleId = "";
st.priorLockKind = 0;
st.priorLockTime = 0;
st.resetSignature = "";
}
//--- an upgraded state must never lose an outstanding entry risk. A
//--- reservation written by the previous schema keeps its money, symbol and
//--- timestamp, but it carries no order, deal or position identity, so it
//--- can no longer be correlated: it becomes UNRESOLVED instead of being
//--- migrated to NONE, which would silently free the reserved risk
if(st.format >= UQ_STATE_RESERVE_FORMAT && st.reserveState != UQ_RESERVE_NONE)
{
if(StringLen(st.reserveId) <= 0)
st.reserveId = UQBuildReserveId("legacy", st.reserveSymbol, st.reserveTime, 0);
st.reserveMagic = 0;
st.reserveScope = "";
st.reserveDirection = 0;
st.reserveVolume = 0.0;
st.reserveEntry = 0.0;
st.reserveStop = 0.0;
st.reserveDeal = 0;
st.reservePosition = 0;
st.reserveState = UQ_RESERVE_UNRESOLVED;
st.format = UQ_STATE_FORMAT;
return;
}
//--- a schema that predates the reservation field set cannot carry any
//--- entry risk at all
st.reserveState = UQ_RESERVE_NONE;
st.reserveMoney = 0.0;
st.reserveTicket = 0;
st.reserveSymbol = "";
st.reserveTime = 0;
st.reserveId = "";
st.reserveMagic = 0;
st.reserveScope = "";
st.reserveDirection = 0;
st.reserveVolume = 0.0;
st.reserveEntry = 0.0;
st.reserveStop = 0.0;
st.reserveDeal = 0;
st.reservePosition = 0;
st.format = UQ_STATE_FORMAT;
}
//+------------------------------------------------------------------+
//| Load and fully validate one state file |
//| UQ_LOAD_ERROR -> file exists but must not be trusted |
//| UQ_LOAD_ABSENT -> no file at this location |
//+------------------------------------------------------------------+
ENUM_UQ_STATE_LOAD UQStateLoadFile(const string fname, UQIdentity &id,
UQRiskState &st, string &err)
{
err = "";
UQStateReset(st);
if(!FileIsExist(fname, FILE_COMMON))
return(UQ_LOAD_ABSENT);
const int h = FileOpen(fname, FILE_READ | FILE_BIN | FILE_COMMON);
if(h == INVALID_HANDLE)
{
err = StringFormat("state file cannot be opened (err=%d)", GetLastError());
return(UQ_LOAD_ERROR);
}
const int size = (int)FileSize(h);
if(size <= 0)
{
FileClose(h);
err = "state file is empty";
return(UQ_LOAD_ERROR);
}
uchar buf[];
ArrayResize(buf, size);
const int read = (int)FileReadArray(h, buf, 0, size);
FileClose(h);
if(read <= 0)
{
err = "state file could not be read";
return(UQ_LOAD_ERROR);
}
string body = CharArrayToString(buf, 0, read, CP_ACP);
//--- strip trailing padding written by earlier builds
int blen = StringLen(body);
while(blen > 0)
{
const ushort c = StringGetCharacter(body, blen - 1);
if(c == 0 || c == '\r' || c == '\n' || c == ' ' || c == '\t')
{
blen--;
continue;
}
break;
}
body = StringSubstr(body, 0, blen);
if(blen <= 0)
{
err = "state file contains no data";
return(UQ_LOAD_ERROR);
}
bool hasChecksum = false;
string checksumStored = "";
if(!UQStateParseBody(body, st, hasChecksum, checksumStored, err))
return(UQ_LOAD_ERROR);
if(st.format < UQ_STATE_LEGACY_FORMAT || st.format > UQ_STATE_FORMAT)
{
err = StringFormat("unsupported state format %d", st.format);
return(UQ_LOAD_ERROR);
}
if(st.format >= UQ_STATE_PREV_FORMAT && !hasChecksum)
{
err = "integrity checksum is missing";
return(UQ_LOAD_ERROR);
}
if(st.format == UQ_STATE_LEGACY_FORMAT && hasChecksum)
{
err = "legacy state unexpectedly carries an integrity checksum";
return(UQ_LOAD_ERROR);
}
if(hasChecksum)
{
const string calc = UQHashString(UQStateCanonicalBody(st));
if(calc != checksumStored)
{
err = "integrity checksum mismatch";
return(UQ_LOAD_ERROR);
}
}
if(!UQStateValidate(id, st, err))
return(UQ_LOAD_ERROR);
//--- a verified state written by an earlier build is upgraded in memory;
//--- the caller persists it in the current schema
const int loadedFormat = st.format;
if(loadedFormat < UQ_STATE_FORMAT)
UQStateMigrateToCurrent(st);
return((loadedFormat == UQ_STATE_FORMAT) ? UQ_LOAD_VALID : UQ_LOAD_LEGACY);
}
//+------------------------------------------------------------------+
//| Copy one common-folder file byte for byte |
//+------------------------------------------------------------------+
bool UQStateCopyFile(const string src, const string dst)
{
const int hs = FileOpen(src, FILE_READ | FILE_BIN | FILE_COMMON);
if(hs == INVALID_HANDLE)
{
PrintFormat("[UQ] ERROR: cannot open '%s' for copy (err=%d)", src, GetLastError());
return(false);
}
const int size = (int)FileSize(hs);
if(size <= 0)
{
FileClose(hs);
return(false);
}
uchar buf[];
ArrayResize(buf, size);
const int read = (int)FileReadArray(hs, buf, 0, size);
FileClose(hs);
if(read <= 0)
return(false);
const int hd = FileOpen(dst, FILE_WRITE | FILE_BIN | FILE_COMMON);
if(hd == INVALID_HANDLE)
{
PrintFormat("[UQ] ERROR: cannot create '%s' (err=%d)", dst, GetLastError());
return(false);
}
FileWriteArray(hd, buf, 0, read);
FileClose(hd);
return(true);
}
//+------------------------------------------------------------------+
Universal Quant EA
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
//| Display text of a state load result |
//+------------------------------------------------------------------+
string UQStateLoadText(const ENUM_UQ_STATE_LOAD r)
{
switch(r)
{
case UQ_LOAD_VALID: return("verified");
case UQ_LOAD_LEGACY: return("verified legacy schema");
case UQ_LOAD_ABSENT: return("absent");
case UQ_LOAD_ERROR: return("unusable");
}
return("unknown");
}
//+------------------------------------------------------------------+
//| Load one state file together with the generation it carries |
//| |
//| An absent or unusable file reports generation 0, so no caller can |
//| mistake "no proven generation" for a generation to build upon. |
//+------------------------------------------------------------------+
ENUM_UQ_STATE_LOAD UQStateLoadGenerational(const string fname, UQIdentity &id,
UQRiskState &st, long &rev, string &err)
{
rev = 0;
const ENUM_UQ_STATE_LOAD r = UQStateLoadFile(fname, id, st, err);
if(r == UQ_LOAD_VALID || r == UQ_LOAD_LEGACY)
rev = (st.stateRevision > 0 ? st.stateRevision : 0);
return(r);
}
//+------------------------------------------------------------------+
//| Verify that one file holds exactly the generation that was |
//| written: usable schema, expected revision and expected content. |
//+------------------------------------------------------------------+
bool UQStateVerifyFile(const string fname, UQIdentity &id, const long expectRevision,
const string expectCanonical, const string role)
{
UQRiskState check;
string err = "";
const ENUM_UQ_STATE_LOAD r = UQStateLoadFile(fname, id, check, err);
if(r != UQ_LOAD_VALID)
{
PrintFormat("[UQ] ERROR: the %s state file '%s' failed verification: %s",
role, fname, (StringLen(err) > 0 ? err : UQStateLoadText(r)));
return(false);
}
if(check.stateRevision != expectRevision)
{
PrintFormat("[UQ] ERROR: the %s state file '%s' carries generation %I64d instead of %I64d.",
role, fname, check.stateRevision, expectRevision);
return(false);
}
if(UQStateCanonicalBody(check) != expectCanonical)
{
PrintFormat("[UQ] ERROR: the %s state file '%s' does not describe the generation that was written.",
role, fname);
return(false);
}
return(true);
}
//+------------------------------------------------------------------+
//| Persist one state generation. |
//| |
//| The order is write-ahead, so no interruption can leave an entry- |
//| permitted state behind an unverified file: |
//| 1. the next generation is derived from what is proven on disk; |
//| 2. the temporary file is written and read back; |
//| 3. the same verified bytes are written to the backup and read |
//| back, so the backup can never lag the latest update; |
//| 4. the primary is replaced by the verified temporary file and |
//| read back; |
//| 5. primary and backup are reconciled on generation and content. |
//| |
//| A failure at any step returns false, which degrades the risk |
//| controller to unhealthy and blocks every new entry. |
//+------------------------------------------------------------------+
bool UQStateSave(UQIdentity &id, UQRiskState &st)
{
st.format = UQ_STATE_FORMAT;
st.identity = id.hash;
st.updatedAt = TimeCurrent();
if(st.updatedAt <= 0)
st.updatedAt = UQServerNow();
if(st.createdAt <= 0)
st.createdAt = st.updatedAt;
const string fname = UQStateFileName(id);
const string tmp = fname + UQ_STATE_TEMP_EXT;
const string bak = UQStateBackupFileName(id);
Universal Quant EA
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
//--- 1. the next generation is derived from the files proven on disk, so a
//--- stale in-memory copy can never overwrite a newer verified state
UQRiskState primaryOnDisk;
UQRiskState backupOnDisk;
string perr = "";
string berr = "";
long revPrimary = 0;
long revBackup = 0;
const ENUM_UQ_STATE_LOAD loadPrimary = UQStateLoadGenerational(fname, id, primaryOnDisk, revPrimary, perr);
const ENUM_UQ_STATE_LOAD loadBackup = UQStateLoadGenerational(bak, id, backupOnDisk, revBackup, berr);
if(loadPrimary == UQ_LOAD_ERROR)
PrintFormat("[UQ] WARNING: the unusable primary state '%s' (%s) is replaced by a verified generation.",
fname, perr);
const long provenMax = (revPrimary > revBackup ? revPrimary : revBackup);
if(provenMax > st.stateRevision)
{
PrintFormat("[UQ] ERROR: the persistent state holds generation %I64d while this instance holds generation %I64d. A newer verified state is never overwritten.",
provenMax, st.stateRevision);
return(false);
}
st.stateRevision = ((st.stateRevision > provenMax) ? st.stateRevision : provenMax) + 1;
//--- 2. serialise and stage
const string canonical = UQStateCanonicalBody(st);
const string body = canonical +
UQ_STATE_CHECKSUM_KEY + "=" + UQHashString(canonical) + "\r\n";
const int len = StringLen(body);
if(len <= 0)
{
PrintFormat("[UQ] ERROR: state serialisation produced no data");
return(false);
}
uchar buf[];
ArrayResize(buf, len);
for(int i = 0; i < len; i++)
buf[i] = (uchar)StringGetCharacter(body, i);
FileDelete(tmp, FILE_COMMON);
const int h = FileOpen(tmp, FILE_WRITE | FILE_BIN | FILE_COMMON);
if(h == INVALID_HANDLE)
{
PrintFormat("[UQ] ERROR: cannot create state file '%s' (err=%d)", tmp, GetLastError());
return(false);
}
FileWriteArray(h, buf, 0, len);
FileClose(h);
Universal Quant EA
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
//--- 3. the staged generation must verify before it is copied anywhere
if(!UQStateVerifyFile(tmp, id, st.stateRevision, canonical, "staged"))
{
FileDelete(tmp, FILE_COMMON);
return(false);
}
Universal Quant EA
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
//--- 4. the backup receives exactly the same verified generation
if(!UQStateCopyFile(tmp, bak))
{
Universal Quant EA
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
PrintFormat("[UQ] ERROR: the verified generation could not be written to the backup '%s'.", bak);
FileDelete(tmp, FILE_COMMON);
return(false);
}
if(!UQStateVerifyFile(bak, id, st.stateRevision, canonical, "backup"))
{
FileDelete(tmp, FILE_COMMON);
return(false);
}
Universal Quant EA
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
//--- 5. commit the verified generation as the primary
if(!FileMove(tmp, FILE_COMMON, fname, FILE_COMMON | FILE_REWRITE))
{
PrintFormat("[UQ] ERROR: cannot commit state file '%s' (err=%d)", fname, GetLastError());
FileDelete(tmp, FILE_COMMON);
return(false);
}
Universal Quant EA
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
if(!UQStateVerifyFile(fname, id, st.stateRevision, canonical, "primary"))
return(false);
Universal Quant EA
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
//--- 6. primary and backup must now describe the same generation
UQRiskState checkPrimary;
UQRiskState checkBackup;
string cerr = "";
Universal Quant EA
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
string xerr = "";
long revP = 0;
long revB = 0;
if(UQStateLoadGenerational(fname, id, checkPrimary, revP, cerr) != UQ_LOAD_VALID ||
UQStateLoadGenerational(bak, id, checkBackup, revB, xerr) != UQ_LOAD_VALID ||
revP != st.stateRevision || revB != st.stateRevision ||
UQStateCanonicalBody(checkPrimary) != UQStateCanonicalBody(checkBackup))
{
Universal Quant EA
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
PrintFormat("[UQ] ERROR: the persisted state could not be reconciled (primary generation %I64d, backup generation %I64d, expected %I64d).",
revP, revB, st.stateRevision);
return(false);
}
return(true);
}
//+------------------------------------------------------------------+
//| Redundant initialization marker |
//| |
//| A terminal global variable records that this identity scope has |
//| already been initialized and the creation timestamp it was |
//| initialized with. It is deliberately not stored inside the state |
//| file, so deleting or truncating the file cannot fabricate a |
//| fresh installation while the terminal data folder survives. |
//+------------------------------------------------------------------+
//--- A terminal global variable whose name is longer than this is silently
//--- dropped by the terminal while GlobalVariableSet() still reports success.
//--- The loss is only observable through the read-back verification performed
//--- by the callers of UQStateMarkerSet(), so the name itself must stay inside
//--- the limit instead of relying on the write to fail.
#define UQ_GV_NAME_MAX_CHARS 63
string UQStateMarkerName(UQIdentity &id)
{
const string name = UQ_GV_STATE_MARKER_PREFIX + id.key;
//--- every scope whose name already fits keeps exactly the name it always
//--- had, so an existing marker stays readable and its scope is never
//--- mistaken for a fresh installation
if(StringLen(name) <= UQ_GV_NAME_MAX_CHARS)
return(name);
//--- an over-long scope is folded into a bounded deterministic name built
//--- from the identity hash, which covers server|login|portfolio. A
//--- sanitised key always carries '_' separators while a hash is
//--- hexadecimal, so the two forms cannot collide with each other, and a
//--- collision would still fail closed through the timestamp comparison.
return(UQ_GV_STATE_MARKER_PREFIX +
StringSubstr(id.hash, 0, UQ_GV_NAME_MAX_CHARS - StringLen(UQ_GV_STATE_MARKER_PREFIX)));
}
ENUM_UQ_MARKER_STATE UQStateMarkerCheck(UQIdentity &id, const datetime createdAt, datetime &markerTime)
{
markerTime = 0;
const string name = UQStateMarkerName(id);
if(!GlobalVariableCheck(name))
return(UQ_MARKER_ABSENT);
const double v = GlobalVariableGet(name);
if(!MathIsValidNumber(v) || v <= 0.0)
return(UQ_MARKER_ABSENT);
markerTime = (datetime)(long)v;
if(markerTime != createdAt)
return(UQ_MARKER_MISMATCH);
return(UQ_MARKER_MATCH);
}
bool UQStateMarkerSet(UQIdentity &id, const datetime createdAt)
{
if(createdAt <= 0)
return(false);
const string name = UQStateMarkerName(id);
if(!GlobalVariableSet(name, (double)(long)createdAt))
{
PrintFormat("[UQ] ERROR: initialization marker '%s' could not be written (err=%d)",
name, GetLastError());
return(false);
}
return(true);
}
#endif // UQ_STATE_MQH
//+------------------------------------------------------------------+