//+------------------------------------------------------------------+ //| 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 #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 #define UQ_STATE_GENERATION_FORMAT 6 // previous published schema (state generation), migrated on load #define UQ_STATE_IDENTITY_FORMAT 5 // previous published schema (reservation identity), migrated on load #define UQ_STATE_RESERVE_FORMAT 4 // previous published schema, migrated on load #define UQ_STATE_CYCLE_FORMAT 3 // previous published risk-cycle schema, migrated on load #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 #define UQ_COMMENT_PREFIX "Universal_Quant_EA" #define UQ_GV_INSTANCE_PREFIX "UQEA_INSTANCE_" #define UQ_GV_STATE_MARKER_PREFIX "UQEA_STATE_MARKER_" //--- 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_" //--- 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. #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 #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 //--- duplicate-instance heartbeat validity window (seconds) #define UQ_INSTANCE_TTL_SECONDS 180 //--- 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 //--- 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 #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 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"); } //+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+ //| 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 | //| | //| 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. | //+------------------------------------------------------------------+ 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 UQ_RESET_BOTH = 3, // Clear both permanent locks UQ_RESET_RECOVERY = 4 // Acknowledge a persistence recovery hold (new cycle) }; //+------------------------------------------------------------------+ //| 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"); } //+------------------------------------------------------------------+ //| 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 UQ_BLOCK_DATA = 9, // Market data or signal state not ready UQ_BLOCK_STATE = 10, // Persistent risk state could not be written UQ_BLOCK_RESET = 11, // Owner reset request rejected, entries blocked UQ_BLOCK_RESERVATION = 12, // Outstanding entry risk cannot be reconciled UQ_BLOCK_RECOVERY = 13, // Persistence recovery hold, owner authorization required UQ_BLOCK_GAP = 14 // Monitoring gap: the daily baseline cannot be reconstructed }; //+------------------------------------------------------------------+ //| 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. | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| 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: | //| | //| | //| | //| 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. | //+------------------------------------------------------------------+ 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. | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| 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)); } 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)); } //+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+ //| Human readable prior lock kind recorded by a cycle reset | //+------------------------------------------------------------------+ string UQPriorLockText(const int kind) { switch(kind) { 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"); } return("unknown"); } //+------------------------------------------------------------------+ //| 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"); case UQ_BLOCK_STATE: return("STATE PERSISTENCE FAILED"); case UQ_BLOCK_RESET: return("RESET REJECTED"); case UQ_BLOCK_RESERVATION: return("RISK RESERVATION UNRESOLVED"); case UQ_BLOCK_RECOVERY: return("RECOVERY HOLD"); case UQ_BLOCK_GAP: return("MONITORING GAP HOLD"); 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); } //+------------------------------------------------------------------+ //| 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); } #endif // UQ_DEFS_MQH //+------------------------------------------------------------------+