2026-10-30 07:19:59 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| UQ_Defs.mqh |
|
| | | //| Universal Quant EA - shared types and helpers |
|
| | | //| |
|
| | | //| Declarations and small pure helpers only. No trading logic and |
|
| | | //| no persistent side effects live in this file. |
|
| | | //+------------------------------------------------------------------+
|
| | | #ifndef UQ_DEFS_MQH
|
| | | #define UQ_DEFS_MQH
|
| | |
|
2026-10-31 13:28:11 +07:00 | | | #define UQ_VERSION "1.03"
|
| | | #define UQ_STATE_FORMAT 8 // current persisted schema (adds the V2 identity fingerprint)
|
| | | #define UQ_STATE_MONITOR_FORMAT 7 // previous published schema (monitoring continuity), migrated on load
|
 fix: enforce account scope and daily monitoring continuity
Schema 7 adds a persisted monitoring heartbeat, the last recorded gap and a gap hold, with deterministic migration from schema 6 that keeps every lock, cycle identity and reservation.
D0 is no longer invented on startup or on a rollover: a trading day is adopted only when its boundary was observed while the EA was executing, or when the equity path across it is provably constant (no position, no pending order, no account deal). Otherwise the previous D0 is preserved, a monitoring-gap hold blocks every new entry and the exact gap is reported; the hold survives restart and clears only at an observed day boundary or an explicit owner cycle. Static, target and daily thresholds keep their priority, so a breach is still locked and liquidated while a day is unverified.
Account scope is now counted and disclosed for owned and unmanaged positions and pending orders; unmanaged pending orders block entries under the external-exposure guard, and the EA states that the account-wide limits cannot be guaranteed while whole-account liquidation stays disabled.
2026-10-30 11:20:47 +07:00 | | | #define UQ_STATE_GENERATION_FORMAT 6 // previous published schema (state generation), migrated on load
|
 fix: recover the latest verified persistent state
The backup was built from the previous primary before the new primary was installed, so it could lag the newest successful update, and a missing primary was still treated as a candidate fresh installation. Neither an interrupted commit nor a stale mirror was detectable.
B1 state schema 6 (defines UQ_STATE_FORMAT=6, UQ_STATE_IDENTITY_FORMAT=5) adds a monotonic stateRevision plus recoveryHold/recoveryHoldTime. Formats 1..5 keep their own field block, so every published checksum still verifies over exactly the block that produced it, and a migration sets generation 1 without inventing a lock event.
B2 UQStateSave is write-ahead and generation-aware: the next generation is derived from what is proven on disk (primary and backup), the staged file is written and read back, the same verified bytes go to the backup and are read back, the primary is replaced and read back, and primary and backup are finally reconciled on generation and content. A newer verified state is never overwritten by an older instance; a failure at any step returns false, which blocks every new entry.
B3 LoadPersistentState orders primary and backup by generation: newer primary wins, equal generations must agree on content (otherwise fail closed), and a backup exactly one generation ahead is the interrupted-commit window and is adopted. Anything else is refused.
B4 a backup is evidence of a previous initialization, so a missing primary is never a fresh installation and never creates a replacement B0; a verified backup is restored only under explicit owner authorization.
B5 a recovered, reordered or superseded state always starts behind a persisted recovery hold (UQ_BLOCK_RECOVERY) that survives restart and only an explicit owner-authorized recovery risk cycle (UQ_RESET_RECOVERY, fresh nonce, explicit B0, flat scope) can clear. A recovery never clears a permanent lock the recovered state carries, and a cycle without a prior permanent lock records no prior-cycle reference instead of an inconsistent one.
B6 the initialization marker is written and then verified; a marker that cannot be persisted or verified fails the load closed.
B7 the status panel reports the persistence generation and an active recovery hold.
2026-10-30 11:09:43 +07:00 | | | #define UQ_STATE_IDENTITY_FORMAT 5 // previous published schema (reservation identity), migrated on load
|
 fix: correlate outstanding entry risk reservations
A reservation could be released merely because an unrelated position or pending order existed under the same Magic Number, and an order ticket was treated as a position ticket. An entry whose submission result was lost also released its reserved risk.
- UQ_Defs: state schema 5 adds the reservation identity; ENUM_UQ_SUBMIT_RESULT and a retcode classifier that only accepts proven non-execution.
- UQ_State: reservation id, magic, scope, direction, volume, entry, stop, order/deal/position tickets are persisted, checksummed and strictly validated. A previous-schema reservation migrates to UNRESOLVED with its money preserved, never to NONE.
- UQ_Risk: UQReserveCorrelate links a reservation to the broker only through its own order ticket, a deal whose DEAL_ORDER is that ticket, or the traced POSITION_IDENTIFIER. Ambiguity, a disconnected terminal and a missing record all keep the money reserved as UNRESOLVED and block entries.
- UQ_Exec: UQSendMarket reports the submission outcome; the caller withdraws reserved risk only on a local preflight failure or a proven broker rejection.
- Universal_Quant_EA.mq5: deals and order transactions reconcile the reservation immediately; status and startup diagnostics expose the reservation identity.
2026-10-30 10:43:15 +07:00 | | | #define UQ_STATE_RESERVE_FORMAT 4 // previous published schema, migrated on load
|
2026-10-30 08:52:18 +07:00 | | | #define UQ_STATE_CYCLE_FORMAT 3 // previous published risk-cycle schema, migrated on load
|
2026-10-30 08:37:46 +07:00 | | | #define UQ_STATE_PREV_FORMAT 2 // previous published checksummed schema, migrated on load
|
| | | #define UQ_STATE_LEGACY_FORMAT 1 // original published schema, migrated on load
|
2026-10-30 07:19:59 +07:00 | | | #define UQ_COMMENT_PREFIX "Universal_Quant_EA"
|
| | | #define UQ_GV_INSTANCE_PREFIX "UQEA_INSTANCE_"
|
 fix: validate persistent risk-state integrity and fail closed on missing state
* UQ_State: checksummed schema v2 with strict typed parsing, duplicate/unknown-field rejection and semantic validation of every mandatory field; legacy v1 files are validated and migrated on load
* UQ_State: verified backup retention, staged-file verification before commit and post-commit re-verification
* UQ_State: redundant identity-scoped initialization marker reconciled against the persisted state
* UQ_Risk: a new risk cycle requires an explicit owner authorization and a deliberately declared positive B0; the current account balance is never used as a substitute
* UQ_Risk: missing, empty, corrupt, foreign, version-unsupported or schema-invalid state fails closed; backup recovery only under explicit owner authorization
* UQ_Risk: a failed state write degrades the controller to unhealthy, blocks all new entries, preserves the in-memory lock and retries at a bounded interval
* UQ_Risk: daily rollover keeps permanent locks and reports unmonitored intervals explicitly
* UQ_Defs: state schema constants, marker global-variable prefix and the STATE PERSISTENCE FAILED block reason
* Universal_Quant_EA: first-initialization and backup-restore authorization inputs, input validation and state-health status line
2026-10-30 08:07:18 +07:00 | | | #define UQ_GV_STATE_MARKER_PREFIX "UQEA_STATE_MARKER_"
|
2026-10-30 07:19:59 +07:00 | | |
|
2026-10-31 13:28:11 +07:00 | | | //--- V2 locator namespaces. They are disjoint from the legacy namespaces above,
|
| | | //--- so a V2 locator can never be mistaken for a legacy one (and the reverse),
|
| | | //--- and no lossy identity key is ever used to derive them.
|
| | | #define UQ_STATE_V2_FILE_PREFIX "UQ_EA_STATE_V2_"
|
| | | #define UQ_GV_STATE_MARKER_V2_PREFIX "UQ2M_"
|
| | | #define UQ_GV_INSTANCE_V2_PREFIX "UQ2I_"
|
| | |
|
2026-10-30 08:37:46 +07:00 | | | //--- owner reset of a permanent lock = explicit start of a new risk cycle
|
| | | //--- owner reset operation. Every reset consumes a unique operator-supplied
|
| | | //--- nonce, so one identical request can never be applied twice, while a later
|
| | | //--- legitimate reset remains possible with a new nonce.
|
| | | //--- legitimate reset remains possible with a new nonce.
|
| | | #define UQ_RESET_NONCE_MIN_LEN 12 // shorter nonces are rejected
|
| | | #define UQ_RESET_NONCE_MAX_LEN 48
|
| | | #define UQ_RESET_HISTORY_MAX 8 // consumed nonces retained for reuse checks
|
| | | #define UQ_RESET_HISTORY_MAX_CHARS 640
|
| | | #define UQ_RESET_CYCLE_ID_MAX_LEN 48
|
| | | #define UQ_RESET_FIELD_SEPARATOR "|"
|
| | |
|
| | | //--- owner-controlled confirmation phrase. It is an operational safeguard,
|
| | | //--- not a secret, and it is published with the source.
|
2026-10-30 07:19:59 +07:00 | | | #define UQ_RESET_TOKEN "CONFIRM-UQ-RESET"
|
| | |
|
| | | //--- internal result codes
|
| | | #define UQ_RET_OK 0
|
| | | #define UQ_RET_DATA -1
|
| | | #define UQ_RET_RISK -2
|
| | | #define UQ_RET_EXEC -3
|
| | | #define UQ_RET_CONFIG -4
|
| | |
|
| | | //--- bounded liquidation policy
|
2026-10-30 08:18:19 +07:00 | | | #define UQ_MAX_LIQ_ATTEMPTS 10 // full cancel/close/verify cycles
|
| | | #define UQ_LIQ_RETRY_SECONDS 2 // retry interval while the budget lasts
|
| | | #define UQ_CLOSE_ATTEMPTS 3 // close requests per position per cycle
|
| | | #define UQ_LIQ_MANUAL_RETRY_SECONDS 30 // retry interval after the budget is gone
|
2026-10-30 07:19:59 +07:00 | | |
|
| | | //--- duplicate-instance heartbeat validity window (seconds)
|
| | | #define UQ_INSTANCE_TTL_SECONDS 180
|
| | |
|
 fix: enforce account scope and daily monitoring continuity
