在 MQL5 中实现来自其他语言的实用模块(第 03 部分):移植 Python 的 Schedule 模块,打造增强版 OnTimer
目录
- 概述
- Python 中提供的 Schedule 模块是什么?
- MQL5 中的 Schedule 类
- 在特定时间运行函数
- 向任务传递参数
- 运行任务直至达到指定时间
- 不考虑作业的调度安排,运行所有作业
- 管理日程安排
- 处理时区问题
- 在您的交易应用程序中使用 schedule 模块
- Schedule 与 OnTimer
- 总结
概述
编程旨在使我们的生活更加便捷,它让我们能够自动化许多关键且有时枯燥/重复的任务,这些任务通常都希望由计算机在无需人工参与的情况下自动完成。一个很好的例子就是我们在许多文本编辑器中看到的自动保存功能。有了这个功能,你无需每次写一个新单词时都担心保存文档,因为文本编辑器会自动处理保存过程,这样你就可以专注于写作,而不必担心在发生意外时丢失工作成果。
交易领域也是如此,其中有许多重复性的操作和任务,我们希望通过编写几行代码将其自动化。
在 MQL5 编程语言中,我们有一个众所周知的 OnTimer 函数,它有助于在程序员设定的特定时间间隔后运行某些函数和代码行。
下面是一个简单的例子 — 每隔 10 秒运行一次 OnTimer 函数。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { EventSetTimer(10); //Creates a timer with 10 seconds period //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); //Destroy the timer after completing the work } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnTimer(void) { Print("Ontimer called: ",TimeLocal()); //This line of code will be run after every 10 seconds }
输出。
MN 0 10:16:39.455 Schedule test (XAUUSD,D1) Ontimer called: 2025.07.21 10:16:39 CD 0 10:16:49.459 Schedule test (XAUUSD,D1) Ontimer called: 2025.07.21 10:16:49
这个函数还算不错,但不够精细,也不够灵活,无法在同一程序中同时运行多个/不同的计划。
一旦你在 EA 或指标中设置了 OnTimer 事件处理程序,你就只能依赖那个单一的“定时计划”。这非常具有局限性,因为我们经常希望在程序中的不同时间(间隔)执行不同的任务。
例如,向用户打印或发送每日、每周或每月交易报告。
在 Python 编程语言中,有一个模块类似于 OnTimer 函数,但它在定时运行函数方面要强得多。在本文中,我们将讨论这个问题,并在 MQL5 编程语言中实现一个类似的模块。
Python 中提供的 Schedule 模块是什么?
这是一个对人类友好的 Python 模块,可帮助我们把特定任务安排在一天、一周等周期中的特定时间执行。该模块使用简单且轻便,因此是每位 Python 开发者都值得了解的一个实用模块。
与 MQL5 中的 OnTimer 函数不同,schedule 模块不仅允许我们安排任务在特定时间间隔后运行,而且还让我们能够更灵活地指定特定任务(函数)应该何时以及如何运行。
以下是该模块提供的一些功能。
import schedule
| 函数 | 描述 |
|---|---|
schedule.every(10).minutes.do(job) | 与 OnTimer 事件类似,每隔 10 分钟,名为 job 的函数就会运行。 |
schedule.every().hour.do(job) | 名为 job 的函数将从脚本开始每小时运行一次。 |
schedule.every().day.at("10:30").do(job) | 名为 job 的函数每天会在 24 小时制的当地时间 10:30 运行。 |
schedule.every().monday.do(job) | 名为 job 的函数将在每周一脚本首次运行的确切时间执行。 |
schedule.every().wednesday.at("13:15").do(job) | 名为job 的函数将于每周三 13:15 执行。 |
schedule.every().day.at("12:42", "Europe/Amsterdam").do(job) | 根据欧洲/阿姆斯特丹时间,名为 job 的函数每天 12:42 调用。 |
schedule.every().minute.at(":17").do(job) | 名为 job 的函数每分钟在第 17 秒调用一次。 |
这些只是该模块提供的部分关键功能。让我们在 MQL5 中实现一个形式相近的类。
MQL5 中的 Schedule 类
Python 中的 schedule 模块旨在为要在特定时间间隔内安排的每个任务提供单独的函数。名为 do 的函数是 schedule 类中所有函数链的终点。
schedule.every(10).minutes.do 为了在 MQL5 中实现类似的语法,我们必须让 CSchedule 类中的一些函数返回整个类的实例,但名为 dO 的函数除外,它是端点。
class CSchedule { private: int m_period; //the number of seconds, minutes, etc to use time_intervals_enum m_unit; //time interval: minutes, hours, etc int m_time_seconds; //datetime in seconds JobFunction m_func; //The function to run for the current schedule public: int m_fixed_time; // time from midnight in seconds CSchedule(void); ~CSchedule(void); CSchedule* every(int period = 1); CSchedule* seconds(); CSchedule* minutes(); CSchedule* hours(); CSchedule* days(); CSchedule* weeks(); CSchedule* months(); CSchedule* years(); void dO(JobFunction func); }
这种语法使我们能够拥有类似于 Python 的 schedule 模块所提供的流畅接口。
#include <schedule.mqh> CSchedule schedule; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { schedule.every().minutes().dO(runthis); }
名为 every 的函数对于设置特定时间范围内的间隔至关重要。例如:
schedule.every(10).minutes()
也就是说,每隔 10 分钟,名为 dO 的函数接收到的特定函数应该被触发。
该函数的核心逻辑是接收一个给定的间隔值,并将该变量赋值给一个名为 m_period 的变量 — 该变量存储在类中。
CSchedule* CSchedule::every(int period = 1) { m_period = period; return GetPointer(this); }
函数:秒、分、小时 等,根据由名为 time_intervals_enum 的枚举类型给出的所有可用时间间隔选项,为时间范围变量赋值。
enum time_intervals_enum
{
SECONDS,
MINUTES,
HOURS,
DAYS,
WEEKS,
MONTHS,
YEARS
}; CSchedule* CSchedule::seconds()
{
m_unit = SECONDS;
return GetPointer(this);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
CSchedule* CSchedule::minutes()
{
m_unit = MINUTES;
return GetPointer(this);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
CSchedule* CSchedule::hours()
{
m_unit = HOURS;
return GetPointer(this);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
CSchedule* CSchedule::days()
{
m_unit = DAYS;
return GetPointer(this);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
CSchedule* CSchedule::weeks(void)
{
m_unit = WEEKS;
return GetPointer(this);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
CSchedule* CSchedule::months(void)
{
m_unit = MONTHS;
return GetPointer(this);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
CSchedule* CSchedule::years(void)
{
m_unit = YEARS;
return GetPointer(this);
} 在我们理解名为 dO 的函数之前,它是所有调度函数的终点。让我们来了解一下每个作业(函数)是如何在名为 schedule.mqh 的文件中处理和存储的。
//+------------------------------------------------------------------+ //| Handling and storing every job (task) used in the class CSchedule| //+------------------------------------------------------------------+ typedef void (*JobFunction)(); // For global functions struct jobs_struct { int prev_run; int next_run; int interval; JobFunction func; // Store the function pointer }; jobs_struct m_jobs[]; // Global job list void jobs_add(jobs_struct &jobs_array[], const jobs_struct &job) { uint size = ArraySize(jobs_array); ArrayResize(jobs_array, size + 1); jobs_array[size] = job; }
由于类 CSchedule 具有引用自身的函数,因此处理该类中的每个作业对象会变得复杂/混乱,且容易出错。
定义一个名为 m_jobs 的全局数组,为类内部使用的所有作业提供了一种通用的存储和处理方式。 我们稍后再讨论。
名为 dO 的函数接收一个函数,该函数将根据接收到的“计划”重复运行。它计算一个函数最后一次运行的时间以及下一次预计运行的时间。
接收到的函数会与作业的其他属性(如上次运行时间和下次运行时间)一起存储在名为 jobs_struct 的结构中。
所有这些值随后会被存储在一个名为 m_jobs 的数组中,该数组是 jobs_struct 类型的数组。
jobs_struct m_jobs[]; // Global job list void CSchedule::dO(JobFunction func) { m_func = func; jobs_struct job; job.func = m_func; datetime now = TimeLocal(); job.prev_run = (int)now; job.interval = timedelta(m_period, m_unit); //Get configs from the every() method and above job.next_run = job.prev_run + timedelta(m_period, m_unit); if (MQLInfoInteger(MQL_DEBUG)) Print("The first function run is schedule at: ", TimeToString((datetime)job.next_run, TIME_DATE | TIME_SECONDS)); //--- jobs_add(m_jobs, job); //store the job object to the list of jobs }
在任务被存入相应的任务数组后,我们需要一个通用函数来持续监控它,并在其预定时间到达时运行其函数。
void CSchedule::run_pending() { int now = (int)TimeLocal(); for(int i = 0; i < ArraySize(m_jobs); i++) { if(now >= m_jobs[i].next_run) { if(m_jobs[i].func != NULL) m_jobs[i].func(); m_jobs[i].prev_run = m_jobs[i].next_run; // Recalculate next_run m_jobs[i].next_run += m_jobs[i].interval; if (MQLInfoInteger(MQL_DEBUG)) printf("Prev run: %s Next run: %s", TimeToString((datetime)m_jobs[i].prev_run, TIME_DATE|TIME_SECONDS), TimeToString((datetime)m_jobs[i].next_run, TIME_DATE|TIME_SECONDS)); } } }
让我们用这个类来安排我们的第一个任务。
#include <schedule.mqh> CSchedule schedule; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- schedule.every(10).seconds().dO(runthis); while (true) { schedule.run_pending(); } } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void runthis() { Print(__FUNCTION__," called at: ",TimeLocal()); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+
我们希望每隔 10 秒执行一次 runthis 函数。使用无限 while 循环只是为了使脚本持续运行,直到被停止。
以下是脚本在调试模式下运行时的日志输出。
NO 0 14:51:56.301 schedule test (XAUUSD,D1) The first function run is schedule at: 2025.07.21 14:52:06 GS 0 14:52:06.000 schedule test (XAUUSD,D1) runthis called at: 2025.07.21 14:52:06 PJ 0 14:52:06.000 schedule test (XAUUSD,D1) Prev run: 2025.07.21 14:52:06 Next run: 2025.07.21 14:52:16 QH 0 14:52:16.000 schedule test (XAUUSD,D1) runthis called at: 2025.07.21 14:52:16 GR 0 14:52:16.000 schedule test (XAUUSD,D1) Prev run: 2025.07.21 14:52:16 Next run: 2025.07.21 14:52:26 KP 0 14:52:26.000 schedule test (XAUUSD,D1) runthis called at: 2025.07.21 14:52:26 FJ 0 14:52:26.000 schedule test (XAUUSD,D1) Prev run: 2025.07.21 14:52:26 Next run: 2025.07.21 14:52:36
我们可以使用同一个类设置多个日程。
void OnStart() { //--- schedule.every(10).seconds().dO(runthis); //run after every 10 seconds schedule.every().minute().dO(runthis2); //run on every minute while (true) { schedule.run_pending(); } } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void runthis() { Print(__FUNCTION__," called at: ",TimeLocal()); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void runthis2() { Print("Hello world!, This function is called after a minute has passed"); }
输出。
FK 0 15:00:55.079 schedule test (XAUUSD,D1) The first function run is schedule at: 2025.07.21 15:01:05 IL 0 15:00:55.079 schedule test (XAUUSD,D1) The first function run is schedule at: 2025.07.21 15:01:55 ER 0 15:01:05.000 schedule test (XAUUSD,D1) runthis called at: 2025.07.21 15:01:05 RK 0 15:01:05.000 schedule test (XAUUSD,D1) Prev run: 2025.07.21 15:01:05 Next run: 2025.07.21 15:01:15 OK 0 15:01:15.000 schedule test (XAUUSD,D1) runthis called at: 2025.07.21 15:01:15 ES 0 15:01:15.000 schedule test (XAUUSD,D1) Prev run: 2025.07.21 15:01:15 Next run: 2025.07.21 15:01:25 IS 0 15:01:25.000 schedule test (XAUUSD,D1) runthis called at: 2025.07.21 15:01:25 LJ 0 15:01:25.000 schedule test (XAUUSD,D1) Prev run: 2025.07.21 15:01:25 Next run: 2025.07.21 15:01:35 CK 0 15:01:35.000 schedule test (XAUUSD,D1) runthis called at: 2025.07.21 15:01:35 KR 0 15:01:35.000 schedule test (XAUUSD,D1) Prev run: 2025.07.21 15:01:35 Next run: 2025.07.21 15:01:45 MP 0 15:01:45.000 schedule test (XAUUSD,D1) runthis called at: 2025.07.21 15:01:45 FJ 0 15:01:45.000 schedule test (XAUUSD,D1) Prev run: 2025.07.21 15:01:45 Next run: 2025.07.21 15:01:55 GH 0 15:01:55.000 schedule test (XAUUSD,D1) runthis called at: 2025.07.21 15:01:55 NM 0 15:01:55.000 schedule test (XAUUSD,D1) Prev run: 2025.07.21 15:01:55 Next run: 2025.07.21 15:02:05 KR 0 15:01:55.000 schedule test (XAUUSD,D1) Hello world!, This function is called after a minute has passed MH 0 15:01:55.000 schedule test (XAUUSD,D1) Prev run: 2025.07.21 15:01:55 Next run: 2025.07.21 15:02:55 NJ 0 15:02:05.001 schedule test (XAUUSD,D1) runthis called at: 2025.07.21 15:02:05 HP 0 15:02:05.001 schedule test (XAUUSD,D1) Prev run: 2025.07.21 15:02:05 Next run: 2025.07.21 15:02:15 GR 0 15:02:15.000 schedule test (XAUUSD,D1) runthis called at: 2025.07.21 15:02:15 LK 0 15:02:15.000 schedule test (XAUUSD,D1) Prev run: 2025.07.21 15:02:15 Next run: 2025.07.21 15:02:25 RK 0 15:02:25.001 schedule test (XAUUSD,D1) runthis called at: 2025.07.21 15:02:25 FS 0 15:02:25.001 schedule test (XAUUSD,D1) Prev run: 2025.07.21 15:02:25 Next run: 2025.07.21 15:02:35 OS 0 15:02:35.004 schedule test (XAUUSD,D1) runthis called at: 2025.07.21 15:02:35 JK 0 15:02:35.004 schedule test (XAUUSD,D1) Prev run: 2025.07.21 15:02:35 Next run: 2025.07.21 15:02:45 EK 0 15:02:45.000 schedule test (XAUUSD,D1) runthis called at: 2025.07.21 15:02:45 KR 0 15:02:45.000 schedule test (XAUUSD,D1) Prev run: 2025.07.21 15:02:45 Next run: 2025.07.21 15:02:55 OP 0 15:02:55.000 schedule test (XAUUSD,D1) runthis called at: 2025.07.21 15:02:55 EJ 0 15:02:55.000 schedule test (XAUUSD,D1) Prev run: 2025.07.21 15:02:55 Next run: 2025.07.21 15:03:05 FK 0 15:02:55.000 schedule test (XAUUSD,D1) Hello world!, This function is called after a minute has passed
在特定时间运行作业(函数)
我们常常希望在非常特定的时间运行函数。例如,运行一个函数,该函数负责根据交易时段在特定时间开立交易,例如在当地时间 19:00 开立交易。
在 CSchedule 类中,所有名为 at 的函数都负责在给定特定“有效时间”时执行此操作。
例如,
schedule.every().day().at(19, 10).dO(job);
这条指令安排名为 job 的函数每天 19:10 运行。
因为我们需要针对秒、分、小时、天、周等不同时间粒度返回不同的构建器类,所以在 MQL5 中实现这一点会比较棘手。
class CSchedule { private: int m_period; //the number of seconds, minutes, etc to use time_intervals_enum m_unit; //time interval: minutes, hours, etc int m_time_seconds; //datetime in seconds JobFunction m_func; //The function to run for the current schedule bool has_fixed_time() const { return m_fixed_time > 0; } datetime TodaysDate(datetime dt) { // Extract year, month, day — and build a new datetime at 00:00:00 MqlDateTime tm; TimeToStruct(dt, tm); tm.hour = 0; tm.min = 0; tm.sec = 0; return StructToTime(tm); } public: int m_fixed_time; // time from midnight in seconds CSchedule(void); ~CSchedule(void); CSchedule* every(int period = 1); CSchedule* seconds(); CSchedule* minutes(); CSchedule* hours(); CSchedule* days(); CSchedule* weeks(); MinuteScheduleBuilder* minute(); HourScheduleBuilder* hour(); DayScheduleBuilder* day(); WeekScheduleBuilder* week();
在每个 “Builder” 类(所有以 “Builder” 结尾的类)中,我们都有一个名为 at 的函数,它负责设置特定的时间间隔。
我们还有一个名为 dO 的函数,它继承自 CSchedule 类的同名函数。
例如, WeekScheduleBuilder 类。
class CSchedule; //forward declaration | VERY IMPORTANT class WeekScheduleBuilder { protected: CSchedule *m_schedule; public: WeekScheduleBuilder(CSchedule *schedule_) { m_schedule = schedule_; } ~WeekScheduleBuilder(void) {}; CSchedule* at(ENUM_DAY_OF_WEEK dayofweek, uint hour=0, uint minutes = 0, uint seconds = 0) { if (CheckPointer(m_schedule) == POINTER_INVALID || m_schedule == NULL) return NULL; datetime now = TimeLocal(); MqlDateTime tm; TimeToStruct(now, tm); int today_dow = tm.day_of_week; //--- Compute days until target day (next week if it's the same day or already passed) int days_ahead = (int)dayofweek - today_dow; if (days_ahead < 0) days_ahead += 7; // ensure it's next week datetime next_target_date = now + timedelta(days_ahead, DAYS); MqlDateTime target_tm; TimeToStruct(next_target_date, target_tm); //--- setting the correct time target_tm.hour = (int)hour; target_tm.min = (int)minutes; target_tm.sec = (int)seconds; m_schedule.m_fixed_time = (int)StructToTime(target_tm); return m_schedule; } void dO(JobFunction func) { m_schedule.dO(func); } };
所有名为 at 的函数都会接收一个具体时间参数,计算首次运行的目标时间,并将其对应的秒数赋给一个名为 m_fixed_time 的变量。
在名为 dO 的函数内部,我们引入了一个条件来检查接收到的时间值是否为固定时间值(例如,19:00)或预定的秒数、分钟数等,用于下一次函数运行,因为这两种情况都需要略微不同的处理方法。
void CSchedule::dO(JobFunction func) { m_func = func; jobs_struct job; job.func = m_func; datetime now = TimeLocal(); job.prev_run = (int)now; job.interval = timedelta(m_period, m_unit); //Get configs from the every() method and above if (has_fixed_time()) { datetime scheduled_time = (datetime)m_fixed_time; //we add today's date to the fixed_time calculated //Add interval repeatedly until scheduled_time >= now while (scheduled_time <= now) scheduled_time += job.interval; //Schedule for the next time if the current time has passed job.next_run = (int)scheduled_time; } else { job.next_run = job.prev_run + job.interval; } if (MQLInfoInteger(MQL_DEBUG)) Print("The first function run is schedule at: ", TimeToString((datetime)job.next_run, TIME_DATE | TIME_SECONDS)); //--- jobs_add(m_jobs, job); //store the job object to the list of jobs }
下面介绍如何设置多个计划任务在指定时间重复运行。
void OnStart() { //--- schedule.every().minute().at(10).dO(job); //Runs on every minute at the 10th second schedule.every().hour().at(10).dO(job); //runs on every hour at the 10th minute schedule.every().day().at(19, 10).dO(job); //runs every day at 19:10 hours schedule.every().week().at(MONDAY).dO(job); //Runs every week on Monday at 00:00 (by default) while (true) { schedule.run_pending(); } } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void job() { Print(__FUNCTION__," run at: ",TimeLocal()); }
为任务添加标识信息
如前一个示例输出日志所示,识别和跟踪作业进度颇为困难,尤其是在同时运行多个计划函数的情况下。为了解决这个问题,我们需要在名为 dO 的函数中添加一个名为 jobs_name 的可选变量,该变量有助于标记所有已安排的任务。
void CSchedule::dO(JobFunction func, const string jobs_name="") { jobs_struct job; job.func = func; //Assigns the function to job's sturucture job.name = jobs_name; //Assigns the name to job's structure datetime now = TimeLocal(); job.prev_run = (int)now; job.interval = timedelta(m_period, m_unit); //Get configs from the every() method and above if (has_fixed_time()) { datetime today_midnight = TodaysDate(now); //Get todays date at 00:00 datetime scheduled_time = today_midnight + m_fixed_time; //we add today's date to the fixed_time calculated //Add interval repeatedly until scheduled_time >= now while (scheduled_time <= now) scheduled_time += job.interval; //Schedule for the next time job.next_run = (int)scheduled_time; } else { job.next_run = job.prev_run + job.interval; } if (MQLInfoInteger(MQL_DEBUG)) printf("Job: %s -> first run schedule at: [%s]",job.name, TimeToString((datetime)job.next_run, TIME_DATE | TIME_SECONDS)); //--- jobs_add(m_jobs, job); //store the job object to the list of jobs }
现在,我们可以更有效地追踪每个任务的进度。
void OnStart() { //--- schedule.every().minute().at(10).dO(Greet, "Jacob"); schedule.every().hour().at(10).dO(Greet, "Anne"); schedule.every().day().at(08, 10).dO(Greet, "Chriss"); schedule.every().week().at(MONDAY).dO(Greet, "Nobody"); while (true) { schedule.run_pending(); } } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void Greet() { Print("Hello there!"); }
输出。
JI 0 06:57:54.817 schedule test (XAUUSD,D1) Job: Jacob -> first run schedule at: [2025.07.22 06:58:10] MH 0 06:57:54.817 schedule test (XAUUSD,D1) Job: Anne -> first run schedule at: [2025.07.22 07:10:00] FL 0 06:57:54.817 schedule test (XAUUSD,D1) Job: Chriss -> first run schedule at: [2025.07.22 08:10:00] JO 0 06:57:54.817 schedule test (XAUUSD,D1) Job: Nobody -> first run schedule at: [2025.07.28 00:00:00] GN 0 06:58:10.014 schedule test (XAUUSD,D1) Hello there! LF 0 06:58:10.014 schedule test (XAUUSD,D1) Job: Jacob -> Prev run: [2025.07.22 06:58:10] Next run: [2025.07.22 06:59:10]
让任务运行到指定时间为止
有时我们有一些计划任务,并不希望它们永远运行下去。在这种情况下,为这些任务设定一个截止时间会更合适。
让我们来介绍一下名为 until 的函数。 类似于 Python 中 schedule 模块提供的功能。
CSchedule* CSchedule::until(datetime expiry_date) { m_expiry_date = expiry_date; return GetPointer(this); }
在名为 dO 的函数内部,我们从类中获取到期日期(从名为 until 的函数接收),并将其分配给作业的结构。
void CSchedule::dO(JobFunction func, const string jobs_name="") { jobs_struct job; job.func = func; //Assigns the function to job's sturucture job.name = jobs_name; //Assigns the name to job's structure datetime now = TimeLocal(); job.prev_run = (int)now; job.interval = timedelta(m_period, m_unit); //Get configs from the every() method and above job.expiry_date = m_expiry_date; //Other lines of code }
在名为 run_pending 的函数中运行作业之前,我们必须检查它是否已过期。
void CSchedule::run_pending() { int now = (int)TimeLocal(); for(int i = 0; i < ArraySize(m_jobs); i++) { if (now >= (int)m_jobs[i].expiry_date && expiry_date != 0) //Check if the job hasn't expired { if (MQLInfoInteger(MQL_DEBUG)) printf("Job: %s -> Expired",m_jobs[i].name); continue; //skip all expired jobs } //... other checks }
最后,我们运行一个任务,并将过期日期设置为从当前时间起 5 分钟。
#include <schedule.mqh> CSchedule schedule; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- schedule.every(1).minutes().until(D'22.7.2025 10:15').dO(Greet, "Greet"); //The current time was 10:10, in the same date while (true) { schedule.run_pending(); Sleep(1000); } } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void Greet() { Print("Hello there!"); }
输出。
EM 0 10:10:02.849 schedule test (XAUUSD,D1) Job: Greet -> first run schedule at: [2025.07.22 10:11:02] CI 0 10:11:02.864 schedule test (XAUUSD,D1) Hello there! NS 0 10:11:02.864 schedule test (XAUUSD,D1) Job: Greet -> Prev run: [2025.07.22 10:11:02] Next run: [2025.07.22 10:12:02] FR 0 10:12:02.873 schedule test (XAUUSD,D1) Hello there! EJ 0 10:12:02.873 schedule test (XAUUSD,D1) Job: Greet -> Prev run: [2025.07.22 10:12:02] Next run: [2025.07.22 10:13:02] ND 0 10:13:02.861 schedule test (XAUUSD,D1) Hello there! OL 0 10:13:02.861 schedule test (XAUUSD,D1) Job: Greet -> Prev run: [2025.07.22 10:13:02] Next run: [2025.07.22 10:14:02] GM 0 10:14:02.922 schedule test (XAUUSD,D1) Hello there! PG 0 10:14:02.922 schedule test (XAUUSD,D1) Job: Greet -> Prev run: [2025.07.22 10:14:02] Next run: [2025.07.22 10:15:02] LH 0 10:15:00.945 schedule test (XAUUSD,D1) Job: Greet -> Expired
无视调度计划,运行所有任务
有时,你可能需要立即运行所有函数,而不考虑它们的调度计划。通常,在测试时,有时我们只是想强制所有计划的操作一次性运行,例如在程序启动时。
在这种情况下,名为 run_all 的函数就派上用场了。
void CSchedule::run_all(void) { datetime now = TimeLocal(); for(int i = 0; i < ArraySize(m_jobs); i++) { if(m_jobs[i].func != NULL) m_jobs[i].func(); m_jobs[i].prev_run = m_jobs[i].next_run; // Recalculate next_run m_jobs[i].next_run += m_jobs[i].interval; if (MQLInfoInteger(MQL_DEBUG)) printf("%s run at: %s",m_jobs[i].name, TimeToString(now, TIME_DATE|TIME_SECONDS)); } if (MQLInfoInteger(MQL_DEBUG)) printf("%s -> All %I64u Jobs have been executed!",__FUNCTION__, m_jobs.Size()); }
使用示例。
#include <schedule.mqh> CSchedule schedule; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- schedule.every(4).minutes().dO(Greet, "Greet every minute"); schedule.every(4).hours().dO(Greet, "Greet hourly"); schedule.every(4).days().dO(Greet, "Greet daily"); schedule.every(4).weeks().dO(Greet, "Greet weekly"); schedule.run_all(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void Greet() { Print("Hello there!"); }
输出。
CM 0 11:04:06.695 schedule test (XAUUSD,D1) Job: Greet every minute -> first run schedule at: [2025.07.22 11:08:06] RR 0 11:04:06.695 schedule test (XAUUSD,D1) Job: Greet hourly -> first run schedule at: [2025.07.22 15:04:06] HR 0 11:04:06.695 schedule test (XAUUSD,D1) Job: Greet daily -> first run schedule at: [2025.07.26 11:04:06] MN 0 11:04:06.695 schedule test (XAUUSD,D1) Job: Greet weekly -> first run schedule at: [2025.08.19 11:04:06] NI 0 11:04:06.695 schedule test (XAUUSD,D1) Hello there! QL 0 11:04:06.695 schedule test (XAUUSD,D1) Greet every minute run at: 2025.07.22 11:04:06 RN 0 11:04:06.695 schedule test (XAUUSD,D1) Hello there! GF 0 11:04:06.695 schedule test (XAUUSD,D1) Greet hourly run at: 2025.07.22 11:04:06 RL 0 11:04:06.695 schedule test (XAUUSD,D1) Hello there! KJ 0 11:04:06.695 schedule test (XAUUSD,D1) Greet daily run at: 2025.07.22 11:04:06 DR 0 11:04:06.695 schedule test (XAUUSD,D1) Hello there! QK 0 11:04:06.695 schedule test (XAUUSD,D1) Greet weekly run at: 2025.07.22 11:04:06 FL 0 11:04:06.695 schedule test (XAUUSD,D1) CSchedule::run_all -> All 4 Jobs have been executed!
尽管这四个函数被设置为在第四个时间周期间隔之后运行,但它们都在同一当前时间被执行。
管理日程
我们需要不同的方式来以编程方式访问和取消不同的计划,因为某些调度任务可能会随着时间推移而不再需要。
获取所有作业
| 函数 | 描述 |
|---|---|
void get_jobs(jobs_struct &jobs_struct_array[]) { ArrayResize(jobs_struct_array, m_jobs.Size()); for (uint i=0; i<m_jobs.Size(); i++) jobs_struct_array[i] = m_jobs[i]; } | 此函数提供了一个按引用返回的参数,该参数是一个数组,其中包含所有作业/任务的所有属性的结构。 |
uint get_jobs() { return m_jobs.Size(); } | 它返回已计划作业的数量。 |
取消已计划的作业
| 函数 | 描述 |
|---|---|
bool Cancel(const string jobs_name); | 该函数可根据作业名称取消对应的已调度任务。 |
bool Cancel(const uint jobs_index); | 它使用其索引号(从 0 到 + 无穷大)来取消一个已计划的作业。如果该作业是第一个计划中的作业,则其索引号为 0。 |
bool Clear() { return ArrayResize(m_jobs, 0)==-1?false:true; } | 这将从内存中清除(删除)所有已计划的作业。调用此函数后,不会运行任何作业/任务。 |
使用示例。
#include <schedule.mqh> CSchedule schedule; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- schedule.every().minute().at(0).dO(Greet, "EveryMin Greetings"); //Job is set at index 0 schedule.every().hour().at(20,10).dO(Greet, "Hourly Greetings"); //Job is set at index 1 schedule.every().day().at(13,20,10).dO(Greet, "Daily Greetings"); //JOb is set at index 2 schedule.every().week().at(MONDAY, 13, 56).dO(Greet, "Weekly Greetings"); //Job is set at index 3 schedule.Cancel(0); //Cancel the job at index 0, the first one Print("Jobs remaining: ",schedule.get_jobs()); schedule.Cancel("Hourly Greetings"); //Cancel the job with this name Print("Jobs remaining: ",schedule.get_jobs()); schedule.Clear(); //Clear all schedules Print("Jobs remaining: ",schedule.get_jobs()); while (true) { schedule.run_pending(); Sleep(1000); } } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void Greet() { Print("Hello there!"); }
输出。
LK 0 16:01:02.017 schedule test (XAUUSD,D1) Job: EveryMin Greetings -> first run schedule at: [2025.07.22 16:02:00] HH 0 16:01:02.017 schedule test (XAUUSD,D1) Job: Hourly Greetings -> first run schedule at: [2025.07.22 17:20:10] EH 0 16:01:02.017 schedule test (XAUUSD,D1) Job: Daily Greetings -> first run schedule at: [2025.07.23 13:20:10] PE 0 16:01:02.017 schedule test (XAUUSD,D1) Job: Weekly Greetings -> first run schedule at: [2025.07.28 13:56:00] OS 0 16:01:02.017 schedule test (XAUUSD,D1) CSchedule::Cancel Job at index [0] removed HS 0 16:01:02.017 schedule test (XAUUSD,D1) Jobs remaining: 3 RG 0 16:01:02.018 schedule test (XAUUSD,D1) CSchedule::Cancel Job 'Hourly Greetings' removed DL 0 16:01:02.018 schedule test (XAUUSD,D1) Jobs remaining: 2 DD 0 16:01:02.018 schedule test (XAUUSD,D1) Jobs remaining: 0
处理时区问题
在前面介绍的所有示例和库实现代码中,我们都使用了本地时间。但是,考虑到 MQL5 编程语言中开发者可以使用多种时间选项,这种做法的局限性非常大。例如,您可能希望根据经纪商服务器时间或 UTC 时间安排在特定时间进行交易操作。
在 CSchedule 类的构造函数中,我们添加了一个可选变量,允许开发人员选择用于所有调度操作的时间类型。
CSchedule::CSchedule(TIME_SOURCE_ENUM time_source=TIME_SOURCE_LOCAL):
m_time_source(time_source)
{
if (MQLInfoInteger(MQL_DEBUG))
printf("Schedule class initialized using %s, current time -> %s",EnumToString(time_source), (string)GetTime(m_time_source));
} 下面介绍“时间源枚举”及其对应的函数。
enum TIME_SOURCE_ENUM { TIME_SOURCE_LOCAL, // TimeLocal() TIME_SOURCE_CURRENT, // TimeCurrent() TIME_SOURCE_TRADE_SERVER, // TimeTradeServer() TIME_SOURCE_GMT // TimeGMT() }; datetime GetTime(TIME_SOURCE_ENUM source) { switch(source) { case TIME_SOURCE_LOCAL: return TimeLocal(); case TIME_SOURCE_CURRENT: return TimeCurrent(); case TIME_SOURCE_TRADE_SERVER: return TimeTradeServer(); case TIME_SOURCE_GMT: return TimeGMT(); default: return TimeLocal(); // Fallback } }
使用示例。
#include <schedule.mqh> CSchedule schedule(TIME_SOURCE_GMT); //Using GMT //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- schedule.every().minute().at(0).dO(Greet, "EveryMin Greetings"); //Job is set at index 0 schedule.every().hour().at(20,10).dO(Greet, "Hourly Greetings"); //Job is set at index 1 schedule.every().day().at(13,20,10).dO(Greet, "Daily Greetings"); //JOb is set at index 2 schedule.every().week().at(MONDAY, 13, 56).dO(Greet, "Weekly Greetings"); //Job is set at index 3 while (true) { schedule.run_pending(); Sleep(1000); } } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void Greet() { Print("Hello there!"); }
输出。
LP 0 16:57:34.961 schedule test (XAUUSD,D1) Schedule class initialized using TIME_SOURCE_GMT, current time -> 2025.07.22 13:57:34 QL 0 16:57:34.964 schedule test (XAUUSD,D1) Job: EveryMin Greetings -> first run schedule at: [2025.07.22 13:58:00] RM 0 16:57:34.964 schedule test (XAUUSD,D1) Job: Hourly Greetings -> first run schedule at: [2025.07.22 14:20:10] RE 0 16:57:34.964 schedule test (XAUUSD,D1) Job: Daily Greetings -> first run schedule at: [2025.07.23 13:20:10] KK 0 16:57:34.964 schedule test (XAUUSD,D1) Job: Weekly Greetings -> first run schedule at: [2025.07.28 13:56:00] HK 0 16:58:00.161 schedule test (XAUUSD,D1) Hello there! KO 0 16:58:00.161 schedule test (XAUUSD,D1) Job: EveryMin Greetings -> Prev run: [2025.07.22 13:58:00] Next run: [2025.07.22 13:59:00]
交易应用程序中日程模块的应用
我们已经了解了如何在简单函数中使用此模块,并通过示例展示了如何按时触发函数执行。下面给出几个在交易应用程序中使用该库的实际示例。
更高效的 NewBar 事件处理
编写一个能够有效检测新 K 线开启的函数并不总是那么容易,因为 CSchedule 类有多种方法可以在非常特定的时间安排任务,我们可以利用它在秒、分、时、日等开始时执行某些操作。
在文件 Schedule Testing EA.mq5 中。
#include <Trade\Trade.mqh> #include <Trade\SymbolInfo.mqh> #include <Trade\PositionInfo.mqh> CTrade m_trade; CSymbolInfo m_symbol; CPositionInfo m_position; //--- #include <schedule.mqh> CSchedule schedule(TIME_SOURCE_CURRENT); //Use the current broker's time //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ input int magic_number = 22072025; input uint slippage = 100; input uint stoploss = 500; input uint takeprofit = 700; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- m_trade.SetExpertMagicNumber(magic_number); m_trade.SetTypeFillingBySymbol(Symbol()); m_trade.SetDeviationInPoints(slippage); if (!m_symbol.Name(Symbol())) { printf("%s -> Failed to select a symbol '%s'. Error = %d", __FUNCTION__,Symbol(),GetLastError()); return INIT_FAILED; } //--- Schedule schedule.every().hour().at(0,0).dO(MainTradingFunction); //every hour when the minute == 0 and second == 0 //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- schedule.run_pending(); //Constanly monitor all the scheduled tasks } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool PosExists(ENUM_POSITION_TYPE type) { for (int i=PositionsTotal()-1; i>=0; i--) if (m_position.SelectByIndex(i)) if (m_position.Symbol()==Symbol() && m_position.Magic() == magic_number && m_position.PositionType()==type) return (true); return (false); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CloseAllTrades() { for (int i = PositionsTotal() - 1; i >= 0; i--) if (m_position.SelectByIndex(i)) if (m_position.Magic() == magic_number && m_position.Symbol() == Symbol()) m_trade.PositionClose(m_position.Ticket(), slippage); } //+------------------------------------------------------------------+ //| The main function for opening trades and performing other | //| trading related tasks | //+------------------------------------------------------------------+ void MainTradingFunction() { printf("New bar detected!"); //--- if (!m_symbol.RefreshRates()) return; if (!PosExists(POSITION_TYPE_BUY)) m_trade.Buy(m_symbol.LotsMin(), Symbol(), m_symbol.Ask(), m_symbol.Ask()-stoploss*m_symbol.Point(), m_symbol.Ask()+takeprofit*m_symbol.Point() ); if (!PosExists(POSITION_TYPE_SELL)) m_trade.Sell(m_symbol.LotsMin(), Symbol(), m_symbol.Bid(), m_symbol.Bid()+stoploss*m_symbol.Point(), m_symbol.Bid()-takeprofit*m_symbol.Point() ); //--- }
策略测试器中的输出。

我们必须像 EventSetTimer 一样,在 OnInit 函数中显式地设置所有内容 — 使用 dO 函数。
由于 schedule_pending 函数负责持续监控这些调度任务,因此它应该在 EA 的 OnTick 函数中运行,在指标的 OnCalculate 函数中运行,以及在 MQL5 脚本的无限循环中运行。
发送每日交易报告
通过跟踪市场收盘前几分钟或几秒钟(例如,00:00 前 5 分钟),我们可以打印或向用户发送每日交易报告。
int OnInit() { //... other lines of code //--- Schedule schedule.every().hour().at(0,0).dO(MainTradingFunction); schedule.every().day().at(23, 55).dO(SendDailyTradingReport); //every day 5 minutes before market closing //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- schedule.run_pending(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void SendDailyTradingReport() { string sdate = TimeToString (TimeCurrent(), TIME_DATE); datetime start = StringToTime(sdate); if (!HistorySelect(start, TimeCurrent())) { printf("%s, line %d failed to obtain closed deals from history error =%d",__FUNCTION__,__LINE__,GetLastError()); return; } Comment(""); //--- double pl = 0.0; int trades_count=0; string report_body = ""; for(int i = 0; i < HistoryDealsTotal(); i++) { if (m_deal.SelectByIndex(i)) if (m_deal.Entry() == DEAL_ENTRY_OUT && m_deal.Magic() == magic_number) { pl += m_deal.Profit(); trades_count++; report_body += StringFormat("Trade[%d] -> | ticket: %I64u | type: %s | entry: %.5f | volume: %.3f | commision: %.3f\n", trades_count, m_deal.Ticket(), EnumToString(m_deal.DealType()), m_deal.Entry(), m_deal.Volume(), m_deal.Commission() ); } } string report_header = StringFormat("<<< Daily Trading Report >>> \r\n\r\nAC Balance: %.3f\r\nAC Equity: %.3f\r\nPL: %.3f\r\nTotal Trades: %d \r\n\r\n", m_account.Balance(), m_account.Equity(), pl, trades_count ); //--- You might choose to send the reports instead of printing Comment(report_header+report_body); Print(report_header+report_body); }
策略测试器的输出结果。
CS 0 11:33:47.902 Schedule testing EA (EURUSD,H1) 2025.03.13 23:55:00 <<< Daily Trading Report >>> CS 0 11:33:47.902 Schedule testing EA (EURUSD,H1) 2025.03.13 23:55:00 CS 0 11:33:47.902 Schedule testing EA (EURUSD,H1) 2025.03.13 23:55:00 AC Balance: 2983.830 CS 0 11:33:47.902 Schedule testing EA (EURUSD,H1) 2025.03.13 23:55:00 AC Equity: 2983.200 CS 0 11:33:47.902 Schedule testing EA (EURUSD,H1) 2025.03.13 23:55:00 PL: -2.960 CS 0 11:33:47.902 Schedule testing EA (EURUSD,H1) 2025.03.13 23:55:00 Total Trades: 3 CS 0 11:33:47.902 Schedule testing EA (EURUSD,H1) 2025.03.13 23:55:00 CS 0 11:33:47.902 Schedule testing EA (EURUSD,H1) 2025.03.13 23:55:00 Trade[1] -> | ticket: 166 | type: DEAL_TYPE_SELL | entry: 1.00000 | volume: 0.010 | commision: 0.000 CS 0 11:33:47.902 Schedule testing EA (EURUSD,H1) 2025.03.13 23:55:00 Trade[2] -> | ticket: 168 | type: DEAL_TYPE_BUY | entry: 1.00000 | volume: 0.010 | commision: 0.000 CS 0 11:33:47.902 Schedule testing EA (EURUSD,H1) 2025.03.13 23:55:00 Trade[3] -> | ticket: 169 | type: DEAL_TYPE_SELL | entry: 1.00000 | volume: 0.010 | commision: 0.000 CS 0 11:33:47.902 Schedule testing EA (EURUSD,H1) 2025.03.13 23:55:00
同样的思路也可以扩展到每周五市场收盘前生成周报,以及生成月报。
Schedule 与 OnTimer
虽然直接比较这两种方法有失公允,因为它们各自都有其优点,但了解它们之间的差异以及何时使用哪种方法会更为有益。
| OnTimer | Schedule |
|---|---|
| 由于它只允许一个定时器事件(一个计划),因此当您想安排单个任务时,这种内置功能非常有用。 | 它允许多个任务,并为每个任务设置不同的间隔时间。当您希望同时执行多个计划时,这个自定义库非常有用。 |
| 它快速且有效 | 不如内置的 OnTimer 快;其效果尚待探索。 |
| 仅限于 EA 和指标。 | 它适用于所有 MQL5 程序;EA、指标和脚本。 |
它全天候 24 小时运行(始终可靠) | 它依赖于交易功能(OnTick 和 OnCalculate),这些功能只有在市场开放时才会触发。 除非在脚本的无限循环中使用,否则,监控函数 run_pending 的工作时段并非全天候,而是受市场开市时间限制。 |
要解决使用此库时出现的可靠性问题(如比较表最后一行所述),您必须在 OnTimer 函数中运行 CSchedule 类。
#include <schedule.mqh> CSchedule schedule(TIME_SOURCE_CURRENT); //Use the current broker's time //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //... other functions & lines of code //--- Schedule schedule.every().hour().at(0,0).dO(MainTradingFunction); schedule.every().day().at(23, 55).dO(SendDailyTradingReport); //every day 5 minutes before market closing //--- Ontimer EventSetTimer(1); //Run the Ontimer function after every 1 second (pretty much always) return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); //Delete the timer } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- //schedule.run_pending(); //❎ } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnTimer(void) { schedule.run_pending(); //✅ }
总结
对于算法交易者而言,能够设置特定时间表并在精确时间执行任务至关重要。不仅许多交易策略依赖于一天中的确切时间,而且我们经常希望自动化许多重复执行的活动,如发送每日交易报告、每月更新等。
这个实现的类提供了一种在 MQL5 程序中设置重复事件的简便方法,与 Python 中的 schedule 模块所提供的方法类似。
虽然 OnTimer 函数不错,但它缺少一些设置人性化时间表的关键方法。因此,在 OnTimer 不够用的场景中,可以考虑使用这个库。
再见。
附件表
| 文件名 | 描述与用途 |
|---|---|
| Experts\Schedule testing EA.mq5 | 用于安排交易操作的 EA。 |
| Include\schedule.mqh | 包含 CSchedule 类,该类可用于调度函数在特定时间和间隔运行。 |
| Scripts\schedule test.mq5 | 用于测试 CSchedule 类的脚本。 |
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/18913
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
交易中的神经网络:摆脱特定数据依赖的时间序列泛化(结论)
从基础到进阶:图表对象(I)
新手在交易中的10个基本错误
科学家群体优化(CoSO):理论