//+------------------------------------------------------------------+ //| 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; | //| * 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 //--- 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 a read-back verification, so every //--- derived name must stay inside the limit instead of relying on the write //--- to fail. #define UQ_GV_NAME_MAX_CHARS 63 //+------------------------------------------------------------------+ //| 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 (LEGACY) string fingerprintV2; // SHA-256 of the canonical, unsanitised identity tuple }; //+------------------------------------------------------------------+ //| Persisted risk state | //+------------------------------------------------------------------+ struct UQRiskState { int format; string identity; // integrity hash of the owning scope (LEGACY) string identityV2; // full V2 fingerprint of the owning scope (schema 8) 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 //--- 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, 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, //--- schema 8. The V2 identity is appended LAST on purpose: every earlier //--- schema hashes only the leading N fields of this table, so inserting a //--- field anywhere else would silently change what an old checksum covers //--- and no previously persisted state could be verified any more. UQ_F_IDENTITY_V2, 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 #define UQ_STATE_IDENTITY_FIELDS 39 #define UQ_STATE_GENERATION_FIELDS 42 #define UQ_STATE_MONITOR_FIELDS 46 #define UQ_STATE_V2_FIELDS 47 //+------------------------------------------------------------------+ //| 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_MONITOR_FORMAT) return((int)UQ_STATE_MONITOR_FIELDS); if(fmt >= UQ_STATE_GENERATION_FORMAT) return((int)UQ_STATE_GENERATION_FIELDS); 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.identityV2 = ""; 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; 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"); 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"); case UQ_F_IDENTITY_V2: return("identityV2"); } 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)); 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)); case UQ_F_IDENTITY_V2: return(st.identityV2); } 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; 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; case UQ_F_IDENTITY_V2: //--- the complete 64 character fingerprint is required, so a //--- truncated or empty value can never pass as an identity if(!UQIdentityFingerprintValid(val)) { err = "field 'identityV2' is not a 64 character fingerprint"; return(false); } st.identityV2 = val; 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); } //--- the legacy identity field keeps its exact previous meaning and is //--- validated for every schema, including schema 8 if(st.identity != id.hash) { err = "state belongs to a different identity scope"; return(false); } if(st.format < UQ_STATE_FORMAT) { //--- a state written before schema 8 has no V2 identity, and its checksum //--- does not cover one: a value appearing there was never proven if(StringLen(st.identityV2) > 0) { err = "legacy state unexpectedly carries a V2 identity"; return(false); } } else { //--- schema 8 must carry the complete V2 fingerprint of exactly this //--- logical identity. An empty or missing value is never accepted. if(!UQIdentityFingerprintValid(st.identityV2)) { err = "V2 identity fingerprint is missing or malformed"; return(false); } if(!UQIdentityFingerprintValid(id.fingerprintV2)) { err = "the live V2 identity fingerprint is unavailable"; return(false); } if(st.identityV2 != id.fingerprintV2) { err = "state belongs to a different V2 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. 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); } } } } //--- 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) { 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_MONITOR_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); } //+------------------------------------------------------------------+ //| Persistence locators | //| | //| The V2 locators are derived from the full 256 bit fingerprint and | //| are the only locators a new state is ever written to. The legacy | //| locators keep the exact names every previously published build | //| used, because an existing installation still owns its state file | //| under that name and must be found (and validated) for migration. | //| | //| The two families are separate functions on purpose: no caller can | //| accidentally derive a V2 name from the lossy legacy key, and the | //| legacy names are never recomputed from the new fingerprint. | //+------------------------------------------------------------------+ string UQStateFileName(UQIdentity &id) { if(!UQIdentityFingerprintValid(id.fingerprintV2)) return(""); return(UQ_STATE_V2_FILE_PREFIX + id.fingerprintV2 + ".cfg"); } string UQStateBackupFileName(UQIdentity &id) { if(!UQIdentityFingerprintValid(id.fingerprintV2)) return(""); return(UQ_STATE_V2_FILE_PREFIX + id.fingerprintV2 + UQ_STATE_BACKUP_EXT); } //+------------------------------------------------------------------+ //| Legacy locators, byte for byte identical to every previously | //| published build. They are read for migration and are otherwise | //| left untouched: a legacy file is never renamed, deleted or | //| overwritten by this build. | //+------------------------------------------------------------------+ string UQStateLegacyFileName(UQIdentity &id) { return("UQ_EA_STATE_" + id.key + ".cfg"); } string UQStateLegacyBackupFileName(UQIdentity &id) { return("UQ_EA_STATE_" + id.key + UQ_STATE_BACKUP_EXT); } //+------------------------------------------------------------------+ //| Build the persistence identity from explicit components | //| | //| Identical to UQBuildIdentity() for the live account; the split | //| exists so the identity of a synthetic tuple can be built and | //| verified without touching the account currently connected. | //+------------------------------------------------------------------+ bool UQBuildIdentityFromParts(const string server, const long login, const string portfolio, UQIdentity &id, string &err) { err = ""; id.server = server; id.login = login; id.portfolio = portfolio; id.key = ""; id.hash = ""; id.fingerprintV2 = ""; 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); } //--- legacy identity: preserved byte for byte, still used for the legacy //--- locators, the legacy validation of persisted state and diagnostics id.key = UQSanitizeKey(id.server) + "_" + IntegerToString(id.login) + "_" + UQSanitizeKey(id.portfolio); id.hash = UQHashString(id.server + "|" + IntegerToString(id.login) + "|" + id.portfolio); //--- V2 identity: built from the ORIGINAL components, so two identities whose //--- sanitised strings happen to match never share a locator. A failure here //--- fails closed: there is no fall back to the lossy legacy hash. string fpErr = ""; if(!UQComputeIdentityFingerprintV2(id.server, id.login, id.portfolio, id.fingerprintV2, fpErr)) { id.fingerprintV2 = ""; err = "identity fingerprint V2 could not be established: " + fpErr; return(false); } if(!UQIdentityFingerprintValid(id.fingerprintV2)) { id.fingerprintV2 = ""; err = "identity fingerprint V2 is malformed"; return(false); } return(true); } //+------------------------------------------------------------------+ //| Build the persistence identity from the live account | //+------------------------------------------------------------------+ bool UQBuildIdentity(const string portfolio, UQIdentity &id, string &err) { return(UQBuildIdentityFromParts(AccountInfoString(ACCOUNT_SERVER), (long)AccountInfoInteger(ACCOUNT_LOGIN), portfolio, id, err)); } //+------------------------------------------------------------------+ //| 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 UQStateMigrateToMonitor(UQRiskState &st) { if(st.format >= UQ_STATE_MONITOR_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_MONITOR_FORMAT; return; } //--- 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_MONITOR_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_MONITOR_FORMAT; } //+------------------------------------------------------------------+ //| Upgrade a state to the current schema. | //| | //| Every earlier schema is upgraded in order: the older builds first | //| reach schema 7 (monitoring continuity), then schema 8 adds the V2 | //| identity. A migration never invents a baseline, never clears a | //| lock, never rebuilds a reservation as zero risk and never creates | //| a new risk cycle. | //+------------------------------------------------------------------+ void UQStateMigrateToCurrent(UQRiskState &st) { if(st.format >= UQ_STATE_FORMAT) return; UQStateMigrateToMonitor(st); if(st.format < UQ_STATE_FORMAT) { //--- 7 -> 8. The V2 field is staged empty here: the only identity that //--- may ever enter it is the live fingerprint of the identity that just //--- passed the legacy identity validation, and UQStateSave() stamps //--- exactly that value, so no invented identity can reach a file. st.identityV2 = ""; 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); } //+------------------------------------------------------------------+ //| 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) { //--- a state may only be persisted together with the identity it belongs. //--- Without a valid V2 fingerprint no locator exists, so saving would write //--- a shared or empty name: the save fails closed instead. if(!UQIdentityFingerprintValid(id.fingerprintV2)) { PrintFormat("[UQ] ERROR: the V2 identity fingerprint is unavailable, so no risk state may be persisted for this scope."); return(false); } st.format = UQ_STATE_FORMAT; st.identity = id.hash; st.identityV2 = id.fingerprintV2; 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); //--- 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); //--- 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); } //--- 4. the backup receives exactly the same verified generation if(!UQStateCopyFile(tmp, bak)) { 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); } //--- 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); } if(!UQStateVerifyFile(fname, id, st.stateRevision, canonical, "primary")) return(false); //--- 6. primary and backup must now describe the same generation UQRiskState checkPrimary; UQRiskState checkBackup; string cerr = ""; 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)) { 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. | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| V2 initialization marker name | //| | //| The name is derived from the V2 fingerprint only. 58 hexadecimal | //| characters carry 232 of the 256 bits and keep the whole name at | //| exactly 63 characters, the terminal limit; the complete 256 bit | //| fingerprint is retained in the persisted state and in the file | //| locators. The "UQ2M_" namespace is disjoint from the legacy | //| "UQEA_STATE_MARKER_" namespace, so neither can be read as the | //| other. | //+------------------------------------------------------------------+ string UQStateMarkerName(UQIdentity &id) { const string shortFp = UQIdentityFingerprintShort(id.fingerprintV2); if(StringLen(shortFp) != UQ_IDV2_GV_CHARS) return(""); const string name = UQ_GV_STATE_MARKER_V2_PREFIX + shortFp; if(StringLen(name) > UQ_GV_NAME_MAX_CHARS) return(""); return(name); } //+------------------------------------------------------------------+ //| Legacy initialization marker name | //| | //| Byte for byte the name every previously published build used. It | //| is only ever read (to detect an already initialized legacy scope) | //| and is never written, renamed or removed by this build. | //+------------------------------------------------------------------+ string UQStateLegacyMarkerName(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))); } //+------------------------------------------------------------------+ //| Read one marker global variable | //+------------------------------------------------------------------+ ENUM_UQ_MARKER_STATE UQMarkerCheckName(const string name, const datetime createdAt, datetime &markerTime) { markerTime = 0; if(StringLen(name) <= 0) return(UQ_MARKER_ABSENT); 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); } ENUM_UQ_MARKER_STATE UQStateMarkerCheck(UQIdentity &id, const datetime createdAt, datetime &markerTime) { return(UQMarkerCheckName(UQStateMarkerName(id), createdAt, markerTime)); } //--- legacy marker evidence. Called with createdAt = 0, so any existing value //--- reports MISMATCH, which is the "this scope was initialized before" signal. ENUM_UQ_MARKER_STATE UQStateLegacyMarkerCheck(UQIdentity &id, const datetime createdAt, datetime &markerTime) { return(UQMarkerCheckName(UQStateLegacyMarkerName(id), createdAt, markerTime)); } //+------------------------------------------------------------------+ //| Write one marker global variable and prove the terminal kept it | //| | //| A write the terminal silently declined to persist must never be | //| read as a recorded initialization, so the exact name and the exact | //| stored value are verified by read-back. | //+------------------------------------------------------------------+ bool UQMarkerSetVerified(const string name, const datetime createdAt) { if(createdAt <= 0) return(false); if(StringLen(name) <= 0 || StringLen(name) > UQ_GV_NAME_MAX_CHARS) { PrintFormat("[UQ] ERROR: initialization marker name is unusable (%d chars).", StringLen(name)); return(false); } if(!GlobalVariableSet(name, (double)(long)createdAt)) { PrintFormat("[UQ] ERROR: initialization marker '%s' could not be written (err=%d)", name, GetLastError()); return(false); } if(!GlobalVariableCheck(name)) { PrintFormat("[UQ] ERROR: initialization marker '%s' could not be read back (err=%d)", name, GetLastError()); return(false); } if((datetime)(long)GlobalVariableGet(name) != createdAt) { PrintFormat("[UQ] ERROR: initialization marker '%s' does not hold the value that was written.", name); return(false); } return(true); } bool UQStateMarkerSet(UQIdentity &id, const datetime createdAt) { return(UQMarkerSetVerified(UQStateMarkerName(id), createdAt)); } //+------------------------------------------------------------------+ //| Name of the V2 duplicate-instance guard | //| | //| Derived from the V2 fingerprint only, so two logical identities | //| that share a sanitised legacy key can never share a guard. 58 of | //| the 64 hexadecimal characters carry 232 of the 256 bits and keep | //| the whole name at exactly 63 characters. An unusable fingerprint | //| yields an empty name, which the publisher refuses: an unproven | //| guard always fails closed. | //+------------------------------------------------------------------+ string UQInstanceGuardName(const string fingerprintV2) { const string shortFp = UQIdentityFingerprintShort(fingerprintV2); if(StringLen(shortFp) != UQ_IDV2_GV_CHARS) return(""); const string name = UQ_GV_INSTANCE_V2_PREFIX + shortFp; if(StringLen(name) > UQ_GV_NAME_MAX_CHARS) return(""); return(name); } //+------------------------------------------------------------------+ //| Name of the legacy duplicate-instance guard | //| | //| Byte for byte the name every previously published build used. It | //| is only ever read, to prove that no instance started by an older | //| build is still running, and it is never written, published as the | //| active guard or deleted: it is derived from the lossy identity | //| key, and publishing it again would reintroduce exactly the | //| cross-portfolio false collision this correction removes. | //| | //| An identity whose name already fits keeps the name it always had. | //| An over-long identity is folded into a bounded deterministic name | //| built from the identity hash (UQHashString), which covers the | //| complete identity key. A hash collision would still fail closed | //| through the heartbeat read-back of the publisher. | //+------------------------------------------------------------------+ string UQInstanceGuardLegacyName(const string identityKey) { const string name = UQ_GV_INSTANCE_PREFIX + identityKey; //--- a name that already fits is returned unchanged, so no existing //--- instance guard is renamed by this correction if(StringLen(name) <= UQ_GV_NAME_MAX_CHARS) return(name); //--- the folded form is bounded by construction and deterministic: the //--- whole identity key is hashed, nothing is truncated from the key const int budget = UQ_GV_NAME_MAX_CHARS - StringLen(UQ_GV_INSTANCE_PREFIX); return(UQ_GV_INSTANCE_PREFIX + StringSubstr(UQHashString(identityKey), 0, budget)); } //+------------------------------------------------------------------+ //| Classification of the persistence scope that is about to load | //| | //| The decision is pure: it is derived from evidence only, so it can | //| be reviewed and tested apart from the file system and the terminal | //| state. Every input is "the evidence exists", never "the evidence | //| loaded successfully", so an unusable file can never be mistaken | //| for an absent one. | //+------------------------------------------------------------------+ enum ENUM_UQ_SCOPE_DECISION { UQ_SCOPE_V2_AUTHORITATIVE = 0, // a V2 persistence scope exists and is authoritative UQ_SCOPE_V2_MARKER_ONLY = 1, // V2 marker proven while the V2 state is missing: refuse UQ_SCOPE_LEGACY_MIGRATE = 2, // verified legacy state of this exact identity: migrate UQ_SCOPE_LEGACY_REFUSE = 3, // legacy evidence that must never be migrated: refuse UQ_SCOPE_FRESH = 4 // no evidence at all: authorized first initialization only }; //+------------------------------------------------------------------+ //| Decide how a persistence scope may be entered | //+------------------------------------------------------------------+ ENUM_UQ_SCOPE_DECISION UQScopeDecision(const bool v2StatePresent, const bool v2MarkerPresent, const bool legacyStatePresent, const bool legacyStateUsable, const bool legacyMarkerPresent, string &reason) { reason = ""; //--- A. the V2 scope exists: only V2 evidence is ever consulted from here if(v2StatePresent) return(UQ_SCOPE_V2_AUTHORITATIVE); //--- B. the V2 marker proves that this scope was initialized under V2. Its //--- missing state is a loss of evidence, not a fresh installation, and //--- it is never repaired from stale legacy data. if(v2MarkerPresent) { reason = "the V2 initialization marker exists while both V2 state files are absent; this scope was initialized before and it must not be rebuilt from older data"; return(UQ_SCOPE_V2_MARKER_ONLY); } //--- C/D. no V2 evidence: a legacy state of this exact identity may migrate if(legacyStatePresent && legacyStateUsable) return(UQ_SCOPE_LEGACY_MIGRATE); //--- E. legacy evidence exists but nothing of it belongs to this identity: //--- this is the confirmed collision case, and it is a persistence //--- ownership conflict, never permission to discard existing history if(legacyStatePresent) { reason = "a legacy state file exists for this locator but it is unusable or belongs to a different identity; it is never overwritten, replaced or treated as a fresh installation"; return(UQ_SCOPE_LEGACY_REFUSE); } if(legacyMarkerPresent) { reason = "a legacy initialization marker exists while no legacy state file does; this scope was initialized before and must not be treated as a fresh installation"; return(UQ_SCOPE_LEGACY_REFUSE); } return(UQ_SCOPE_FRESH); } //+------------------------------------------------------------------+ //| Prove that a migration preserved every risk-bearing field | //| | //| Relocating the persistence scope must not change a single risk | //| decision. The values are compared through the canonical field | //| serializer, i.e. exactly the text that is persisted, so a value | //| that survives the round trip unchanged compares equal. | //| | //| The schema version, the update timestamp and the persistence | //| generation are expected to advance, and the two identity fields | //| are checked separately against the live identity. | //+------------------------------------------------------------------+ bool UQStatePreserved(UQRiskState &before, UQRiskState &after, string &detail) { detail = ""; for(int i = 0; i < (int)UQ_F_COUNT; i++) { if(i == (int)UQ_F_FORMAT || i == (int)UQ_F_UPDATED_AT || i == (int)UQ_F_STATE_REVISION) continue; if(i == (int)UQ_F_IDENTITY || i == (int)UQ_F_IDENTITY_V2) continue; const string a = UQStateFieldValue(before, i); const string b = UQStateFieldValue(after, i); if(a != b) { detail = StringFormat("field '%s' changed from '%s' to '%s'", UQFieldKey(i), a, b); return(false); } } return(true); } //+------------------------------------------------------------------+ //| Prove that a migrated state carries no weaker control than the | //| state it was migrated from | //| | //| A relocation is allowed to keep a lock armed that the evaluation | //| pass has just re-armed, but it is never allowed to disarm one. | //+------------------------------------------------------------------+ bool UQStateLocksNotCleared(UQRiskState &before, UQRiskState &after, string &detail) { detail = ""; if(before.staticLock && !after.staticLock) { detail = "the static-drawdown lock was cleared by the migration"; return(false); } if(before.targetLock && !after.targetLock) { detail = "the equity-target lock was cleared by the migration"; return(false); } if(before.dailyLock && !after.dailyLock && before.dailyDate == after.dailyDate) { detail = "the daily-loss lock was cleared inside the same trading day by the migration"; return(false); } if(before.recoveryHold && !after.recoveryHold) { detail = "the recovery hold was cleared by the migration"; return(false); } if(before.gapHold && !after.gapHold && before.dailyDate == after.dailyDate) { detail = "the monitoring-gap hold was cleared inside the same trading day by the migration"; return(false); } if(before.reserveState != UQ_RESERVE_NONE && after.reserveState == UQ_RESERVE_NONE) { detail = "an outstanding entry reservation was discarded by the migration"; return(false); } if(before.cycleId != after.cycleId || before.cycleSequence != after.cycleSequence) { detail = "the risk-cycle identity changed during the migration"; return(false); } return(true); } #endif // UQ_STATE_MQH //+------------------------------------------------------------------+