MQL5交易工具(第四部分):为多周期扫描仪表盘添加动态定位与切换功能
概述
在本系列先前的文章(第三部分)中,我们使用MetaQuotes Language 5(MQL5)开发了一款多周期扫描仪表盘,该工具可以同时显示当前交易品种在多个时间框架下的相对强弱指数(RSI)、随机指标(Stochastic)、商品通道指数(CCI)、平均趋向指数(ADX)及动量震荡指标(AO),通过跨周期指标联动分析捕捉交易信号。本文(第四部分)将对该仪表盘进行功能升级,新增动态定位系统,支持在图表上任意位置拖拽仪表盘,并新增切换功能以最小化或最大化显示,从而提升可用性和屏幕管理效率。我们将涵盖以下主题:
阅读完全文,您将掌握如何构建一个具备灵活定位和切换功能的高级MQL5仪表盘,并可对其进行测试与定制。话不多说,让我们开始!
理解动态定位与切换功能架构
我们通过添加动态定位功能来优化多时间周期扫描仪表盘,允许在图表上拖动,并提供最小化或最大化切换,从而提升可用性。这些功能对于避免图表杂乱、优化屏幕空间以进行高效交易分析至关重要。我们将实现鼠标驱动的拖动以重新定位仪表盘,以及切换按钮在紧凑视图和完整视图之间切换,同时保持指标无缝更新,以支持更好的交易决策。简单来说,目标实现效果如下所示。