Schema 7 adds a persisted monitoring heartbeat, the last recorded gap and a gap hold, with deterministic migration from schema 6 that keeps every lock, cycle identity and reservation.
D0 is no longer invented on startup or on a rollover: a trading day is adopted only when its boundary was observed while the EA was executing, or when the equity path across it is provably constant (no position, no pending order, no account deal). Otherwise the previous D0 is preserved, a monitoring-gap hold blocks every new entry and the exact gap is reported; the hold survives restart and clears only at an observed day boundary or an explicit owner cycle. Static, target and daily thresholds keep their priority, so a breach is still locked and liquidated while a day is unverified.
Account scope is now counted and disclosed for owned and unmanaged positions and pending orders; unmanaged pending orders block entries under the external-exposure guard, and the EA states that the account-wide limits cannot be guaranteed while whole-account liquidation stays disabled.
2026-10-30 11:20:47 +07:00 | | | //--- Daily monitoring continuity
|
| | | //--- The three account limits can only be enforced while the EA is executing.
|
| | | //--- A heartbeat is persisted with the risk state, so a restart can prove
|
| | | //--- whether the current broker-server trading day was observed from its
|
| | | //--- boundary. A longer unobserved interval is a monitoring gap: it is
|
| | | //--- reported, never presented as a monitored trading day, and the daily
|
| | | //--- baseline is never silently re-measured after it.
|
| | | #define UQ_MONITOR_HEARTBEAT_SECONDS 60 // persisted heartbeat interval
|
| | | #define UQ_MONITOR_GAP_SECONDS 300 // unobserved interval that counts as a gap
|
| | | #define UQ_MONITOR_GAP_MAX_SECONDS 8640000 // 100 days, keeps the diagnostic bounded
|
| | |
|
2026-10-30 08:52:18 +07:00 | | | //--- Entry-risk reservation
|
| | | //--- An accepted entry order is not necessarily visible as a position or as a
|
| | | //--- pending order at the same instant. Until the broker state proves where the
|
| | | //--- order went, the reserved money stays part of the aggregate exposure and
|
| | | //--- new entries stay blocked, so an uncertain order is never treated as zero.
|
| | | #define UQ_RESERVE_RECONCILE_SECONDS 20 // wait for an accepted order to become visible
|
| | | #define UQ_RESERVE_SYMBOL_MAX_LEN 32
|
 fix: correlate outstanding entry risk reservations
A reservation could be released merely because an unrelated position or pending order existed under the same Magic Number, and an order ticket was treated as a position ticket. An entry whose submission result was lost also released its reserved risk.
- UQ_Defs: state schema 5 adds the reservation identity; ENUM_UQ_SUBMIT_RESULT and a retcode classifier that only accepts proven non-execution.
- UQ_State: reservation id, magic, scope, direction, volume, entry, stop, order/deal/position tickets are persisted, checksummed and strictly validated. A previous-schema reservation migrates to UNRESOLVED with its money preserved, never to NONE.
- UQ_Risk: UQReserveCorrelate links a reservation to the broker only through its own order ticket, a deal whose DEAL_ORDER is that ticket, or the traced POSITION_IDENTIFIER. Ambiguity, a disconnected terminal and a missing record all keep the money reserved as UNRESOLVED and block entries.
- UQ_Exec: UQSendMarket reports the submission outcome; the caller withdraws reserved risk only on a local preflight failure or a proven broker rejection.
- Universal_Quant_EA.mq5: deals and order transactions reconcile the reservation immediately; status and startup diagnostics expose the reservation identity.
2026-10-30 10:43:15 +07:00 | | | #define UQ_RESERVE_ID_MAX_LEN 64 // reservation identifier
|
| | | #define UQ_RESERVE_SCOPE_MAX_LEN 64 // portfolio scope recorded with a reservation
|
| | | #define UQ_HISTORY_LOOKBACK_SECONDS 86400 // order/deal window searched for correlation
|
2026-10-30 08:52:18 +07:00 | | |
|
| | | enum ENUM_UQ_RESERVE_STATE
|
| | | {
|
| | | UQ_RESERVE_NONE = 0, // No outstanding entry risk
|
| | | UQ_RESERVE_SENT = 1, // Accepted by the server, not yet reconciled
|
| | | UQ_RESERVE_UNRESOLVED = 2 // Broker state cannot be verified: entries blocked
|
| | | };
|
| | |
|
| | | string UQReserveStateText(const ENUM_UQ_RESERVE_STATE s)
|
| | | {
|
| | | switch(s)
|
| | | {
|
| | | case UQ_RESERVE_NONE: return("none");
|
| | | case UQ_RESERVE_SENT: return("SENT_UNRECONCILED");
|
| | | case UQ_RESERVE_UNRESOLVED: return("UNRESOLVED");
|
| | | }
|
| | | return("unknown");
|
| | | }
|
| | |
|
 fix: correlate outstanding entry risk reservations
