//+------------------------------------------------------------------+ //| MT5_Economic_Calendar_Live_Exporter.mq5 | //| Exports MT5 Economic Calendar events to a bot-compatible CSV. | //| | //| The destination is one ordinary, editable Windows full path. | //| Copying outside the MQL5 file sandbox uses Windows CopyFileW, | //| so "Allow DLL imports" must be enabled for this EA. | //+------------------------------------------------------------------+ #property strict #property version "2.01" #property description "Exports a rolling 21-day MT5 calendar to timetable.csv every four hours" #import "kernel32.dll" int CopyFileW(string existing_file,string new_file,int fail_if_exists); int MoveFileExW(string existing_file,string new_file,uint flags); int DeleteFileW(string file_name); uint GetLastError(); #import #define MOVEFILE_REPLACE_EXISTING 0x00000001 #define MOVEFILE_WRITE_THROUGH 0x00000008 //--- The only export-location field. Type a complete Windows path including //--- the CSV filename. In the EA Inputs window, use ordinary single slashes: //--- C:\Users\3dstudio\Desktop\Backup-2\XAU\timetable.csv input string InpTimetableFullPath = "C:\\Users\\3dstudio\\Desktop\\Backup-2\\XAU\\timetable.csv"; input bool InpWriteDetailedFile = false; //--- Calendar selection. // Multiple currencies are separated by semicolons. input string InpCurrencies = "USD;EUR"; input ENUM_CALENDAR_EVENT_IMPORTANCE InpMinimumImportance = CALENDAR_IMPORTANCE_HIGH; input bool InpOnlyExactTimeEvents = true; input int InpDaysAhead = 21; //--- Optional event-name filtering (case-insensitive, semicolon-separated). // Blank IncludeKeywords means include every event passing the other filters. input string InpIncludeKeywords = ""; input string InpExcludeKeywords = ""; //--- Live-trading default: exported rows are enabled immediately. //--- Set this to false only when intentionally producing a preview timetable. input bool InpRowsEnabled = true; //--- A successful export is followed by the next export four hours later. //--- A failed export retries after five minutes without replacing the old CSV. input int InpRefreshHours = 4; input int InpRetryMinutes = 5; input int InpQueryChunkDays = 7; input int InpRequestRetries = 4; input int InpRetryDelayMilliseconds = 1250; input bool InpRequireValuesForEveryCurrency = true; //--- Calendar timestamps use trade-server time. 999999 enables RoboForex //--- EET/EEST conversion for each future event (UTC+2 winter, UTC+3 summer). //--- A numeric value forces one fixed offset for every event. input int InpManualServerUtcOffsetMinutes = 999999; input int InpWinterServerUtcOffsetMinutes = 120; input int InpSummerServerUtcOffsetMinutes = 180; struct ExportRow { datetime utc_minute; datetime server_minute; string names; string source_event_ids; string event_codes; string source_urls; string currencies; int max_importance; }; ExportRow g_rows[]; long g_server_utc_offset_seconds = 0; ulong g_next_export_tick = 0; bool g_export_running = false; string g_timetable_full_path = ""; string g_detailed_full_path = ""; const string STAGING_FOLDER = "__MT5_Calendar_Exporter"; const string STAGING_TIMETABLE = "timetable_export.csv"; const string STAGING_DETAILED = "mt5_calendar_detailed.csv"; const string DETAILED_OUTPUT_NAME = "mt5_calendar_detailed.csv"; //+------------------------------------------------------------------+ //| String helpers | //+------------------------------------------------------------------+ string TrimCopy(string value) { StringTrimLeft(value); StringTrimRight(value); return value; } string LowerCopy(string value) { StringToLower(value); return value; } string CleanCsvText(string value) { StringReplace(value,"\r"," "); StringReplace(value,"\n"," "); StringReplace(value,",",";"); value=TrimCopy(value); return value; } bool IsBlank(const string value) { return StringLen(TrimCopy(value))==0; } bool MatchesAnyKeyword(const string text,const string keyword_list) { string cleaned=TrimCopy(keyword_list); if(StringLen(cleaned)==0) return false; string parts[]; ushort separator=(ushort)StringGetCharacter(";",0); int count=StringSplit(cleaned,separator,parts); string haystack=LowerCopy(text); for(int i=0;i0 && StringFind(haystack,needle)>=0) return true; } return false; } bool PassesNameFilter(const string event_name,const string event_code) { string searchable=event_name+" "+event_code; if(!IsBlank(InpIncludeKeywords) && !MatchesAnyKeyword(searchable,InpIncludeKeywords)) return false; if(!IsBlank(InpExcludeKeywords) && MatchesAnyKeyword(searchable,InpExcludeKeywords)) return false; return true; } void AppendUnique(string &target,const string value,const string delimiter) { string cleaned=CleanCsvText(value); if(StringLen(cleaned)==0) return; string wrapped_delimiter=delimiter; if(StringLen(target)==0) { target=cleaned; return; } // Exact token check using the same delimiter prevents duplicate names/IDs. string probe=wrapped_delimiter+target+wrapped_delimiter; string token=wrapped_delimiter+cleaned+wrapped_delimiter; if(StringFind(probe,token)<0) target+=wrapped_delimiter+cleaned; } //+------------------------------------------------------------------+ //| Path helpers | //+------------------------------------------------------------------+ string NormalizeAbsolutePath(string value) { value=TrimCopy(value); StringReplace(value,"/","\\"); if(StringLen(value)>=2 && StringSubstr(value,0,1)=="\"" && StringSubstr(value,StringLen(value)-1,1)=="\"") value=StringSubstr(value,1,StringLen(value)-2); // Keep drive roots such as C:\ intact; remove only superfluous trailing slashes. while(StringLen(value)>3 && StringSubstr(value,StringLen(value)-1,1)=="\\") value=StringSubstr(value,0,StringLen(value)-1); return value; } bool IsAbsoluteWindowsFilePath(const string value) { int length=StringLen(value); if(length>=3 && StringSubstr(value,1,1)==":" && StringSubstr(value,2,1)=="\\") return true; if(length>=2 && StringSubstr(value,0,2)=="\\\\") return true; // UNC path return false; } string ParentFolder(const string full_path) { for(int i=StringLen(full_path)-1;i>=0;i--) if(StringSubstr(full_path,i,1)=="\\") return StringSubstr(full_path,0,i); return ""; } string JoinWindowsPath(string folder,const string name) { folder=NormalizeAbsolutePath(folder); if(StringLen(folder)==0) return name; return folder+"\\"+name; } string StagingRelativePath(const string file_name) { return STAGING_FOLDER+"\\"+file_name; } string StagingFullPath(const string relative_path) { return TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Files\\"+relative_path; } bool PublishToAbsolutePath(const string staging_relative,const string target_full_path) { string source_full_path=StagingFullPath(staging_relative); string staged_target=target_full_path+".new"; // A previous interrupted attempt may have left a .new file. DeleteFileW(staged_target); ResetLastError(); int copied=CopyFileW(source_full_path,staged_target,0); // overwrite .new if(copied!=1) { uint windows_error=kernel32::GetLastError(); PrintFormat("CopyFileW failed: %s -> %s; Windows error=%u. " "Confirm that the destination folder exists and is writable.", source_full_path,staged_target,windows_error); DeleteFileW(staged_target); return false; } uint move_flags=MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH; for(int attempt=1;attempt<=20;attempt++) { ResetLastError(); if(MoveFileExW(staged_target,target_full_path,move_flags)!=0) return true; if(attempt<20) Sleep(250); } uint move_error=kernel32::GetLastError(); PrintFormat("MoveFileExW failed after retries: %s -> %s; Windows error=%u. " "The previous timetable.csv remains unchanged.", staged_target,target_full_path,move_error); DeleteFileW(staged_target); return false; } //+------------------------------------------------------------------+ //| Time helpers | //+------------------------------------------------------------------+ bool IsLeapYear(const int year) { if(year%400==0) return true; if(year%100==0) return false; return year%4==0; } int DaysInMonth(const int year,const int month) { if(month==2) return IsLeapYear(year) ? 29 : 28; if(month==4 || month==6 || month==9 || month==11) return 30; return 31; } int LastSundayDay(const int year,const int month) { MqlDateTime dt; ZeroMemory(dt); dt.year=year; dt.mon=month; dt.day=DaysInMonth(year,month); dt.hour=12; datetime last_day=StructToTime(dt); TimeToStruct(last_day,dt); return DaysInMonth(year,month)-dt.day_of_week; } long DetectServerUtcOffsetSeconds() { if(InpManualServerUtcOffsetMinutes!=999999) return (long)InpManualServerUtcOffsetMinutes*60; datetime server_now=TimeTradeServer(); datetime utc_now=TimeGMT(); if(server_now<=0 || utc_now<=0) { Print("Could not auto-detect the current server UTC offset; using zero for diagnostics."); return 0; } long raw=(long)server_now-(long)utc_now; return (long)MathRound((double)raw/60.0)*60; } long ServerUtcOffsetSecondsAt(const datetime server_time) { if(InpManualServerUtcOffsetMinutes!=999999) return (long)InpManualServerUtcOffsetMinutes*60; MqlDateTime current; TimeToStruct(server_time,current); int march_sunday=LastSundayDay(current.year,3); int october_sunday=LastSundayDay(current.year,10); MqlDateTime start_dt; ZeroMemory(start_dt); start_dt.year=current.year; start_dt.mon=3; start_dt.day=march_sunday; start_dt.hour=3; // 03:00 broker winter time MqlDateTime end_dt; ZeroMemory(end_dt); end_dt.year=current.year; end_dt.mon=10; end_dt.day=october_sunday; end_dt.hour=4; // 04:00 broker summer time datetime summer_start=StructToTime(start_dt); datetime summer_end=StructToTime(end_dt); if(server_time>=summer_start && server_time=0 && g_rows[j].utc_minute>key.utc_minute) { g_rows[j+1]=g_rows[j]; j--; } g_rows[j+1]=key; } } bool ProcessCalendarValue(const MqlCalendarValue &value,const string currency) { MqlCalendarEvent event; ResetLastError(); if(!CalendarEventById(value.event_id,event)) { PrintFormat("CalendarEventById failed for event_id=%I64u, error=%d",value.event_id,::GetLastError()); return false; } if((int)event.importance<(int)InpMinimumImportance) return true; if(InpOnlyExactTimeEvents && event.time_mode!=CALENDAR_TIMEMODE_DATETIME) return true; if(!PassesNameFilter(event.name,event.event_code)) return true; long event_offset_seconds=ServerUtcOffsetSecondsAt(value.time); datetime utc_time=(datetime)((long)value.time-event_offset_seconds); datetime utc_minute=(datetime)(((long)utc_time/60)*60); datetime server_minute=(datetime)(((long)value.time/60)*60); int row_index=FindRowByUtcMinute(utc_minute); if(row_index<0) row_index=AddRow(utc_minute,server_minute); if(row_index<0) { Print("ArrayResize failed while aggregating calendar rows."); return false; } AppendUnique(g_rows[row_index].names,event.name," + "); AppendUnique(g_rows[row_index].source_event_ids,StringFormat("%I64u",event.id),"|"); AppendUnique(g_rows[row_index].event_codes,event.event_code,"|"); AppendUnique(g_rows[row_index].source_urls,event.source_url,"|"); AppendUnique(g_rows[row_index].currencies,currency,"|"); if((int)event.importance>g_rows[row_index].max_importance) g_rows[row_index].max_importance=(int)event.importance; return true; } int RequestCalendarRange(const string currency, const datetime server_from, const datetime server_to, MqlCalendarValue &values[]) { for(int attempt=1;attempt<=MathMax(1,InpRequestRetries);attempt++) { ArrayFree(values); ResetLastError(); int count=CalendarValueHistory(values,server_from,server_to,NULL,currency); if(count>=0) return count; int error=::GetLastError(); PrintFormat("CalendarValueHistory failed: currency=%s from=%s to=%s " "attempt=%d/%d error=%d", currency,FormatServerTime(server_from),FormatServerTime(server_to), attempt,MathMax(1,InpRequestRetries),error); if(attemptserver_to) chunk_to=server_to; MqlCalendarValue values[]; int count=RequestCalendarRange(currency,chunk_from,chunk_to,values); if(count<0) return false; total_count+=count; for(int i=0;i=server_to) break; chunk_from=(datetime)((long)chunk_to+1); } PrintFormat("Calendar query currency=%s returned %d value(s).",currency,total_count); if(InpRequireValuesForEveryCurrency && total_count==0) { PrintFormat("No calendar values were returned for %s; previous timetable will be retained.",currency); return false; } return true; } bool CollectCalendarRows() { ArrayResize(g_rows,0); g_server_utc_offset_seconds=DetectServerUtcOffsetSeconds(); datetime server_now=TimeTradeServer(); if(server_now<=0) { Print("TimeTradeServer() returned zero; calendar export aborted."); return false; } int safe_days=MathMax(1,MathMin(InpDaysAhead,366)); datetime server_to=(datetime)((long)server_now+(long)safe_days*86400); string currency_list=TrimCopy(InpCurrencies); if(StringLen(currency_list)==0) { MqlCalendarValue values[]; ResetLastError(); int count=CalendarValueHistory(values,server_now,server_to,NULL,NULL); if(count<0) { PrintFormat("CalendarValueHistory failed without a currency filter, error=%d",::GetLastError()); return false; } for(int i=0;i %s, error=%d", attempt,temp_relative,final_relative,error); return false; } Sleep(100); } return false; } bool WriteTimetableCsv() { string final_relative=StagingRelativePath(STAGING_TIMETABLE); string temp_relative=final_relative+".tmp"; int flags=FILE_WRITE|FILE_CSV|FILE_ANSI|FILE_SHARE_READ; ResetLastError(); int handle=FileOpen(temp_relative,flags,',',CP_UTF8); if(handle==INVALID_HANDLE) { PrintFormat("FileOpen failed for %s, error=%d",temp_relative,::GetLastError()); return false; } FileWrite(handle, "event_id","event_name","utc_datetime","enabled", "source_status","source_url","notes"); int count=ArraySize(g_rows); for(int i=0;i60) { Print("InpDaysAhead must be between 1 and 60."); return INIT_PARAMETERS_INCORRECT; } if(InpRefreshHours<1 || InpRefreshHours>24) { Print("InpRefreshHours must be between 1 and 24."); return INIT_PARAMETERS_INCORRECT; } if(InpRetryMinutes<1 || InpRetryMinutes>60) { Print("InpRetryMinutes must be between 1 and 60."); return INIT_PARAMETERS_INCORRECT; } if(InpQueryChunkDays<1 || InpQueryChunkDays>14) { Print("InpQueryChunkDays must be between 1 and 14."); return INIT_PARAMETERS_INCORRECT; } // The timer checks once a minute. Actual successful exports are four hours apart. if(!EventSetTimer(60)) { PrintFormat("EventSetTimer failed, error=%d",::GetLastError()); return INIT_FAILED; } PrintFormat("Calendar exporter started. Target: %s",g_timetable_full_path); PrintFormat("Selection: currencies=%s, minimum importance=%s, horizon=%d days.", InpCurrencies,EnumToString(InpMinimumImportance),InpDaysAhead); Print("The destination folder must already exist and Allow DLL imports must remain enabled."); bool initial_success=RunExport("startup"); ScheduleNextExport(initial_success); return INIT_SUCCEEDED; } void OnDeinit(const int reason) { EventKillTimer(); PrintFormat("Calendar exporter stopped, reason=%d",reason); } void OnTimer() { if(g_export_running) return; if(g_next_export_tick==0 || GetTickCount64()>=g_next_export_tick) { bool success=RunExport("scheduled"); ScheduleNextExport(success); } } void OnTick() { // No trading logic. Exporting is timer-driven. } //+------------------------------------------------------------------+