在MQL5中的实现
为了在MQL5中实现这些优化功能,我们将定义一个额外的切换按钮对象,后续用于在最大化和最小化状态之间切换,而悬停和拖拽操作将基于已实现的标题栏对象完成。
// Define identifiers and properties for UI elements #define MAIN_PANEL "PANEL_MAIN" //--- Main panel rectangle identifier //--- THE REST OF THE EXISTING OBJECTS #define TOGGLE_BUTTON "BUTTON_TOGGLE" //--- Toggle (minimize/maximize) button identifier //--- THE REST OF THE EXISTING OBJECTS #define COLOR_DARK_GRAY C'105,105,105' //--- Dark gray color for indicator backgrounds
我们开始对多周期扫描仪表盘进行功能优化,首先更新用户界面(UI)元素定义,为切换按钮新增标识符,以支持添加最小化/最大化功能的目标。保留现有定义以维持仪表盘的核心结构不变。关键性改动是新增"TOGGLE_BUTTON"标识符(定义为"BUTTON_TOGGLE"),将用于创建切换仪表盘在最小化与最大化状态间转换的按钮。
这一点至关重要,它开启了全新的切换功能,用户可以折叠仪表盘节省空间,或展开查看全景,在不改动现有指标逻辑的同时提升了可用性。接下来,我们需要进一步控制全局变量,这些变量将用于存储仪表盘的当前状态。
bool panel_minimized = false; //--- Flag to control minimized state int panel_x = 632, panel_y = 40; //--- Panel position coordinates bool panel_dragging = false; //--- Flag to track if panel is being dragged int panel_drag_x = 0, panel_drag_y = 0; //--- Mouse coordinates when drag starts int panel_start_x = 0, panel_start_y = 0; //--- Panel coordinates when drag starts int prev_mouse_state = 0; //--- Variable to track previous mouse state bool header_hovered = false; //--- Header hover state bool toggle_hovered = false; //--- Toggle button hover state bool close_hovered = false; //--- Close button hover state int last_mouse_x = 0, last_mouse_y = 0; //--- Track last mouse position for optimization bool prev_header_hovered = false; //--- Track previous header hover state bool prev_toggle_hovered = false; //--- Track previous toggle hover state bool prev_close_hovered = false; //--- Track previous close button hover state
为了追踪仪表盘状态,我们新增部分全局变量。引入"panel_minimized"记录最小化状态,"panel_x"和"panel_y"存储仪表盘位置坐标,"panel_dragging"、"panel_drag_x"、"panel_drag_y"、"panel_start_x"和"panel_start_y"用于拖拽操作。添加"prev_mouse_state"、"header_hovered"、"toggle_hovered"、"close_hovered"、"last_mouse_x"、"last_mouse_y"、"prev_header_hovered"、"prev_toggle_hovered"、"prev_close_hovered"管理鼠标事件与悬停状态。这些变量将支持实现可拖拽、可交互且带切换功能的仪表盘。
接下来,我们开始将切换按钮添加至仪表盘。为管理状态切换,需要为最大化面板和最小化面板分别编写独立的创建逻辑函数。让我们先从最大化仪表盘的实现开始。
//+------------------------------------------------------------------+ //| Create full dashboard UI | //+------------------------------------------------------------------+ void create_full_dashboard() { create_rectangle(MAIN_PANEL, panel_x, panel_y, 617, 374, C'30,30,30', BORDER_FLAT); //--- Create main panel background create_rectangle(HEADER_PANEL, panel_x, panel_y, 617, 27, C'60,60,60', BORDER_FLAT); //--- Create header panel background create_label(HEADER_PANEL_ICON, CharToString(91), panel_x - 12, panel_y + 14, 18, clrAqua, "Wingdings"); //--- Create header icon create_label(HEADER_PANEL_TEXT, "TimeframeScanner", panel_x - 105, panel_y + 12, 13, COLOR_WHITE); //--- Create header title create_label(CLOSE_BUTTON, CharToString('r'), panel_x - 600, panel_y + 14, 18, clrYellow, "Webdings"); //--- Create close button create_label(TOGGLE_BUTTON, CharToString('r'), panel_x - 570, panel_y + 14, 18, clrYellow, "Wingdings"); //--- Create minimize button (-) // Create header rectangle and label create_rectangle(SYMBOL_RECTANGLE, panel_x - 2, panel_y + 35, WIDTH_TIMEFRAME, HEIGHT_RECTANGLE, clrGray); //--- Create symbol rectangle create_label(SYMBOL_TEXT, _Symbol, panel_x - 47, panel_y + 45, 11, COLOR_WHITE); //--- Create symbol label // Create summary and indicator headers (rectangles and labels) string header_names[] = {"BUY", "SELL", "RSI", "STOCH", "CCI", "ADX", "AO"}; //--- Define header titles for(int header_index = 0; header_index < ArraySize(header_names); header_index++) { //--- Loop through headers int x_offset = panel_x - WIDTH_TIMEFRAME - (header_index < 2 ? header_index * WIDTH_SIGNAL : 2 * WIDTH_SIGNAL + (header_index - 2) * WIDTH_INDICATOR) + (1 + header_index); //--- Calculate x position int width = (header_index < 2 ? WIDTH_SIGNAL : WIDTH_INDICATOR); //--- Set width based on header type create_rectangle(HEADER_RECTANGLE + IntegerToString(header_index), x_offset, panel_y + 35, width, HEIGHT_RECTANGLE, clrGray); //--- Create header rectangle create_label(HEADER_TEXT + IntegerToString(header_index), header_names[header_index], x_offset - width/2, panel_y + 45, 11, COLOR_WHITE); //--- Create header label } // Create timeframe rectangles and labels, and summary/indicator cells for(int timeframe_index = 0; timeframe_index < ArraySize(timeframes_array); timeframe_index++) { //--- Loop through timeframes // Highlight current timeframe color timeframe_background = (timeframes_array[timeframe_index] == _Period) ? clrLimeGreen : clrGray; //--- Set background color for current timeframe color timeframe_text_color = (timeframes_array[timeframe_index] == _Period) ? COLOR_BLACK : COLOR_WHITE; //--- Set text color for current timeframe create_rectangle(TIMEFRAME_RECTANGLE + IntegerToString(timeframe_index), panel_x - 2, (panel_y + 35 + HEIGHT_RECTANGLE) + timeframe_index * HEIGHT_RECTANGLE - (1 + timeframe_index), WIDTH_TIMEFRAME, HEIGHT_RECTANGLE, timeframe_background); //--- Create timeframe rectangle create_label(TIMEFRAME_TEXT + IntegerToString(timeframe_index), truncate_timeframe_name(timeframe_index), panel_x - 47, (panel_y + 45 + HEIGHT_RECTANGLE) + timeframe_index * HEIGHT_RECTANGLE - (1 + timeframe_index), 11, timeframe_text_color); //--- Create timeframe label // Create summary and indicator cells for(int header_index = 0; header_index < ArraySize(header_names); header_index++) { //--- Loop through headers for cells string cell_rectangle_name, cell_text_name; //--- Declare cell name and label variables color cell_background = (header_index < 2) ? COLOR_LIGHT_GRAY : COLOR_BLACK; //--- Set cell background color switch(header_index) { //--- Select cell type case 0: cell_rectangle_name = BUY_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = BUY_TEXT + IntegerToString(timeframe_index); break; //--- Buy cell case 1: cell_rectangle_name = SELL_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = SELL_TEXT + IntegerToString(timeframe_index); break; //--- Sell cell case 2: cell_rectangle_name = RSI_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = RSI_TEXT + IntegerToString(timeframe_index); break; //--- RSI cell case 3: cell_rectangle_name = STOCH_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = STOCH_TEXT + IntegerToString(timeframe_index); break; //--- Stochastic cell case 4: cell_rectangle_name = CCI_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = CCI_TEXT + IntegerToString(timeframe_index); break; //--- CCI cell case 5: cell_rectangle_name = ADX_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = ADX_TEXT + IntegerToString(timeframe_index); break; //--- ADX cell case 6: cell_rectangle_name = AO_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = AO_TEXT + IntegerToString(timeframe_index); break; //--- AO cell } int x_offset = panel_x - WIDTH_TIMEFRAME - (header_index < 2 ? header_index * WIDTH_SIGNAL : 2 * WIDTH_SIGNAL + (header_index - 2) * WIDTH_INDICATOR) + (1 + header_index); //--- Calculate x position int width = (header_index < 2 ? WIDTH_SIGNAL : WIDTH_INDICATOR); //--- Set width based on cell type create_rectangle(cell_rectangle_name, x_offset, (panel_y + 35 + HEIGHT_RECTANGLE) + timeframe_index * HEIGHT_RECTANGLE - (1 + timeframe_index), width, HEIGHT_RECTANGLE, cell_background); //--- Create cell rectangle create_label(cell_text_name, "-/-", x_offset - width/2, (panel_y + 45 + HEIGHT_RECTANGLE) + timeframe_index * HEIGHT_RECTANGLE - (1 + timeframe_index), 10, COLOR_WHITE); //--- Create cell label } } ChartRedraw(0); }
我们通过创建"create_full_dashboard"函数实现仪表盘状态管理,将原有的静态创建逻辑迁移至该函数,并更新为支持动态定位和切换功能。核心变更是将"panel_x"和"panel_y"全局变量集成到所有仪表盘元素的定位逻辑中,使仪表盘可放置在图表的任意位置。我们使用"create_rectangle"函数并传入"MAIN_PANEL"创建主面板,通过"panel_x"和"panel_y"确定位置,保持尺寸为617x374像素:同样地,我们使用"create_rectangle"和"create_label"定位标题面板、图标和标题,分别为"HEADER_PANEL"、"HEADER_PANEL_ICON"和"HEADER_PANEL_TEXT",并根据"panel_x"和"panel_y"调整它们的x和y坐标。
我们通过"create_label"函数新增切换按钮"TOGGLE_BUTTON",将其定位在"panel_x - 570"和"panel_y + 14"处,并使用Webdings字体中的缩小符号('r')作为按钮图标。符号背景矩形"SYMBOL_RECTANGLE"和图标文本"SYMBOL_TEXT",以及标题栏背景矩形"HEADER_RECTANGLE"和标题文本"HEADER_TEXT"的坐标全部基于"panel_x"和"panel_y"偏移,确保它们随面板整体移动。对"timeframes_array"中的每个时间周期,我们创建时间周期矩形TIMEFRAME_RECTANGLE"、标签文本"TIMEFRAME_TEXT"及指标单元格(如"RSI_RECTANGLE"、"STOCH_RECTANGLE"等),其位置根据"panel_x"和 "panel_y"动态计算,保留原有布局但可以重新定位。我们调用ChartRedraw函数更新显示效果。与上一版本固定坐标(如632, 40)不同,本函数使用动态坐标,实现仪表盘组件可拖拽,并新增最小化/最大化切换按钮。最终效果与以下示例相似:

