Universal_Quant_EA/Include/UQ_Defs.mqh
1021 lines
42 KiB
MQL5
//+------------------------------------------------------------------+
//| 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: |
//| |
//| <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
//+------------------------------------------------------------------+