//+------------------------------------------------------------------+ //| UniversalXAUUSD_M5_Scalper.mq5 v2.0.0 | //| Implements: Universal_XAUUSD_M5_Scalping_EA_Specification.pdf | //| M5 trend-pullback scalper, broker-agnostic, fail-closed. | //| | //| DEMO-ONLY BUILD: live accounts are refused (AURA_LIVE_TRADING_ | //| ENABLED=false). No backtest has been run. No profit guarantee. | //| | //| Operational choices NOT defined in the PDF (documented in | //| README): session windows, news source, rollover/weekly-close | //| windows, reset time zone, SL/TP-failure emergency response, | //| loss-streak review mechanism, ATR percentile method. | //+------------------------------------------------------------------+ #property copyright "AURA Algo-Trading" #property version "2.20" #property description "Broker-agnostic XAUUSD M5 trend-pullback scalper per spec. DEMO-ONLY build." #define AURA_EA_VERSION "2.2.0" #define AURA_LIVE_TRADING_ENABLED false enum ENUM_TESTER_TZ { TESTER_TZ_NY_CLOSE_MIDNIGHT = 0, // Server = UTC+2 / UTC+3 with US DST (common broker convention) TESTER_TZ_FIXED = 1 // Fixed offset (InpTesterFixedOffsetMin) }; input group "Licence & backend" input string InpActivationToken = ""; // Activation token from AURA dashboard input string InpBackendURL = "https://connectx.website"; // Must be whitelisted in Tools > Options > Expert Advisors input int InpHeartbeatSeconds = 30; // Heartbeat interval (s) input int InpLicenceGraceMinutes = 30; // Backend unreachable grace before blocking new entries input bool InpTesterBypassLicence = true; // Strategy Tester only: skip licence (WebRequest unavailable) input bool InpBridgeMode = false; // Controlled by AURA Windows Bot app (Common\\Files\\AURA control file) input group "Symbol & identity" input string InpSymbol = ""; // Explicit broker symbol (empty = chart symbol if gold, else Market Watch discovery) input ulong InpMagic = 26050501; input group "Risk (spec section 4)" input double InpRiskPercent = 0.25; // Risk per trade, % of current equity input double InpCostReservePerLot = 0.0; // REQUIRED: broker round-trip commission+fees per 1.0 lot, deposit currency input bool InpAllowZeroCostReserve = false; // true ONLY for preliminary code tests (spec): results will be misleading input double InpDailyLossPct = 1.0; // Daily loss cutoff, % of start-of-day equity (incl. floating) input double InpWeeklyLossPct = 2.5; // Weekly loss cutoff, % of start-of-week equity input double InpPeakDrawdownPct = 8.0; // Peak-to-current equity circuit breaker input int InpMaxLossStreak = 4; // Stop new entries after N consecutive losses (review required) input int InpMaxDailyEntries = 8; // Max entries per UTC day input int InpMaxPositions = 1; // Max positions on symbol (all magics, hedging mode) input group "Strategy (spec section 2)" input int InpTrendFastEMA = 50; // M15 EMA fast input int InpTrendSlowEMA = 200; // M15 EMA slow input int InpPullbackEMA = 20; // M5 EMA pullback input int InpRsiPeriod = 14; input double InpRsiBuyMin = 50.0; input double InpRsiBuyMax = 68.0; input double InpRsiSellMin = 32.0; input double InpRsiSellMax = 50.0; input int InpAtrPeriod = 14; input int InpAtrLookback = 100; // Previous completed ATR values for percentile band input double InpAtrPctLow = 20.0; input double InpAtrPctHigh = 90.0; input double InpEntryMaxMoveAtr = 0.25; // Max |Ask/Bid - signal close| in ATR at execution input double InpSlAtrMult = 1.2; // SL = 1.2 x ATR from actual entry input double InpTpRR = 1.8; // TP = 1.8 x initial stop distance input int InpTimeStopBars = 12; // Close after N completed M5 candles input group "Costs & filters (spec sections 5-6)" input double InpApprovedSpreadCap = 0.35; // Spread cap in PRICE units (USD per oz, Ask-Bid). NOT points/pips. 0.35 = 35 pts @2 digits, 350 pts @3 digits input double InpSpreadAtrFraction = 0.10; // Spread must be <= min(0.10 x ATR, cap) input int InpMaxDeviationPoints = 20; // Max slippage (points) accepted by server input int InpMaxOrderRetries = 2; // Bounded retries on requote/price change only input int InpStaleQuoteSeconds = 30; // Block if last quote older than this input bool InpSessionFilter = true; // false = explicit test variant only input int InpLondonStartMin = 480; // London local 08:00 input int InpLondonEndMin = 720; // London local 12:00 input int InpNewYorkStartMin = 510; // New York local 08:30 input int InpNewYorkEndMin = 720; // New York local 12:00 input int InpRolloverBlockMin = 30; // Block +/- N min around 17:00 New York input int InpFridayCloseBlockMin = 960; // Block after Friday 16:00 New York input bool InpNewsFilter = true; input int InpNewsWindowMinutes = 15; // +/- window around high-impact USD events (test 30 as variant) input string InpTesterNewsFile = "AURA_USD_high_impact_utc.csv"; // Tester: Common\Files, one 'YYYY.MM.DD HH:MM' (UTC) per line input ENUM_TESTER_TZ InpTesterServerTZ = TESTER_TZ_NY_CLOSE_MIDNIGHT; input int InpTesterFixedOffsetMin = 120; //--- state struct SignalData { int dir; double close; double atr; double vwap; datetime bar; string detail; bool ready; }; string g_sym = ""; bool g_tester = false; bool g_hedging = false; int hEmaFast15 = INVALID_HANDLE, hEmaSlow15 = INVALID_HANDLE, hEma20 = INVALID_HANDLE, hRsi = INVALID_HANDLE, hAtr = INVALID_HANDLE; datetime g_last_bar = 0; bool g_activated = false; bool g_licence_valid = false; string g_licence_reason = "unchecked"; datetime g_last_licence_ok = 0, g_last_licence_try = 0, g_last_hb = 0, g_last_activate_try = 0; bool g_url_blocked = false; string g_block_reason = "starting"; string g_spec_hash = ""; datetime g_news_csv[]; bool g_news_feed_ok = true; datetime g_news_stale_check = 0; string g_last_remote_code = ""; datetime g_last_remote_time = 0; int g_log = INVALID_HANDLE; double g_measured_cost = -1.0; string g_token = "", g_backend = ""; string g_bridge_state = "STOP", g_live_confirm = ""; bool g_server_live = false; datetime g_bridge_read = 0; //+------------------------------------------------------------------+ //| Persistence (GlobalVariables survive terminal restarts) | //+------------------------------------------------------------------+ string GvName(string k) { return "AURA_" + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)) + "_" + IntegerToString((long)InpMagic) + "_" + k; } double GvGet(string k, double def = 0.0) { string n = GvName(k); return GlobalVariableCheck(n) ? GlobalVariableGet(n) : def; } void GvSet(string k, double v) { GlobalVariableSet(GvName(k), v); } //+------------------------------------------------------------------+ //| Time zone helpers (DST-aware, no fixed offsets) | //+------------------------------------------------------------------+ datetime MakeTime(int y, int mo, int d, int h) { MqlDateTime t; ZeroMemory(t); t.year = y; t.mon = mo; t.day = d; t.hour = h; return StructToTime(t); } int Dow(datetime t) { MqlDateTime s; TimeToStruct(t, s); return s.day_of_week; } int YearOf(datetime t) { MqlDateTime s; TimeToStruct(t, s); return s.year; } int MinOfDay(datetime t) { MqlDateTime s; TimeToStruct(t, s); return s.hour * 60 + s.min; } datetime NthSunday(int y, int mo, int n) { datetime f = MakeTime(y, mo, 1, 0); int day = 1 + ((7 - Dow(f)) % 7) + 7 * (n - 1); return MakeTime(y, mo, day, 0); } datetime LastSunday31(int y, int mo) { datetime l = MakeTime(y, mo, 31, 0); return l - Dow(l) * 86400; } bool IsUKDST(datetime utc) { int y = YearOf(utc); return utc >= LastSunday31(y, 3) + 3600 && utc < LastSunday31(y, 10) + 3600; } bool IsUSDST(datetime utc) { int y = YearOf(utc); return utc >= NthSunday(y, 3, 2) + 7 * 3600 && utc < NthSunday(y, 11, 1) + 6 * 3600; } datetime LondonLocal(datetime utc) { return utc + (IsUKDST(utc) ? 3600 : 0); } datetime NewYorkLocal(datetime utc) { return utc - (IsUSDST(utc) ? 4 : 5) * 3600; } datetime ServerNow() { return g_tester ? TimeCurrent() : TimeTradeServer(); } int ServerOffset() { if(!g_tester) { long d = (long)TimeTradeServer() - (long)TimeGMT(); return (int)(MathRound(d / 900.0) * 900); } if(InpTesterServerTZ == TESTER_TZ_FIXED) return InpTesterFixedOffsetMin * 60; datetime approxUtc = TimeCurrent() - 2 * 3600; return IsUSDST(approxUtc) ? 3 * 3600 : 2 * 3600; } datetime UtcNow() { return (datetime)((long)ServerNow() - ServerOffset()); } //+------------------------------------------------------------------+ //| Logging: structured CSV + Experts tab + rate-limited backend | //+------------------------------------------------------------------+ void LogEvent(string ev, string detail) { StringReplace(detail, ",", ";"); Print("[AURA] ", ev, " | ", detail); if(g_log != INVALID_HANDLE) { FileWrite(g_log, TimeToString(UtcNow(), TIME_DATE | TIME_SECONDS), ev, detail); FileFlush(g_log); } } string JsonEscape(string s) { StringReplace(s, "\\", "\\\\"); StringReplace(s, "\"", "\\\""); return s; } string LoginStr() { return IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)); } int Http(string method, string path, string extraHeaders, string body, string &reply) { char data[]; char result[]; string rh; if(StringLen(body) > 0) { StringToCharArray(body, data, 0, WHOLE_ARRAY, CP_UTF8); ArrayResize(data, ArraySize(data) - 1); } string headers = "Content-Type: application/json\r\n" + extraHeaders; ResetLastError(); int code = WebRequest(method, g_backend + path, headers, 5000, data, result, rh); if(code == -1) { int err = GetLastError(); if(err == 4014 && !g_url_blocked) { g_url_blocked = true; LogEvent("webrequest_blocked", "Add " + g_backend + " in Tools > Options > Expert Advisors > Allow WebRequest"); Alert("AURA: allow WebRequest for ", g_backend); } reply = ""; return -err; } g_url_blocked = false; reply = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); return code; } void RemoteLog(string level, string code, string msg) { if(g_tester || StringLen(g_token) == 0) return; if(code == g_last_remote_code && TimeGMT() - g_last_remote_time < 300) return; g_last_remote_code = code; g_last_remote_time = TimeGMT(); string body = StringFormat("{\"activation_token\":\"%s\",\"account_login\":\"%s\",\"level\":\"%s\",\"code\":\"%s\",\"message\":\"%s\"}", g_token, LoginStr(), level, code, JsonEscape(msg)); string r; Http("POST", "/api/ea/log", "", body, r); } string JsonGet(const string js, const string key) { string pat = "\"" + key + "\":"; int p = StringFind(js, pat); if(p < 0) return ""; p += StringLen(pat); int n = StringLen(js); while(p < n && StringGetCharacter(js, p) == ' ') p++; bool quoted = (p < n && StringGetCharacter(js, p) == '"'); if(quoted) p++; int e = p; while(e < n) { ushort c = StringGetCharacter(js, e); if(quoted && c == '"') break; if(!quoted && (c == ',' || c == '}')) break; e++; } return StringSubstr(js, p, e - p); } //+------------------------------------------------------------------+ //| Symbol discovery & broker adapter | //+------------------------------------------------------------------+ bool IsGoldName(string s) { string u = s; StringToUpper(u); return StringFind(u, "XAUUSD") >= 0 || StringFind(u, "GOLD") >= 0 || (StringFind(u, "XAU") >= 0 && StringFind(u, "USD") >= 0); } bool ResolveSymbol() { if(StringLen(InpSymbol) > 0) { if(!SymbolSelect(InpSymbol, true)) { LogEvent("init_failed", "symbol not found: " + InpSymbol); return false; } g_sym = InpSymbol; return true; } if(IsGoldName(_Symbol)) { g_sym = _Symbol; return true; } string found = ""; int cnt = 0; for(int i = 0; i < SymbolsTotal(true); i++) { string s = SymbolName(i, true); if(IsGoldName(s)) { cnt++; found = s; } } if(cnt == 1) { g_sym = found; return true; } LogEvent("init_failed", cnt == 0 ? "no gold symbol in Market Watch" : "ambiguous gold symbols: set InpSymbol explicitly"); return false; } double TickSize() { return SymbolInfoDouble(g_sym, SYMBOL_TRADE_TICK_SIZE); } double RoundToTick(double p) { double t = TickSize(); return NormalizeDouble(MathRound(p / t) * t, (int)SymbolInfoInteger(g_sym, SYMBOL_DIGITS)); } ENUM_ORDER_TYPE_FILLING Filling() { long f = SymbolInfoInteger(g_sym, SYMBOL_FILLING_MODE); if((f & SYMBOL_FILLING_FOK) == SYMBOL_FILLING_FOK) return ORDER_FILLING_FOK; if((f & SYMBOL_FILLING_IOC) == SYMBOL_FILLING_IOC) return ORDER_FILLING_IOC; return ORDER_FILLING_RETURN; } bool ValidateSpec(string &why) { double ts = TickSize(); double tvl = SymbolInfoDouble(g_sym, SYMBOL_TRADE_TICK_VALUE_LOSS); double ctr = SymbolInfoDouble(g_sym, SYMBOL_TRADE_CONTRACT_SIZE); double vmin = SymbolInfoDouble(g_sym, SYMBOL_VOLUME_MIN), vmax = SymbolInfoDouble(g_sym, SYMBOL_VOLUME_MAX); double vstep = SymbolInfoDouble(g_sym, SYMBOL_VOLUME_STEP), vlim = SymbolInfoDouble(g_sym, SYMBOL_VOLUME_LIMIT); if(ts <= 0 || tvl <= 0 || ctr <= 0 || vmin <= 0 || vstep <= 0 || vmax < vmin) { why = "invalid_symbol_spec"; return false; } MqlTick t; if(!SymbolInfoTick(g_sym, t) || t.bid <= 0 || t.ask <= 0) { why = "no_quote"; return false; } double p = 0.0; if(!OrderCalcProfit(ORDER_TYPE_BUY, g_sym, 1.0, t.bid, t.bid - ts, p)) { why = "ordercalcprofit_failed_" + IntegerToString(GetLastError()); return false; } double ratio = MathAbs(p) / tvl; if(ratio < 0.9 || ratio > 1.1) { why = StringFormat("tick_value_mismatch calc=%.6f symbol=%.6f", MathAbs(p), tvl); return false; } string h = StringFormat("%s ctr=%.2f digits=%d tick=%.5f tvloss=%.5f vmin=%.2f vmax=%.2f step=%.2f limit=%.2f stops=%d freeze=%d filling=%d exe=%d tmode=%d deposit=%s profit=%s mode=%s offset_min=%d", g_sym, ctr, (int)SymbolInfoInteger(g_sym, SYMBOL_DIGITS), ts, tvl, vmin, vmax, vstep, vlim, (int)SymbolInfoInteger(g_sym, SYMBOL_TRADE_STOPS_LEVEL), (int)SymbolInfoInteger(g_sym, SYMBOL_TRADE_FREEZE_LEVEL), (int)SymbolInfoInteger(g_sym, SYMBOL_FILLING_MODE), (int)SymbolInfoInteger(g_sym, SYMBOL_TRADE_EXEMODE), (int)SymbolInfoInteger(g_sym, SYMBOL_TRADE_MODE), AccountInfoString(ACCOUNT_CURRENCY), SymbolInfoString(g_sym, SYMBOL_CURRENCY_PROFIT), g_hedging ? "hedging" : "netting", ServerOffset() / 60); if(h != g_spec_hash) { g_spec_hash = h; LogEvent("symbol_spec", h); } return true; } //+------------------------------------------------------------------+ //| Position sizing (spec section 8, adapted with reasons) | //+------------------------------------------------------------------+ double NormalizeVolumeDown(double volume) { double minLot = SymbolInfoDouble(g_sym, SYMBOL_VOLUME_MIN); double maxLot = SymbolInfoDouble(g_sym, SYMBOL_VOLUME_MAX); double step = SymbolInfoDouble(g_sym, SYMBOL_VOLUME_STEP); if(step <= 0.0 || minLot <= 0.0 || maxLot < minLot) return 0.0; volume = MathMin(volume, maxLot); double steps = MathFloor((volume - minLot) / step + 1e-9); double result = minLot + steps * step; if(result < minLot - 1e-9) return 0.0; return NormalizeDouble(result, 8); } double CalculateRiskVolume(ENUM_ORDER_TYPE type, double entry, double stop, string &why) { if(InpRiskPercent <= 0.0 || InpCostReservePerLot < 0.0 || entry <= 0.0 || stop <= 0.0) { why = "invalid_sizing_inputs"; return 0.0; } double equity = AccountInfoDouble(ACCOUNT_EQUITY); double riskCash = equity * InpRiskPercent / 100.0; if(equity <= 0.0 || riskCash <= 0.0) { why = "invalid_equity"; return 0.0; } double lossOneLot = 0.0; if(!OrderCalcProfit(type, g_sym, 1.0, entry, stop, lossOneLot)) { why = "ordercalcprofit_failed_" + IntegerToString(GetLastError()); return 0.0; } if(lossOneLot >= 0.0) { why = "stop_not_a_loss"; return 0.0; } double riskOneLot = MathAbs(lossOneLot) + InpCostReservePerLot; double rawVolume = riskCash / riskOneLot; double minLot = SymbolInfoDouble(g_sym, SYMBOL_VOLUME_MIN); if(rawVolume < minLot) { why = StringFormat("min_lot_exceeds_risk raw=%.4f min=%.2f", rawVolume, minLot); return 0.0; } double volume = NormalizeVolumeDown(rawVolume); if(volume < minLot) { why = "volume_below_min_after_rounding"; return 0.0; } double actualLoss = 0.0; if(!OrderCalcProfit(type, g_sym, volume, entry, stop, actualLoss)) { why = "ordercalcprofit_recheck_failed"; return 0.0; } double est = MathAbs(actualLoss) + InpCostReservePerLot * volume; if(est > riskCash + 1e-6) { why = StringFormat("risk_exceeded est=%.2f budget=%.2f", est, riskCash); return 0.0; } return volume; } //+------------------------------------------------------------------+ //| VWAP (00:00 UTC reset, M1 bars, tick volume) | //+------------------------------------------------------------------+ bool SessionVWAP(datetime signalCloseServer, double &vwap) { int off = ServerOffset(); long utcClose = (long)signalCloseServer - off; datetime startServer = (datetime)(utcClose - (utcClose % 86400) + off); if(signalCloseServer <= startServer) return false; MqlRates m1[]; int n = CopyRates(g_sym, PERIOD_M1, startServer, signalCloseServer - 1, m1); if(n <= 0) return false; int expected = (int)((signalCloseServer - startServer) / 60); if(n < expected / 2) return false; double pv = 0.0, v = 0.0; for(int i = 0; i < n; i++) { if(m1[i].time < startServer || m1[i].time >= signalCloseServer) continue; double tp = (m1[i].high + m1[i].low + m1[i].close) / 3.0; pv += tp * (double)m1[i].tick_volume; v += (double)m1[i].tick_volume; } if(v <= 0.0) return false; vwap = pv / v; return true; } double Percentile(const double &sorted[], double pct) { int n = ArraySize(sorted); int k = (int)MathCeil(pct / 100.0 * n) - 1; if(k < 0) k = 0; if(k >= n) k = n - 1; return sorted[k]; } //+------------------------------------------------------------------+ //| Signal engine: completed bars only, no look-ahead | //+------------------------------------------------------------------+ void EvaluateSignal(SignalData &s) { s.dir = 0; s.ready = false; s.detail = ""; MqlRates r[]; ArraySetAsSeries(r, true); if(CopyRates(g_sym, PERIOD_M5, 1, 2, r) != 2) { s.detail = "m5_rates_not_ready"; return; } double e20[], rsi[], atr[], ef[], es[]; ArraySetAsSeries(e20, true); ArraySetAsSeries(rsi, true); ArraySetAsSeries(atr, true); ArraySetAsSeries(ef, true); ArraySetAsSeries(es, true); if(CopyBuffer(hEma20, 0, 1, 2, e20) != 2 || CopyBuffer(hRsi, 0, 1, 1, rsi) != 1 || CopyBuffer(hAtr, 0, 1, InpAtrLookback + 1, atr) != InpAtrLookback + 1 || CopyBuffer(hEmaFast15, 0, 1, 1, ef) != 1 || CopyBuffer(hEmaSlow15, 0, 1, 1, es) != 1) { s.detail = "indicators_not_ready"; return; } s.bar = r[0].time; datetime signalClose = s.bar + PeriodSeconds(PERIOD_M5); datetime m15Open0 = iTime(g_sym, PERIOD_M15, 0); if((long)m15Open0 != (long)signalClose - ((long)signalClose % 900)) { s.detail = "m15_not_synced"; return; } if(!SessionVWAP(signalClose, s.vwap)) { s.ready = true; s.detail = "vwap_unavailable_fail_closed"; return; } s.ready = true; s.close = r[0].close; s.atr = atr[0]; double hist[]; ArrayResize(hist, InpAtrLookback); for(int i = 0; i < InpAtrLookback; i++) hist[i] = atr[i + 1]; ArraySort(hist); double pLo = Percentile(hist, InpAtrPctLow), pHi = Percentile(hist, InpAtrPctHigh); bool atrOk = s.atr >= pLo && s.atr <= pHi; bool touched = r[1].low <= e20[1] && r[1].high >= e20[1]; bool up = ef[0] > es[0], dn = ef[0] < es[0]; bool buy = up && s.close > e20[0] && touched && rsi[0] >= InpRsiBuyMin && rsi[0] <= InpRsiBuyMax && s.close > s.vwap && atrOk; bool sell = dn && s.close < e20[0] && touched && rsi[0] >= InpRsiSellMin && rsi[0] <= InpRsiSellMax && s.close < s.vwap && atrOk; s.detail = StringFormat("bar=%s m15ema50=%.3f m15ema200=%.3f close=%.3f ema20=%.3f prevL=%.3f prevH=%.3f prevEma20=%.3f rsi=%.2f vwap=%.3f atr=%.4f p20=%.4f p90=%.4f", TimeToString(s.bar), ef[0], es[0], s.close, e20[0], r[1].low, r[1].high, e20[1], rsi[0], s.vwap, s.atr, pLo, pHi); if(buy) s.dir = 1; else if(sell) s.dir = -1; } //+------------------------------------------------------------------+ //| Gates (fail closed) | //+------------------------------------------------------------------+ bool LicenceGate(string &why) { if(g_tester) { if(InpTesterBypassLicence) return true; why = "tester_without_licence_bypass"; return false; } if(StringLen(g_token) == 0) { why = "missing_activation_token"; return false; } if(g_url_blocked) { why = "webrequest_url_not_allowed"; return false; } if(!g_activated) { why = "not_activated"; return false; } if(!g_licence_valid) { why = "licence_invalid_" + g_licence_reason; return false; } if(TimeGMT() - g_last_licence_ok > InpLicenceGraceMinutes * 60) { why = "licence_unverified_grace_expired"; return false; } return true; } bool AccountGate(string &why) { if(g_tester) return true; if(AccountInfoInteger(ACCOUNT_TRADE_MODE) != ACCOUNT_TRADE_MODE_DEMO) { if(!AURA_LIVE_TRADING_ENABLED) { why = "live_trading_disabled_demo_only_build"; return false; } if(!g_server_live) { why = "live_trading_disabled_on_server"; return false; } if(g_live_confirm != LoginStr()) { why = "live_not_confirmed_in_app"; return false; } } return true; } //+------------------------------------------------------------------+ //| Windows Bot bridge (Common\Files\AURA\control_.txt) | //+------------------------------------------------------------------+ void BridgeRead() { g_bridge_read = TimeGMT(); if(!InpBridgeMode) { g_bridge_state = "RUN"; return; } int h = FileOpen("AURA\\control_" + LoginStr() + ".txt", FILE_READ | FILE_TXT | FILE_ANSI | FILE_COMMON | FILE_SHARE_READ | FILE_SHARE_WRITE); if(h == INVALID_HANDLE) { g_bridge_state = "STOP"; return; } string prev = g_bridge_state; while(!FileIsEnding(h)) { string ln = FileReadString(h); int eq = StringFind(ln, "="); if(eq <= 0) continue; string k = StringSubstr(ln, 0, eq), v = StringSubstr(ln, eq + 1); StringTrimLeft(v); StringTrimRight(v); if(k == "state") g_bridge_state = (v == "RUN") ? "RUN" : "STOP"; else if(k == "token" && StringLen(v) > 0) g_token = v; else if(k == "backend" && StringLen(v) > 0) g_backend = v; else if(k == "live_confirm") g_live_confirm = v; } FileClose(h); if(prev != g_bridge_state) LogEvent("bridge_state", g_bridge_state); } bool BridgeGate(string &why) { if(g_tester || !InpBridgeMode) return true; if(g_bridge_state != "RUN") { why = "stopped_by_app_new_entries_off"; return false; } return true; } void BridgeWriteStatus() { if(!InpBridgeMode || g_tester) return; int n = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong tk = PositionGetTicket(i); if(tk > 0 && PositionGetInteger(POSITION_MAGIC) == (long)InpMagic) n++; } string tmp = "AURA\\status_" + LoginStr() + ".tmp"; int h = FileOpen(tmp, FILE_WRITE | FILE_TXT | FILE_ANSI | FILE_COMMON); if(h == INVALID_HANDLE) return; long tm = AccountInfoInteger(ACCOUNT_TRADE_MODE); FileWriteString(h, "ts=" + IntegerToString((long)TimeGMT()) + "\n"); FileWriteString(h, "ea_version=" + AURA_EA_VERSION + "\n"); FileWriteString(h, "login=" + LoginStr() + "\n"); FileWriteString(h, "server=" + AccountInfoString(ACCOUNT_SERVER) + "\n"); FileWriteString(h, "company=" + AccountInfoString(ACCOUNT_COMPANY) + "\n"); FileWriteString(h, "trade_mode=" + (tm == ACCOUNT_TRADE_MODE_DEMO ? "demo" : (tm == ACCOUNT_TRADE_MODE_CONTEST ? "contest" : "live")) + "\n"); FileWriteString(h, "symbol=" + g_sym + "\n"); FileWriteString(h, "state=" + g_bridge_state + "\n"); FileWriteString(h, "terminal_connected=" + IntegerToString(TerminalInfoInteger(TERMINAL_CONNECTED)) + "\n"); FileWriteString(h, "algo_allowed=" + IntegerToString(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && MQLInfoInteger(MQL_TRADE_ALLOWED)) + "\n"); FileWriteString(h, "licence_valid=" + IntegerToString(g_licence_valid) + "\n"); FileWriteString(h, "licence_reason=" + g_licence_reason + "\n"); FileWriteString(h, "webrequest_blocked=" + IntegerToString(g_url_blocked) + "\n"); FileWriteString(h, "block_reason=" + g_block_reason + "\n"); FileWriteString(h, "balance=" + DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2) + "\n"); FileWriteString(h, "equity=" + DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2) + "\n"); FileWriteString(h, "positions=" + IntegerToString(n) + "\n"); FileClose(h); FileMove(tmp, FILE_COMMON, "AURA\\status_" + LoginStr() + ".txt", FILE_COMMON | FILE_REWRITE); } bool InBrokerSession() { MqlDateTime s; TimeToStruct(ServerNow(), s); long secs = s.hour * 3600 + s.min * 60 + s.sec; datetime from, to; for(uint i = 0; SymbolInfoSessionTrade(g_sym, (ENUM_DAY_OF_WEEK)s.day_of_week, i, from, to); i++) if(secs >= (long)from && secs < (long)to) return true; return false; } bool PlatformGate(string &why) { if(!g_tester) { if(!TerminalInfoInteger(TERMINAL_CONNECTED)) { why = "terminal_disconnected"; return false; } if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { why = "algo_trading_disabled_in_terminal"; return false; } } if(!MQLInfoInteger(MQL_TRADE_ALLOWED)) { why = "ea_trading_permission_off"; return false; } if(!AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)) { why = "account_trading_not_allowed"; return false; } if(!AccountInfoInteger(ACCOUNT_TRADE_EXPERT)) { why = "account_expert_trading_not_allowed"; return false; } if(SymbolInfoInteger(g_sym, SYMBOL_TRADE_MODE) != SYMBOL_TRADE_MODE_FULL) { why = "symbol_close_only_or_disabled"; return false; } if(!InBrokerSession()) { why = "market_closed"; return false; } MqlTick t; if(!SymbolInfoTick(g_sym, t) || t.bid <= 0 || t.ask <= 0) { why = "no_quote"; return false; } if((long)ServerNow() - (long)t.time > InpStaleQuoteSeconds) { why = "stale_quote"; return false; } return true; } bool TimeGate(string &why) { datetime u = UtcNow(); if(InpSessionFilter) { datetime ln = LondonLocal(u), ny = NewYorkLocal(u); int dl = Dow(ln), dn = Dow(ny), ml = MinOfDay(ln), mn = MinOfDay(ny); bool lon = dl >= 1 && dl <= 5 && ml >= InpLondonStartMin && ml < InpLondonEndMin; bool nyk = dn >= 1 && dn <= 5 && mn >= InpNewYorkStartMin && mn < InpNewYorkEndMin; if(!lon && !nyk) { why = "outside_session"; return false; } } datetime nyl = NewYorkLocal(u); int m = MinOfDay(nyl); if(m >= 17 * 60 - InpRolloverBlockMin && m < 17 * 60 + InpRolloverBlockMin) { why = "rollover_window"; return false; } if(Dow(nyl) == 5 && m >= InpFridayCloseBlockMin) { why = "weekly_close_window"; return false; } return true; } void LoadTesterNews() { ArrayResize(g_news_csv, 0); int h = FileOpen(InpTesterNewsFile, FILE_READ | FILE_TXT | FILE_ANSI | FILE_COMMON); if(h == INVALID_HANDLE) { g_news_feed_ok = false; LogEvent("news_feed_missing", "tester news CSV not found in Common\\Files: " + InpTesterNewsFile); return; } while(!FileIsEnding(h)) { string line = FileReadString(h); StringTrimLeft(line); StringTrimRight(line); if(StringLen(line) < 10 || StringGetCharacter(line, 0) == '#') continue; datetime t = StringToTime(line); if(t <= 0) continue; int n = ArraySize(g_news_csv); ArrayResize(g_news_csv, n + 1); g_news_csv[n] = t; } FileClose(h); g_news_feed_ok = ArraySize(g_news_csv) > 0; LogEvent("news_feed", "tester events loaded=" + IntegerToString(ArraySize(g_news_csv))); } bool NewsGate(string &why) { if(!InpNewsFilter) return true; int w = InpNewsWindowMinutes * 60; if(g_tester) { if(!g_news_feed_ok) { why = "news_feed_missing_fail_closed"; return false; } datetime u = UtcNow(); for(int i = 0; i < ArraySize(g_news_csv); i++) if(MathAbs((double)((long)g_news_csv[i] - (long)u)) <= w) { why = "news_window"; return false; } return true; } datetime now = ServerNow(); if(now - g_news_stale_check > 3600 || !g_news_feed_ok) { MqlCalendarValue chk[]; ResetLastError(); int c = CalendarValueHistory(chk, now - 7 * 86400, now, NULL, "USD"); g_news_feed_ok = (c > 0); g_news_stale_check = now; if(!g_news_feed_ok) LogEvent("news_feed_stale", "calendar USD events last 7d=" + IntegerToString(c) + " err=" + IntegerToString(GetLastError())); } if(!g_news_feed_ok) { why = "news_feed_missing_or_stale_fail_closed"; return false; } MqlCalendarValue vals[]; ResetLastError(); int n = CalendarValueHistory(vals, now - w, now + w + 1, NULL, "USD"); if(n < 0) { why = "news_feed_error_" + IntegerToString(GetLastError()); return false; } for(int i = 0; i < n; i++) { MqlCalendarEvent ev; if(CalendarEventById(vals[i].event_id, ev) && ev.importance == CALENDAR_IMPORTANCE_HIGH) { why = "news_window_" + ev.name; return false; } } return true; } void UpdateRiskAnchors() { datetime u = UtcNow(); datetime day = (datetime)((long)u - ((long)u % 86400)); datetime week = day - ((Dow(day) + 6) % 7) * 86400; double eq = AccountInfoDouble(ACCOUNT_EQUITY); if((long)GvGet("DAY_ANCHOR") != (long)day) { GvSet("DAY_ANCHOR", (double)day); GvSet("DAY_EQ", eq); GvSet("DAY_ENTRIES", 0); LogEvent("daily_reset", StringFormat("utc_day=%s equity=%.2f", TimeToString(day, TIME_DATE), eq)); } if((long)GvGet("WEEK_ANCHOR") != (long)week) { GvSet("WEEK_ANCHOR", (double)week); GvSet("WEEK_EQ", eq); LogEvent("weekly_reset", StringFormat("utc_week=%s equity=%.2f", TimeToString(week, TIME_DATE), eq)); } if(eq > GvGet("PEAK_EQ")) GvSet("PEAK_EQ", eq); double peak = GvGet("PEAK_EQ"); if(peak > 0 && (peak - eq) / peak * 100.0 >= InpPeakDrawdownPct && GvGet("PEAK_LOCK") == 0) { GvSet("PEAK_LOCK", 1); GvSet("LOCK_TIME", (double)TimeGMT()); LogEvent("peak_circuit_breaker", StringFormat("peak=%.2f equity=%.2f", peak, eq)); RemoteLog("error", "peak_circuit_breaker", "Peak-equity drawdown limit hit; review required"); } if(GvGet("STREAK") >= InpMaxLossStreak && GvGet("STREAK_LOCK") == 0) { GvSet("STREAK_LOCK", 1); GvSet("LOCK_TIME", (double)TimeGMT()); LogEvent("loss_streak_lock", "streak=" + DoubleToString(GvGet("STREAK"), 0)); RemoteLog("warning", "loss_streak_lock", "Consecutive-loss limit hit; review required"); } } bool RiskGate(string &why) { double eq = AccountInfoDouble(ACCOUNT_EQUITY); double dayEq = GvGet("DAY_EQ"), weekEq = GvGet("WEEK_EQ"); if(dayEq > 0 && eq <= dayEq * (1.0 - InpDailyLossPct / 100.0)) { why = "daily_loss_cutoff"; return false; } if(weekEq > 0 && eq <= weekEq * (1.0 - InpWeeklyLossPct / 100.0)) { why = "weekly_loss_cutoff"; return false; } if(GvGet("PEAK_LOCK") != 0) { why = "peak_drawdown_breaker_review_required"; return false; } if(GvGet("STREAK_LOCK") != 0) { why = "loss_streak_review_required"; return false; } if(GvGet("DAY_ENTRIES") >= InpMaxDailyEntries) { why = "daily_entry_cap"; return false; } return true; } bool ExposureGate(string &why) { int symCount = 0, foreign = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong tk = PositionGetTicket(i); if(tk == 0 || PositionGetString(POSITION_SYMBOL) != g_sym) continue; symCount++; if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagic) foreign++; } if(!g_hedging && symCount > 0) { why = foreign > 0 ? "netting_foreign_position_on_symbol" : "position_already_open"; return false; } if(g_hedging && symCount >= InpMaxPositions) { why = foreign > 0 ? "position_cap_incl_foreign" : "position_cap"; return false; } for(int j = OrdersTotal() - 1; j >= 0; j--) { ulong o = OrderGetTicket(j); if(o != 0 && OrderGetString(ORDER_SYMBOL) == g_sym && (ulong)OrderGetInteger(ORDER_MAGIC) == InpMagic) { why = "pending_order_exists"; return false; } } return true; } bool DuplicateGate(datetime sigBar, string &why) { if((long)GvGet("LAST_SIGNAL_BAR") == (long)sigBar) { why = "duplicate_signal_bar"; return false; } if(HistorySelect(sigBar + PeriodSeconds(PERIOD_M5), ServerNow() + 60)) for(int i = HistoryDealsTotal() - 1; i >= 0; i--) { ulong d = HistoryDealGetTicket(i); if((ulong)HistoryDealGetInteger(d, DEAL_MAGIC) == InpMagic && HistoryDealGetString(d, DEAL_SYMBOL) == g_sym && HistoryDealGetInteger(d, DEAL_ENTRY) == DEAL_ENTRY_IN) { why = "entry_already_made_this_bar"; return false; } } return true; } bool ConfigGate(string &why) { if(InpCostReservePerLot < 0.0) { why = "cost_reserve_negative"; return false; } if(InpCostReservePerLot == 0.0 && !InpAllowZeroCostReserve) { why = "cost_reserve_not_configured_set_InpCostReservePerLot"; return false; } if(InpApprovedSpreadCap <= 0.0) { why = "spread_cap_not_configured"; return false; } return true; } // Spec 5: estimate round-trip commission+fees per lot from executed deal history (last 90 days, this symbol, any magic). void LogMeasuredCosts() { if(!HistorySelect(ServerNow() - 90 * 86400, ServerNow() + 60)) return; double cost = 0.0, lotsIn = 0.0; int n = 0; for(int i = 0; i < HistoryDealsTotal(); i++) { ulong d = HistoryDealGetTicket(i); if(HistoryDealGetString(d, DEAL_SYMBOL) != g_sym) continue; cost += HistoryDealGetDouble(d, DEAL_COMMISSION) + HistoryDealGetDouble(d, DEAL_FEE); if(HistoryDealGetInteger(d, DEAL_ENTRY) == DEAL_ENTRY_IN) { lotsIn += HistoryDealGetDouble(d, DEAL_VOLUME); n++; } } if(lotsIn <= 0) { LogEvent("measured_costs", "no executed deals on symbol in last 90 days; take commission from account terms"); return; } double perLot = MathAbs(cost) / lotsIn; g_measured_cost = perLot; LogEvent("measured_costs", StringFormat("round_trip_commission_fee_per_lot=%.2f %s from %d entries; configured=%.2f", perLot, AccountInfoString(ACCOUNT_CURRENCY), n, InpCostReservePerLot)); if(InpCostReservePerLot + 1e-6 < perLot) LogEvent("config_warning", "InpCostReservePerLot is below the measured cost; raise it to at least " + DoubleToString(perLot, 2)); } bool GeneralGates(string &why) { return BridgeGate(why) && ConfigGate(why) && LicenceGate(why) && AccountGate(why) && PlatformGate(why) && TimeGate(why) && NewsGate(why) && RiskGate(why) && ExposureGate(why); } //+------------------------------------------------------------------+ //| Execution | //+------------------------------------------------------------------+ bool StopsValid(bool buy, double sl, double tp, const MqlTick &t, string &why) { double pt = SymbolInfoDouble(g_sym, SYMBOL_POINT); double minD = MathMax((double)SymbolInfoInteger(g_sym, SYMBOL_TRADE_STOPS_LEVEL), (double)SymbolInfoInteger(g_sym, SYMBOL_TRADE_FREEZE_LEVEL)) * pt; if(buy) { if(!(sl < t.bid && tp > t.bid)) { why = "stop_direction_invalid"; return false; } if(t.bid - sl <= minD || tp - t.bid <= minD) { why = "stop_distance_below_broker_min"; return false; } } else { if(!(sl > t.ask && tp < t.ask)) { why = "stop_direction_invalid"; return false; } if(sl - t.ask <= minD || t.ask - tp <= minD) { why = "stop_distance_below_broker_min"; return false; } } return true; } ulong FindOurPosition() { for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong tk = PositionGetTicket(i); if(tk != 0 && PositionGetString(POSITION_SYMBOL) == g_sym && (ulong)PositionGetInteger(POSITION_MAGIC) == InpMagic) return tk; } return 0; } double OurSymbolVolume() { double v = 0.0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong tk = PositionGetTicket(i); if(tk != 0 && PositionGetString(POSITION_SYMBOL) == g_sym) v += PositionGetDouble(POSITION_VOLUME); } return v; } bool ModifySLTP(ulong pos, double sl, double tp) { MqlTradeRequest rq; MqlTradeResult rs; ZeroMemory(rq); ZeroMemory(rs); rq.action = TRADE_ACTION_SLTP; rq.position = pos; rq.symbol = g_sym; rq.sl = sl; rq.tp = tp; rq.magic = InpMagic; bool ok = OrderSend(rq, rs); LogEvent("sltp_modify", StringFormat("pos=%I64u sl=%.3f tp=%.3f ok=%d retcode=%u %s", pos, sl, tp, ok, rs.retcode, rs.comment)); return rs.retcode == TRADE_RETCODE_DONE; } bool ClosePosition(ulong pos, string reason) { for(int attempt = 0; attempt <= InpMaxOrderRetries; attempt++) { if(!PositionSelectByTicket(pos)) return true; long type = PositionGetInteger(POSITION_TYPE); MqlTick t; if(!SymbolInfoTick(g_sym, t)) return false; MqlTradeRequest rq; MqlTradeResult rs; ZeroMemory(rq); ZeroMemory(rs); rq.action = TRADE_ACTION_DEAL; rq.position = pos; rq.symbol = g_sym; rq.volume = PositionGetDouble(POSITION_VOLUME); rq.type = (type == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; rq.price = (type == POSITION_TYPE_BUY) ? t.bid : t.ask; rq.deviation = InpMaxDeviationPoints; rq.magic = InpMagic; rq.type_filling = Filling(); rq.comment = "AURA close " + reason; bool ok = OrderSend(rq, rs); LogEvent("close_send", StringFormat("pos=%I64u reason=%s attempt=%d ok=%d retcode=%u %s", pos, reason, attempt, ok, rs.retcode, rs.comment)); if(rs.retcode == TRADE_RETCODE_DONE || rs.retcode == TRADE_RETCODE_DONE_PARTIAL || rs.retcode == TRADE_RETCODE_PLACED) return true; if(rs.retcode != TRADE_RETCODE_REQUOTE && rs.retcode != TRADE_RETCODE_PRICE_CHANGED && rs.retcode != TRADE_RETCODE_PRICE_OFF) break; } RemoteLog("error", "close_failed", "Close failed for position " + IntegerToString((long)pos) + " reason=" + reason); return false; } // Emergency response: 3 attempts to attach SL/TP, then close immediately. void EnsureProtected(ulong pos, double slDist) { if(!PositionSelectByTicket(pos)) return; if(PositionGetDouble(POSITION_SL) > 0 && PositionGetDouble(POSITION_TP) > 0) return; bool buy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY; double fill = PositionGetDouble(POSITION_PRICE_OPEN); if(slDist <= 0) { double atr[]; ArraySetAsSeries(atr, true); if(CopyBuffer(hAtr, 0, 1, 1, atr) == 1) slDist = InpSlAtrMult * atr[0]; } for(int i = 0; i < 3 && slDist > 0; i++) { double sl = RoundToTick(buy ? fill - slDist : fill + slDist); double tp = RoundToTick(buy ? fill + InpTpRR * slDist : fill - InpTpRR * slDist); if(ModifySLTP(pos, sl, tp)) break; Sleep(300); } if(PositionSelectByTicket(pos) && (PositionGetDouble(POSITION_SL) <= 0 || PositionGetDouble(POSITION_TP) <= 0)) { LogEvent("emergency_close", "protection not confirmed after 3 attempts pos=" + IntegerToString((long)pos)); RemoteLog("error", "emergency_close_unprotected", "SL/TP could not be confirmed; position closed"); ClosePosition(pos, "unprotected"); } } void ConfirmProtection(bool buy, double slDist) { ulong pos = 0; for(int i = 0; i < 15 && pos == 0; i++) { pos = FindOurPosition(); if(pos == 0) Sleep(200); } if(pos == 0) { LogEvent("protection_check", "position not visible yet; ManagePositions will verify"); return; } if(!PositionSelectByTicket(pos)) return; double fill = PositionGetDouble(POSITION_PRICE_OPEN); double sl = RoundToTick(buy ? fill - slDist : fill + slDist); double tp = RoundToTick(buy ? fill + InpTpRR * slDist : fill - InpTpRR * slDist); double half = TickSize() / 2.0; if(PositionGetDouble(POSITION_SL) > 0 && (MathAbs(PositionGetDouble(POSITION_SL) - sl) > half || MathAbs(PositionGetDouble(POSITION_TP) - tp) > half)) { MqlTick t; string why; if(SymbolInfoTick(g_sym, t) && StopsValid(buy, sl, tp, t, why)) ModifySLTP(pos, sl, tp); else LogEvent("sltp_rebase_skipped", why + " (original server-side SL/TP kept)"); } EnsureProtected(pos, slDist); if(PositionSelectByTicket(pos)) LogEvent("position_protected", StringFormat("pos=%I64u fill=%.3f sl=%.3f tp=%.3f", pos, fill, PositionGetDouble(POSITION_SL), PositionGetDouble(POSITION_TP))); } void ExecuteTrade(SignalData &s) { bool buy = s.dir > 0; ENUM_ORDER_TYPE type = buy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; double slDist = InpSlAtrMult * s.atr; MqlTick t; string why = ""; MqlTradeRequest rq; MqlTradeResult rs; MqlTradeCheckResult ck; ZeroMemory(rq); ZeroMemory(ck); rq.action = TRADE_ACTION_DEAL; rq.symbol = g_sym; rq.type = type; rq.magic = InpMagic; rq.deviation = InpMaxDeviationPoints; rq.type_filling = Filling(); rq.type_time = ORDER_TIME_GTC; rq.comment = "AURA " + TimeToString(s.bar, TIME_DATE | TIME_MINUTES); bool sent = false; for(int attempt = 0; attempt <= InpMaxOrderRetries; attempt++) { if(attempt > 0 && FindOurPosition() != 0) { sent = true; break; } if(!SymbolInfoTick(g_sym, t)) { why = "no_tick"; break; } double entry = buy ? t.ask : t.bid; if(MathAbs(entry - s.close) > InpEntryMaxMoveAtr * s.atr) { why = StringFormat("entry_drift %.3f > %.3f", MathAbs(entry - s.close), InpEntryMaxMoveAtr * s.atr); break; } double sl = RoundToTick(buy ? entry - slDist : entry + slDist); double tp = RoundToTick(buy ? entry + InpTpRR * slDist : entry - InpTpRR * slDist); if(!StopsValid(buy, sl, tp, t, why)) break; if(attempt == 0) { double vol = CalculateRiskVolume(type, entry, sl, why); if(vol <= 0) break; double lim = SymbolInfoDouble(g_sym, SYMBOL_VOLUME_LIMIT); if(lim > 0 && OurSymbolVolume() + vol > lim + 1e-9) { why = "aggregate_volume_limit"; break; } double margin = 0.0; if(!OrderCalcMargin(type, g_sym, vol, entry, margin)) { why = "ordercalcmargin_failed"; break; } if(margin >= AccountInfoDouble(ACCOUNT_MARGIN_FREE)) { why = StringFormat("insufficient_margin need=%.2f free=%.2f", margin, AccountInfoDouble(ACCOUNT_MARGIN_FREE)); break; } rq.volume = vol; } rq.price = entry; rq.sl = sl; rq.tp = tp; if(attempt == 0) { if(!OrderCheck(rq, ck)) { why = StringFormat("order_check_failed retcode=%u %s", ck.retcode, ck.comment); break; } GvSet("LAST_SIGNAL_BAR", (double)s.bar); // duplicate guard set BEFORE sending } ZeroMemory(rs); bool ok = OrderSend(rq, rs); LogEvent("order_send", StringFormat("attempt=%d dir=%s ok=%d retcode=%u deal=%I64u price=%.3f req=%.3f vol=%.2f sl=%.3f tp=%.3f %s", attempt, buy ? "buy" : "sell", ok, rs.retcode, rs.deal, rs.price, entry, rq.volume, sl, tp, rs.comment)); if(rs.retcode == TRADE_RETCODE_DONE || rs.retcode == TRADE_RETCODE_PLACED || rs.retcode == TRADE_RETCODE_DONE_PARTIAL) { sent = true; break; } if(rs.retcode != TRADE_RETCODE_REQUOTE && rs.retcode != TRADE_RETCODE_PRICE_CHANGED && rs.retcode != TRADE_RETCODE_PRICE_OFF) { why = StringFormat("order_rejected retcode=%u %s", rs.retcode, rs.comment); break; } } if(!sent) { LogEvent("entry_blocked", why); g_block_reason = why; if(StringFind(why, "order_") == 0) RemoteLog("warning", "order_rejected", why); return; } GvSet("DAY_ENTRIES", GvGet("DAY_ENTRIES") + 1); ConfirmProtection(buy, slDist); } void ManagePositions() { for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong tk = PositionGetTicket(i); if(tk == 0 || PositionGetString(POSITION_SYMBOL) != g_sym || (ulong)PositionGetInteger(POSITION_MAGIC) != InpMagic) continue; if(PositionGetDouble(POSITION_SL) <= 0 || PositionGetDouble(POSITION_TP) <= 0) { EnsureProtected(tk, 0); continue; } int shift = iBarShift(g_sym, PERIOD_M5, (datetime)PositionGetInteger(POSITION_TIME), false); if(shift >= InpTimeStopBars) ClosePosition(tk, "time_stop"); } } //+------------------------------------------------------------------+ //| History-based reconciliation | //+------------------------------------------------------------------+ double DealNet(ulong d) { return HistoryDealGetDouble(d, DEAL_PROFIT) + HistoryDealGetDouble(d, DEAL_SWAP) + HistoryDealGetDouble(d, DEAL_COMMISSION) + HistoryDealGetDouble(d, DEAL_FEE); } bool IsOurDeal(ulong d) { return (ulong)HistoryDealGetInteger(d, DEAL_MAGIC) == InpMagic && HistoryDealGetString(d, DEAL_SYMBOL) == g_sym; } bool IsExitDeal(ulong d) { long e = HistoryDealGetInteger(d, DEAL_ENTRY); return e == DEAL_ENTRY_OUT || e == DEAL_ENTRY_OUT_BY; } int ComputeLossStreak() { datetime baseUtc = (datetime)(long)GvGet("STREAK_BASE", 0); datetime from = (datetime)MathMax((double)((long)baseUtc + ServerOffset()), (double)((long)ServerNow() - 30 * 86400)); if(!HistorySelect(from, ServerNow() + 60)) return (int)GvGet("STREAK"); int total = HistoryDealsTotal(), streak = 0; for(int i = 0; i < total; i++) { ulong d = HistoryDealGetTicket(i); if(!IsOurDeal(d) || !IsExitDeal(d)) continue; long pid = HistoryDealGetInteger(d, DEAL_POSITION_ID); double net = 0.0; for(int j = 0; j < total; j++) { ulong x = HistoryDealGetTicket(j); if(HistoryDealGetInteger(x, DEAL_POSITION_ID) == pid) net += DealNet(x); } streak = (net < 0) ? streak + 1 : 0; } return streak; } void ReconcileOnInit() { UpdateRiskAnchors(); datetime dayServer = (datetime)((long)GvGet("DAY_ANCHOR") + ServerOffset()); int entries = 0; if(HistorySelect(dayServer, ServerNow() + 60)) for(int i = 0; i < HistoryDealsTotal(); i++) { ulong d = HistoryDealGetTicket(i); if(IsOurDeal(d) && HistoryDealGetInteger(d, DEAL_ENTRY) == DEAL_ENTRY_IN) entries++; } if(entries > GvGet("DAY_ENTRIES")) GvSet("DAY_ENTRIES", entries); GvSet("STREAK", ComputeLossStreak()); LogEvent("reconcile", StringFormat("day_entries=%d streak=%d peak=%.2f day_eq=%.2f week_eq=%.2f open_ours=%s", (int)GvGet("DAY_ENTRIES"), (int)GvGet("STREAK"), GvGet("PEAK_EQ"), GvGet("DAY_EQ"), GvGet("WEEK_EQ"), FindOurPosition() != 0 ? "yes" : "no")); ManagePositions(); } double RealizedToday() { double sum = 0.0; datetime dayServer = (datetime)((long)GvGet("DAY_ANCHOR") + ServerOffset()); if(HistorySelect(dayServer, ServerNow() + 60)) for(int i = 0; i < HistoryDealsTotal(); i++) { ulong d = HistoryDealGetTicket(i); if(IsOurDeal(d)) sum += DealNet(d); } return sum; } //+------------------------------------------------------------------+ //| Backend: activation, licence, heartbeat, telemetry | //+------------------------------------------------------------------+ void Activate() { g_last_activate_try = TimeGMT(); string atype = (AccountInfoInteger(ACCOUNT_TRADE_MODE) == ACCOUNT_TRADE_MODE_DEMO) ? "demo" : "live"; string body = StringFormat("{\"activation_token\":\"%s\",\"broker_name\":\"%s\",\"broker_server\":\"%s\",\"account_login\":\"%s\",\"account_type\":\"%s\",\"account_currency\":\"%s\",\"leverage\":%d,\"ea_version\":\"%s\",\"terminal_build\":%d}", g_token, JsonEscape(AccountInfoString(ACCOUNT_COMPANY)), JsonEscape(AccountInfoString(ACCOUNT_SERVER)), LoginStr(), atype, AccountInfoString(ACCOUNT_CURRENCY), (int)AccountInfoInteger(ACCOUNT_LEVERAGE), AURA_EA_VERSION, (int)TerminalInfoInteger(TERMINAL_BUILD)); string r; int code = Http("POST", "/api/ea/activate", "", body, r); g_activated = (code >= 200 && code < 300); LogEvent("activate", "http=" + IntegerToString(code) + " " + r); } void CheckLicence() { g_last_licence_try = TimeGMT(); string r; int code = Http("GET", "/api/ea/licence-check", "X-Activation-Token: " + g_token + "\r\nX-Account-Login: " + LoginStr() + "\r\n", "", r); if(code < 200 || code >= 300) { LogEvent("licence_check_unreachable", "http=" + IntegerToString(code)); return; } g_last_licence_ok = TimeGMT(); bool valid = JsonGet(r, "valid") == "true"; if(valid != g_licence_valid) LogEvent("licence_state", r); g_licence_valid = valid; g_licence_reason = valid ? "ok" : JsonGet(r, "reason"); g_server_live = JsonGet(r, "live_trading_enabled") == "true"; double ack = StringToDouble(JsonGet(r, "review_ack_at")); if(ack > 0 && ack > GvGet("LOCK_TIME") && (GvGet("STREAK_LOCK") != 0 || GvGet("PEAK_LOCK") != 0)) { GvSet("STREAK_LOCK", 0); GvSet("PEAK_LOCK", 0); GvSet("STREAK", 0); GvSet("STREAK_BASE", (double)UtcNow()); GvSet("PEAK_EQ", AccountInfoDouble(ACCOUNT_EQUITY)); LogEvent("risk_review_ack", "locks cleared by dashboard review at " + DoubleToString(ack, 0)); } } void SendHeartbeat() { g_last_hb = TimeGMT(); double unreal = 0.0; int openp = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong tk = PositionGetTicket(i); if(tk == 0 || PositionGetString(POSITION_SYMBOL) != g_sym || (ulong)PositionGetInteger(POSITION_MAGIC) != InpMagic) continue; openp++; unreal += PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP); } string why = "ready"; GeneralGates(why); g_block_reason = why; MqlTick t; SymbolInfoTick(g_sym, t); double spreadPts = (t.ask - t.bid) / SymbolInfoDouble(g_sym, SYMBOL_POINT); bool allowed = TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && MQLInfoInteger(MQL_TRADE_ALLOWED); string body = StringFormat("{\"activation_token\":\"%s\",\"account_login\":\"%s\",\"balance\":%.2f,\"equity\":%.2f,\"margin\":%.2f,\"free_margin\":%.2f," "\"open_positions\":%d,\"unrealized_pl\":%.2f,\"realized_pl_today\":%.2f,\"spread_points\":%.1f,\"ea_version\":\"%s\"," "\"trading_allowed\":%s,\"symbol\":\"%s\",\"block_reason\":\"%s\",\"loss_streak\":%d,\"daily_entries\":%d," "\"risk_lock\":%s,\"news_feed_ok\":%s,\"server_utc_offset_min\":%d,\"digits\":%d,\"point\":%.5f," "\"spread_cap_price\":%.3f,\"spread_cap_points\":%.0f,\"cost_reserve_per_lot\":%.2f,\"measured_cost_per_lot\":%.2f," "\"terminal_build\":%d,\"account_type\":\"%s\"}", g_token, LoginStr(), AccountInfoDouble(ACCOUNT_BALANCE), AccountInfoDouble(ACCOUNT_EQUITY), AccountInfoDouble(ACCOUNT_MARGIN), AccountInfoDouble(ACCOUNT_MARGIN_FREE), openp, unreal, RealizedToday(), spreadPts, AURA_EA_VERSION, allowed ? "true" : "false", g_sym, JsonEscape(why), (int)GvGet("STREAK"), (int)GvGet("DAY_ENTRIES"), (GvGet("STREAK_LOCK") != 0 || GvGet("PEAK_LOCK") != 0) ? "true" : "false", g_news_feed_ok ? "true" : "false", ServerOffset() / 60, (int)SymbolInfoInteger(g_sym, SYMBOL_DIGITS), SymbolInfoDouble(g_sym, SYMBOL_POINT), InpApprovedSpreadCap, InpApprovedSpreadCap / SymbolInfoDouble(g_sym, SYMBOL_POINT), InpCostReservePerLot, g_measured_cost, (int)TerminalInfoInteger(TERMINAL_BUILD), AccountInfoInteger(ACCOUNT_TRADE_MODE) == ACCOUNT_TRADE_MODE_DEMO ? "demo" : "live"); string r; Http("POST", "/api/ea/heartbeat", "", body, r); string pb = "{\"activation_token\":\"" + g_token + "\",\"account_login\":\"" + LoginStr() + "\",\"positions\":["; bool first = true; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong tk = PositionGetTicket(i); if(tk == 0 || PositionGetString(POSITION_SYMBOL) != g_sym || (ulong)PositionGetInteger(POSITION_MAGIC) != InpMagic) continue; if(!first) pb += ","; first = false; pb += StringFormat("{\"ticket\":%I64u,\"symbol\":\"%s\",\"side\":\"%s\",\"volume\":%.2f,\"open_price\":%.3f,\"sl\":%.3f,\"tp\":%.3f,\"open_time\":\"%s\",\"unrealized_pl\":%.2f,\"swap\":%.2f}", tk, g_sym, PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY ? "buy" : "sell", PositionGetDouble(POSITION_VOLUME), PositionGetDouble(POSITION_PRICE_OPEN), PositionGetDouble(POSITION_SL), PositionGetDouble(POSITION_TP), TimeToString((datetime)PositionGetInteger(POSITION_TIME), TIME_DATE | TIME_SECONDS), PositionGetDouble(POSITION_PROFIT), PositionGetDouble(POSITION_SWAP)); } pb += "]}"; Http("POST", "/api/ea/positions", "", pb, r); } void SendClosedTrade(long pid) { if(g_tester || !HistorySelectByPosition(pid)) return; double openP = 0, closeP = 0, vol = 0, profit = 0, comm = 0, swap = 0; datetime openT = 0, closeT = 0; string side = "buy"; for(int i = 0; i < HistoryDealsTotal(); i++) { ulong d = HistoryDealGetTicket(i); long e = HistoryDealGetInteger(d, DEAL_ENTRY); if(e == DEAL_ENTRY_IN) { openP = HistoryDealGetDouble(d, DEAL_PRICE); openT = (datetime)HistoryDealGetInteger(d, DEAL_TIME); vol = HistoryDealGetDouble(d, DEAL_VOLUME); side = HistoryDealGetInteger(d, DEAL_TYPE) == DEAL_TYPE_BUY ? "buy" : "sell"; } else { closeP = HistoryDealGetDouble(d, DEAL_PRICE); closeT = (datetime)HistoryDealGetInteger(d, DEAL_TIME); } profit += HistoryDealGetDouble(d, DEAL_PROFIT); comm += HistoryDealGetDouble(d, DEAL_COMMISSION) + HistoryDealGetDouble(d, DEAL_FEE); swap += HistoryDealGetDouble(d, DEAL_SWAP); } string body = StringFormat("{\"activation_token\":\"%s\",\"account_login\":\"%s\",\"trades\":[{\"ticket\":%I64d,\"symbol\":\"%s\",\"side\":\"%s\",\"volume\":%.2f,\"open_price\":%.3f,\"close_price\":%.3f,\"open_time\":\"%s\",\"close_time\":\"%s\",\"profit\":%.2f,\"commission\":%.2f,\"swap\":%.2f,\"comment\":\"AURA\"}]}", g_token, LoginStr(), pid, g_sym, side, vol, openP, closeP, TimeToString(openT, TIME_DATE | TIME_SECONDS), TimeToString(closeT, TIME_DATE | TIME_SECONDS), profit, comm, swap); string r; Http("POST", "/api/ea/trades", "", body, r); } //+------------------------------------------------------------------+ //| Lifecycle | //+------------------------------------------------------------------+ int OnInit() { g_tester = (bool)MQLInfoInteger(MQL_TESTER); g_token = InpActivationToken; g_backend = InpBackendURL; g_log = FileOpen("AURA_XAU_" + LoginStr() + ".csv", FILE_READ | FILE_WRITE | FILE_CSV | FILE_ANSI | FILE_SHARE_READ, ','); if(g_log != INVALID_HANDLE) FileSeek(g_log, 0, SEEK_END); if(!g_tester && AccountInfoInteger(ACCOUNT_TRADE_MODE) != ACCOUNT_TRADE_MODE_DEMO && !AURA_LIVE_TRADING_ENABLED) { LogEvent("init_failed", "LIVE account detected. This build is DEMO-ONLY."); Alert("AURA XAU: live trading is disabled in this build. Attach to a DEMO account."); return INIT_FAILED; } if(!ResolveSymbol()) return INIT_PARAMETERS_INCORRECT; long mm = AccountInfoInteger(ACCOUNT_MARGIN_MODE); g_hedging = (mm == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING); hEmaFast15 = iMA(g_sym, PERIOD_M15, InpTrendFastEMA, 0, MODE_EMA, PRICE_CLOSE); hEmaSlow15 = iMA(g_sym, PERIOD_M15, InpTrendSlowEMA, 0, MODE_EMA, PRICE_CLOSE); hEma20 = iMA(g_sym, PERIOD_M5, InpPullbackEMA, 0, MODE_EMA, PRICE_CLOSE); hRsi = iRSI(g_sym, PERIOD_M5, InpRsiPeriod, PRICE_CLOSE); hAtr = iATR(g_sym, PERIOD_M5, InpAtrPeriod); if(hEmaFast15 == INVALID_HANDLE || hEmaSlow15 == INVALID_HANDLE || hEma20 == INVALID_HANDLE || hRsi == INVALID_HANDLE || hAtr == INVALID_HANDLE) { LogEvent("init_failed", "indicator handle creation failed"); return INIT_FAILED; } if(InpCostReservePerLot == 0.0) LogEvent("config_warning", InpAllowZeroCostReserve ? "Zero cost reserve ALLOWED for preliminary tests: results are misleading" : "InpCostReservePerLot=0: new entries BLOCKED until broker round-trip commission is set"); LogEvent("config", StringFormat("spread_cap=%.3f price = %.0f points (digits=%d)", InpApprovedSpreadCap, InpApprovedSpreadCap / SymbolInfoDouble(g_sym, SYMBOL_POINT), (int)SymbolInfoInteger(g_sym, SYMBOL_DIGITS))); LogEvent("config_unapproved_defaults", "sessions/news/rollover/emergency/review rules are defaults NOT approved by owner; see README section 2"); LogMeasuredCosts(); if(!InpSessionFilter) LogEvent("config_warning", "Session filter disabled: explicit test variant only"); if(!InpNewsFilter) LogEvent("config_warning", "News filter disabled: test variant only"); if(g_tester && InpNewsFilter) LoadTesterNews(); g_last_bar = iTime(g_sym, PERIOD_M5, 0); // wait for the next freshly completed bar string why; ValidateSpec(why); ReconcileOnInit(); LogEvent("init", StringFormat("version=%s symbol=%s mode=%s tester=%d live_enabled=%d", AURA_EA_VERSION, g_sym, g_hedging ? "hedging" : "netting", g_tester, AURA_LIVE_TRADING_ENABLED)); BridgeRead(); if(!g_tester && StringLen(g_token) > 0) { Activate(); if(g_activated) CheckLicence(); SendHeartbeat(); } BridgeWriteStatus(); EventSetTimer(5); return INIT_SUCCEEDED; } void OnDeinit(const int reason) { EventKillTimer(); LogEvent("deinit", "reason=" + IntegerToString(reason)); if(g_log != INVALID_HANDLE) FileClose(g_log); IndicatorRelease(hEmaFast15); IndicatorRelease(hEmaSlow15); IndicatorRelease(hEma20); IndicatorRelease(hRsi); IndicatorRelease(hAtr); } void OnTimer() { UpdateRiskAnchors(); if(!g_tester) { BridgeRead(); BridgeWriteStatus(); } if(g_tester || StringLen(g_token) == 0) return; datetime now = TimeGMT(); if(!g_activated && now - g_last_activate_try >= 60) Activate(); if(g_activated && now - g_last_licence_try >= 300) CheckLicence(); if(now - g_last_hb >= InpHeartbeatSeconds) SendHeartbeat(); } void OnTick() { UpdateRiskAnchors(); ManagePositions(); datetime bar0 = iTime(g_sym, PERIOD_M5, 0); if(bar0 == 0 || bar0 == g_last_bar) return; string why; if(!ValidateSpec(why)) { g_block_reason = why; if(ServerNow() - bar0 > 60) { g_last_bar = bar0; LogEvent("bar_skipped", why); } return; } SignalData s; EvaluateSignal(s); if(!s.ready) { if(ServerNow() - bar0 > 60) { g_last_bar = bar0; LogEvent("bar_skipped", s.detail); } return; } g_last_bar = bar0; if(s.dir == 0) { if(StringFind(s.detail, "vwap_unavailable") == 0) LogEvent("bar_skipped", s.detail); return; } LogEvent("signal", (s.dir > 0 ? "BUY " : "SELL ") + s.detail); double spread = SymbolInfoDouble(g_sym, SYMBOL_ASK) - SymbolInfoDouble(g_sym, SYMBOL_BID); double cap = MathMin(InpSpreadAtrFraction * s.atr, InpApprovedSpreadCap); if(!GeneralGates(why) || !DuplicateGate(s.bar, why)) { g_block_reason = why; LogEvent("entry_blocked", why); return; } if(spread > cap) { why = StringFormat("spread_gate %.3f > %.3f", spread, cap); g_block_reason = why; LogEvent("entry_blocked", why); return; } ExecuteTrade(s); } void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result) { if(trans.type != TRADE_TRANSACTION_DEAL_ADD) return; if(!HistoryDealSelect(trans.deal) || !IsOurDeal(trans.deal)) return; LogEvent("deal", StringFormat("deal=%I64u entry=%d type=%d vol=%.2f price=%.3f profit=%.2f commission=%.2f swap=%.2f fee=%.2f req_price=%.3f", trans.deal, (int)HistoryDealGetInteger(trans.deal, DEAL_ENTRY), (int)HistoryDealGetInteger(trans.deal, DEAL_TYPE), HistoryDealGetDouble(trans.deal, DEAL_VOLUME), HistoryDealGetDouble(trans.deal, DEAL_PRICE), HistoryDealGetDouble(trans.deal, DEAL_PROFIT), HistoryDealGetDouble(trans.deal, DEAL_COMMISSION), HistoryDealGetDouble(trans.deal, DEAL_SWAP), HistoryDealGetDouble(trans.deal, DEAL_FEE), request.price)); if(IsExitDeal(trans.deal)) { long pid = HistoryDealGetInteger(trans.deal, DEAL_POSITION_ID); GvSet("STREAK", ComputeLossStreak()); UpdateRiskAnchors(); SendClosedTrade(pid); } } //+------------------------------------------------------------------+