接下来,我们可以构建一个函数来生成最小化面板。
//+------------------------------------------------------------------+ //| Create minimized dashboard UI | //+------------------------------------------------------------------+ void create_minimized_dashboard() { create_rectangle(HEADER_PANEL, panel_x, panel_y, 617, 27, C'60,60,60', BORDER_FLAT); //--- Create header panel background create_label(HEADER_PANEL_ICON, CharToString(91), panel_x - 12, panel_y + 14, 18, clrAqua, "Wingdings"); //--- Create header icon create_label(HEADER_PANEL_TEXT, "TimeframeScanner", panel_x - 105, panel_y + 12, 13, COLOR_WHITE); //--- Create header title create_label(CLOSE_BUTTON, CharToString('r'), panel_x - 600, panel_y + 14, 18, clrYellow, "Webdings"); //--- Create close button create_label(TOGGLE_BUTTON, CharToString('o'), panel_x - 570, panel_y + 14, 18, clrYellow, "Wingdings"); //--- Create maximize button (+) ChartRedraw(0); }
为支持切换至紧凑视图,我们定义"create_minimized_dashboard"函数。我们使用"create_rectangle"在"panel_x"和"panel_y"处创建标题面板"HEADER_PANEL",通过"create_label"添加"HEADER_PANEL_ICON"和"HEADER_PANEL_TEXT",包含"CLOSE_BUTTON",并使用最大化符号(Webdings字体中的 'o')在"panel_x - 570"和"panel_y + 14"处添加切换按钮"TOGGLE_BUTTON"。我们调用"ChartRedraw"更新显示,实现可移动的最小化仪表盘状态。正如您所见,我们为最大化和最小化按钮选择Wingdings字体以保持风格一致。您可以根据喜好选择任意字体。在我们的示例中,图标字符分别为'r'(最小化)和'o'(最大化)以下是它们的集中展示效果:

当运行最小化面板状态时,结果如下:

