| //+------------------------------------------------------------------+
|
| //| 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.02"
|
| #define UQ_STATE_FORMAT 7 // current persisted schema (adds monitoring continuity)
|
| #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_"
|
|
|
| //--- 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. |
|
| //+------------------------------------------------------------------+
|
| 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
|
| //+------------------------------------------------------------------+
|