#property strict #property version "1.42" #property description "D2 MT5 thin command executor and telemetry bridge" #include input string BridgeBaseUrl="http://192.168.1.102:8765"; input string TelemetrySymbolsCsv=""; input int CycleMilliseconds=500; input bool EnableExecution=false; input bool EnableCalendarEventDiagnosticFallback=false; input int HistoryBackfillDays=90; input int HistoryBackfillChunkDays=7; input int HistoryBackfillCycleSeconds=90; input string IndexEconomyMap="US30:USD,DJ30:USD,SPX500:USD,US500:USD,NAS100:USD,USTEC:USD,US100:USD,DAX:EUR,DAX40:EUR,DE30:EUR,DE40:EUR,FTSE:GBP,FTSE100:GBP,UK100:GBP,NIKKEI:JPY,NIKKEI225:JPY,JP225:JPY"; CTrade trade; string LOCK_KEY="D2_MT5_BRIDGE_LOCK"; bool lock_owner=false; string symbols[]; int symbol_count=0; int phase=0; ulong last_history_push_msc=0,last_history_backfill_msc=0,last_calendar_push_msc=0,last_watchlist_poll_msc=0; const int WATCHLIST_POLL_SECONDS=45; string fallback_symbols=""; long watchlist_version=-1; string watchlist_source="startup_fallback_attached_chart",watchlist_configured_json="[]",watchlist_error_code="not_polled",watchlist_error_text="",watchlist_select_failures_json="[]";datetime watchlist_last_poll_at=0,watchlist_last_success_at=0;int watchlist_last_http_status=0;bool watchlist_parse_ok=false; const int HISTORY_LOOKBACK_SECONDS=7*24*60*60; const int HISTORY_PUSH_SECONDS=45; const int HISTORY_MAX_CLOSING_DEALS=100; datetime history_backfill_from=0,history_backfill_to=0,history_backfill_cursor=0;int history_backfill_chunks_total=0,history_backfill_chunks_done=0,history_backfill_deals_last_chunk=0;bool history_backfill_complete=false; const int CALENDAR_PAST_SECONDS=30*60; const int CALENDAR_FUTURE_SECONDS=48*60*60; const int CALENDAR_PUSH_SECONDS=60; const int CALENDAR_MAX_EVENTS=100; const int CALENDAR_MAX_DISCOVERED=600; const int CALENDAR_MAX_EVENT_IDS=16; const int CALENDAR_DISCOVERY_REQUESTS_PER_CYCLE=2; const int CALENDAR_VALUE_REQUESTS_PER_CYCLE=1; const int CALENDAR_WINDOW_REQUESTS_PER_CYCLE=2; const int CALENDAR_WINDOW_SECONDS=3*60*60; const int CALENDAR_SPLIT_WINDOW_SECONDS=90*60; const int CALENDAR_CACHE_STALE_SECONDS=2*60*60; const int CALENDAR_RETRY_BASE_SECONDS=120; struct CalendarEventState {MqlCalendarEvent event;string currency;string country;string country_code;int status;int attempts;datetime fetched_at;datetime retry_after;}; struct CalendarWindowState {string currency;string country_code;string mapping_source;datetime from;datetime to;int status;int attempts;datetime retry_after;bool split;}; CalendarEventState calendar_event_defs[];int calendar_query_cursor=0,calendar_discovery_cursor=0;datetime calendar_window_created_at=0,calendar_requested_start=0,calendar_requested_end=0; CalendarWindowState calendar_windows[];int calendar_window_cursor=0;datetime calendar_covered_until=0,calendar_window_build_start=0,calendar_window_build_end=0;string calendar_window_build_reason="not_built"; datetime calendar_discovery_cached_at=0;int calendar_zero_window_cycles=0;bool calendar_scheduler_stalled=false;string calendar_last_stall_reason=""; bool calendar_discovery_done[20],calendar_discovery_ok[20];int calendar_discovery_counts[20],calendar_discovery_errors[20]; string calendar_currencies[];int calendar_currency_count=0;string asset_context_json="{}",asset_contexts_json="[]"; int calendar_filtered_count=0;bool calendar_ranking_finalized=false; ulong calendar_value_ids[];ulong calendar_value_event_ids[];datetime calendar_value_times[];string calendar_value_json[]; int calendar_queries_ok=0,calendar_queries_timeout=0,calendar_queries_error=0; int calendar_window_calls_ok=0,calendar_window_calls_timeout=0,calendar_window_calls_error=0; string transport_boundary="not_attempted",transport_method="",transport_path="",connectivity_status="unknown";int transport_ret=0,transport_error=0,transport_elapsed_ms=0,transport_header_length=0,transport_body_length=0;datetime transport_observed_at=0; string JsonEscape(string s){StringReplace(s,"\\","\\\\");StringReplace(s,"\"","\\\"");StringReplace(s,"\r","\\r");StringReplace(s,"\n","\\n");return s;} string BoolJson(bool v){return v?"true":"false";} string SafeRequestPath(string path){int query=StringFind(path,"?");return StringSubstr(path,0,query<0?StringLen(path):query);} bool IsRealHttpResponse(int ret,int err){return err==0&&ret>=100&&ret<=599;} void RecordTransport(string method,string path,int ret,int err,ulong elapsed,string headers,string body){transport_method=method;transport_path=SafeRequestPath(path);transport_ret=ret;transport_error=err;transport_elapsed_ms=(int)MathMin((double)elapsed,2147483647.0);transport_header_length=StringLen(headers);transport_body_length=StringLen(body);transport_observed_at=TimeCurrent();bool real_http=IsRealHttpResponse(ret,err);transport_boundary=!real_http?"webrequest_internal_failure":((ret>=200&&ret<300)?"server_success":"http_non_2xx");if(method=="GET"&&StringFind(path,"/api/mt5/command?")==0)connectivity_status=!real_http?"request_failed":((ret>=200&&ret<300)?"connected":"server_non_2xx");string fields=StringFormat(" method=%s path=%s webrequest_ret=%d last_error=%d elapsed_ms=%I64u response_header_length=%d response_body_length=%d",method,transport_path,ret,err,elapsed,transport_header_length,transport_body_length);if(!real_http)Print("D2 webrequest_internal_failure",fields);else if(ret<200||ret>=300)Print("D2 http_non_2xx",fields," body_preview=",JsonEscape(StringSubstr(body,0,240)));} bool PerformWebRequest(string method,string path,string request_body,string &response,int &ret,int &err){char data[];if(method=="POST"){StringToCharArray(request_body,data,0,WHOLE_ARRAY,CP_UTF8);if(ArraySize(data)>0)ArrayResize(data,ArraySize(data)-1);}char result[];string response_headers="",request_headers=method=="POST"?"Content-Type: application/json\r\nConnection: close\r\n":"Accept: application/json\r\nConnection: close\r\n";ulong started=GetTickCount64();ResetLastError();ret=WebRequest(method,BridgeBaseUrl+path,request_headers,5000,data,result,response_headers);err=GetLastError();ulong elapsed=GetTickCount64()-started;response=CharArrayToString(result,0,-1,CP_UTF8);RecordTransport(method,path,ret,err,elapsed,response_headers,response);return IsRealHttpResponse(ret,err)&&ret>=200&&ret<300;} string PostJson(string path,string body){string response="";int ret=0,err=0;if(!PerformWebRequest("POST",path,body,response,ret,err))return "";return response;} string GetText(string path){string response="";int ret=0,err=0;if(!PerformWebRequest("GET",path,"",response,ret,err))return "";return response;} string GetWatchlistText(string path,int &http_status,int &error_code){string response="";if(!PerformWebRequest("GET",path,"",response,http_status,error_code))return "";return response;} void RecordJsonParseFailure(string method,string path,string reason){transport_boundary="json_parse_failure";transport_method=method;transport_path=SafeRequestPath(path);transport_observed_at=TimeCurrent();Print("D2 json_parse_failure method=",method," path=",transport_path," reason=",StringSubstr(reason,0,120));} string TransportDiagnosticJson(){return StringFormat("{\"boundary\":\"%s\",\"method\":\"%s\",\"path\":\"%s\",\"webrequest_ret\":%d,\"last_error\":%d,\"elapsed_ms\":%d,\"response_header_length\":%d,\"response_body_length\":%d,\"connectivity_status\":\"%s\",\"observed_at\":%I64d}",transport_boundary,transport_method,JsonEscape(transport_path),transport_ret,transport_error,transport_elapsed_ms,transport_header_length,transport_body_length,connectivity_status,(long)transport_observed_at);} string StringArrayJson(string &values[],int count); string ActiveSymbolsJson(){return StringArrayJson(symbols,symbol_count);} string WatchlistStatusJson(){return StringFormat("{\"source\":\"%s\",\"configured_symbols\":%s,\"active_symbols\":%s,\"count\":%d,\"last_poll_at\":%I64d,\"last_success_at\":%I64d,\"last_http_status\":%d,\"last_error_code\":\"%s\",\"last_error_text\":\"%s\",\"response_version\":%I64d,\"parse_ok\":%s,\"symbol_select_failures\":%s}",JsonEscape(watchlist_source),watchlist_configured_json,ActiveSymbolsJson(),symbol_count,(long)watchlist_last_poll_at,(long)watchlist_last_success_at,watchlist_last_http_status,JsonEscape(watchlist_error_code),JsonEscape(StringSubstr(watchlist_error_text,0,160)),watchlist_version,BoolJson(watchlist_parse_ok),watchlist_select_failures_json);} string TicksJson(){string out="[";bool first=true;for(int i=0;istart_seconds)return seconds>=start_seconds&&seconds=start_seconds||seconds='A'&&c<='Z')||(c>='0'&&c<='9'))out+=ShortToString(c);}return out;} void AddCalendarCurrency(string value){if(!IsFiat(value)||CurrencyAdded(calendar_currencies,calendar_currency_count,value)||calendar_currency_count>=20)return;ArrayResize(calendar_currencies,calendar_currency_count+1);calendar_currencies[calendar_currency_count++]=value;} string StringArrayJson(string &values[],int count){string out="[";for(int i=0;i0)out+=",";out+="\""+JsonEscape(values[i])+"\"";}return out+"]";} bool IndexCurrency(string normalized,string &name,string ¤cy){string entries[];int total=StringSplit(IndexEconomyMap,',',entries);for(int i=0;i0&&StringFind(normalized,key)>=0&&IsFiat(value)){name=key;currency=value;return true;}}return false;} void ParsePairFallback(string normalized,string &base,string "e){string assets[]={"USDT","USDC","USD","EUR","GBP","JPY","CHF","CAD","AUD","NZD","CNY","CNH","HKD","SGD","THB","XAU","XAG","BTC","ETH","SOL","XRP","ADA","DOGE","BNB","LTC"};for(int i=0;i=0){base=assets[i];quote=assets[j];return;}} void ResolveWatchlistContexts(){asset_contexts_json="[";for(int i=0;i0)asset_contexts_json+=",";asset_contexts_json+=StringFormat("{\"symbol\":\"%s\",\"base_asset\":\"%s\",\"quote_asset\":\"%s\"}",JsonEscape(s),JsonEscape(base),JsonEscape(quote));}asset_contexts_json+="]";} void ResolveAssetContext(){ArrayResize(calendar_currencies,0);calendar_currency_count=0;string symbol=_Symbol,normalized=NormalizedSymbol(symbol);string base=SymbolInfoString(symbol,SYMBOL_CURRENCY_BASE),quote=SymbolInfoString(symbol,SYMBOL_CURRENCY_PROFIT);StringToUpper(base);StringToUpper(quote);bool metadata=StringLen(base)>0&&StringLen(quote)>0;string source=metadata?"mt5_symbol_metadata":"normalized_symbol_fallback",asset_class="unknown",confidence="none",reason="unsupported_symbol_no_bounded_mapping";string tags[];int tag_count=0;if(!metadata)ParsePairFallback(normalized,base,quote);if(base=="XAU"||base=="XAG"){asset_class="metal";confidence=metadata?"high":"medium";reason=IsFiat(quote)?"":"metal_quote_currency_not_supported";AddCalendarCurrency(quote);ArrayResize(tags,2);tags[0]=base=="XAU"?"gold":"silver";tags[1]="metal";tag_count=2;}else if(IsCrypto(base)){asset_class="crypto";confidence=metadata&"e!="USDT"&"e!="USDC"?"high":"medium";reason="";ArrayResize(tags,3);tags[0]="crypto";tags[1]=base;tag_count=2;if(quote=="USDT"||quote=="USDC"){AddCalendarCurrency("USD");source+="_stablecoin_alias";tags[2]="stablecoin_quote_alias";tag_count=3;}else AddCalendarCurrency(quote);if(calendar_currency_count==0)reason="crypto_quote_currency_not_supported";}else if(IsFiat(base)&&IsFiat(quote)){asset_class="fx";confidence=metadata?"high":"medium";reason="";AddCalendarCurrency(base);AddCalendarCurrency(quote);ArrayResize(tags,1);tags[0]="fx";tag_count=1;}else{string index_name="",index_currency="";if(IndexCurrency(normalized,index_name,index_currency)){asset_class="index";base=index_name;quote=index_currency;source="configured_index_mapping";confidence="high";reason="";AddCalendarCurrency(index_currency);ArrayResize(tags,2);tags[0]="index";tags[1]=index_name;tag_count=2;}}asset_context_json=StringFormat("{\"symbol\":\"%s\",\"asset_class\":\"%s\",\"base_asset\":\"%s\",\"quote_asset\":\"%s\",\"calendar_currencies\":%s,\"context_tags\":%s,\"mapping_source\":\"%s\",\"mapping_confidence\":\"%s\",\"unsupported_reason\":%s}",JsonEscape(symbol),asset_class,JsonEscape(base),JsonEscape(quote),StringArrayJson(calendar_currencies,calendar_currency_count),StringArrayJson(tags,tag_count),source,confidence,StringLen(reason)>0?"\""+reason+"\"":"null");} string OptionalCalendarNumber(MqlCalendarValue &value,int field){bool available=false;double number=0;if(field==0){available=value.HasActualValue();if(available)number=value.GetActualValue();}else if(field==1){available=value.HasForecastValue();if(available)number=value.GetForecastValue();}else if(field==2){available=value.HasPreviousValue();if(available)number=value.GetPreviousValue();}else if(field==3){available=value.HasRevisedValue();if(available)number=value.GetRevisedValue();}return available?DoubleToString(number,8):"null";} string CalendarErrorText(int code){if(code==0)return "ok";if(code==5400)return "more_data";if(code==5401)return "timeout";if(code==5402)return "no_data";return "calendar_error";} string BoundedText(string value,int limit); bool CalendarEventKnown(ulong event_id){for(int i=0;i0&&p.day_of_week<6;} bool CalendarWindowPriorityBefore(CalendarWindowState &a,CalendarWindowState &b,datetime next_open){bool aw=CalendarWindowWeekday(a.from,a.to),bw=CalendarWindowWeekday(b.from,b.to);if(aw!=bw)return aw;long ad=(long)MathAbs((double)(a.from-next_open)),bd=(long)MathAbs((double)(b.from-next_open));if(ad!=bd)return adcalendar_window_build_start&&ArraySize(calendar_windows)==0)calendar_window_build_reason="no_windows_built";} void ResetCalendarDiscovery(datetime now){calendar_discovery_cursor=0;calendar_query_cursor=0;calendar_discovery_cached_at=now;calendar_queries_ok=0;calendar_queries_timeout=0;calendar_queries_error=0;calendar_filtered_count=0;calendar_ranking_finalized=false;ArrayResize(calendar_event_defs,0);for(int i=0;i<20;i++){calendar_discovery_done[i]=false;calendar_discovery_ok[i]=false;calendar_discovery_counts[i]=0;calendar_discovery_errors[i]=0;}} void ResetCalendarWindow(datetime now){ResolveAssetContext();ResolveWatchlistContexts();calendar_window_created_at=now;calendar_requested_start=now-CALENDAR_PAST_SECONDS;calendar_requested_end=now+CALENDAR_FUTURE_SECONDS;calendar_window_calls_ok=0;calendar_window_calls_timeout=0;calendar_window_calls_error=0;calendar_covered_until=0;calendar_zero_window_cycles=0;calendar_scheduler_stalled=false;calendar_last_stall_reason="";ArrayResize(calendar_value_ids,0);ArrayResize(calendar_value_event_ids,0);ArrayResize(calendar_value_times,0);ArrayResize(calendar_value_json,0);BuildCalendarWindows(now);} void InitializeCalendar(datetime now){ResetCalendarWindow(now);ResetCalendarDiscovery(now);} bool RelevantCalendarEvent(MqlCalendarEvent &event){int importance=(int)event.importance;if(importance<2)return false;string text=event.name+" "+event.event_code;StringToLower(text);string noise[]={"holiday","tentative","all day","auction","bond sale","bill sale","reserve asset","vehicle registration"};for(int i=0;i=0)return false;string macro[]={"interest rate","rate decision","central bank","ecb","fed ","fomc","speech","speaks","testimony","inflation","cpi","ppi","gdp","gross domestic","employment","unemployment","payroll","jobless","labor","labour","wage","earnings","pmi","purchasing manager","retail sales","industrial production","consumer confidence","business confidence","trade balance","current account","durable goods","housing","manufacturing","services"};for(int i=0;i=0)return true;return false;} void AddDiscoveredEvent(MqlCalendarEvent &event,string currency){if(ArraySize(calendar_event_defs)>=CALENDAR_MAX_DISCOVERED||CalendarEventKnown(event.id)||!RelevantCalendarEvent(event))return;calendar_filtered_count++;MqlCalendarCountry country;string country_name="",country_code="";if(CalendarCountryById(event.country_id,country)){country_name=BoundedText(country.name,80);country_code=BoundedText(country.code,16);}int n=ArraySize(calendar_event_defs);ArrayResize(calendar_event_defs,n+1);calendar_event_defs[n].event=event;calendar_event_defs[n].currency=BoundedText(currency,8);calendar_event_defs[n].country=country_name;calendar_event_defs[n].country_code=country_code;calendar_event_defs[n].status=0;calendar_event_defs[n].attempts=0;calendar_event_defs[n].fetched_at=0;calendar_event_defs[n].retry_after=0;} bool EventRanksBefore(CalendarEventState &a,CalendarEventState &b){int ai=(int)a.event.importance,bi=(int)b.event.importance;if(ai!=bi)return ai>bi;string an=a.event.name,bn=b.event.name;StringToLower(an);StringToLower(bn);int cmp=StringCompare(an,bn);if(cmp!=0)return cmp<0;return a.event.id=0?0:err;if(total<0){calendar_discovery_ok[currency_index]=false;return;}calendar_discovery_ok[currency_index]=true;calendar_discovery_counts[currency_index]=MathMin(total,received);for(int i=0;icalendar_requested_end||CalendarValueKnown(value.id,value.event_id,value.time))continue;int n=ArraySize(calendar_value_ids);ArrayResize(calendar_value_ids,n+1);ArrayResize(calendar_value_event_ids,n+1);ArrayResize(calendar_value_times,n+1);ArrayResize(calendar_value_json,n+1);calendar_value_ids[n]=value.id;calendar_value_event_ids[n]=value.event_id;calendar_value_times[n]=value.time;calendar_value_json[n]=StringFormat("{\"value_id\":%I64u,\"event_id\":%I64u,\"time\":%I64d,\"country\":\"%s\",\"country_code\":\"%s\",\"currency\":\"%s\",\"name\":\"%s\",\"code\":\"%s\",\"importance\":%d,\"impact\":%d,\"actual\":%s,\"forecast\":%s,\"previous\":%s,\"revised\":%s,\"unit\":\"%d\"}",value.id,value.event_id,(long)value.time,JsonEscape(meta.country),JsonEscape(meta.country_code),JsonEscape(meta.currency),BoundedText(meta.event.name,160),BoundedText(meta.event.event_code,40),(int)meta.event.importance,(int)value.impact_type,OptionalCalendarNumber(value,0),OptionalCalendarNumber(value,1),OptionalCalendarNumber(value,2),OptionalCalendarNumber(value,3),(int)meta.event.unit);}} int CalendarEventIndex(ulong event_id){for(int i=0;icalendar_requested_end||CalendarValueKnown(value.id,value.event_id,value.time)||ArraySize(calendar_value_ids)>=CALENDAR_MAX_EVENTS)return;int n=ArraySize(calendar_value_ids);ArrayResize(calendar_value_ids,n+1);ArrayResize(calendar_value_event_ids,n+1);ArrayResize(calendar_value_times,n+1);ArrayResize(calendar_value_json,n+1);calendar_value_ids[n]=value.id;calendar_value_event_ids[n]=value.event_id;calendar_value_times[n]=value.time;calendar_value_json[n]=StringFormat("{\"value_id\":%I64u,\"event_id\":%I64u,\"time\":%I64d,\"country\":\"%s\",\"country_code\":\"%s\",\"currency\":\"%s\",\"name\":\"%s\",\"code\":\"%s\",\"importance\":%d,\"impact\":%d,\"actual\":%s,\"forecast\":%s,\"previous\":%s,\"revised\":%s,\"unit\":\"%d\"}",value.id,value.event_id,(long)value.time,JsonEscape(meta.country),JsonEscape(meta.country_code),JsonEscape(meta.currency),BoundedText(meta.event.name,160),BoundedText(meta.event.event_code,40),(int)meta.event.importance,(int)value.impact_type,OptionalCalendarNumber(value,0),OptionalCalendarNumber(value,1),OptionalCalendarNumber(value,2),OptionalCalendarNumber(value,3),(int)meta.event.unit);} void CacheWindowValues(MqlCalendarValue &values[],int usable){for(int i=0;i=0)CacheWindowValue(idx,values[i]);}} int NextWindowQuery(datetime now){int count=ArraySize(calendar_windows);for(int scanned=0;scanned=ArraySize(calendar_windows))return "null";return StringFormat("{\"country_code\":\"%s\",\"currency\":\"%s\",\"from\":%I64d,\"to\":%I64d}",calendar_windows[idx].country_code,calendar_windows[idx].currency,(long)calendar_windows[idx].from,(long)calendar_windows[idx].to);} void SplitTimedOutWindow(int idx){if(calendar_windows[idx].split)return;datetime middle=calendar_windows[idx].from+CALENDAR_SPLIT_WINDOW_SECONDS,retry=calendar_windows[idx].retry_after;calendar_windows[idx].status=4;AddCalendarWindow(calendar_windows[idx].currency,calendar_windows[idx].country_code,calendar_windows[idx].mapping_source,calendar_windows[idx].from,middle,true);calendar_windows[ArraySize(calendar_windows)-1].retry_after=retry;AddCalendarWindow(calendar_windows[idx].currency,calendar_windows[idx].country_code,calendar_windows[idx].mapping_source,middle,calendar_windows[idx].to,true);calendar_windows[ArraySize(calendar_windows)-1].retry_after=retry;} void QueryCalendarWindow(int idx,datetime now,string &queries,bool &first){MqlCalendarValue values[];ResetLastError();int total=CalendarValueHistory(values,calendar_windows[idx].from,calendar_windows[idx].to,calendar_windows[idx].country_code,calendar_windows[idx].currency);int err=GetLastError(),received=ArraySize(values);string classification="error";calendar_windows[idx].attempts++;if(total>=0||(err==5400&&received>0)){int usable=total>=0?MathMin(total,received):received;CacheWindowValues(values,usable);calendar_windows[idx].status=1;calendar_windows[idx].retry_after=0;calendar_window_calls_ok++;classification="success";}else if(err==5401){calendar_windows[idx].status=2;calendar_windows[idx].retry_after=now+CALENDAR_RETRY_BASE_SECONDS*(1<=0||(err==5400&&received>0)){int usable=total>=0?MathMin(total,received):received;CacheEventValues(idx,values,usable);calendar_event_defs[idx].status=1;calendar_event_defs[idx].fetched_at=now;calendar_event_defs[idx].retry_after=0;calendar_queries_ok++;classification="success";}else if(err==5401){calendar_event_defs[idx].status=2;int shift=MathMin(calendar_event_defs[idx].attempts-1,4);calendar_event_defs[idx].retry_after=now+CALENDAR_RETRY_BASE_SECONDS*(1<0)out+=",";out+=calendar_value_json[best];used[best]=true;emitted++;}return out+"]";} string TopFilteredEventsJson(){string out="[";int count=MathMin(ArraySize(calendar_event_defs),8);for(int i=0;i0)out+=",";out+=StringFormat("{\"name\":\"%s\",\"importance\":%d}",BoundedText(calendar_event_defs[i].event.name,160),(int)calendar_event_defs[i].event.importance);}return out+"]";} string CurrencyMetricJson(int &values[]){string out="{";for(int i=0;i0)out+=",";out+=StringFormat("\"%s\":%d",calendar_currencies[i],values[i]);}return out+"}";} string CalendarPairsJson(){string out="[";for(int i=0;i0)out+=",";out+=StringFormat("{\"country_code\":\"%s\",\"currency\":\"%s\",\"mapping_source\":\"%s\"}",code,calendar_currencies[i],source);}return out+"]";} string CalendarWindowsJson(int wanted_status){string out="[";bool first=true;for(int i=0;icalendar_discovery_cached_at+CALENDAR_CACHE_STALE_SECONDS)ResetCalendarDiscovery(now);int discovery_requests=0;while(calendar_discovery_cursor=calendar_currency_count&&!calendar_ranking_finalized)FinalizeCalendarRanking();string queries="[";bool first=true;int cursor_start=calendar_window_cursor,window_requests=0;while(window_requests=3&&window_requests==0&&calendar_ranking_finalized){int idx=NextEventQuery(now);if(idx>=0)QueryEventValues(idx,now,queries,first);}queries+="]";int discovered=0,successful=0,timed_out=0,pending=0,window_count_total=ArraySize(calendar_windows);bool discovery_ok=calendar_currency_count>0,discovery_timeout=false;for(int c=0;ccalendar_window_build_start,no_windows_error=non_empty_interval&&window_count_total==0,valid_work=calendar_currency_count>0&&pending>0;if(valid_work&&window_requests==0){calendar_zero_window_cycles++;calendar_last_stall_reason=next_idx<0?"all_pending_windows_in_backoff":"eligible_window_not_executed";}else{calendar_zero_window_cycles=0;calendar_last_stall_reason=no_windows_error?"no_windows_built":"";}calendar_scheduler_stalled=no_windows_error||calendar_zero_window_cycles>2;bool complete=window_count_total>0&&discovery_ok&&pending==0&&calendar_covered_until>=calendar_requested_end;string scheduler_state=no_windows_error?"scheduler_error":(!valid_work?(complete?"complete":"idle"):(next_idx<0?"backoff":(window_requests>0?"ready":"error")));string state=no_windows_error?"no_windows_built":(calendar_currency_count==0?"unsupported_asset":(calendar_discovery_cursor0?"window_partial_timeout":"loading_windows")))));int emitted=ArraySize(calendar_value_json),considered=ArraySize(calendar_event_defs),requests=discovery_requests+window_requests;string covered=calendar_covered_until>0?IntegerToString((long)calendar_covered_until):"null";return StringFormat("{\"source\":\"mt5_country_currency_windowed_v135\",\"retrieval_mode\":\"country_currency_windowed\",\"asset_context\":%s,\"observed_at\":%I64d,\"window_start\":%I64d,\"window_end\":%I64d,\"covered_until\":%s,\"window_count_total\":%d,\"window_build_reason\":\"%s\",\"window_build_start\":%I64d,\"window_build_end\":%I64d,\"queried_currencies\":%s,\"country_currency_pairs\":%s,\"discovery_ok\":%s,\"discovery_counts\":%s,\"discovery_errors\":%s,\"discovered_count\":%d,\"filtered_count\":%d,\"considered_count\":%d,\"window_calls_ok\":%d,\"window_calls_timeout\":%d,\"window_calls_error\":%d,\"successful_windows\":%s,\"timed_out_windows\":%s,\"emitted_event_count\":%d,\"requests_this_cycle\":%d,\"discovery_requests_this_cycle\":%d,\"window_requests_this_cycle\":%d,\"window_cursor_index\":%d,\"window_cursor_start\":%d,\"window_cursor_end\":%d,\"next_window_pair\":%s,\"window_scheduler_state\":\"%s\",\"scheduler_stalled\":%s,\"last_stall_reason\":%s,\"partial_coverage\":%s,\"query_ok\":%s,\"query_state\":\"%s\",\"top_filtered_events\":%s,\"queries\":%s,\"events\":%s}",asset_context_json,(long)now,(long)calendar_requested_start,(long)calendar_requested_end,covered,window_count_total,JsonEscape(calendar_window_build_reason),(long)calendar_window_build_start,(long)calendar_window_build_end,StringArrayJson(calendar_currencies,calendar_currency_count),CalendarPairsJson(),BoolJson(discovery_ok),CurrencyMetricJson(calendar_discovery_counts),CurrencyMetricJson(calendar_discovery_errors),discovered,calendar_filtered_count,considered,calendar_window_calls_ok,calendar_window_calls_timeout,calendar_window_calls_error,CalendarWindowsJson(1),CalendarWindowsJson(2),emitted,requests,discovery_requests,window_requests,calendar_window_cursor,cursor_start,calendar_window_cursor,WindowPairJson(next_idx),scheduler_state,BoolJson(calendar_scheduler_stalled),StringLen(calendar_last_stall_reason)>0?"\""+calendar_last_stall_reason+"\"":"null",BoolJson(!complete),BoolJson(complete),state,TopFilteredEventsJson(),queries,SortedCalendarValues());} void PushCalendar(){string account=VersionedAccountJson(),calendar=CalendarJson();StringReplace(calendar,"\"observed_at\":","\"asset_contexts\":"+asset_contexts_json+",\"observed_at\":");string body=StringSubstr(account,0,StringLen(account)-1)+",\"calendar\":"+calendar+"}";PostJson("/api/mt5/telemetry",body);} string BoundedText(string value,int limit){return JsonEscape(StringSubstr(value,0,limit));} bool FindPositionEntry(ulong position_id,int total,double &entry_price,long &open_time,long &open_time_msc){ bool found=false;long earliest=0; for(int i=0;i=0&&added0&&FindPositionEntry(position_id,total,entry_price,open_time,open_time_msc); string item=StringFormat("{\"event\":\"closed\",\"deal\":%I64u,\"order\":%I64u,\"position_id\":%I64u,\"ticket\":%I64u,\"symbol\":\"%s\",\"side\":\"%s\",\"volume\":%.8f,\"price\":%.10f,\"profit\":%.8f,\"commission\":%.8f,\"swap\":%.8f,\"fee\":%.8f,\"time\":%I64d,\"time_msc\":%I64d,\"deal_type\":%d,\"entry\":%d,\"reason_code\":%d,\"reason\":%d,\"comment\":\"%s\"",deal,(ulong)HistoryDealGetInteger(deal,DEAL_ORDER),position_id,position_id,BoundedText(HistoryDealGetString(deal,DEAL_SYMBOL),64),deal_type==DEAL_TYPE_SELL?"buy":"sell",HistoryDealGetDouble(deal,DEAL_VOLUME),HistoryDealGetDouble(deal,DEAL_PRICE),HistoryDealGetDouble(deal,DEAL_PROFIT),HistoryDealGetDouble(deal,DEAL_COMMISSION),HistoryDealGetDouble(deal,DEAL_SWAP),HistoryDealGetDouble(deal,DEAL_FEE),HistoryDealGetInteger(deal,DEAL_TIME),HistoryDealGetInteger(deal,DEAL_TIME_MSC),(int)deal_type,(int)entry,(int)HistoryDealGetInteger(deal,DEAL_REASON),BoundedText(HistoryDealGetString(deal,DEAL_COMMENT),120)); if(has_entry)item+=StringFormat(",\"entry_price\":%.10f,\"open_time\":%I64d,\"open_time_msc\":%I64d",entry_price,open_time,open_time_msc); item+="}";if(!first)out+=",";first=false;out+=item;added++; } return out+"]"; } string HistoricalDealsJson(){int added=0;datetime to=TimeCurrent();return HistoricalDealsRangeJson(to-HISTORY_LOOKBACK_SECONDS,to,added);} void PushHistoricalDeals(){string account=VersionedAccountJson();string body=StringSubstr(account,0,StringLen(account)-1)+",\"historical_deals\":"+HistoricalDealsJson()+"}";PostJson("/api/mt5/telemetry",body);} void InitializeHistoryBackfill(){datetime now=TimeCurrent();int days=MathMax(1,MathMin(HistoryBackfillDays,365)),chunk_days=MathMax(1,MathMin(HistoryBackfillChunkDays,30));history_backfill_to=now;history_backfill_from=now-days*24*60*60;history_backfill_cursor=history_backfill_from;history_backfill_chunks_total=(days+chunk_days-1)/chunk_days;history_backfill_chunks_done=0;history_backfill_deals_last_chunk=0;history_backfill_complete=false;} string BackfillDiagnosticFields(){return StringFormat(",\"backfill_from\":%I64d,\"backfill_to\":%I64d,\"chunks_total\":%d,\"chunks_done\":%d,\"deals_sent_last_chunk\":%d,\"backfill_complete\":%s",(long)history_backfill_from,(long)history_backfill_to,history_backfill_chunks_total,history_backfill_chunks_done,history_backfill_deals_last_chunk,BoolJson(history_backfill_complete));} void PushHistoryBackfillChunk(){if(history_backfill_complete)return;int chunk_days=MathMax(1,MathMin(HistoryBackfillChunkDays,30));datetime from=history_backfill_cursor,to=(datetime)MathMin(history_backfill_to,from+chunk_days*24*60*60);int added=0;string deals=HistoricalDealsRangeJson(from,to,added);datetime previous_cursor=history_backfill_cursor;int previous_done=history_backfill_chunks_done,previous_sent=history_backfill_deals_last_chunk;bool previous_complete=history_backfill_complete;history_backfill_deals_last_chunk=added;history_backfill_chunks_done++;history_backfill_cursor=to;history_backfill_complete=history_backfill_cursor>=history_backfill_to;string account=VersionedAccountJson();string body=StringSubstr(account,0,StringLen(account)-1)+BackfillDiagnosticFields()+",\"historical_deals\":"+deals+"}",response="";int ret=0,err=0;if(!PerformWebRequest("POST","/api/mt5/telemetry",body,response,ret,err)){history_backfill_cursor=previous_cursor;history_backfill_chunks_done=previous_done;history_backfill_deals_last_chunk=previous_sent;history_backfill_complete=previous_complete;}} void PushSnapshot(){string body=StringFormat("{\"login\":\"%I64d\",\"positions_total\":%d,\"orders_total\":%d,\"symbols_total\":%d,\"terminal_connected\":%s,\"terminal_trade_allowed\":%s,\"mql_trade_allowed\":%s,\"build\":%I64d}",AccountInfoInteger(ACCOUNT_LOGIN),PositionsTotal(),OrdersTotal(),SymbolsTotal(true),BoolJson((bool)TerminalInfoInteger(TERMINAL_CONNECTED)),BoolJson((bool)TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)),BoolJson((bool)MQLInfoInteger(MQL_TRADE_ALLOWED)),TerminalInfoInteger(TERMINAL_BUILD));PostJson("/api/mt5/snapshot",body);} string JsonString(string json,string key){string mark="\""+key+"\":\"";int a=StringFind(json,mark);if(a<0)return "";a+=StringLen(mark);int b=StringFind(json,"\"",a);return b<0?"":StringSubstr(json,a,b-a);} double JsonNumber(string json,string key){string mark="\""+key+"\":";int a=StringFind(json,mark);if(a<0)return 0;a+=StringLen(mark);return StringToDouble(StringSubstr(json,a));} long JsonInteger(string json,string key){return (long)JsonNumber(json,key);} bool ApplyWatchlistJson(string json){int key=StringFind(json,"\"symbols\":[");if(key<0){watchlist_error_code="parse_failure";watchlist_error_text="symbols array missing";return false;}long version=JsonInteger(json,"version");int a=StringFind(json,"[",key),b=StringFind(json,"]",a);if(a<0||b<=a){watchlist_error_code="parse_failure";watchlist_error_text="symbols array malformed";return false;}string body=StringSubstr(json,a+1,b-a-1);string configured_body=body;StringReplace(body,"\"","");string proposed[];int proposed_count=StringLen(body)==0?0:StringSplit(body,',',proposed);if(proposed_count>20){watchlist_error_code="parse_failure";watchlist_error_text="symbol limit exceeded";return false;}watchlist_configured_json="["+configured_body+"]";string selected[],failed[];int selected_count=0,failed_count=0;if(proposed_count==0){proposed_count=StringSplit(fallback_symbols,',',proposed);watchlist_source=StringLen(TelemetrySymbolsCsv)>0?"server_empty_fallback_telemetry_symbols":"server_empty_fallback_attached_chart";}else watchlist_source="server";for(int i=0;i0&&SymbolSelect(proposed[i],true)){ArrayResize(selected,selected_count+1);selected[selected_count++]=proposed[i];}else{ArrayResize(failed,failed_count+1);failed[failed_count++]=proposed[i];}}watchlist_select_failures_json=StringArrayJson(failed,failed_count);if(selected_count<=0){watchlist_error_code="symbol_select_failure";watchlist_error_text="no configured or fallback symbol selectable";return false;}ArrayResize(symbols,selected_count);for(int i=0;i0?"partial_symbol_select_failure":"";watchlist_error_text=failed_count>0?"one or more symbols unavailable at broker":"";if(failed_count>0)watchlist_source+="_partial";ResetCalendarWindow(TimeCurrent());return true;} void PollWatchlist(){watchlist_last_poll_at=TimeCurrent();watchlist_parse_ok=false;watchlist_select_failures_json="[]";string path="/api/mt5/watchlist?login="+(string)AccountInfoInteger(ACCOUNT_LOGIN);int http_status=0,error_code=0;string response=GetWatchlistText(path,http_status,error_code);watchlist_last_http_status=IsRealHttpResponse(http_status,error_code)?http_status:0;if(!IsRealHttpResponse(http_status,error_code)||http_status<200||http_status>=300){watchlist_source=watchlist_last_success_at>0?"http_failure_last_known_good":(StringLen(TelemetrySymbolsCsv)>0?"http_failure_fallback_telemetry_symbols":"http_failure_fallback_attached_chart");watchlist_error_code=!IsRealHttpResponse(http_status,error_code)?"webrequest_internal_failure":"http_failure";watchlist_error_text=!IsRealHttpResponse(http_status,error_code)?"WebRequest failed in MT5 ("+(string)error_code+")":"Server HTTP status "+(string)http_status;return;}if(!ApplyWatchlistJson(response)){RecordJsonParseFailure("GET",path,watchlist_error_text);watchlist_source=watchlist_last_success_at>0?"parse_failure_last_known_good":(StringLen(TelemetrySymbolsCsv)>0?"parse_failure_fallback_telemetry_symbols":"parse_failure_fallback_attached_chart");}} void Ack(long id,bool ok,string message){ulong order=trade.ResultOrder();ulong deal=trade.ResultDeal();ulong position_id=0;if(deal>0&&HistoryDealSelect(deal))position_id=(ulong)HistoryDealGetInteger(deal,DEAL_POSITION_ID);string body=StringFormat("{\"ok\":%s,\"message\":\"%s\",\"order\":%I64u,\"deal\":%I64u,\"position_id\":%I64u,\"retcode\":%u}",BoolJson(ok),JsonEscape(message),order,deal,position_id,trade.ResultRetcode());PostJson("/api/mt5/command/"+(string)id+"/ack",body);} void PollCommand(){string path="/api/mt5/command?login="+(string)AccountInfoInteger(ACCOUNT_LOGIN);string response=GetText(path);if(StringLen(response)==0){if(transport_boundary=="server_success")RecordJsonParseFailure("GET",path,"empty command response");return;}if(StringFind(response,"\"command\":\"none\"")>=0)return;long id=JsonInteger(response,"id");if(id<=0){RecordJsonParseFailure("GET",path,"command id missing");return;}if(!EnableExecution){Ack(id,false,"execution disabled");return;}string command=JsonString(response,"command"),symbol=JsonString(response,"symbol"),side=JsonString(response,"side");double volume=JsonNumber(response,"volume");ulong ticket=(ulong)JsonInteger(response,"ticket");bool ok=false;if(command=="open"&&StringLen(symbol)>0&&volume>0&&(side=="buy"||side=="sell")){ok=side=="buy"?trade.Buy(volume,symbol):trade.Sell(volume,symbol);}else if(command=="close"&&(ticket>0||StringLen(symbol)>0)){ok=ticket>0?trade.PositionClose(ticket):trade.PositionClose(symbol);}else{Ack(id,false,"malformed command");return;}Ack(id,ok,ok?command+" executed":trade.ResultRetcodeDescription());} bool AcquireLock(){double now=(double)TimeLocal();if(GlobalVariableCheck(LOCK_KEY)&&now-GlobalVariableGet(LOCK_KEY)<10.0)return false;GlobalVariableSet(LOCK_KEY,now);lock_owner=true;return true;} void RefreshLock(){if(lock_owner)GlobalVariableSet(LOCK_KEY,(double)TimeLocal());} int OnInit(){if(CycleMilliseconds<250||HistoryBackfillDays<1||HistoryBackfillChunkDays<1||HistoryBackfillCycleSeconds<60)return INIT_PARAMETERS_INCORRECT;if(!AcquireLock()){Print("D2 bridge already active on another chart; remove duplicate EA instance.");return INIT_FAILED;}fallback_symbols=TelemetrySymbolsCsv;if(StringLen(fallback_symbols)==0)fallback_symbols=_Symbol;symbol_count=StringSplit(fallback_symbols,',',symbols);for(int i=0;i