借助这两个函数,我们就能根据用户指令切换仪表盘视图模式(完整视图/紧凑视图)。由于涉及多个图形对象,我们需要集中管理对象的销毁逻辑——每次切换视图时,必须先删除旧对象,再创建新对象。
//+------------------------------------------------------------------+ //| Delete all dashboard objects | //+------------------------------------------------------------------+ void delete_all_objects() { ObjectDelete(0, MAIN_PANEL); //--- Delete main panel ObjectDelete(0, HEADER_PANEL); //--- Delete header panel ObjectDelete(0, HEADER_PANEL_ICON); //--- Delete header icon ObjectDelete(0, HEADER_PANEL_TEXT); //--- Delete header title ObjectDelete(0, CLOSE_BUTTON); //--- Delete close button ObjectDelete(0, TOGGLE_BUTTON); //--- Delete toggle button ObjectsDeleteAll(0, SYMBOL_RECTANGLE); //--- Delete all symbol rectangles ObjectsDeleteAll(0, SYMBOL_TEXT); //--- Delete all symbol labels ObjectsDeleteAll(0, TIMEFRAME_RECTANGLE); //--- Delete all timeframe rectangles ObjectsDeleteAll(0, TIMEFRAME_TEXT); //--- Delete all timeframe labels ObjectsDeleteAll(0, HEADER_RECTANGLE); //--- Delete all header rectangles ObjectsDeleteAll(0, HEADER_TEXT); //--- Delete all header labels ObjectsDeleteAll(0, RSI_RECTANGLE); //--- Delete all RSI rectangles ObjectsDeleteAll(0, RSI_TEXT); //--- Delete all RSI labels ObjectsDeleteAll(0, STOCH_RECTANGLE); //--- Delete all Stochastic rectangles ObjectsDeleteAll(0, STOCH_TEXT); //--- Delete all Stochastic labels ObjectsDeleteAll(0, CCI_RECTANGLE); //--- Delete all CCI rectangles ObjectsDeleteAll(0, CCI_TEXT); //--- Delete all CCI labels ObjectsDeleteAll(0, ADX_RECTANGLE); //--- Delete all ADX rectangles ObjectsDeleteAll(0, ADX_TEXT); //--- Delete all ADX labels ObjectsDeleteAll(0, AO_RECTANGLE); //--- Delete all AO rectangles ObjectsDeleteAll(0, AO_TEXT); //--- Delete all AO labels ObjectsDeleteAll(0, BUY_RECTANGLE); //--- Delete all buy rectangles ObjectsDeleteAll(0, BUY_TEXT); //--- Delete all buy labels ObjectsDeleteAll(0, SELL_RECTANGLE); //--- Delete all sell rectangles ObjectsDeleteAll(0, SELL_TEXT); //--- Delete all sell labels }
为了更精准地控制对象的删除时机,我们创建并更新"delete_all_objects"函数,新增对"TOGGLE_BUTTON"的移除逻辑,以支持切换功能的改进。我们将ObjectDelete函数用于"TOGGLE_BUTTON"添加到待删除对象列表中,确保切换按钮在仪表盘关闭或切换时被正确删除。我们保留对其它对象的删除操作,包括"MAIN_PANEL"、"HEADER_PANEL"、"HEADER_PANEL_ICON"、"HEADER_PANEL_TEXT"、"CLOSE_BUTTON",以及所有交易品种、时间周期、标题、指标以及信号相关的矩形和标签,通过调用ObjectsDeleteAll函数,完成对所有关联元素的清理。这一变更确保可移动、可最小化的仪表盘在隐藏或重新初始化时,能够清理所有组件(包括新增的切换按钮),保持图表整洁。
为了优化动态定位功能,我们需要构建一个函数,能够根据光标位置创建和更新仪表盘元素。以下是实现这一逻辑的方法:
//+------------------------------------------------------------------+ //| Update panel object positions | //+------------------------------------------------------------------+ void update_panel_positions() { // Update header and buttons ObjectSetInteger(0, HEADER_PANEL, OBJPROP_XDISTANCE, panel_x); //--- Set header panel x position ObjectSetInteger(0, HEADER_PANEL, OBJPROP_YDISTANCE, panel_y); //--- Set header panel y position ObjectSetInteger(0, HEADER_PANEL_ICON, OBJPROP_XDISTANCE, panel_x - 12); //--- Set header icon x position ObjectSetInteger(0, HEADER_PANEL_ICON, OBJPROP_YDISTANCE, panel_y + 14); //--- Set header icon y position ObjectSetInteger(0, HEADER_PANEL_TEXT, OBJPROP_XDISTANCE, panel_x - 105); //--- Set header title x position ObjectSetInteger(0, HEADER_PANEL_TEXT, OBJPROP_YDISTANCE, panel_y + 12); //--- Set header title y position ObjectSetInteger(0, CLOSE_BUTTON, OBJPROP_XDISTANCE, panel_x - 600); //--- Set close button x position ObjectSetInteger(0, CLOSE_BUTTON, OBJPROP_YDISTANCE, panel_y + 14); //--- Set close button y position ObjectSetInteger(0, TOGGLE_BUTTON, OBJPROP_XDISTANCE, panel_x - 570); //--- Set toggle button x position ObjectSetInteger(0, TOGGLE_BUTTON, OBJPROP_YDISTANCE, panel_y + 14); //--- Set toggle button y position if (!panel_minimized) { // Update main panel ObjectSetInteger(0, MAIN_PANEL, OBJPROP_XDISTANCE, panel_x); //--- Set main panel x position ObjectSetInteger(0, MAIN_PANEL, OBJPROP_YDISTANCE, panel_y); //--- Set main panel y position // Update symbol rectangle and label ObjectSetInteger(0, SYMBOL_RECTANGLE, OBJPROP_XDISTANCE, panel_x - 2); //--- Set symbol rectangle x position ObjectSetInteger(0, SYMBOL_RECTANGLE, OBJPROP_YDISTANCE, panel_y + 35); //--- Set symbol rectangle y position ObjectSetInteger(0, SYMBOL_TEXT, OBJPROP_XDISTANCE, panel_x - 47); //--- Set symbol text x position ObjectSetInteger(0, SYMBOL_TEXT, OBJPROP_YDISTANCE, panel_y + 45); //--- Set symbol text y position // Update header rectangles and labels string header_names[] = {"BUY", "SELL", "RSI", "STOCH", "CCI", "ADX", "AO"}; for(int header_index = 0; header_index < ArraySize(header_names); header_index++) { //--- Loop through headers int x_offset = panel_x - WIDTH_TIMEFRAME - (header_index < 2 ? header_index * WIDTH_SIGNAL : 2 * WIDTH_SIGNAL + (header_index - 2) * WIDTH_INDICATOR) + (1 + header_index); //--- Calculate x position ObjectSetInteger(0, HEADER_RECTANGLE + IntegerToString(header_index), OBJPROP_XDISTANCE, x_offset); //--- Set header rectangle x position ObjectSetInteger(0, HEADER_RECTANGLE + IntegerToString(header_index), OBJPROP_YDISTANCE, panel_y + 35); //--- Set header rectangle y position ObjectSetInteger(0, HEADER_TEXT + IntegerToString(header_index), OBJPROP_XDISTANCE, x_offset - (header_index < 2 ? WIDTH_SIGNAL/2 : WIDTH_INDICATOR/2)); //--- Set header text x position ObjectSetInteger(0, HEADER_TEXT + IntegerToString(header_index), OBJPROP_YDISTANCE, panel_y + 45); //--- Set header text y position } // Update timeframe rectangles, labels, and cells for(int timeframe_index = 0; timeframe_index < ArraySize(timeframes_array); timeframe_index++) { //--- Loop through timeframes int y_offset = (panel_y + 35 + HEIGHT_RECTANGLE) + timeframe_index * HEIGHT_RECTANGLE - (1 + timeframe_index); //--- Calculate y position ObjectSetInteger(0, TIMEFRAME_RECTANGLE + IntegerToString(timeframe_index), OBJPROP_XDISTANCE, panel_x - 2); //--- Set timeframe rectangle x position ObjectSetInteger(0, TIMEFRAME_RECTANGLE + IntegerToString(timeframe_index), OBJPROP_YDISTANCE, y_offset); //--- Set timeframe rectangle y position ObjectSetInteger(0, TIMEFRAME_TEXT + IntegerToString(timeframe_index), OBJPROP_XDISTANCE, panel_x - 47); //--- Set timeframe text x position ObjectSetInteger(0, TIMEFRAME_TEXT + IntegerToString(timeframe_index), OBJPROP_YDISTANCE, y_offset + 10); //--- Set timeframe text y position for(int header_index = 0; header_index < ArraySize(header_names); header_index++) { //--- Loop through cells string cell_rectangle_name, cell_text_name; switch(header_index) { //--- Select cell type case 0: cell_rectangle_name = BUY_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = BUY_TEXT + IntegerToString(timeframe_index); break; //--- Buy cell case 1: cell_rectangle_name = SELL_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = SELL_TEXT + IntegerToString(timeframe_index); break; //--- Sell cell case 2: cell_rectangle_name = RSI_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = RSI_TEXT + IntegerToString(timeframe_index); break; //--- RSI cell case 3: cell_rectangle_name = STOCH_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = STOCH_TEXT + IntegerToString(timeframe_index); break; //--- Stochastic cell case 4: cell_rectangle_name = CCI_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = CCI_TEXT + IntegerToString(timeframe_index); break; //--- CCI cell case 5: cell_rectangle_name = ADX_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = ADX_TEXT + IntegerToString(timeframe_index); break; //--- ADX cell case 6: cell_rectangle_name = AO_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = AO_TEXT + IntegerToString(timeframe_index); break; //--- AO cell } int x_offset = panel_x - WIDTH_TIMEFRAME - (header_index < 2 ? header_index * WIDTH_SIGNAL : 2 * WIDTH_SIGNAL + (header_index - 2) * WIDTH_INDICATOR) + (1 + header_index); //--- Calculate x position int width = (header_index < 2 ? WIDTH_SIGNAL : WIDTH_INDICATOR); //--- Set cell width ObjectSetInteger(0, cell_rectangle_name, OBJPROP_XDISTANCE, x_offset); //--- Set cell rectangle x position ObjectSetInteger(0, cell_rectangle_name, OBJPROP_YDISTANCE, y_offset); //--- Set cell rectangle y position ObjectSetInteger(0, cell_text_name, OBJPROP_XDISTANCE, x_offset - width/2); //--- Set cell text x position ObjectSetInteger(0, cell_text_name, OBJPROP_YDISTANCE, y_offset + 10); //--- Set cell text y position } } } ChartRedraw(0); //--- Redraw chart }
为支持仪表盘的动态定位,我们引入新的"update_panel_positions"函数。该函数根据当前的"panel_x"和"panel_y"坐标调整所有仪表盘元素的位置,以便能够在图表上拖拽仪表盘。我们使用ObjectSetInteger函数配合OBJPROP_XDISTANCE和"OBJPROP_YDISTANCE"更新标题面板、图标、标题文本、关闭按钮和切换按钮("HEADER_PANEL"、"HEADER_PANEL_ICON"、"HEADER_PANEL_TEXT"、"CLOSE_BUTTON"、"TOGGLE_BUTTON")的位置,使它们全部基于"panel_x"和"panel_y"定位。
如果"panel_minimized"为 false,我们重新定位主面板"MAIN_PANEL"、品种矩形"SYMBOL_RECTANGLE"与标签"SYMBOL_TEXT"、标题矩形"HEADER_RECTANGLE"与标签"HEADER_TEXT",以及时间周期矩形"TIMEFRAME_RECTANGLE"、标签文本"TIMEFRAME_TEXT"和指标单元格("BUY_RECTANGLE"、"RSI_RECTANGLE"等),并使用基于"panel_x"和"panel_y"计算的偏移量。我们调用ChartRedraw函数刷新显示效果。该函数确保拖拽仪表盘时所有元素同步移动,这是可移动仪表盘优化功能的关键特性。接下来,我们来测试仪表盘。为此,我们将在OnInit事件处理器中调用创建完整仪表盘的函数,并在OnDeinit事件处理器中调用销毁函数。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() //--- Initialize EA { create_full_dashboard(); //--- Create full dashboard ArraySetAsSeries(rsi_values, true); //--- Set RSI array as timeseries ArraySetAsSeries(stochastic_values, true); //--- Set Stochastic array as timeseries ArraySetAsSeries(cci_values, true); //--- Set CCI array as timeseries ArraySetAsSeries(adx_values, true); //--- Set ADX array as timeseries ArraySetAsSeries(ao_values, true); //--- Set AO array as timeseries ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); //--- Enable mouse move events return(INIT_SUCCEEDED); //--- Return initialization success } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) //--- Deinitialize EA { delete_all_objects(); //--- Delete all objects ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); //--- Disable mouse move events ChartRedraw(0); //--- Redraw chart }
在此阶段,我们仅在程序初始化和反初始化时调用相应函数,以便在进入下一阶段之前测试基本响应。更规范的编程方式是始终分阶段编译程序,以确保其正常运行。编译后,我们得到以下结果:

