| //+------------------------------------------------------------------+
|
| //| 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 |
|
| //| |
|
| //| This is the ONE clock of the program. Every timestamp that is |
|
| //| compared with another timestamp, persisted, or used to decide a |
|
| //| calendar-day boundary comes from here. |
|
| //| |
|
| //| TimeTradeServer() is the broker-server estimate and is always |
|
| //| preferred. TimeCurrent() is the time of the LAST QUOTE and is |
|
| //| used only as the documented fallback for a terminal that has not |
|
| //| published a server-time estimate yet. It is not a substitute for |
|
| //| broker-server time: on a quiet market it trails the server clock, |
|
| //| so a value taken from it can be older than a value taken from this|
|
| //| function earlier in the same cycle. Callers that need a monotonic |
|
| //| stamp therefore use this function and never TimeCurrent() directly|
|
| //| (see UQStateStampResolve()). |
|
| //| |
|
| //| A result of 0 means "no time is available". It is never a valid |
|
| //| timestamp: callers fail closed instead of stamping with it. |
|
| //+------------------------------------------------------------------+
|
| 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: |
|
| //| |
|
| //| <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. |
|
| //+------------------------------------------------------------------+
|
| 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
|
| //+------------------------------------------------------------------+
|