A reservation could be released merely because an unrelated position or pending order existed under the same Magic Number, and an order ticket was treated as a position ticket. An entry whose submission result was lost also released its reserved risk.
- UQ_Defs: state schema 5 adds the reservation identity; ENUM_UQ_SUBMIT_RESULT and a retcode classifier that only accepts proven non-execution.
- UQ_State: reservation id, magic, scope, direction, volume, entry, stop, order/deal/position tickets are persisted, checksummed and strictly validated. A previous-schema reservation migrates to UNRESOLVED with its money preserved, never to NONE.
- UQ_Risk: UQReserveCorrelate links a reservation to the broker only through its own order ticket, a deal whose DEAL_ORDER is that ticket, or the traced POSITION_IDENTIFIER. Ambiguity, a disconnected terminal and a missing record all keep the money reserved as UNRESOLVED and block entries.
- UQ_Exec: UQSendMarket reports the submission outcome; the caller withdraws reserved risk only on a local preflight failure or a proven broker rejection.
- Universal_Quant_EA.mq5: deals and order transactions reconcile the reservation immediately; status and startup diagnostics expose the reservation identity.
2026-10-30 10:43:15 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| Outcome of one market-order submission. |
|
| | | //| |
|
| | | //| The distinction is the whole point of the reservation. A local |
|
| | | //| preflight failure and a definitive broker rejection both prove |
|
| | | //| that no exposure was created, so only those two release the |
|
| | | //| reserved risk. An accepted request keeps it until the resulting |
|
| | | //| exposure is verified, and an uncertain result keeps it as |
|
| | | //| UNRESOLVED forever, because the request may have reached the |
|
| | | //| broker after the answer was lost. |
|
| | | //+------------------------------------------------------------------+
|
| | | enum ENUM_UQ_SUBMIT_RESULT
|
| | | {
|
| | | UQ_SUBMIT_NONE_LOCAL = 0, // refused locally, nothing was submitted
|
| | | UQ_SUBMIT_REJECTED = 1, // definitively rejected, no exposure exists
|
| | | UQ_SUBMIT_ACCEPTED = 2, // accepted by the server, exposure expected
|
| | | UQ_SUBMIT_UNCERTAIN = 3 // may have reached the broker, may have executed
|
| | | };
|
| | |
|
| | | string UQSubmitResultText(const ENUM_UQ_SUBMIT_RESULT r)
|
| | | {
|
| | | switch(r)
|
| | | {
|
| | | case UQ_SUBMIT_NONE_LOCAL: return("refused locally before submission");
|
| | | case UQ_SUBMIT_REJECTED: return("definitively rejected by the broker");
|
| | | case UQ_SUBMIT_ACCEPTED: return("accepted by the broker");
|
| | | case UQ_SUBMIT_UNCERTAIN: return("submission result is uncertain");
|
| | | }
|
| | | return("unknown");
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Does a server retcode prove that nothing was executed at all? |
|
| | | //| |
|
| | | //| Only the codes that the server returns instead of executing the |
|
| | | //| request qualify. Timeout, connection loss, the generic error code |
|
| | | //| and any unrecognised code leave the outcome open, because the |
|
| | | //| request may still have been executed. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool UQRetcodeIsDefinitiveRejection(const uint retcode)
|
| | | {
|
| | | switch(retcode)
|
| | | {
|
| | | case TRADE_RETCODE_REQUOTE:
|
| | | case TRADE_RETCODE_REJECT:
|
| | | case TRADE_RETCODE_CANCEL:
|
| | | case TRADE_RETCODE_INVALID:
|
| | | case TRADE_RETCODE_INVALID_VOLUME:
|
| | | case TRADE_RETCODE_INVALID_PRICE:
|
| | | case TRADE_RETCODE_INVALID_STOPS:
|
| | | case TRADE_RETCODE_TRADE_DISABLED:
|
| | | case TRADE_RETCODE_MARKET_CLOSED:
|
| | | case TRADE_RETCODE_NO_MONEY:
|
| | | case TRADE_RETCODE_PRICE_CHANGED:
|
| | | case TRADE_RETCODE_PRICE_OFF:
|
| | | case TRADE_RETCODE_INVALID_EXPIRATION:
|
| | | case TRADE_RETCODE_ORDER_CHANGED:
|
| | | case TRADE_RETCODE_TOO_MANY_REQUESTS:
|
| | | case TRADE_RETCODE_NO_CHANGES:
|
| | | case TRADE_RETCODE_SERVER_DISABLES_AT:
|
| | | case TRADE_RETCODE_CLIENT_DISABLES_AT:
|
| | | case TRADE_RETCODE_LOCKED:
|
| | | case TRADE_RETCODE_FROZEN:
|
| | | case TRADE_RETCODE_INVALID_FILL:
|
| | | case TRADE_RETCODE_ONLY_REAL:
|
| | | case TRADE_RETCODE_LIMIT_ORDERS:
|
| | | case TRADE_RETCODE_LIMIT_VOLUME:
|
| | | case TRADE_RETCODE_INVALID_ORDER:
|
| | | case TRADE_RETCODE_POSITION_CLOSED:
|
| | | case TRADE_RETCODE_INVALID_CLOSE_VOLUME:
|
| | | case TRADE_RETCODE_CLOSE_ORDER_EXIST:
|
| | | case TRADE_RETCODE_LIMIT_POSITIONS:
|
| | | case TRADE_RETCODE_REJECT_CANCEL:
|
| | | case TRADE_RETCODE_LONG_ONLY:
|
| | | case TRADE_RETCODE_SHORT_ONLY:
|
| | | case TRADE_RETCODE_CLOSE_ONLY:
|
| | | case TRADE_RETCODE_FIFO_CLOSE:
|
| | | case TRADE_RETCODE_HEDGE_PROHIBITED:
|
| | | return(true);
|
| | | }
|
| | | return(false);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Classify a failed submission from its server retcode. |
|
| | | //| |
|
| | | //| Retcode 0 means the terminal never received an answer, which is |
|
| | | //| exactly the ambiguous case: the request may be live at the broker.|
|
| | | //+------------------------------------------------------------------+
|
| | | ENUM_UQ_SUBMIT_RESULT UQSubmitResultFromRetcode(const uint retcode)
|
| | | {
|
| | | if(retcode == 0)
|
| | | return(UQ_SUBMIT_UNCERTAIN);
|
| | | if(UQRetcodeIsDefinitiveRejection(retcode))
|
| | | return(UQ_SUBMIT_REJECTED);
|
| | | return(UQ_SUBMIT_UNCERTAIN);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Can a text value be carried by the persisted state format? |
|
| | | //| |
|
| | | //| '=' and '|' delimit the file and the nonce history, and a line |
|
| | | //| break would split a record, so a value carrying them is refused |
|
| | | //| instead of being escaped into an ambiguous record. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool UQReserveTextSafe(const string s, const int maxLen)
|
| | | {
|
| | | const int len = StringLen(s);
|
| | | if(len <= 0 || len > maxLen)
|
| | | return(false);
|
| | | if(StringFind(s, "=") >= 0 || StringFind(s, "|") >= 0 ||
|
| | | StringFind(s, "\r") >= 0 || StringFind(s, "\n") >= 0)
|
| | | return(false);
|
| | | return(true);
|
| | | }
|
| | |
|
2026-10-30 07:19:59 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| Symbol selection mode |
|
| | | //+------------------------------------------------------------------+
|
| | | enum ENUM_UQ_SYMBOL_MODE
|
| | | {
|
| | | UQ_SYM_CURRENT_CHART = 0, // Current chart symbol only
|
| | | UQ_SYM_EXPLICIT_LIST = 1 // Explicit symbol list
|
| | | };
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Owner reset request for the permanent locks |
|
2026-10-30 08:37:46 +07:00 | | | //| |
|
| | | //| A reset never clears a lock in place: it starts a new risk cycle |
|
| | | //| with an explicitly authorized baseline, a new cycle identifier |
|
| | | //| and a consumed nonce. The daily-loss lock is never affected by a |
|
| | | //| reset; it clears only on a broker-server trading-day rollover. |
|
2026-10-30 07:19:59 +07:00 | | | //+------------------------------------------------------------------+
|
| | | enum ENUM_UQ_RESET_REQUEST
|
| | | {
|
| | | UQ_RESET_NONE = 0, // No reset
|
| | | UQ_RESET_STATIC = 1, // Clear static-drawdown lock
|
| | | UQ_RESET_TARGET = 2, // Clear equity-target lock
|
 fix: recover the latest verified persistent state
The backup was built from the previous primary before the new primary was installed, so it could lag the newest successful update, and a missing primary was still treated as a candidate fresh installation. Neither an interrupted commit nor a stale mirror was detectable.
B1 state schema 6 (defines UQ_STATE_FORMAT=6, UQ_STATE_IDENTITY_FORMAT=5) adds a monotonic stateRevision plus recoveryHold/recoveryHoldTime. Formats 1..5 keep their own field block, so every published checksum still verifies over exactly the block that produced it, and a migration sets generation 1 without inventing a lock event.
B2 UQStateSave is write-ahead and generation-aware: the next generation is derived from what is proven on disk (primary and backup), the staged file is written and read back, the same verified bytes go to the backup and are read back, the primary is replaced and read back, and primary and backup are finally reconciled on generation and content. A newer verified state is never overwritten by an older instance; a failure at any step returns false, which blocks every new entry.
B3 LoadPersistentState orders primary and backup by generation: newer primary wins, equal generations must agree on content (otherwise fail closed), and a backup exactly one generation ahead is the interrupted-commit window and is adopted. Anything else is refused.
B4 a backup is evidence of a previous initialization, so a missing primary is never a fresh installation and never creates a replacement B0; a verified backup is restored only under explicit owner authorization.
B5 a recovered, reordered or superseded state always starts behind a persisted recovery hold (UQ_BLOCK_RECOVERY) that survives restart and only an explicit owner-authorized recovery risk cycle (UQ_RESET_RECOVERY, fresh nonce, explicit B0, flat scope) can clear. A recovery never clears a permanent lock the recovered state carries, and a cycle without a prior permanent lock records no prior-cycle reference instead of an inconsistent one.
B6 the initialization marker is written and then verified; a marker that cannot be persisted or verified fails the load closed.
B7 the status panel reports the persistence generation and an active recovery hold.
2026-10-30 11:09:43 +07:00 | | | UQ_RESET_BOTH = 3, // Clear both permanent locks
|
| | | UQ_RESET_RECOVERY = 4 // Acknowledge a persistence recovery hold (new cycle)
|
2026-10-30 07:19:59 +07:00 | | | };
|
| | |
|
2026-10-30 08:18:19 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| Liquidation state machine. A liquidation is reported as complete |
|
| | | //| only after the order and position pools of the authorized scope |
|
| | | //| have been re-queried and verified empty. |
|
| | | //+------------------------------------------------------------------+
|
| | | enum ENUM_UQ_LIQ_STATE
|
| | | {
|
| | | UQ_LIQ_IDLE = 0, // No liquidation in progress
|
| | | UQ_LIQ_CANCEL_PENDING = 1, // Cancelling pending orders in scope
|
| | | UQ_LIQ_CLOSE_POSITIONS = 2, // Closing positions in scope
|
| | | UQ_LIQ_VERIFY_FLAT = 3, // Re-querying the order and position pools
|
| | | UQ_LIQ_RETRY_REQUIRED = 4, // Exposure remains, retry scheduled
|
| | | UQ_LIQ_MANUAL_INTERVENTION = 5, // Attempt budget exhausted, exposure remains
|
| | | UQ_LIQ_COMPLETE = 6 // Verified flat in the authorized scope
|
| | | };
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Human readable liquidation state |
|
| | | //+------------------------------------------------------------------+
|
| | | string UQLiqStateText(const ENUM_UQ_LIQ_STATE s)
|
| | | {
|
| | | switch(s)
|
| | | {
|
| | | case UQ_LIQ_IDLE: return("IDLE");
|
| | | case UQ_LIQ_CANCEL_PENDING: return("CANCEL_PENDING");
|
| | | case UQ_LIQ_CLOSE_POSITIONS: return("CLOSE_POSITIONS");
|
| | | case UQ_LIQ_VERIFY_FLAT: return("VERIFY_FLAT");
|
| | | case UQ_LIQ_RETRY_REQUIRED: return("RETRY_REQUIRED");
|
| | | case UQ_LIQ_MANUAL_INTERVENTION: return("MANUAL_INTERVENTION");
|
| | | case UQ_LIQ_COMPLETE: return("LIQUIDATION_COMPLETE");
|
| | | }
|
| | | return("UNKNOWN");
|
| | | }
|
| | |
|
2026-10-30 07:19:59 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| Reason why new entries are blocked (evaluated in priority order) |
|
| | | //+------------------------------------------------------------------+
|
| | | enum ENUM_UQ_BLOCK
|
| | | {
|
| | | UQ_BLOCK_NONE = 0, // OK
|
| | | UQ_BLOCK_STATIC = 1, // Static-drawdown permanent lock
|
| | | UQ_BLOCK_TARGET = 2, // Equity-target permanent lock
|
| | | UQ_BLOCK_DAILY = 3, // Daily-loss lock
|
| | | UQ_BLOCK_BASELINE = 4, // Baseline or persistent state invalid
|
| | | UQ_BLOCK_DISABLED = 5, // Trading input disabled
|
| | | UQ_BLOCK_PERMISSIONS = 6, // Terminal/account/symbol permission
|
| | | UQ_BLOCK_EXPOSURE = 7, // External or unmanageable exposure
|
| | | UQ_BLOCK_INSTANCE = 8, // Duplicate EA instance detected
|
 fix: validate persistent risk-state integrity and fail closed on missing state
* UQ_State: checksummed schema v2 with strict typed parsing, duplicate/unknown-field rejection and semantic validation of every mandatory field; legacy v1 files are validated and migrated on load
* UQ_State: verified backup retention, staged-file verification before commit and post-commit re-verification
* UQ_State: redundant identity-scoped initialization marker reconciled against the persisted state
* UQ_Risk: a new risk cycle requires an explicit owner authorization and a deliberately declared positive B0; the current account balance is never used as a substitute
* UQ_Risk: missing, empty, corrupt, foreign, version-unsupported or schema-invalid state fails closed; backup recovery only under explicit owner authorization
* UQ_Risk: a failed state write degrades the controller to unhealthy, blocks all new entries, preserves the in-memory lock and retries at a bounded interval
* UQ_Risk: daily rollover keeps permanent locks and reports unmonitored intervals explicitly
* UQ_Defs: state schema constants, marker global-variable prefix and the STATE PERSISTENCE FAILED block reason
* Universal_Quant_EA: first-initialization and backup-restore authorization inputs, input validation and state-health status line
2026-10-30 08:07:18 +07:00 | | | UQ_BLOCK_DATA = 9, // Market data or signal state not ready
|
2026-10-30 08:37:46 +07:00 | | | UQ_BLOCK_STATE = 10, // Persistent risk state could not be written
|
2026-10-30 08:52:18 +07:00 | | | UQ_BLOCK_RESET = 11, // Owner reset request rejected, entries blocked
|
 fix: recover the latest verified persistent state
The backup was built from the previous primary before the new primary was installed, so it could lag the newest successful update, and a missing primary was still treated as a candidate fresh installation. Neither an interrupted commit nor a stale mirror was detectable.
B1 state schema 6 (defines UQ_STATE_FORMAT=6, UQ_STATE_IDENTITY_FORMAT=5) adds a monotonic stateRevision plus recoveryHold/recoveryHoldTime. Formats 1..5 keep their own field block, so every published checksum still verifies over exactly the block that produced it, and a migration sets generation 1 without inventing a lock event.
B2 UQStateSave is write-ahead and generation-aware: the next generation is derived from what is proven on disk (primary and backup), the staged file is written and read back, the same verified bytes go to the backup and are read back, the primary is replaced and read back, and primary and backup are finally reconciled on generation and content. A newer verified state is never overwritten by an older instance; a failure at any step returns false, which blocks every new entry.
B3 LoadPersistentState orders primary and backup by generation: newer primary wins, equal generations must agree on content (otherwise fail closed), and a backup exactly one generation ahead is the interrupted-commit window and is adopted. Anything else is refused.
B4 a backup is evidence of a previous initialization, so a missing primary is never a fresh installation and never creates a replacement B0; a verified backup is restored only under explicit owner authorization.
B5 a recovered, reordered or superseded state always starts behind a persisted recovery hold (UQ_BLOCK_RECOVERY) that survives restart and only an explicit owner-authorized recovery risk cycle (UQ_RESET_RECOVERY, fresh nonce, explicit B0, flat scope) can clear. A recovery never clears a permanent lock the recovered state carries, and a cycle without a prior permanent lock records no prior-cycle reference instead of an inconsistent one.
B6 the initialization marker is written and then verified; a marker that cannot be persisted or verified fails the load closed.
B7 the status panel reports the persistence generation and an active recovery hold.
2026-10-30 11:09:43 +07:00 | | | UQ_BLOCK_RESERVATION = 12, // Outstanding entry risk cannot be reconciled
|
 fix: enforce account scope and daily monitoring continuity
Schema 7 adds a persisted monitoring heartbeat, the last recorded gap and a gap hold, with deterministic migration from schema 6 that keeps every lock, cycle identity and reservation.
D0 is no longer invented on startup or on a rollover: a trading day is adopted only when its boundary was observed while the EA was executing, or when the equity path across it is provably constant (no position, no pending order, no account deal). Otherwise the previous D0 is preserved, a monitoring-gap hold blocks every new entry and the exact gap is reported; the hold survives restart and clears only at an observed day boundary or an explicit owner cycle. Static, target and daily thresholds keep their priority, so a breach is still locked and liquidated while a day is unverified.
Account scope is now counted and disclosed for owned and unmanaged positions and pending orders; unmanaged pending orders block entries under the external-exposure guard, and the EA states that the account-wide limits cannot be guaranteed while whole-account liquidation stays disabled.
2026-10-30 11:20:47 +07:00 | | | UQ_BLOCK_RECOVERY = 13, // Persistence recovery hold, owner authorization required
|
| | | UQ_BLOCK_GAP = 14 // Monitoring gap: the daily baseline cannot be reconstructed
|
2026-10-30 07:19:59 +07:00 | | | };
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Broker-server time, with a safe fallback |
|
| | | //+------------------------------------------------------------------+
|
| | | datetime UQServerNow(void)
|
| | | {
|
| | | datetime t = TimeTradeServer();
|
| | | if(t <= 0)
|
| | | t = TimeCurrent();
|
| | | return(t);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Truncate a datetime to the start of its calendar day |
|
| | | //+------------------------------------------------------------------+
|
| | | datetime UQDayStart(const datetime t)
|
| | | {
|
| | | if(t <= 0)
|
| | | return(0);
|
| | | return((datetime)(((long)t / 86400) * 86400));
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Lowercase a string and replace every non-alphanumeric character |
|
| | | //| with '_'. Used to build file-system safe persistence keys. |
|
| | | //+------------------------------------------------------------------+
|
2026-10-31 13:28:11 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| UQ-V2 identity fingerprint |
|
| | | //| |
|
| | | //| The legacy identity is lossy by construction: UQSanitizeKey() |
|
| | | //| lowercases and folds every non-alphanumeric character to '_', so |
|
| | | //| 'A-B', 'A.B' and 'A_B' all become 'a_b' and two different logical |
|
| | | //| portfolios can share one legacy locator. The V2 fingerprint is |
|
| | | //| computed from the ORIGINAL components instead, so it tells every |
|
| | | //| distinct identity apart. |
|
| | | //| |
|
| | | //| Preimage layout. Nothing is delimited by a character, so no |
|
| | | //| component value can be confused with a field boundary: |
|
| | | //| |
|
| | | //| <tag:u8><length:u32 big-endian><value: exactly 'length' bytes> |
|
| | | //| |
|
| | | //| repeated for the fixed domain prefix and for every component, in |
|
| | | //| the fixed order domain, server, login, portfolio. A field carries |
|
| | | //| its own tag and its own length, so shifting bytes between fields |
|
| | | //| always changes the preimage. |
|
| | | //| |
|
| | | //| The server and the portfolio are encoded as UTF-8 (never a legacy |
|
| | | //| ANSI code page), preserving case and punctuation as supplied. The |
|
| | | //| login is encoded as canonical positive decimal ASCII. |
|
| | | //+------------------------------------------------------------------+
|
| | | #define UQ_IDV2_DOMAIN "UQID-V2"
|
| | | #define UQ_IDV2_TAG_DOMAIN 0
|
| | | #define UQ_IDV2_TAG_SERVER 1
|
| | | #define UQ_IDV2_TAG_LOGIN 2
|
| | | #define UQ_IDV2_TAG_PORTFOLIO 3
|
| | | #define UQ_IDV2_SHA256_BYTES 32
|
| | | #define UQ_IDV2_HEX_CHARS 64 // full 256 bit fingerprint
|
| | | #define UQ_IDV2_GV_CHARS 58 // 232 bit, so "UQ2M_"/"UQ2I_" + 58 = 63
|
| | | #define UQ_IDV2_MAX_FIELD_BYTES 8192
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Append one unsigned 32-bit big-endian length to a byte frame |
|
| | | //+------------------------------------------------------------------+
|
| | | void UQAppendU32BE(uchar &buf[], const uint v)
|
| | | {
|
| | | const int n = ArraySize(buf);
|
| | | ArrayResize(buf, n + 4);
|
| | | buf[n + 0] = (uchar)((v >> 24) & 0xFF);
|
| | | buf[n + 1] = (uchar)((v >> 16) & 0xFF);
|
| | | buf[n + 2] = (uchar)((v >> 8) & 0xFF);
|
| | | buf[n + 3] = (uchar)(v & 0xFF);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Append one tagged, length-prefixed field to a byte frame |
|
| | | //+------------------------------------------------------------------+
|
| | | bool UQAppendTaggedField(uchar &buf[], const uchar tag, const uchar &value[], const int len)
|
| | | {
|
| | | if(len < 0 || len > UQ_IDV2_MAX_FIELD_BYTES)
|
| | | return(false);
|
| | |
|
| | | const int n = ArraySize(buf);
|
| | | ArrayResize(buf, n + 1);
|
| | | buf[n] = tag;
|
| | | UQAppendU32BE(buf, (uint)len);
|
| | |
|
| | | if(len > 0)
|
| | | {
|
| | | const int m = ArraySize(buf);
|
| | | ArrayResize(buf, m + len);
|
| | | if(ArrayCopy(buf, value, m, 0, len) != len)
|
| | | return(false);
|
| | | }
|
| | | return(true);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Convert a string to its exact UTF-8 byte sequence |
|
| | | //| |
|
| | | //| StringToCharArray() appends one terminating zero byte. It must |
|
| | | //| never enter the preimage, otherwise a trailing byte would join |
|
| | | //| the hash input and two different values could collide. The |
|
| | | //| conversion is verified by converting the produced bytes back and |
|
| | | //| demanding the identical string; a failure is never replaced by |
|
| | | //| another code page. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool UQUtf8Bytes(const string src, uchar &out[], string &err)
|
| | | {
|
| | | err = "";
|
| | | ArrayResize(out, 0);
|
| | |
|
| | | uchar tmp[];
|
| | | const int copied = StringToCharArray(src, tmp, 0, -1, CP_UTF8);
|
| | | if(copied <= 0 || ArraySize(tmp) < copied)
|
| | | {
|
| | | err = StringFormat("UTF-8 conversion produced no bytes (result %d)", copied);
|
| | | return(false);
|
| | | }
|
| | |
|
| | | //--- exactly one terminating zero is appended by the conversion
|
| | | const int len = copied - 1;
|
| | | if(len < 0)
|
| | | {
|
| | | err = "UTF-8 conversion returned an unexpected result";
|
| | | return(false);
|
| | | }
|
| | |
|
| | | if(len == 0)
|
| | | {
|
| | | if(StringLen(src) != 0)
|
| | | {
|
| | | err = "UTF-8 conversion lost the whole value";
|
| | | return(false);
|
| | | }
|
| | | }
|
| | | else
|
| | | {
|
| | | const string back = CharArrayToString(tmp, 0, len, CP_UTF8);
|
| | | if(back != src)
|
| | | {
|
| | | err = "UTF-8 conversion does not round-trip to the original value";
|
| | | return(false);
|
| | | }
|
| | | }
|
| | |
|
| | | if(len > UQ_IDV2_MAX_FIELD_BYTES)
|
| | | {
|
| | | err = "identity component exceeds the supported byte length";
|
| | | return(false);
|
| | | }
|
| | |
|
| | | ArrayResize(out, len);
|
| | | if(len > 0 && ArrayCopy(out, tmp, 0, 0, len) != len)
|
| | | {
|
| | | err = "UTF-8 conversion could not be staged";
|
| | | return(false);
|
| | | }
|
| | | return(true);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| True when the string is a well-formed V2 fingerprint |
|
| | | //+------------------------------------------------------------------+
|
| | | bool UQIdentityFingerprintValid(const string fp)
|
| | | {
|
| | | if(StringLen(fp) != UQ_IDV2_HEX_CHARS)
|
| | | return(false);
|
| | |
|
| | | for(int i = 0; i < UQ_IDV2_HEX_CHARS; i++)
|
| | | {
|
| | | const ushort c = StringGetCharacter(fp, i);
|
| | | const bool hex = ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'));
|
| | | if(!hex)
|
| | | return(false);
|
| | | }
|
| | | return(true);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Bounded fingerprint prefix used in terminal global-variable |
|
| | | //| names, which the terminal limits to 63 characters |
|
| | | //+------------------------------------------------------------------+
|
| | | string UQIdentityFingerprintShort(const string fp)
|
| | | {
|
| | | if(!UQIdentityFingerprintValid(fp))
|
| | | return("");
|
| | | return(StringSubstr(fp, 0, UQ_IDV2_GV_CHARS));
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Build the V2 identity fingerprint over the complete identity |
|
| | | //| |
|
| | | //| Fails closed: an unavailable SHA-256 primitive, an empty |
|
| | | //| component, a non-positive login or an unconvertible value never |
|
| | | //| falls back to the legacy hash. The result is always 64 uppercase |
|
| | | //| hexadecimal characters covering all 256 bits. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool UQComputeIdentityFingerprintV2(const string server, const long login,
|
| | | const string portfolio, string &fp, string &err)
|
| | | {
|
| | | fp = "";
|
| | | err = "";
|
| | |
|
| | | if(StringLen(server) <= 0)
|
| | | {
|
| | | err = "broker server name is unavailable";
|
| | | return(false);
|
| | | }
|
| | | if(login <= 0)
|
| | | {
|
| | | err = "account login is unavailable";
|
| | | return(false);
|
| | | }
|
| | | if(StringLen(portfolio) <= 0)
|
| | | {
|
| | | err = "portfolio identifier is empty";
|
| | | return(false);
|
| | | }
|
| | |
|
| | | //--- the login is encoded as canonical positive decimal ASCII: no sign, no
|
| | | //--- padding and no separators, so its textual form is unambiguous
|
| | | const string loginText = IntegerToString(login);
|
| | | const int loginLen = StringLen(loginText);
|
| | | if(loginLen <= 0 || loginLen > 20)
|
| | | {
|
| | | err = "account login has no canonical decimal form";
|
| | | return(false);
|
| | | }
|
| | | for(int i = 0; i < loginLen; i++)
|
| | | {
|
| | | const ushort c = StringGetCharacter(loginText, i);
|
| | | if(c < '0' || c > '9')
|
| | | {
|
| | | err = "account login has no canonical decimal form";
|
| | | return(false);
|
| | | }
|
| | | }
|
| | | if(StringToInteger(loginText) != login)
|
| | | {
|
| | | err = "account login cannot be represented exactly";
|
| | | return(false);
|
| | | }
|
| | |
|
| | | uchar vDomain[], vServer[], vLogin[], vPortfolio[];
|
| | | if(!UQUtf8Bytes(UQ_IDV2_DOMAIN, vDomain, err))
|
| | | return(false);
|
| | | if(!UQUtf8Bytes(server, vServer, err))
|
| | | return(false);
|
| | | if(!UQUtf8Bytes(loginText, vLogin, err))
|
| | | return(false);
|
| | | if(!UQUtf8Bytes(portfolio, vPortfolio, err))
|
| | | return(false);
|
| | |
|
| | | //--- the length prefixes are derived from the verified byte arrays, never
|
| | | //--- from the character count of the source string
|
| | | uchar frame[];
|
| | | if(!UQAppendTaggedField(frame, (uchar)UQ_IDV2_TAG_DOMAIN, vDomain, ArraySize(vDomain)) ||
|
| | | !UQAppendTaggedField(frame, (uchar)UQ_IDV2_TAG_SERVER, vServer, ArraySize(vServer)) ||
|
| | | !UQAppendTaggedField(frame, (uchar)UQ_IDV2_TAG_LOGIN, vLogin, ArraySize(vLogin)) ||
|
| | | !UQAppendTaggedField(frame, (uchar)UQ_IDV2_TAG_PORTFOLIO, vPortfolio, ArraySize(vPortfolio)))
|
| | | {
|
| | | err = "identity preimage could not be assembled";
|
| | | return(false);
|
| | | }
|
| | |
|
| | | if(ArraySize(frame) <= 0)
|
| | | {
|
| | | err = "identity preimage is empty";
|
| | | return(false);
|
| | | }
|
| | |
|
| | | uchar key[];
|
| | | uchar digest[];
|
| | | ArrayResize(key, 0);
|
| | | const int produced = CryptEncode(CRYPT_HASH_SHA256, frame, key, digest);
|
| | | if(produced != UQ_IDV2_SHA256_BYTES || ArraySize(digest) != UQ_IDV2_SHA256_BYTES)
|
| | | {
|
| | | err = StringFormat("SHA-256 digest is unavailable (%d of %d bytes)",
|
| | | produced, UQ_IDV2_SHA256_BYTES);
|
| | | return(false);
|
| | | }
|
| | |
|
| | | string hex = "";
|
| | | for(int i = 0; i < UQ_IDV2_SHA256_BYTES; i++)
|
| | | hex += StringFormat("%02X", digest[i]);
|
| | |
|
| | | if(!UQIdentityFingerprintValid(hex))
|
| | | {
|
| | | err = "SHA-256 digest could not be rendered as 64 hexadecimal characters";
|
| | | return(false);
|
| | | }
|
| | |
|
| | | fp = hex;
|
| | | return(true);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Lowercase a string and replace every non-alphanumeric character |
|
| | | //| with '_'. Used to build file-system safe persistence keys. |
|
| | | //| |
|
| | | //| This is the LEGACY identity representation and is preserved |
|
| | | //| byte for byte. It is lossy ('A-B', 'A.B' and 'A_B' all collapse |
|
| | | //| to 'a_b'), so it is never used to derive a new persistence or |
|
| | | //| operational locator; those are derived from the V2 fingerprint. |
|
| | | //+------------------------------------------------------------------+
|
2026-10-30 07:19:59 +07:00 | | | string UQSanitizeKey(const string src)
|
| | | {
|
| | | string out = src;
|
| | | StringToLower(out);
|
| | | const int n = StringLen(out);
|
| | | for(int i = 0; i < n; i++)
|
| | | {
|
| | | const ushort c = StringGetCharacter(out, i);
|
| | | const bool ok = ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'));
|
| | | if(!ok)
|
| | | StringSetCharacter(out, i, '_');
|
| | | }
|
| | | return(out);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| FNV-1a hash rendered as a fixed-width hexadecimal string. Used |
|
| | | //| to bind persisted state to one broker server / login / portfolio |
|
| | | //| without writing any account text to disk. |
|
| | | //+------------------------------------------------------------------+
|
 fix: correlate outstanding entry risk reservations
A reservation could be released merely because an unrelated position or pending order existed under the same Magic Number, and an order ticket was treated as a position ticket. An entry whose submission result was lost also released its reserved risk.
- UQ_Defs: state schema 5 adds the reservation identity; ENUM_UQ_SUBMIT_RESULT and a retcode classifier that only accepts proven non-execution.
- UQ_State: reservation id, magic, scope, direction, volume, entry, stop, order/deal/position tickets are persisted, checksummed and strictly validated. A previous-schema reservation migrates to UNRESOLVED with its money preserved, never to NONE.
- UQ_Risk: UQReserveCorrelate links a reservation to the broker only through its own order ticket, a deal whose DEAL_ORDER is that ticket, or the traced POSITION_IDENTIFIER. Ambiguity, a disconnected terminal and a missing record all keep the money reserved as UNRESOLVED and block entries.
- UQ_Exec: UQSendMarket reports the submission outcome; the caller withdraws reserved risk only on a local preflight failure or a proven broker rejection.
- Universal_Quant_EA.mq5: deals and order transactions reconcile the reservation immediately; status and startup diagnostics expose the reservation identity.
2026-10-30 10:43:15 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| Reservation identifier of one entry request. |
|
| | | //| |
|
| | | //| It is derived from the scope, the symbol, the server time of the |
|
| | | //| request and a per-process sequence number, so two requests in the |
|
| | | //| same second cannot share an identity. |
|
| | | //+------------------------------------------------------------------+
|
| | | string UQBuildReserveId(const string seed, const string symbol,
|
| | | const datetime when, const ulong seq)
|
| | | {
|
| | | return(IntegerToString((long)when) + "-" + IntegerToString((long)seq) + "-" +
|
| | | StringSubstr(UQHashString(seed + "|" + symbol + "|" +
|
| | | IntegerToString((long)when) + "|" +
|
| | | IntegerToString((long)seq)), 0, 12));
|
| | | }
|
| | |
|
2026-10-30 07:19:59 +07:00 | | | string UQHashString(const string src)
|
| | | {
|
| | | ulong h = 1469598103934665603;
|
| | | const int n = StringLen(src);
|
| | | for(int i = 0; i < n; i++)
|
| | | {
|
| | | h ^= (ulong)StringGetCharacter(src, i);
|
| | | h *= 1099511628211;
|
| | | }
|
| | | return(StringFormat("%016I64X", h));
|
| | | }
|
| | |
|
2026-10-30 08:37:46 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| Reset nonce helpers |
|
| | | //| |
|
| | | //| A reset nonce is operator supplied, unique per reset operation |
|
| | | //| and never a secret. It is restricted to printable characters |
|
| | | //| that cannot collide with the serialisation format or with the |
|
| | | //| field separator of the consumed-nonce history. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool UQResetNonceCharOk(const ushort c)
|
| | | {
|
| | | if(c < 33 || c > 126) // printable ASCII, no whitespace
|
| | | return(false);
|
| | | if(c == '=' || c == '|') // reserved by the state format
|
| | | return(false);
|
| | | return(true);
|
| | | }
|
| | |
|
| | | bool UQResetNonceValid(const string nonce)
|
| | | {
|
| | | const int n = StringLen(nonce);
|
| | | if(n < UQ_RESET_NONCE_MIN_LEN || n > UQ_RESET_NONCE_MAX_LEN)
|
| | | return(false);
|
| | | for(int i = 0; i < n; i++)
|
| | | if(!UQResetNonceCharOk(StringGetCharacter(nonce, i)))
|
| | | return(false);
|
| | | return(true);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Number of consumed nonces recorded in a history string |
|
| | | //+------------------------------------------------------------------+
|
| | | int UQResetNonceHistoryCount(const string history)
|
| | | {
|
| | | if(StringLen(history) <= 0)
|
| | | return(0);
|
| | | string parts[];
|
| | | const int n = StringSplit(history, (ushort)'|', parts);
|
| | | int count = 0;
|
| | | for(int i = 0; i < n; i++)
|
| | | if(StringLen(parts[i]) > 0)
|
| | | count++;
|
| | | return(count);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Has this nonce already been consumed by an earlier reset? |
|
| | | //+------------------------------------------------------------------+
|
| | | bool UQResetNonceConsumed(const string history, const string nonce)
|
| | | {
|
| | | if(StringLen(history) <= 0 || StringLen(nonce) <= 0)
|
| | | return(false);
|
| | |
|
| | | string parts[];
|
| | | const int n = StringSplit(history, (ushort)'|', parts);
|
| | | for(int i = 0; i < n; i++)
|
| | | if(parts[i] == nonce)
|
| | | return(true);
|
| | | return(false);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Append a consumed nonce, keeping the bounded most-recent window |
|
| | | //+------------------------------------------------------------------+
|
| | | string UQResetNonceHistoryAppend(const string history, const string nonce)
|
| | | {
|
| | | if(StringLen(nonce) <= 0)
|
| | | return(history);
|
| | | if(UQResetNonceConsumed(history, nonce))
|
| | | return(history);
|
| | |
|
| | | string parts[];
|
| | | int count = 0;
|
| | | if(StringLen(history) > 0)
|
| | | {
|
| | | const int n = StringSplit(history, (ushort)'|', parts);
|
| | | for(int i = 0; i < n; i++)
|
| | | if(StringLen(parts[i]) > 0)
|
| | | count++;
|
| | | }
|
| | |
|
| | | //--- drop the oldest entries so the newest nonce always fits
|
| | | int first = 0;
|
| | | if(count >= UQ_RESET_HISTORY_MAX)
|
| | | first = count - (UQ_RESET_HISTORY_MAX - 1);
|
| | |
|
| | | string result = "";
|
| | | for(int i = first; i < count; i++)
|
| | | {
|
| | | if(StringLen(result) > 0)
|
| | | result += UQ_RESET_FIELD_SEPARATOR;
|
| | | result += parts[i];
|
| | | }
|
| | | if(StringLen(result) > 0)
|
| | | result += UQ_RESET_FIELD_SEPARATOR;
|
| | | result += nonce;
|
| | | return(result);
|
| | | }
|
| | |
|
2026-10-30 10:20:54 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| Prior permanent-lock kind recorded when an owner reset closed a |
|
| | | //| risk cycle. |
|
| | | //| |
|
| | | //| The static-drawdown lock and the equity-target lock are |
|
| | | //| independent permanent locks, so one cycle can end with both |
|
| | | //| armed. The recorded kind is therefore a bit field with exactly |
|
| | | //| four representable values: |
|
| | | //| 0 = no prior permanent lock |
|
| | | //| 1 = prior static-drawdown lock |
|
| | | //| 2 = prior equity-target lock |
|
| | | //| 3 = both prior permanent locks |
|
| | | //| |
|
| | | //| A writer, the state validator and every display path use the |
|
| | | //| helpers below, so no path can invent a fifth value and no path |
|
| | | //| can silently drop the second lock. |
|
| | | //+------------------------------------------------------------------+
|
| | | #define UQ_PRIOR_LOCK_NONE 0
|
| | | #define UQ_PRIOR_LOCK_STATIC 1
|
| | | #define UQ_PRIOR_LOCK_TARGET 2
|
| | | #define UQ_PRIOR_LOCK_BOTH 3
|
| | |
|
| | | //--- the state schema accepts exactly these four values
|
| | | bool UQPriorLockKindValid(const int kind)
|
| | | {
|
| | | return(kind >= UQ_PRIOR_LOCK_NONE && kind <= UQ_PRIOR_LOCK_BOTH);
|
| | | }
|
| | |
|
| | | //--- does the recorded kind include the static-drawdown lock?
|
| | | bool UQPriorLockHasStatic(const int kind)
|
| | | {
|
| | | return((kind & UQ_PRIOR_LOCK_STATIC) != 0);
|
| | | }
|
| | |
|
| | | //--- does the recorded kind include the equity-target lock?
|
| | | bool UQPriorLockHasTarget(const int kind)
|
| | | {
|
| | | return((kind & UQ_PRIOR_LOCK_TARGET) != 0);
|
| | | }
|
| | |
|
| | | //--- permanent locks of the closed cycle -> recorded prior lock kind
|
| | | int UQPriorLockKindFromFlags(const bool staticLock, const bool targetLock)
|
| | | {
|
| | | int kind = UQ_PRIOR_LOCK_NONE;
|
| | | if(staticLock)
|
| | | kind |= UQ_PRIOR_LOCK_STATIC;
|
| | | if(targetLock)
|
| | | kind |= UQ_PRIOR_LOCK_TARGET;
|
| | | return(kind);
|
| | | }
|
| | |
|
2026-10-30 08:37:46 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| Human readable prior lock kind recorded by a cycle reset |
|
| | | //+------------------------------------------------------------------+
|
| | | string UQPriorLockText(const int kind)
|
| | | {
|
| | | switch(kind)
|
| | | {
|
2026-10-30 10:20:54 +07:00 | | | case UQ_PRIOR_LOCK_NONE: return("none");
|
| | | case UQ_PRIOR_LOCK_STATIC: return("static-drawdown");
|
| | | case UQ_PRIOR_LOCK_TARGET: return("equity-target");
|
| | | case UQ_PRIOR_LOCK_BOTH: return("static-drawdown+equity-target");
|
2026-10-30 08:37:46 +07:00 | | | }
|
| | | return("unknown");
|
| | | }
|
| | |
|
2026-10-30 07:19:59 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| Normalise a price to the broker tick size for the symbol |
|
| | | //+------------------------------------------------------------------+
|
| | | double UQNormalizePrice(const string symbol, const double price)
|
| | | {
|
| | | const int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
|
| | | const double tick = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
|
| | | if(tick > 0.0)
|
| | | return(NormalizeDouble(MathRound(price / tick) * tick, digits));
|
| | | return(NormalizeDouble(price, digits));
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Human readable block reason |
|
| | | //+------------------------------------------------------------------+
|
| | | string UQBlockText(const ENUM_UQ_BLOCK b)
|
| | | {
|
| | | switch(b)
|
| | | {
|
| | | case UQ_BLOCK_STATIC: return("STATIC-DD LOCK");
|
| | | case UQ_BLOCK_TARGET: return("TARGET LOCK");
|
| | | case UQ_BLOCK_DAILY: return("DAILY-LOSS LOCK");
|
| | | case UQ_BLOCK_BASELINE: return("BASELINE INVALID");
|
| | | case UQ_BLOCK_DISABLED: return("TRADING DISABLED");
|
| | | case UQ_BLOCK_PERMISSIONS: return("NO TRADE PERMISSION");
|
| | | case UQ_BLOCK_EXPOSURE: return("EXPOSURE GUARD");
|
| | | case UQ_BLOCK_INSTANCE: return("DUPLICATE INSTANCE");
|
| | | case UQ_BLOCK_DATA: return("DATA NOT READY");
|
 fix: validate persistent risk-state integrity and fail closed on missing state
* UQ_State: checksummed schema v2 with strict typed parsing, duplicate/unknown-field rejection and semantic validation of every mandatory field; legacy v1 files are validated and migrated on load
* UQ_State: verified backup retention, staged-file verification before commit and post-commit re-verification
* UQ_State: redundant identity-scoped initialization marker reconciled against the persisted state
* UQ_Risk: a new risk cycle requires an explicit owner authorization and a deliberately declared positive B0; the current account balance is never used as a substitute
* UQ_Risk: missing, empty, corrupt, foreign, version-unsupported or schema-invalid state fails closed; backup recovery only under explicit owner authorization
* UQ_Risk: a failed state write degrades the controller to unhealthy, blocks all new entries, preserves the in-memory lock and retries at a bounded interval
* UQ_Risk: daily rollover keeps permanent locks and reports unmonitored intervals explicitly
* UQ_Defs: state schema constants, marker global-variable prefix and the STATE PERSISTENCE FAILED block reason
* Universal_Quant_EA: first-initialization and backup-restore authorization inputs, input validation and state-health status line
2026-10-30 08:07:18 +07:00 | | | case UQ_BLOCK_STATE: return("STATE PERSISTENCE FAILED");
|
2026-10-30 08:37:46 +07:00 | | | case UQ_BLOCK_RESET: return("RESET REJECTED");
|
2026-10-30 08:52:18 +07:00 | | | case UQ_BLOCK_RESERVATION: return("RISK RESERVATION UNRESOLVED");
|
 fix: recover the latest verified persistent state
The backup was built from the previous primary before the new primary was installed, so it could lag the newest successful update, and a missing primary was still treated as a candidate fresh installation. Neither an interrupted commit nor a stale mirror was detectable.
B1 state schema 6 (defines UQ_STATE_FORMAT=6, UQ_STATE_IDENTITY_FORMAT=5) adds a monotonic stateRevision plus recoveryHold/recoveryHoldTime. Formats 1..5 keep their own field block, so every published checksum still verifies over exactly the block that produced it, and a migration sets generation 1 without inventing a lock event.
B2 UQStateSave is write-ahead and generation-aware: the next generation is derived from what is proven on disk (primary and backup), the staged file is written and read back, the same verified bytes go to the backup and are read back, the primary is replaced and read back, and primary and backup are finally reconciled on generation and content. A newer verified state is never overwritten by an older instance; a failure at any step returns false, which blocks every new entry.
B3 LoadPersistentState orders primary and backup by generation: newer primary wins, equal generations must agree on content (otherwise fail closed), and a backup exactly one generation ahead is the interrupted-commit window and is adopted. Anything else is refused.
B4 a backup is evidence of a previous initialization, so a missing primary is never a fresh installation and never creates a replacement B0; a verified backup is restored only under explicit owner authorization.
B5 a recovered, reordered or superseded state always starts behind a persisted recovery hold (UQ_BLOCK_RECOVERY) that survives restart and only an explicit owner-authorized recovery risk cycle (UQ_RESET_RECOVERY, fresh nonce, explicit B0, flat scope) can clear. A recovery never clears a permanent lock the recovered state carries, and a cycle without a prior permanent lock records no prior-cycle reference instead of an inconsistent one.
B6 the initialization marker is written and then verified; a marker that cannot be persisted or verified fails the load closed.
B7 the status panel reports the persistence generation and an active recovery hold.
2026-10-30 11:09:43 +07:00 | | | case UQ_BLOCK_RECOVERY: return("RECOVERY HOLD");
|
 fix: enforce account scope and daily monitoring continuity
Schema 7 adds a persisted monitoring heartbeat, the last recorded gap and a gap hold, with deterministic migration from schema 6 that keeps every lock, cycle identity and reservation.
D0 is no longer invented on startup or on a rollover: a trading day is adopted only when its boundary was observed while the EA was executing, or when the equity path across it is provably constant (no position, no pending order, no account deal). Otherwise the previous D0 is preserved, a monitoring-gap hold blocks every new entry and the exact gap is reported; the hold survives restart and clears only at an observed day boundary or an explicit owner cycle. Static, target and daily thresholds keep their priority, so a breach is still locked and liquidated while a day is unverified.
Account scope is now counted and disclosed for owned and unmanaged positions and pending orders; unmanaged pending orders block entries under the external-exposure guard, and the EA states that the account-wide limits cannot be guaranteed while whole-account liquidation stays disabled.
2026-10-30 11:20:47 +07:00 | | | case UQ_BLOCK_GAP: return("MONITORING GAP HOLD");
|
2026-10-30 07:19:59 +07:00 | | | default: return("OK");
|
| | | }
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Number of decimals implied by a volume step |
|
| | | //+------------------------------------------------------------------+
|
| | | int UQVolumeDigits(const double step)
|
| | | {
|
| | | if(!(step > 0.0))
|
| | | return(2);
|
| | | const double lg = MathLog10(step);
|
| | | return((int)MathMax(0.0, MathCeil(-lg)));
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Broker-supported order filling mode for a symbol |
|
| | | //+------------------------------------------------------------------+
|
| | | ENUM_ORDER_TYPE_FILLING UQPickFillingMode(const string symbol)
|
| | | {
|
| | | const long flags = (long)SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE);
|
| | | if((flags & (long)SYMBOL_FILLING_FOK) != 0)
|
| | | return(ORDER_FILLING_FOK);
|
| | | if((flags & (long)SYMBOL_FILLING_IOC) != 0)
|
| | | return(ORDER_FILLING_IOC);
|
| | | return(ORDER_FILLING_RETURN);
|
| | | }
|
| | |
|
2026-10-30 09:19:45 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| Symbol resolution outcome. Resolution never guesses: an exact |
|
| | | //| catalogue name wins, a broker prefix/suffix variant is accepted |
|
| | | //| only when it is unique among eligible instruments, and every other |
|
| | | //| case is reported instead of resolved arbitrarily. |
|
| | | //+------------------------------------------------------------------+
|
| | | enum ENUM_UQ_RESOLVE
|
| | | {
|
| | | UQ_RESOLVE_OK = 0, // Unique eligible instrument
|
| | | UQ_RESOLVE_EMPTY = 1, // Empty request
|
| | | UQ_RESOLVE_NOT_FOUND = 2, // No catalogue symbol carries the token
|
| | | UQ_RESOLVE_AMBIGUOUS = 3, // Several eligible instruments remain
|
| | | UQ_RESOLVE_INELIGIBLE = 4 // Candidate is not an eligible FX instrument
|
| | | };
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Human readable resolution outcome |
|
| | | //+------------------------------------------------------------------+
|
| | | string UQResolveText(const ENUM_UQ_RESOLVE r)
|
| | | {
|
| | | switch(r)
|
| | | {
|
| | | case UQ_RESOLVE_OK: return("RESOLVED");
|
| | | case UQ_RESOLVE_EMPTY: return("EMPTY REQUEST");
|
| | | case UQ_RESOLVE_NOT_FOUND: return("NOT FOUND");
|
| | | case UQ_RESOLVE_AMBIGUOUS: return("AMBIGUOUS");
|
| | | case UQ_RESOLVE_INELIGIBLE: return("INELIGIBLE");
|
| | | }
|
| | | return("UNKNOWN");
|
| | | }
|
| | |
|
| | | //--- Resolution limits. The extension window stops a short pair token from
|
| | | //--- matching a much longer broker name, and the fixed probe set is what makes
|
| | | //--- the broker prefix/suffix convention verifiable instead of guessed.
|
| | | #define UQ_SYMBOL_EXT_MAX_LEN 8 // longest accepted prefix/suffix extension
|
| | | #define UQ_SYMBOL_CANDIDATE_MAX 32 // bounded candidate list per request
|
| | | #define UQ_SYMBOL_PROBE_MAX 10 // standard pairs used to verify a convention
|
| | | #define UQ_SYMBOL_PROBE_MIN_VOTES 3 // votes required before a convention is trusted
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Is this broker symbol an eligible instrument for the FX strategy? |
|
| | | //| |
|
| | | //| Only static trading specifications are checked here. Quotes and |
|
| | | //| monetary values are quote dependent, so they are validated at the |
|
| | | //| moment an order is sized rather than at resolution time. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool UQSymbolEligible(const string symbol, string &reason)
|
| | | {
|
| | | reason = "";
|
| | |
|
| | | if(StringLen(symbol) <= 0)
|
| | | {
|
| | | reason = "the symbol name is empty";
|
| | | return(false);
|
| | | }
|
| | |
|
| | | const ENUM_SYMBOL_TRADE_MODE mode =
|
| | | (ENUM_SYMBOL_TRADE_MODE)SymbolInfoInteger(symbol, SYMBOL_TRADE_MODE);
|
| | |
|
| | | if(mode == SYMBOL_TRADE_MODE_DISABLED)
|
| | | {
|
| | | reason = "trading is disabled for the symbol";
|
| | | return(false);
|
| | | }
|
| | | if(mode == SYMBOL_TRADE_MODE_CLOSEONLY)
|
| | | {
|
| | | reason = "the symbol only allows closing positions";
|
| | | return(false);
|
| | | }
|
| | |
|
| | | //--- the strategy is defined for FX pairs; a CFD, index or future is not
|
| | | const ENUM_SYMBOL_CALC_MODE calc =
|
| | | (ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol, SYMBOL_TRADE_CALC_MODE);
|
| | | if(calc != SYMBOL_CALC_MODE_FOREX && calc != SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE)
|
| | | {
|
| | | reason = "calculation mode " + EnumToString(calc) + " is not an FX pair";
|
| | | return(false);
|
| | | }
|
| | |
|
| | | const string baseCurrency = SymbolInfoString(symbol, SYMBOL_CURRENCY_BASE);
|
| | | const string profitCurrency = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT);
|
| | | if(StringLen(baseCurrency) < 3 || StringLen(profitCurrency) < 3)
|
| | | {
|
| | | reason = "the base or profit currency is not reported";
|
| | | return(false);
|
| | | }
|
| | | if(baseCurrency == profitCurrency)
|
| | | {
|
| | | reason = "the base and profit currency are identical";
|
| | | return(false);
|
| | | }
|
| | |
|
| | | if(!(SymbolInfoDouble(symbol, SYMBOL_POINT) > 0.0))
|
| | | {
|
| | | reason = "the point size is invalid";
|
| | | return(false);
|
| | | }
|
| | | if(!(SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE) > 0.0))
|
| | | {
|
| | | reason = "the tick size is invalid";
|
| | | return(false);
|
| | | }
|
| | |
|
| | | const double volumeMin = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
| | | const double volumeMax = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
|
| | | const double volumeStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
|
| | | if(!(volumeMin > 0.0) || !(volumeMax >= volumeMin) || !(volumeStep > 0.0))
|
| | | {
|
| | | reason = "the broker volume constraints are invalid";
|
| | | return(false);
|
| | | }
|
| | |
|
| | | return(true);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Does the broker trade mode permit this signal direction? |
|
| | | //+------------------------------------------------------------------+
|
| | | bool UQSymbolAllowsDirection(const string symbol, const int direction, string &reason)
|
| | | {
|
| | | reason = "";
|
| | |
|
| | | if(direction == 0)
|
| | | {
|
| | | reason = "no signal direction";
|
| | | return(false);
|
| | | }
|
| | |
|
| | | const ENUM_SYMBOL_TRADE_MODE mode =
|
| | | (ENUM_SYMBOL_TRADE_MODE)SymbolInfoInteger(symbol, SYMBOL_TRADE_MODE);
|
| | |
|
| | | if(mode == SYMBOL_TRADE_MODE_FULL)
|
| | | return(true);
|
| | | if(direction > 0 && mode == SYMBOL_TRADE_MODE_LONGONLY)
|
| | | return(true);
|
| | | if(direction < 0 && mode == SYMBOL_TRADE_MODE_SHORTONLY)
|
| | | return(true);
|
| | |
|
| | | reason = "broker trade mode " + EnumToString(mode) + " does not allow this direction";
|
| | | return(false);
|
| | | }
|
| | |
|
2026-10-30 07:19:59 +07:00 | | | #endif // UQ_DEFS_MQH
|
| | | //+------------------------------------------------------------------+
|