从可视化效果来看,我们已成功实现面板的初始化与移除功能。接下来,我们将开始实现仪表盘的交互响应性。我们需要追踪坐标位置,以判断其是否位于标题栏或者按钮区域,从而明确用户的具体操作意图。我们还需获取按钮的移动路径,以便通过改变颜色来感知和可视化悬停状态与点击状态。首先,让我们定义一个函数来确定光标相对于仪表盘元素的位置。
//+------------------------------------------------------------------+ //| Check if cursor is inside header or buttons | //+------------------------------------------------------------------+ bool is_cursor_in_header_or_buttons(int mouse_x, int mouse_y) { int chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); // Header panel bounds int header_x = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_XDISTANCE); int header_y = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_YDISTANCE); int header_width = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_XSIZE); int header_height = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_YSIZE); int header_left = chart_width - header_x; int header_right = header_left + header_width; bool in_header = (mouse_x >= header_left && mouse_x <= header_right && mouse_y >= header_y && mouse_y <= header_y + header_height); // Close button bounds int close_x = (int)ObjectGetInteger(0, CLOSE_BUTTON, OBJPROP_XDISTANCE); int close_y = (int)ObjectGetInteger(0, CLOSE_BUTTON, OBJPROP_YDISTANCE); int close_width = 20; int close_height = 20; int close_left = chart_width - close_x; int close_right = close_left + close_width; bool in_close = (mouse_x >= close_left && mouse_x <= close_right && mouse_y >= close_y && mouse_y <= close_y + close_height); // Toggle button bounds int toggle_x = (int)ObjectGetInteger(0, TOGGLE_BUTTON, OBJPROP_XDISTANCE); int toggle_y = (int)ObjectGetInteger(0, TOGGLE_BUTTON, OBJPROP_YDISTANCE); int toggle_width = 20; int toggle_height = 20; int toggle_left = chart_width - toggle_x; int toggle_right = toggle_left + toggle_width; bool in_toggle = (mouse_x >= toggle_left && mouse_x <= toggle_right && mouse_y >= toggle_y && mouse_y <= toggle_y + toggle_height); return in_header || in_close || in_toggle; }
首先,我们引入一个全新的"is_cursor_in_header_or_buttons"函数,用于支持仪表盘的动态定位和切换功能。该函数会检测光标是否悬停在标题面板、关闭按钮或切换按钮上,从而启用交互式拖拽和按钮操作。我们首先使用ChartGetInteger配合CHART_WIDTH_IN_PIXELS获取图表宽度。对于标题面板,我们使用ObjectGetInteger配合"OBJPROP_XDISTANCE"、"OBJPROP_YDISTANCE"、"OBJPROP_XSIZE"和OBJPROP_YSIZE,获取"HEADER_PANEL"的坐标和尺寸属性,如"header_x"、"header_y"、"header_width"和 "header_height",并且相对于图表宽度计算"header_left"和"header_right"。我们检查"mouse_x"和"mouse_y"是否在此范围内,如果是,则将"in_header"设置为true。
对于关闭按钮,我们获取"CLOSE_BUTTON"的"close_x"和"close_y",定义一个20×20像素的点击区域,计算"close_left"和"close_right",如果光标在此区域内,则将"in_close"设置为true。类似地,对于切换按钮,我们获取"TOGGLE_BUTTON"的"toggle_x"和"toggle_y",定义一个20×20像素的点击区域,如果光标在此区域内,则将"in_toggle"设置为true。如果光标在任一区域内("in_header", "in_close"或"in_toggle"),我们返回true。该函数对于检测鼠标与仪表盘可拖动标题及按钮的交互至关重要,是实现优化可移动和交互功能的核心。接下来,我们可以通过颜色可视化来更新悬停状态,以便更容易识别。
//+------------------------------------------------------------------+ //| Update button hover states | //+------------------------------------------------------------------+ void update_button_hover_states(int mouse_x, int mouse_y) { int chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); // Close button hover int close_x = (int)ObjectGetInteger(0, CLOSE_BUTTON, OBJPROP_XDISTANCE); int close_y = (int)ObjectGetInteger(0, CLOSE_BUTTON, OBJPROP_YDISTANCE); int close_width = 20; int close_height = 20; int close_left = chart_width - close_x; int close_right = close_left + close_width; bool is_close_hovered = (mouse_x >= close_left && mouse_x <= close_right && mouse_y >= close_y && mouse_y <= close_y + close_height); if (is_close_hovered != prev_close_hovered) { ObjectSetInteger(0, CLOSE_BUTTON, OBJPROP_COLOR, is_close_hovered ? clrWhite : clrYellow); ObjectSetInteger(0, CLOSE_BUTTON, OBJPROP_BGCOLOR, is_close_hovered ? clrDodgerBlue : clrNONE); prev_close_hovered = is_close_hovered; ChartRedraw(0); } // Toggle button hover int toggle_x = (int)ObjectGetInteger(0, TOGGLE_BUTTON, OBJPROP_XDISTANCE); int toggle_y = (int)ObjectGetInteger(0, TOGGLE_BUTTON, OBJPROP_YDISTANCE); int toggle_width = 20; int toggle_height = 20; int toggle_left = chart_width - toggle_x; int toggle_right = toggle_left + toggle_width; bool is_toggle_hovered = (mouse_x >= toggle_left && mouse_x <= toggle_right && mouse_y >= toggle_y && mouse_y <= toggle_y + toggle_height); if (is_toggle_hovered != prev_toggle_hovered) { ObjectSetInteger(0, TOGGLE_BUTTON, OBJPROP_COLOR, is_toggle_hovered ? clrWhite : clrYellow); ObjectSetInteger(0, TOGGLE_BUTTON, OBJPROP_BGCOLOR, is_toggle_hovered ? clrDodgerBlue : clrNONE); prev_toggle_hovered = is_toggle_hovered; ChartRedraw(0); } } //+------------------------------------------------------------------+ //| Update header hover state | //+------------------------------------------------------------------+ void update_header_hover_state(int mouse_x, int mouse_y) { int chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int header_x = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_XDISTANCE); int header_y = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_YDISTANCE); int header_width = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_XSIZE); int header_height = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_YSIZE); int header_left = chart_width - header_x; int header_right = header_left + header_width; // Exclude button areas from header hover int close_x = (int)ObjectGetInteger(0, CLOSE_BUTTON, OBJPROP_XDISTANCE); int close_y = (int)ObjectGetInteger(0, CLOSE_BUTTON, OBJPROP_YDISTANCE); int close_width = 20; int close_height = 20; int close_left = chart_width - close_x; int close_right = close_left + close_width; int toggle_x = (int)ObjectGetInteger(0, TOGGLE_BUTTON, OBJPROP_XDISTANCE); int toggle_y = (int)ObjectGetInteger(0, TOGGLE_BUTTON, OBJPROP_YDISTANCE); int toggle_width = 20; int toggle_height = 20; int toggle_left = chart_width - toggle_x; int toggle_right = toggle_left + toggle_width; bool is_header_hovered = (mouse_x >= header_left && mouse_x <= header_right && mouse_y >= header_y && mouse_y <= header_y + header_height && !(mouse_x >= close_left && mouse_x <= close_right && mouse_y >= close_y && mouse_y <= close_y + close_height) && !(mouse_x >= toggle_left && mouse_x <= toggle_right && mouse_y >= toggle_y && mouse_y <= toggle_y + toggle_height)); if (is_header_hovered != prev_header_hovered && !panel_dragging) { ObjectSetInteger(0, HEADER_PANEL, OBJPROP_BGCOLOR, is_header_hovered ? clrRed : C'60,60,60'); prev_header_hovered = is_header_hovered; ChartRedraw(0); } update_button_hover_states(mouse_x, mouse_y); }
在此阶段,我们引入两个新函数"update_button_hover_states"和"update_header_hover_state",为用户交互添加视觉反馈,提升了仪表盘的易用性。我们从"update_button_hover_states"函数开始,该函数接收"mouse_x"和"mouse_y"以检测关闭按钮和切换按钮上的悬停。对于"CLOSE_BUTTON",我们使用ObjectGetInteger配合"OBJPROP_XDISTANCE"和"OBJPROP_YDISTANCE"获取"close_x"和"close_y",根据ChartGetInteger配合"CHART_WIDTH_IN_PIXELS"获取的图表宽度计算20×20像素区域,如果光标在此区域内,则设置"is_close_hovered"。
如果"is_close_hovered"与"prev_close_hovered"不同,我们使用"ObjectSetInteger"更新"CLOSE_BUTTON":悬停时设置"OBJPROP_COLOR"为"clrWhite"、OBJPROP_BGCOLOR为"clrDodgerBlue";未悬停时设置 OBJPROP_COLOR 为 clrYellow、OBJPROP_BGCOLOR 为 clrNONE。之后更新"prev_close_hovered"并调用ChartRedraw函数。类似地,对于"TOGGLE_BUTTON",我们获取"toggle_x"和"toggle_y",检查20×20像素区域,如果"is_toggle_hovered"发生变更,则更新其颜色和"prev_toggle_hovered",确保按钮反馈相应足够灵敏。
接下来,我们构建"update_header_hover_state"函数,同样接收"mouse_x"和"mouse_y"。我们通过"ObjectGetInteger"获取"HEADER_PANEL"的 "header_x"、"header_y"、"header_width"和 "header_height",计算标题栏边界,并排除"CLOSE_BUTTON"和"TOGGLE_BUTTON"的区域(各20×20像素),以避免重叠。如果"is_header_hovered"与"prev_header_hovered"不同且"panel_dragging"为 false,我们更新"HEADER_PANEL"的"OBJPROP_BGCOLOR":悬停时为"clrRed",未悬停时为"C'60,60,60'",然后更新"prev_header_hovered"并调用 "ChartRedraw"。接下来调用"update_button_hover_states"确保按钮状态同步更新。这些函数为拖拽和按钮交互提供视觉提示,优化仪表盘的交互性。然后我们可以在OnChartEvent事件处理器中使用这些函数以实现完整的功能。我们应用的逻辑如下:
//+------------------------------------------------------------------+ //| Expert chart event handler | //+------------------------------------------------------------------+ void OnChartEvent(const int event_id, const long& long_param, const double& double_param, const string& string_param) { if (event_id == CHARTEVENT_OBJECT_CLICK) { //--- Handle object click event if (string_param == CLOSE_BUTTON) { //--- Check if close button clicked Print("Closing the panel now"); //--- Log panel closure PlaySound("alert.wav"); //--- Play alert sound panel_is_visible = false; //--- Hide panel delete_all_objects(); //--- Delete all objects ChartRedraw(0); //--- Redraw chart } else if (string_param == TOGGLE_BUTTON) { //--- Toggle button clicked delete_all_objects(); //--- Delete current UI panel_minimized = !panel_minimized; //--- Toggle minimized state if (panel_minimized) { Print("Minimizing the panel"); //--- Log minimization create_minimized_dashboard(); //--- Create minimized UI } else { Print("Maximizing the panel"); //--- Log maximization create_full_dashboard(); //--- Create full UI } // Reset hover states after toggle prev_header_hovered = false; prev_close_hovered = false; prev_toggle_hovered = false; ObjectSetInteger(0, HEADER_PANEL, OBJPROP_BGCOLOR, C'60,60,60'); ObjectSetInteger(0, CLOSE_BUTTON, OBJPROP_COLOR, clrYellow); ObjectSetInteger(0, CLOSE_BUTTON, OBJPROP_BGCOLOR, clrNONE); ObjectSetInteger(0, TOGGLE_BUTTON, OBJPROP_COLOR, clrYellow); ObjectSetInteger(0, TOGGLE_BUTTON, OBJPROP_BGCOLOR, clrNONE); ChartRedraw(0); } } else if (event_id == CHARTEVENT_MOUSE_MOVE && panel_is_visible) { //--- Handle mouse move events int mouse_x = (int)long_param; //--- Get mouse x-coordinate int mouse_y = (int)double_param; //--- Get mouse y-coordinate int mouse_state = (int)string_param; //--- Get mouse state if (mouse_x == last_mouse_x && mouse_y == last_mouse_y && !panel_dragging) { //--- Skip redundant updates return; } last_mouse_x = mouse_x; //--- Update last mouse x position last_mouse_y = mouse_y; //--- Update last mouse y position update_header_hover_state(mouse_x, mouse_y); //--- Update header and button hover states int chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int header_x = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_XDISTANCE); int header_y = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_YDISTANCE); int header_width = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_XSIZE); int header_height = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_YSIZE); int header_left = chart_width - header_x; int header_right = header_left + header_width; int close_x = (int)ObjectGetInteger(0, CLOSE_BUTTON, OBJPROP_XDISTANCE); int close_width = 20; int close_left = chart_width - close_x; int close_right = close_left + close_width; int toggle_x = (int)ObjectGetInteger(0, TOGGLE_BUTTON, OBJPROP_XDISTANCE); int toggle_width = 20; int toggle_left = chart_width - toggle_x; int toggle_right = toggle_left + toggle_width; if (prev_mouse_state == 0 && mouse_state == 1) { //--- Detect mouse button down if (mouse_x >= header_left && mouse_x <= header_right && mouse_y >= header_y && mouse_y <= header_y + header_height && !(mouse_x >= close_left && mouse_x <= close_right) && !(mouse_x >= toggle_left && mouse_x <= toggle_right)) { //--- Exclude button areas panel_dragging = true; //--- Start dragging panel_drag_x = mouse_x; //--- Store mouse x-coordinate panel_drag_y = mouse_y; //--- Store mouse y-coordinate panel_start_x = header_x; //--- Store panel x-coordinate panel_start_y = header_y; //--- Store panel y-coordinate ObjectSetInteger(0, HEADER_PANEL, OBJPROP_BGCOLOR, clrMediumBlue); //--- Set header to blue on drag start ChartSetInteger(0, CHART_MOUSE_SCROLL, false); //--- Disable chart scrolling } } if (panel_dragging && mouse_state == 1) { //--- Handle dragging int dx = mouse_x - panel_drag_x; //--- Calculate x displacement int dy = mouse_y - panel_drag_y; //--- Calculate y displacement panel_x = panel_start_x - dx; //--- Update panel x-position (inverted for CORNER_RIGHT_UPPER) panel_y = panel_start_y + dy; //--- Update panel y-position int chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get chart width int chart_height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get chart height panel_x = MathMax(617, MathMin(chart_width, panel_x)); //--- Keep panel within right edge panel_y = MathMax(0, MathMin(chart_height - (panel_minimized ? 27 : 374), panel_y)); //--- Adjust height based on state update_panel_positions(); //--- Update all panel object positions ChartRedraw(0); //--- Redraw chart during dragging } if (mouse_state == 0 && prev_mouse_state == 1) { //--- Detect mouse button release if (panel_dragging) { panel_dragging = false; //--- Stop dragging update_header_hover_state(mouse_x, mouse_y); //--- Update hover state immediately after drag ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Re-enable chart scrolling ChartRedraw(0); //--- Redraw chart } } prev_mouse_state = mouse_state; //--- Update previous mouse state } }
在此阶段,我们通过更新OnChartEvent事件处理器来优化程序,支持动态定位和切换功能,较之前版本有显著提升。我们保留对"CLOSE_BUTTON"的CHARTEVENT_OBJECT_CLICK处理:使用Print记录关闭日志,使用PlaySound播放音效,将"panel_is_visible"设置为false,调用"delete_all_objects"函数,并重绘图表。
新增功能是处理"TOGGLE_BUTTON"点击:我们调用"delete_all_objects"函数,切换"panel_minimized"状态,再根据状态选择创建最小化仪表盘"create_minimized_dashboard"(记录"最小化面板")或完整仪表盘 "create_full_dashboard"(记录"最大化面板")。我们将悬停状态("prev_header_hovered"、"prev_close_hovered"、"prev_toggle_hovered")重置为false,使用ObjectSetInteger函数恢复 "HEADER_PANEL"、"CLOSE_BUTTON"和"TOGGLE_BUTTON"的默认颜色,并重绘图表。
对于动态定位,当"panel_is_visible"为true时,我们添加CHARTEVENT_MOUSE_MOVE处理。从事件参数获取"mouse_x"、"mouse_y"和"mouse_state",如果坐标与"last_mouse_x"和"last_mouse_y"相同,且"panel_dragging"为false,则跳过冗余的更新,否则,更新这些记录的坐标。我们调用"update_header_hover_state"管理悬停效果。如果"prev_mouse_state"为0且"mouse_state"为 1,我们使用"ObjectGetInteger"和ChartGetInteger检查光标是否在"HEADER_PANEL"上(排除"CLOSE_BUTTON"和"TOGGLE_BUTTON"区域),然后设置"panel_dragging" 为true,将坐标存入"panel_drag_x"、"panel_drag_y"、"panel_start_x"和"panel_start_y",设置"HEADER_PANEL"颜色为"clrMediumBlue",并通过"ChartSetInteger"禁用图表滚动。
当"panel_dragging"和"mouse_state"均为1时,我们计算位移,在图表边界内更新"panel_x"和"panel_y",调用"update_panel_positions",并重绘图表。当鼠标释放时,我们停止拖拽,更新悬停状态,重新启用滚动,并重绘。之后更新"prev_mouse_state"。这些改动实现了拖拽和切换功能,与之前仅支持静态点击的版本不同。编译后,我们得到以下结果:

