| //+------------------------------------------------------------------+
|
| //| 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
|
|
|
| //+------------------------------------------------------------------+
|
| //| 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
|
|
|
| //--- 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,
|
| 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
|
|
|
| //+------------------------------------------------------------------+
|
| //| 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);
|
| 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;
|
| 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");
|
| }
|
| 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));
|
| }
|
| 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;
|
|
|
| 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.
|
| 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_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;
|
| }
|
|
|
| //--- 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);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| 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);
|
|
|
| //--- 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. |
|
| //+------------------------------------------------------------------+
|
| string UQStateMarkerName(UQIdentity &id)
|
| {
|
| return(UQ_GV_STATE_MARKER_PREFIX + id.key);
|
| }
|
|
|
| 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
|
| //+------------------------------------------------------------------+
|