XAU_system/calendar.mq5

838 lines
26 KiB
MQL5
Raw Permalink Normal View History

2026-08-18 08:39:46 +00:00
//+------------------------------------------------------------------+
//| 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.
}
//+------------------------------------------------------------------+