forked from biomaster/XAU_system
838 lines
26 KiB
MQL5
838 lines
26 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| 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;i<count;i++)
| |||
{
| |||
string needle=LowerCopy(TrimCopy(parts[i]));
| |||
if(StringLen(needle)>0 && 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<summer_end)
| |||
return (long)InpSummerServerUtcOffsetMinutes*60;
| |||
return (long)InpWinterServerUtcOffsetMinutes*60;
| |||
}
| |||
| |||
string FormatIsoUtc(const datetime value)
| |||
{
| |||
MqlDateTime dt;
| |||
TimeToStruct(value,dt);
| |||
return StringFormat("%04d-%02d-%02dT%02d:%02d:%02dZ",
| |||
dt.year,dt.mon,dt.day,dt.hour,dt.min,dt.sec);
| |||
}
| |||
| |||
string FormatServerTime(const datetime value)
| |||
{
| |||
MqlDateTime dt;
| |||
TimeToStruct(value,dt);
| |||
return StringFormat("%04d-%02d-%02d %02d:%02d:%02d",
| |||
dt.year,dt.mon,dt.day,dt.hour,dt.min,dt.sec);
| |||
}
| |||
| |||
string BuildSyntheticEventId(const datetime utc_minute)
| |||
{
| |||
MqlDateTime dt;
| |||
TimeToStruct(utc_minute,dt);
| |||
return StringFormat("MT5_%04d%02d%02d_%02d%02d",
| |||
dt.year,dt.mon,dt.day,dt.hour,dt.min);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Row aggregation |
| |||
//+------------------------------------------------------------------+
| |||
int FindRowByUtcMinute(const datetime utc_minute)
| |||
{
| |||
int count=ArraySize(g_rows);
| |||
for(int i=0;i<count;i++)
| |||
if(g_rows[i].utc_minute==utc_minute)
| |||
return i;
| |||
return -1;
| |||
}
| |||
| |||
int AddRow(const datetime utc_minute,const datetime server_minute)
| |||
{
| |||
int index=ArraySize(g_rows);
| |||
if(ArrayResize(g_rows,index+1)!=index+1)
| |||
return -1;
| |||
| |||
g_rows[index].utc_minute=utc_minute;
| |||
g_rows[index].server_minute=server_minute;
| |||
g_rows[index].names="";
| |||
g_rows[index].source_event_ids="";
| |||
g_rows[index].event_codes="";
| |||
g_rows[index].source_urls="";
| |||
g_rows[index].currencies="";
| |||
g_rows[index].max_importance=(int)CALENDAR_IMPORTANCE_NONE;
| |||
return index;
| |||
}
| |||
| |||
void SortRowsByTime()
| |||
{
| |||
int count=ArraySize(g_rows);
| |||
for(int i=1;i<count;i++)
| |||
{
| |||
ExportRow key=g_rows[i];
| |||
int j=i-1;
| |||
while(j>=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(attempt<MathMax(1,InpRequestRetries))
| |||
Sleep(MathMax(100,InpRetryDelayMilliseconds));
| |||
}
| |||
return -1;
| |||
}
| |||
| |||
bool CollectCurrency(const string currency,const datetime server_from,const datetime server_to)
| |||
{
| |||
long chunk_seconds=(long)MathMax(1,InpQueryChunkDays)*86400;
| |||
datetime chunk_from=server_from;
| |||
int total_count=0;
| |||
| |||
while(chunk_from<server_to)
| |||
{
| |||
datetime chunk_to=(datetime)((long)chunk_from+chunk_seconds);
| |||
if(chunk_to>server_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<count;i++)
| |||
if(!ProcessCalendarValue(values[i],currency))
| |||
return false;
| |||
| |||
if(chunk_to>=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<count;i++)
| |||
if(!ProcessCalendarValue(values[i],"ALL"))
| |||
return false;
| |||
}
| |||
else
| |||
{
| |||
string currencies[];
| |||
ushort separator=(ushort)StringGetCharacter(";",0);
| |||
int currency_count=StringSplit(currency_list,separator,currencies);
| |||
bool queried_any=false;
| |||
| |||
for(int i=0;i<currency_count;i++)
| |||
{
| |||
string currency=TrimCopy(currencies[i]);
| |||
StringToUpper(currency);
| |||
if(StringLen(currency)==0)
| |||
continue;
| |||
queried_any=true;
| |||
if(!CollectCurrency(currency,server_now,server_to))
| |||
return false;
| |||
}
| |||
| |||
if(!queried_any)
| |||
{
| |||
Print("InpCurrencies contains no usable currency codes.");
| |||
return false;
| |||
}
| |||
}
| |||
| |||
SortRowsByTime();
| |||
if(ArraySize(g_rows)==0)
| |||
{
| |||
Print("No events passed the filters; previous timetable will be retained.");
| |||
return false;
| |||
}
| |||
PrintFormat("Aggregated %d unique UTC event minute(s); current server offset=%.2f hours.",
| |||
ArraySize(g_rows),(double)g_server_utc_offset_seconds/3600.0);
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Atomic CSV writing |
| |||
//+------------------------------------------------------------------+
| |||
bool ReplaceWithTemporaryFile(const string temp_relative,const string final_relative)
| |||
{
| |||
int destination_flags=FILE_REWRITE;
| |||
for(int attempt=1;attempt<=5;attempt++)
| |||
{
| |||
ResetLastError();
| |||
if(FileMove(temp_relative,0,final_relative,destination_flags))
| |||
return true;
| |||
int error=::GetLastError();
| |||
if(attempt==5)
| |||
{
| |||
PrintFormat("FileMove failed after %d attempts: %s -> %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;i<count;i++)
| |||
{
| |||
string event_id=BuildSyntheticEventId(g_rows[i].utc_minute);
| |||
string importance=EnumToString((ENUM_CALENDAR_EVENT_IMPORTANCE)g_rows[i].max_importance);
| |||
string notes=StringFormat(
| |||
"MT5 calendar; importance=%s; currencies=%s; server_time=%s; server_utc_offset_minutes=%d; source_event_ids=%s; event_codes=%s",
| |||
importance,
| |||
g_rows[i].currencies,
| |||
FormatServerTime(g_rows[i].server_minute),
| |||
(int)(ServerUtcOffsetSecondsAt(g_rows[i].server_minute)/60),
| |||
g_rows[i].source_event_ids,
| |||
g_rows[i].event_codes);
| |||
| |||
FileWrite(handle,
| |||
event_id,
| |||
CleanCsvText(g_rows[i].names),
| |||
FormatIsoUtc(g_rows[i].utc_minute),
| |||
(InpRowsEnabled ? "1" : "0"),
| |||
"MT5 economic calendar",
| |||
CleanCsvText(g_rows[i].source_urls),
| |||
CleanCsvText(notes));
| |||
}
| |||
| |||
FileFlush(handle);
| |||
FileClose(handle);
| |||
| |||
if(!ReplaceWithTemporaryFile(temp_relative,final_relative))
| |||
return false;
| |||
if(!PublishToAbsolutePath(final_relative,g_timetable_full_path))
| |||
return false;
| |||
| |||
PrintFormat("Timetable exported: %s (%d row(s), enabled=%s)",
| |||
g_timetable_full_path,count,(InpRowsEnabled ? "true" : "false"));
| |||
return true;
| |||
}
| |||
| |||
bool WriteDetailedCsv()
| |||
{
| |||
if(!InpWriteDetailedFile)
| |||
return true;
| |||
| |||
string final_relative=StagingRelativePath(STAGING_DETAILED);
| |||
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,
| |||
"group_event_id","event_name","utc_datetime","server_datetime",
| |||
"server_utc_offset_minutes","importance","currencies",
| |||
"source_event_ids","event_codes","source_urls");
| |||
| |||
int count=ArraySize(g_rows);
| |||
for(int i=0;i<count;i++)
| |||
{
| |||
FileWrite(handle,
| |||
BuildSyntheticEventId(g_rows[i].utc_minute),
| |||
CleanCsvText(g_rows[i].names),
| |||
FormatIsoUtc(g_rows[i].utc_minute),
| |||
FormatServerTime(g_rows[i].server_minute),
| |||
(int)(ServerUtcOffsetSecondsAt(g_rows[i].server_minute)/60),
| |||
EnumToString((ENUM_CALENDAR_EVENT_IMPORTANCE)g_rows[i].max_importance),
| |||
CleanCsvText(g_rows[i].currencies),
| |||
CleanCsvText(g_rows[i].source_event_ids),
| |||
CleanCsvText(g_rows[i].event_codes),
| |||
CleanCsvText(g_rows[i].source_urls));
| |||
}
| |||
| |||
FileFlush(handle);
| |||
FileClose(handle);
| |||
| |||
if(!ReplaceWithTemporaryFile(temp_relative,final_relative))
| |||
return false;
| |||
if(!PublishToAbsolutePath(final_relative,g_detailed_full_path))
| |||
return false;
| |||
| |||
PrintFormat("Detailed calendar exported: %s",g_detailed_full_path);
| |||
return true;
| |||
}
| |||
| |||
bool ExportCalendar()
| |||
{
| |||
if(!CollectCalendarRows())
| |||
return false;
| |||
if(!WriteTimetableCsv())
| |||
return false;
| |||
if(!WriteDetailedCsv())
| |||
return false;
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Scheduling |
| |||
//+------------------------------------------------------------------+
| |||
void ScheduleNextExport(const bool success)
| |||
{
| |||
long wait_seconds;
| |||
if(success)
| |||
wait_seconds=(long)MathMax(1,InpRefreshHours)*3600;
| |||
else
| |||
wait_seconds=(long)MathMax(1,InpRetryMinutes)*60;
| |||
g_next_export_tick=GetTickCount64()+(ulong)wait_seconds*1000;
| |||
}
| |||
| |||
bool RunExport(const string reason)
| |||
{
| |||
if(g_export_running)
| |||
return false;
| |||
| |||
g_export_running=true;
| |||
PrintFormat("Calendar export started: %s",reason);
| |||
bool success=ExportCalendar();
| |||
if(success)
| |||
PrintFormat("Calendar export completed; next regular export in %d hour(s).",
| |||
MathMax(1,InpRefreshHours));
| |||
else
| |||
PrintFormat("Calendar export failed; previous timetable retained; retry in %d minute(s).",
| |||
MathMax(1,InpRetryMinutes));
| |||
g_export_running=false;
| |||
return success;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Expert lifecycle |
| |||
//+------------------------------------------------------------------+
| |||
int OnInit()
| |||
{
| |||
if(!MQLInfoInteger(MQL_DLLS_ALLOWED))
| |||
{
| |||
Print("DLL imports are disabled. Enable 'Allow DLL imports' in this EA's Dependencies tab.");
| |||
return INIT_FAILED;
| |||
}
| |||
| |||
g_timetable_full_path=NormalizeAbsolutePath(InpTimetableFullPath);
| |||
if(StringLen(g_timetable_full_path)==0 || !IsAbsoluteWindowsFilePath(g_timetable_full_path))
| |||
{
| |||
PrintFormat("InpTimetableFullPath must be a complete absolute Windows file path, for example: "
| |||
"C:\\Users\\3dstudio\\Desktop\\Backup-2\\XAU\\timetable.csv. Received: %s",
| |||
g_timetable_full_path);
| |||
return INIT_PARAMETERS_INCORRECT;
| |||
}
| |||
| |||
string output_folder=ParentFolder(g_timetable_full_path);
| |||
if(StringLen(output_folder)==0)
| |||
{
| |||
PrintFormat("Could not determine the destination folder from: %s",g_timetable_full_path);
| |||
return INIT_PARAMETERS_INCORRECT;
| |||
}
| |||
g_detailed_full_path=JoinWindowsPath(output_folder,DETAILED_OUTPUT_NAME);
| |||
| |||
if(InpDaysAhead<1 || InpDaysAhead>60)
| |||
{
| |||
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.
| |||
}
| |||
//+------------------------------------------------------------------+
|