从可视化效果来看,仪表盘已经初具雏形,但我们需要处理事件处理器之间的冲突。我们需要赋予某个事件处理器优先权,以提高效率。例如,当我们处于悬停、拖动状态,甚至最小化模式时,需要优先处理图表事件。如果处于休眠状态或最大化状态,则需要优先更新仪表盘。我们将通过修改行情数据(tick)更新事件来实现这一目标,因为这是更新仪表盘数据的核心环节。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() //--- Handle tick events { if (panel_is_visible && !panel_minimized && !is_cursor_in_header_or_buttons(last_mouse_x, last_mouse_y)) { //--- Update indicators only if panel is visible, not minimized, and cursor is not in header/buttons updateIndicators(); //--- Update indicators } }
我们通过改进OnTick事件处理器,优化了指标的更新逻辑。与之前仅根据"panel_is_visible"状态调用"updateIndicators"函数的版本不同,我们现在新增了条件:仅当"panel_is_visible"为true,且"panel_minimized"为 false,且光标未悬停在标题栏或按钮区域(通过"is_cursor_in_header_or_buttons"函数,结合"last_mouse_x"和"last_mouse_y"坐标判断)此修改将确保当仪表盘最小化,或用户正在与标题栏、关闭按钮、切换按钮交互时,暂停指标更新,从而减少拖拽或切换操作期间的冗余计算,提升整体性能。编译后,呈现如下效果:

由可视化结果可见,仪表盘的性能得到了显著提升,且所有预设目标均已达成。当前仅需完成项目可操作性的测试工作,该部分内容已在前文章节中详细阐述。
回测
我们已完成测试,以下是整合后的可视化结果,以单张图形交换格式(GIF)位图图像形式呈现。

结论
总体而言,我们在第三部分可移动界面的基础上,为MQL5多时间周期扫描仪表盘新增了动态定位与切换功能,实现了窗口最小化/最大化切换及交互式悬停效果,显著提升了用户的操控体验。我们通过"create_minimized_dashboard"和"update_header_hover_state"等函数演示了这些改进的具体实现方式,确保新功能与现有指标网格无缝集成,为实时交易分析提供流畅体验。用户可根据自身交易需求进一步自定义该仪表盘,从而更高效地监控多周期市场信号,提升决策效率。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/18786
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
从基础到中级:事件(二)
MQL5交易策略自动化(第二十二部分):构建基于包络线趋势交易的区间补仓系统
您应当知道的 MQL5 向导技术(第 64 部分):运用 DeMarker 和包络通道形态,搭配白噪内核
从新手到专家:使用 MQL5 制作动画新闻标题(七)—— 新闻交易的后冲击策略
谢谢 Allan,这很酷,记录得很好,而且涵盖了我不知道的功能,非常感谢。