EA定制

MQL 语言介绍

MQL (MetaQuotes Language) 是 MetaQuotes 公司为 MT4/MT5 平台开发的专用编程语言,用于编写 EA(交易机器人)、自定义指标和脚本。

🤖

EA (Expert Advisor)

自动交易机器人,可执行开仓、平仓、止损止盈等操作

📈

Custom Indicator

自定义技术指标,在图表上显示独特的分析线和信号

⚡

Script

一次性执行的脚本,用于批量操作或特定任务

🔌

DLL 扩展

通过DLL调用外部C/C++函数,扩展MQL能力边界

MQL4 vs MQL5 对比
特性MQL4 (MT4)MQL5 (MT5)
订单模型单订单系统持仓+订单双系统
执行模式OnTick 事件驱动OnTick + 多事件模型
面向对象部分支持完整OOP支持
回测引擎单线程多线程遗传算法
市场深度有限完整DOM支持

开发环境搭建

MetaEditor 是 MT4/MT5 内置的集成开发环境(IDE),无需额外安装,随客户端一同发布。

一、安装步骤
  1. 安装 MT4/MT5 客户端

    从交易商官网下载 MetaTrader 客户端并安装。建议同时安装 MT4 和 MT5,方便对照学习。

  2. 打开 MetaEditor

    方式一:在 MT 菜单点击「工具 → MetaQuotes 语言编辑器」;方式二:直接按 F4 快捷键;方式三:点击工具栏上的 MetaEditor 图标。

  3. 认识编辑器界面

    左侧 Navigator 面板(文件导航)、中间代码编辑区、下方 Toolbox 面板(编译输出/调试/搜索结果)。

  4. 创建新项目

    在 Navigator 面板的 Experts 文件夹上右键 → 新建 → 选择「Expert Advisor (template)」→ 输入名称 → 一路下一步完成。

二、编辑器界面介绍

📂 Navigator 导航面板

按分类显示所有 EA、指标、脚本、库文件,支持新建、删除、编译操作。

✏️ 代码编辑区

支持语法高亮、行号显示、括号匹配、代码折叠、自动补全等功能。

🛠 Toolbox 工具箱

显示编译错误、搜索结果、断点列表、调用栈等调试信息。

📖 Help 帮助

按 F1 打开 MQL 官方文档,所有内置函数都有详细说明和示例。

三、常用快捷键
快捷键功能说明
F4打开 MetaEditor从 MT 图表快速跳转到编辑器
F5编译并运行编译当前文件并附加到图表
F7编译编译当前文件,错误显示在 Toolbox
F9设置/取消断点在当前行设置调试断点
Ctrl+F查找在当前文件中搜索文本
Ctrl+H替换查找并替换文本
Ctrl+Shift+F全局搜索在所有文件中搜索
Ctrl+Space自动补全显示函数/变量名提示
Ctrl+D删除整行删除当前光标所在行
Ctrl+Z / Ctrl+Y撤销/重做撤销或恢复上一步操作
四、代码编辑器设置

推荐在「工具 → 选项」中进行以下配置,提升开发效率:

  • 字体设置:使用 Consolas 或 Fira Code 等宽字体,字号 12-14,保护眼睛
  • 缩进设置:Tab 大小设为 3 个空格,勾选「用空格代替 Tab」
  • 语法高亮:可自定义关键字、注释、字符串的颜色方案
  • 自动保存:建议开启「编译前自动保存」,避免代码丢失
  • 代码折叠:开启函数/类/注释块折叠,方便浏览大文件
五、如何新建 EA 文件

按照以下步骤创建你的第一个 EA 项目:

  1. Step 1:打开新建向导

    在 Navigator 面板的 Experts 文件夹上右键 → 点击「新建」,或在菜单栏选择「文件 → 新建」。

  2. Step 2:选择程序类型

    在弹出的向导中选择「Expert Advisor (template)」,点击下一步。

  3. Step 3:填写基本信息

    输入 EA 名称(如 MyFirstEA)、作者名称、链接地址,这些会自动生成文件头注释。

  4. Step 4:选择事件函数

    勾选 OnInit、OnDeinit、OnTick 三个核心函数,新手建议全部勾选。

  5. Step 5:完成创建

    点击「完成」,系统自动生成包含基本框架的 .mq4 文件,可以直接开始编写代码。

第一个EA程序

让我们编写一个完整的 HelloWorld EA,理解 OnInit / OnDeinit / OnTick 三大核心函数的作用。

完整的 HelloWorld EA 代码
//+------------------------------------------------------------------+
//|                                                 HelloEA.mq4      |
//|                        Copyright 2024, 晓辉编程 eafxtech.com     |
//|                                             https://www.eafxtech.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, 晓辉编程"
#property link      "https://www.eafxtech.com"
#property version   "1.00"
#property strict

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//| EA加载到图表时执行一次,用于初始化变量、检查环境                |
//| 返回 INIT_SUCCEEDED 表示成功,返回 INIT_FAILED 表示失败          |
//+------------------------------------------------------------------+
int OnInit()
  {
   Print("======================================");
   Print("HelloEA 已加载");
   Print("品种: ", Symbol());
   Print("周期: ", Period());
   Print("点值: ", Point);
   Print("小数位: ", Digits);
   Print("======================================");

   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//| EA从图表移除时执行一次,用于清理资源、保存数据                  |
//| reason 参数表示卸载原因:REASON_REMOVE/REASON_RECOMPILE等       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   Print("HelloEA 已卸载, 原因代码: ", reason);
   Comment("");  // 清除图表上的显示
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//| 每次价格变动(tick)都会触发,是EA的主循环                       |
//| 所有交易逻辑、信号判断都在这里执行                              |
//+------------------------------------------------------------------+
void OnTick()
  {
   // 获取当前价格
   double bid = Bid;       // 买价(你卖出时的价格)
   double ask = Ask;       // 卖价(你买入时的价格)

   // 计算点差(以点为单位)
   int spread = (int)((ask - bid) / Point);

   // 获取当前K线信息
   double curClose = Close[0];  // 当前未完成K线的收盘价
   double prevClose = Close[1]; // 上一根已完成K线的收盘价

   // 在图表左上角显示实时信息
   Comment(
      "┌── Hello EA ──┐\n",
      "│ Bid:  ", DoubleToStr(bid, Digits), "\n",
      "│ Ask:  ", DoubleToStr(ask, Digits), "\n",
      "│ 点差: ", spread, " 点\n",
      "│ 收盘: ", DoubleToStr(prevClose, Digits), "\n",
      "└──────────────┘"
   );

   // 价格变动时打印日志(避免刷屏,只在整点打印)
   static int lastHour = -1;
   if (Hour() != lastHour)
     {
      Print("整点播报: 当前价格 Bid=", bid, " Ask=", ask, " 点差=", spread);
      lastHour = Hour();
     }
  }
//+------------------------------------------------------------------+
三大核心函数详解
🚀

OnInit()

初始化函数,EA加载时只执行一次。用来初始化变量、读取配置、检查参数合法性。

⚡

OnTick()

主函数,每次价格变动都会触发。所有交易逻辑、信号判断、订单管理都写在这里。

👋

OnDeinit()

清理函数,EA卸载时执行一次。用来保存数据、关闭文件、清除图表对象。

如何运行这个 EA
  1. 创建文件

    在 MetaEditor 中新建一个 Expert Advisor,命名为 HelloEA,把上面的代码复制进去替换全部内容。

  2. 编译

    按 F7 编译,如果下方 Toolbox 显示 0 errors 0 warnings 就说明编译成功了。

  3. 拖到图表

    回到 MT4,在左侧 Navigator 面板找到 HelloEA,拖到任意图表上。

  4. 开启自动交易

    点击工具栏上的「自动交易」按钮,确保图表右上角显示😊笑脸。

  5. 查看效果

    图表左上角会出现实时价格信息,按 Ctrl+J 打开工具箱的 Experts 标签可以看到日志输出。

数据类型与变量

MQL 是强类型语言,变量使用前必须声明类型。正确的数据类型选择直接影响代码效率和准确性。

一、基本数据类型
类型说明占用字节示例
int32位有符号整数4int count = 10;
double双精度浮点数8double price = 1.23456;
bool布尔值 true/false1bool isLong = true;
string字符串8(指针)string name = "EURUSD";
datetime日期时间(秒数)4datetime now = TimeCurrent();
colorRGB颜色值4color c = clrRed;
char单字符1char ch = 'A';
long64位整数8long bigNum = 1234567890L;
二、整数类型详解

整数常用于计数、索引、订单号、魔术号等场景。MQL4 中 int 是最常用的整数类型。

// 整数变量声明与初始化
int i_count     = 0;           // 计数器,初始化为0
int i_magicNum  = 12345;       // EA魔术号,用于识别自己的订单
int i_bars      = Bars;        // 当前图表K线总数
int i_hour      = Hour();      // 当前小时数

// 整数运算
int a = 10, b = 3;
Print("加法: ", a + b);        // 13
Print("减法: ", a - b);        // 7
Print("乘法: ", a * b);        // 30
Print("除法: ", a / b);        // 3  (整数除法自动取整)
Print("取模: ", a % b);        // 1  (取余数)

// 自增自减
int n = 0;
n++;   // 等价于 n = n + 1
n--;   // 等价于 n = n - 1
三、浮点数类型详解

浮点数用于价格、指标值、盈亏金额等。交易中绝大多数数值计算都用 double 类型。

// 浮点数变量
double d_price    = Ask;          // 当前卖出价
double d_pipValue = 10.0;         // 每点价值
double d_lot      = 0.1;          // 交易手数
double d_profit   = 0.0;          // 浮动盈亏

// 浮点数比较注意事项(重要!)
// 不要直接用 == 比较浮点数,应该比较差值是否小于一个极小值
bool IsEqual(double a, double b)
  {
   return MathAbs(a - b) < Point;
  }

// 价格精度处理(非常重要)
double d_sl = Bid - 50 * Point;    // 止损价
d_sl = NormalizeDouble(d_sl, Digits);  // 修正到品种的小数位数
四、字符串类型详解

字符串用于品种名称、订单注释、日志输出等。

// 字符串声明
string s_symbol   = Symbol();      // 当前品种名
string s_comment  = "EA订单";      // 订单注释
string s_empty    = "";            // 空字符串

// 字符串拼接(MQL4 中用 + 号)
string s_msg = "品种: " + s_symbol + ", 价格: " + DoubleToStr(Ask, Digits);
Print(s_msg);

// 字符串长度
int len = StringLen(s_symbol);
Print("字符串长度: ", len);

// 字符串比较
if (s_symbol == "EURUSD")
  {
   Print("当前是欧元美元");
  }

// 常用字符串函数
string s_upper = StringUpper("hello");   // 转大写: "HELLO"
string s_lower = StringLower("HELLO");   // 转小写: "hello"
int pos = StringFind(s_symbol, "USD");   // 查找子串位置
string s_sub = StringSubstr(s_symbol, 0, 3);  // 截取子串
五、布尔类型与枚举

布尔类型只有 true 和 false 两个值;枚举用于定义一组有限的可选值。

// 布尔变量
bool b_isLong    = true;     // 是否做多
bool b_useStop   = false;    // 是否使用止损

// 布尔表达式
bool b_overbought = (iRSI(_Symbol, _Period, 14, PRICE_CLOSE, 1) > 70);

// 逻辑运算
bool b_a = true, b_b = false;
Print("与: ", b_a && b_b);   // 两个都为true才为true
Print("或: ", b_a || b_b);   // 一个为true就为true
Print("非: ", !b_a);         // 取反

// 枚举类型
enum ENUM_DIRECTION
  {
   DIR_LONG   = 0,    // 做多
   DIR_SHORT  = 1,    // 做空
   DIR_BOTH   = 2     // 双向
  };

input ENUM_DIRECTION InpDirection = DIR_BOTH;
六、结构体 (struct)

结构体可以把多个相关数据打包在一起,方便传参和管理。

// 交易信号结构体
struct SSignal
  {
   string symbol;     // 品种
   int    type;       // 订单类型 OP_BUY/OP_SELL
   double entry;      // 入场价
   double sl;         // 止损价
   double tp;         // 止盈价
   double lot;        // 手数
  };

// 使用结构体
void OnTick()
  {
   SSignal sig;
   sig.symbol = _Symbol;
   sig.type   = OP_BUY;
   sig.entry  = Ask;
   sig.sl     = Bid - 50 * Point;
   sig.tp     = Ask + 100 * Point;
   sig.lot    = 0.1;

   if (sig.type == OP_BUY)
     {
      Print("收到做多信号, 入场=", sig.entry);
     }
  }

流程控制

MQL 支持标准的条件判断和循环结构,掌握这些是编写交易逻辑的基础。

一、if-else 条件判断
// 基础 if 判断
double rsi = iRSI(_Symbol, _Period, 14, PRICE_CLOSE, 1);
if (rsi > 70)
  {
   Print("超买区域, RSI=", rsi);
  }

// if-else 二选一
double ma20 = iMA(_Symbol, _Period, 20, 0, MODE_SMA, PRICE_CLOSE, 1);
if (Close[1] > ma20)
  {
   Print("价格在20日均线上方,偏多");
  }
else
  {
   Print("价格在20日均线下方,偏空");
  }

// if-else if-else 多分支
if (rsi > 80)
  {
   Print("严重超买");
  }
else if (rsi > 70)
  {
   Print("超买");
  }
else if (rsi < 20)
  {
   Print("严重超卖");
  }
else if (rsi < 30)
  {
   Print("超卖");
  }
else
  {
   Print("中性区域");
  }

// 多条件组合
double fastMA = iMA(_Symbol, _Period, 5, 0, MODE_SMA, PRICE_CLOSE, 1);
double slowMA = iMA(_Symbol, _Period, 20, 0, MODE_SMA, PRICE_CLOSE, 1);
double fastPrev = iMA(_Symbol, _Period, 5, 0, MODE_SMA, PRICE_CLOSE, 2);
double slowPrev = iMA(_Symbol, _Period, 20, 0, MODE_SMA, PRICE_CLOSE, 2);

// 金叉条件:快线从下往上穿慢线
bool goldenCross = (fastMA > slowMA) && (fastPrev <= slowPrev);

if (goldenCross && OrdersTotal() == 0)
  {
   Print("金叉信号,且当前无持仓");
  }
二、for 循环
// 示例1:计算最近N根K线的收盘价均值
double CalcSMA(int period, int shift)
  {
   if (Bars < period + shift) return 0;
   double sum = 0;
   for (int i = shift; i < shift + period; i++)
     {
      sum += Close[i];
     }
   return sum / period;
  }

// 示例2:遍历订单(倒序遍历,防止删除后索引错位)
void CloseAllBuyOrders()
  {
   for (int i = OrdersTotal() - 1; i >= 0; i--)
     {
      if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
         if (OrderSymbol() == _Symbol && OrderType() == OP_BUY)
           {
            OrderClose(OrderTicket(), OrderLots(), Bid, 3, clrRed);
           }
        }
     }
  }

// 示例3:带 continue 的循环
for (int i = 0; i < OrdersTotal(); i++)
  {
   if (!OrderSelect(i, SELECT_BY_POS)) continue;
   if (OrderMagicNumber() != InpMagic) continue;
   if (OrderSymbol() != _Symbol) continue;
   Print("订单号: ", OrderTicket());
  }
三、while 循环

while 循环在条件为真时反复执行。注意必须确保循环能终止,防止死循环!

// 查找最近一个满足条件的K线
int FindLastHighAbove(double level)
  {
   int i = 1;
   while (i < Bars)
     {
      if (High[i] > level) return i;
      i++;
     }
   return -1;
  }
⚠️ 注意: while 循环必须确保条件最终会变为 false,否则会导致 MT4 卡死。务必加上最大循环次数限制。
四、switch 分支
// 根据订单类型输出描述
void PrintOrderType(int type)
  {
   switch (type)
     {
      case OP_BUY:       Print("市价买单"); break;
      case OP_SELL:      Print("市价卖单"); break;
      case OP_BUYLIMIT:  Print("限价买单"); break;
      case OP_SELLLIMIT: Print("限价卖单"); break;
      case OP_BUYSTOP:   Print("止损买单"); break;
      case OP_SELLSTOP:  Print("止损卖单"); break;
      default:           Print("未知类型: ", type); break;
     }
  }

// 根据周期返回描述
string PeriodName()
  {
   switch (Period())
     {
      case PERIOD_M1:  return "1分钟";
      case PERIOD_M5:  return "5分钟";
      case PERIOD_M15: return "15分钟";
      case PERIOD_M30: return "30分钟";
      case PERIOD_H1:  return "1小时";
      case PERIOD_H4:  return "4小时";
      case PERIOD_D1:  return "日线";
      default:         return "其他周期";
     }
  }
五、三元运算符
// 格式: 条件 ? 值1 : 值2
double direction = 1;
double slPrice = (direction == 1) ? (Bid - 50*Point) : (Ask + 50*Point);

// 获取较大值
double maxPrice = (High[0] > High[1]) ? High[0] : High[1];

// 判断持仓方向
string posType = (OrderType() == OP_BUY) ? "多单" : "空单";

函数与参数

函数是 MQL 程序的基本构建块,将代码模块化封装,提高复用性和可维护性。

一、函数定义与返回值
// 语法:返回值类型 函数名(参数列表) { 函数体 return 返回值; }

// 有返回值的函数
double CalcSMA(int period)
  {
   if (period <= 0 || Bars < period) return 0;
   double sum = 0.0;
   for (int i = 0; i < period; i++) sum += Close[i];
   return sum / period;
  }

// 无返回值的函数(void)
void PrintSymbolInfo()
  {
   Print("品种: ", Symbol(), " 现价: ", Bid);
   Print("点差: ", (int)((Ask - Bid) / Point), " 点");
  }

// 带提前返回的函数
bool IsValidPrice(double price)
  {
   if (price <= 0) return false;
   if (price < MarketInfo(_Symbol, MODE_LOW)) return false;
   return true;
  }
二、参数传递方式
// ===== 传值方式(默认)=====
// 函数内部修改参数不会影响外部变量
void AddOne(int n) { n = n + 1; }

// ===== 传引用方式(& 符号)=====
// 函数内部修改参数会改变外部变量
void AddOneRef(int &n) { n = n + 1; }

// ===== 传引用的典型用途:返回多个值 =====
void GetAccountStats(double &balance, double &equity, int &orders)
  {
   balance = AccountBalance();
   equity  = AccountEquity();
   orders  = OrdersTotal();
  }

// 调用
void OnTick()
  {
   double bal, eq;
   int cnt;
   GetAccountStats(bal, eq, cnt);
   Print("余额=", bal, " 净值=", eq, " 订单数=", cnt);
  }
三、函数重载与默认参数
// 函数重载:同名不同参
double CalcMA(int period)
  { return iMA(_Symbol, _Period, period, 0, MODE_SMA, PRICE_CLOSE, 0); }

double CalcMA(int period, int shift)
  { return iMA(_Symbol, _Period, period, 0, MODE_SMA, PRICE_CLOSE, shift); }

// 默认参数:从右往左连续设置
double CalcLotSize(double riskPercent = 2.0, int stopLossPoints = 50)
  {
   double riskAmount = AccountBalance() * riskPercent / 100.0;
   double lot = riskAmount / (stopLossPoints * 10.0);
   lot = NormalizeDouble(lot, 2);
   return MathMax(MarketInfo(_Symbol, MODE_MINLOT), lot);
  }

// 多种调用方式
double lot1 = CalcLotSize();                // risk=2%, sl=50点
double lot2 = CalcLotSize(1.5);             // risk=1.5%, sl=50点
double lot3 = CalcLotSize(2.0, 80);         // risk=2%, sl=80点

数组操作

MQL 支持一维和多维数组,是处理 K 线数据、指标序列、批量数据的核心工具。

一、一维数组
// 静态数组声明
double prices[100];    // 100个double元素
int    indices[50];    // 50个int元素

// 初始化
double arr1[] = {1.5, 2.5, 3.5, 4.5};

// 赋值与访问
prices[0] = Bid;
prices[1] = Close[1];
double v = prices[0];

// 遍历数组
double sum = 0;
for (int i = 0; i < 100; i++) sum += prices[i];
double avg = sum / 100;

// 动态数组
double dynamicArr[];
ArrayResize(dynamicArr, 50);      // 调整大小
int size = ArraySize(dynamicArr); // 获取大小

// 常用数组函数
double data[] = {5, 2, 8, 1, 9};
ArraySort(data);                   // 升序排序
double minVal = ArrayMinimum(data); // 最小值
double maxVal = ArrayMaximum(data); // 最大值
二、二维数组
// 二维数组声明
double matrix[5][5];
string multiData[10][3];

// 赋值
matrix[0][0] = 1.0;   // 第1行第1列
matrix[0][1] = 2.0;   // 第1行第2列

// 遍历二维数组
for (int row = 0; row < 5; row++)
  for (int col = 0; col < 5; col++)
    matrix[row][col] = row * 10 + col;

// 实用场景:多品种数据存储
double symbolData[5][4];  // [品种索引][0=开 1=高 2=低 3=收]
三、ArraySetAsSeries 系列数组

设为系列数组后,索引 0 表示最新数据,和内置 Close[]、High[] 行为一致。

double myMA[];
ArraySetAsSeries(myMA, true);  // 设为系列数组
ArrayResize(myMA, 100);

// 填充后 myMA[0] 是最新值,和 Close[0] 索引方式一致
for (int i = 0; i < 100; i++)
  {
   myMA[i] = iMA(_Symbol, _Period, 20, 0, MODE_SMA, PRICE_CLOSE, i);
  }

// 现在 myMA[0] = 当前K线MA值
//     myMA[1] = 前一根K线MA值
// 这和 Close[0] High[0] 的索引方式完全一致
四、复制 K 线数据
// MQL4 手动复制(兼容性最好)
void CopyRatesManual(string symbol, int tf,
                     double &open[], double &high[],
                     double &low[], double &close[],
                     datetime &timeArr[], int count)
  {
   ArraySetAsSeries(open, true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low, true);
   ArraySetAsSeries(close, true);
   ArraySetAsSeries(timeArr, true);
   ArrayResize(open, count);
   ArrayResize(high, count);
   ArrayResize(low, count);
   ArrayResize(close, count);
   ArrayResize(timeArr, count);

   for (int i = 0; i < count; i++)
     {
      open[i]    = iOpen(symbol, tf, i);
      high[i]    = iHigh(symbol, tf, i);
      low[i]     = iLow(symbol, tf, i);
      close[i]   = iClose(symbol, tf, i);
      timeArr[i] = iTime(symbol, tf, i);
     }
  }

账户信息

通过内置函数获取账户余额、净值、保证金等信息,是风控和资金管理的基础。

常用账户函数一览
函数返回类型说明
AccountBalance()double账户余额(已平仓后的资金)
AccountEquity()double账户净值(余额 + 浮动盈亏)
AccountProfit()double当前浮动盈亏总额
AccountFreeMargin()double可用保证金
AccountMargin()double已用保证金
AccountLeverage()int杠杆倍数
AccountCurrency()string账户币种
AccountFreeMarginCheck()double检查开仓后剩余可用保证金
AccountCompany()string交易商公司名称
AccountNumber()int账户号码
代码示例
// 1. 显示账户基本信息
void PrintAccountInfo()
  {
   Print("账户号: ", AccountNumber());
   Print("余额: ", AccountBalance(), " ", AccountCurrency());
   Print("净值: ", AccountEquity(), " ", AccountCurrency());
   Print("浮动盈亏: ", AccountProfit());
   Print("可用保证金: ", AccountFreeMargin());
   Print("杠杆: 1:", AccountLeverage());
  }

// 2. 检查最大回撤限制
input double InpMaxDrawdown = 20.0;
double g_initialBalance = 0;

int OnInit()
  {
   g_initialBalance = AccountBalance();
   return INIT_SUCCEEDED;
  }

bool IsDrawdownExceeded()
  {
   double equity = AccountEquity();
   double drawdown = ((g_initialBalance - equity) / g_initialBalance) * 100;
   if (drawdown >= InpMaxDrawdown)
     {
      Print("⚠️ 回撤已达 ", drawdown, "%");
      return true;
     }
   return false;
  }

// 3. 检查保证金是否足够
bool CheckFreeMarginForLot(double lot)
  {
   double freeAfter = AccountFreeMarginCheck(_Symbol, OP_BUY, lot);
   if (freeAfter <= 0)
     {
      Print("❌ 保证金不足");
      return false;
     }
   return true;
  }

订单系统 (MQL4)

MQL4 使用订单编号系统,每张订单有唯一的 ticket 号。

订单类型

OP_BUY
市价买单
OP_SELL
市价卖单
OP_BUYLIMIT
限价买单
OP_SELLLIMIT
限价卖单
OP_BUYSTOP
止损买单
OP_SELLSTOP
止损卖单

交易函数

OrderSend / OrderClose / OrderModify 是 MQL4 交易的三大核心函数,掌握它们是编写 EA 的关键。

一、OrderSend 开仓函数
// 市价买入示例
int OpenBuyOrder(double lot, double slPoints, double tpPoints)
  {
   double sl = (slPoints > 0) ? NormalizeDouble(Bid - slPoints * Point, Digits) : 0;
   double tp = (tpPoints > 0) ? NormalizeDouble(Ask + tpPoints * Point, Digits) : 0;

   int ticket = OrderSend(
      _Symbol,           // 品种
      OP_BUY,            // 类型:市价买单
      lot,               // 手数
      Ask,               // 价格:用卖价开多
      30,                // 滑点:30点(实盘建议设大一点)
      sl,                // 止损价
      tp,                // 止盈价
      "eafxtech EA",     // 注释
      12345,             // 魔术号
      0,                 // 有效期(市价单为0)
      clrGreen           // 箭头颜色
   );

   if (ticket > 0)
      Print("✅ 做多成功,订单号:", ticket);
   else
      Print("❌ 做多失败,错误码:", GetLastError());

   return ticket;
  }

// 市价卖出示例
int OpenSellOrder(double lot, double slPoints, double tpPoints)
  {
   double sl = (slPoints > 0) ? NormalizeDouble(Ask + slPoints * Point, Digits) : 0;
   double tp = (tpPoints > 0) ? NormalizeDouble(Bid - tpPoints * Point, Digits) : 0;

   int ticket = OrderSend(_Symbol, OP_SELL, lot, Bid, 30, sl, tp,
                          "eafxtech EA", 12345, 0, clrRed);
   return ticket;
  }
二、OrderClose 平仓函数
// 按订单号平仓
bool CloseOrderByTicket(int ticket)
  {
   if (!OrderSelect(ticket, SELECT_BY_TICKET)) return false;

   double closePrice = (OrderType() == OP_BUY) ? Bid : Ask;
   bool result = OrderClose(ticket, OrderLots(), closePrice, 30, clrRed);

   if (!result) Print("❌ 平仓失败,错误码:", GetLastError());
   return result;
  }

// 平掉当前品种所有订单
void CloseAllOrders()
  {
   for (int i = OrdersTotal() - 1; i >= 0; i--)
     {
      if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
      if (OrderSymbol() != _Symbol || OrderMagicNumber() != 12345) continue;

      int type = OrderType();
      if (type == OP_BUY || type == OP_SELL)
        {
         double price = (type == OP_BUY) ? Bid : Ask;
         OrderClose(OrderTicket(), OrderLots(), price, 30, clrRed);
        }
      else
        {
         OrderDelete(OrderTicket());  // 挂单删除
        }
     }
  }
三、OrderModify 修改订单
// 修改止损止盈
bool ModifySLTP(int ticket, double newSL, double newTP)
  {
   if (!OrderSelect(ticket, SELECT_BY_TICKET)) return false;
   return OrderModify(ticket, OrderOpenPrice(),
                      NormalizeDouble(newSL, Digits),
                      NormalizeDouble(newTP, Digits), 0, clrYellow);
  }

// 移动止损(Trailing Stop)
void TrailStop(int magicNumber, double trailPoints)
  {
   for (int i = OrdersTotal() - 1; i >= 0; i--)
     {
      if (!OrderSelect(i, SELECT_BY_POS)) continue;
      if (OrderMagicNumber() != magicNumber) continue;
      if (OrderSymbol() != _Symbol) continue;

      if (OrderType() == OP_BUY)
        {
         double newSL = Bid - trailPoints * Point;
         if (newSL > OrderStopLoss() + Point)
            OrderModify(OrderTicket(), OrderOpenPrice(),
                       NormalizeDouble(newSL, Digits),
                       OrderTakeProfit(), 0, clrYellow);
        }
      else if (OrderType() == OP_SELL)
        {
         double newSL = Ask + trailPoints * Point;
         if (OrderStopLoss() == 0 || newSL < OrderStopLoss() - Point)
            OrderModify(OrderTicket(), OrderOpenPrice(),
                       NormalizeDouble(newSL, Digits),
                       OrderTakeProfit(), 0, clrYellow);
        }
     }
  }
四、订单选择与遍历
// 按订单号选择
bool ok = OrderSelect(1234567, SELECT_BY_TICKET);

// 按索引位置选择(遍历用)
bool ok2 = OrderSelect(0, SELECT_BY_POS, MODE_TRADES);

// 订单信息获取(必须先 OrderSelect)
int    ticket = OrderTicket();       // 订单号
int    type   = OrderType();         // 订单类型
double lots   = OrderLots();         // 手数
double open   = OrderOpenPrice();    // 开仓价
double sl     = OrderStopLoss();     // 止损价
double tp     = OrderTakeProfit();   // 止盈价
double profit = OrderProfit();       // 盈亏金额
string sym    = OrderSymbol();       // 品种
int    magic  = OrderMagicNumber();  // 魔术号

// 标准遍历写法(从后往前,防止删除后索引错乱)
void ListAllOrders()
  {
   for (int i = OrdersTotal() - 1; i >= 0; i--)
     {
      if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
         Print("订单:", OrderTicket(), " 类型:", OrderType(),
               " 手数:", OrderLots(), " 盈亏:", OrderProfit());
        }
     }
  }

风控管理

风控是 EA 的生命线。再好的策略,没有风控也迟早爆仓。开仓前必须先算好手数和止损。

一、手数计算函数(最常用)
// 按风险百分比计算手数
double CalcLotByRisk(double riskPercent, double stopLossPoints)
  {
   // 1. 计算愿意亏损的金额
   double riskAmount = AccountBalance() * riskPercent / 100.0;

   // 2. 计算每点价值
   double tickValue = MarketInfo(_Symbol, MODE_TICKVALUE);
   double tickSize  = MarketInfo(_Symbol, MODE_TICKSIZE);
   double valuePerPoint = tickValue / tickSize * Point;

   // 3. 计算手数
   double lot = riskAmount / (stopLossPoints * valuePerPoint);

   // 4. 修正精度和范围
   double lotStep = MarketInfo(_Symbol, MODE_LOTSTEP);
   lot = MathFloor(lot / lotStep) * lotStep;
   lot = NormalizeDouble(lot, 2);

   double minLot = MarketInfo(_Symbol, MODE_MINLOT);
   double maxLot = MarketInfo(_Symbol, MODE_MAXLOT);
   lot = MathMax(minLot, MathMin(maxLot, lot));

   // 5. 二次确认保证金足够
   if (AccountFreeMarginCheck(_Symbol, OP_BUY, lot) <= 0)
      lot = minLot;

   return lot;
  }
二、止损止盈设置
// 多单止损止盈计算
void CalcBuySLTP(double slPoints, double tpPoints, double &sl, double &tp)
  {
   sl = (slPoints > 0) ? NormalizeDouble(Bid - slPoints * Point, Digits) : 0;
   tp = (tpPoints > 0) ? NormalizeDouble(Ask + tpPoints * Point, Digits) : 0;
  }

// 空单止损止盈计算
void CalcSellSLTP(double slPoints, double tpPoints, double &sl, double &tp)
  {
   sl = (slPoints > 0) ? NormalizeDouble(Ask + slPoints * Point, Digits) : 0;
   tp = (tpPoints > 0) ? NormalizeDouble(Bid - tpPoints * Point, Digits) : 0;
  }

// 基于 ATR 的动态止损(进阶)
double CalcATRSl(int atrPeriod, double atrMultiplier)
  {
   double atr = iATR(_Symbol, _Period, atrPeriod, 0);
   return (atr * atrMultiplier) / Point;
  }
三、最大回撤限制
input double InpMaxDrawdown    = 20.0;   // 最大回撤百分比
input double InpDailyLossLimit  = 5.0;   // 单日最大亏损百分比

double g_peakEquity       = 0;
double g_dayStartEquity   = 0;
int    g_lastDay          = 0;

int OnInit()
  {
   g_peakEquity = AccountEquity();
   g_dayStartEquity = AccountEquity();
   g_lastDay = Day();
   return INIT_SUCCEEDED;
  }

bool CanOpenNewPosition()
  {
   double equity = AccountEquity();

   // 更新峰值
   if (equity > g_peakEquity) g_peakEquity = equity;

   // 检查从峰值的回撤
   double drawdown = ((g_peakEquity - equity) / g_peakEquity) * 100;
   if (drawdown >= InpMaxDrawdown) return false;

   // 检查单日亏损
   if (Day() != g_lastDay)
     {
      g_dayStartEquity = equity;
      g_lastDay = Day();
     }
   double dailyLoss = ((g_dayStartEquity - equity) / g_dayStartEquity) * 100;
   if (dailyLoss >= InpDailyLossLimit) return false;

   return true;
  }
风控原则清单
  • 单笔风险原则:单笔交易风险不超过账户的 1-2%
  • 总风险原则:所有持仓总风险敞口不超过账户的 10%
  • 强制止损原则:所有开仓必须带止损,禁止裸奔
  • 动态仓位原则:根据波动率(ATR)动态调整手数
  • 回撤熔断原则:最大回撤超 20% 停止交易
  • 同方向限制:同一品种同方向最多 1 单
  • 保证金安全:保留至少 50% 的可用保证金

多品种交易

高级 EA 通常需要监控多个品种,捕捉跨品种交易机会,分散单一品种风险。

一、遍历所有交易品种
// 获取所有可交易品种列表
void ListAllSymbols()
  {
   int total = SymbolsTotal(true);
   Print("可交易品种总数: ", total);
   for (int i = 0; i < total; i++)
     {
      Print(i, ": ", SymbolName(i, true));
     }
  }

// 筛选符合条件的品种
void FilterSymbols()
  {
   int total = SymbolsTotal(true);
   for (int i = 0; i < total; i++)
     {
      string symbol = SymbolName(i, true);
      double spread = MarketInfo(symbol, MODE_SPREAD);
      double minLot = MarketInfo(symbol, MODE_MINLOT);
      double ask    = MarketInfo(symbol, MODE_ASK);

      if (ask <= 0 || spread > 30 || minLot > 0.1) continue;

      double atr = iATR(symbol, PERIOD_H1, 14, 0);
      double atrPercent = (atr / ask) * 100;

      if (atrPercent > 0.1 && atrPercent < 2.0)
         Print("✅ ", symbol, " 点差:", spread, " ATR%:", DoubleToStr(atrPercent, 2));
     }
  }
二、多品种信号判断
string g_watchList[] = {"EURUSD", "GBPUSD", "USDJPY", "AUDUSD", "USDCAD"};
int    g_magic       = 12345;

// 判断某品种是否有做多信号
bool HasBuySignal(string symbol)
  {
   double close = iClose(symbol, PERIOD_M15, 1);
   double ma20  = iMA(symbol, PERIOD_M15, 20, 0, MODE_SMA, PRICE_CLOSE, 1);
   double ma60  = iMA(symbol, PERIOD_M15, 60, 0, MODE_SMA, PRICE_CLOSE, 1);
   return (close > ma20 && ma20 > ma60);
  }

// 检查某品种是否已有持仓
bool HasPosition(string symbol, int type)
  {
   for (int i = OrdersTotal() - 1; i >= 0; i--)
     {
      if (OrderSelect(i, SELECT_BY_POS) &&
          OrderSymbol() == symbol &&
          OrderMagicNumber() == g_magic &&
          OrderType() == type)
         return true;
     }
   return false;
  }

// 主循环:扫描所有品种
void ScanAndTrade()
  {
   for (int i = 0; i < ArraySize(g_watchList); i++)
     {
      string sym = g_watchList[i];
      if (HasBuySignal(sym) && !HasPosition(sym, OP_BUY))
        {
         Print("🚀 ", sym, " 做多信号");
         // 开仓逻辑...
        }
     }
  }
三、新 K 线判断(信号防抖)
// 记录每个品种上次处理的K线时间
datetime g_lastBarTime[];

int OnInit()
  {
   ArrayResize(g_lastBarTime, ArraySize(g_watchList));
   ArrayInitialize(g_lastBarTime, 0);
   return INIT_SUCCEEDED;
  }

bool IsNewBar(int index)
  {
   string sym = g_watchList[index];
   datetime curBarTime = iTime(sym, PERIOD_M15, 0);
   if (curBarTime != g_lastBarTime[index])
     {
      g_lastBarTime[index] = curBarTime;
      return true;
     }
   return false;
  }

⚠️ 指标调用开销

每多监控一个品种,指标函数就要多调用一次。品种太多会导致 OnTick 执行变慢。

🔗 相关性风险

正相关品种同时开同向单等于加杠杆,比如EURUSD和GBPUSD经常同向波动。

性能优化

EA 运行效率直接影响实盘表现。优化的核心是:能不计算的就不算,能少算的就少算。

一、减少指标调用(新K线才更新)
// 反例:每tick都重新计算指标(浪费)
void OnTickBad()
  {
   double ma5  = iMA(_Symbol, _Period, 5, 0, MODE_SMA, PRICE_CLOSE, 1);
   double ma20 = iMA(_Symbol, _Period, 20, 0, MODE_SMA, PRICE_CLOSE, 1);
   double rsi  = iRSI(_Symbol, _Period, 14, PRICE_CLOSE, 1);
  }

// 正例:只在新K线时计算(缓存结果)
double g_ma5 = 0, g_ma20 = 0, g_rsi = 0;
datetime g_lastBar = 0;

bool IsNewBar()
  {
   datetime cur = iTime(_Symbol, _Period, 0);
   if (cur != g_lastBar) { g_lastBar = cur; return true; }
   return false;
  }

void UpdateIndicators()
  {
   g_ma5  = iMA(_Symbol, _Period, 5, 0, MODE_SMA, PRICE_CLOSE, 1);
   g_ma20 = iMA(_Symbol, _Period, 20, 0, MODE_SMA, PRICE_CLOSE, 1);
   g_rsi  = iRSI(_Symbol, _Period, 14, PRICE_CLOSE, 1);
  }

void OnTickGood()
  {
   if (IsNewBar()) UpdateIndicators();
   // 直接使用缓存的变量
   if (g_ma5 > g_ma20 && g_rsi < 70) { /* 交易逻辑 */ }
  }
二、缓存计算结果
// 用静态变量缓存(函数内也能保留值)
double GetSMA20()
  {
   static double cachedValue = 0;
   static datetime lastCalc = 0;

   datetime curBar = iTime(_Symbol, _Period, 0);
   if (curBar != lastCalc)
     {
      cachedValue = iMA(_Symbol, _Period, 20, 0, MODE_SMA, PRICE_CLOSE, 1);
      lastCalc = curBar;
     }
   return cachedValue;
  }
三、限制 K 线计算范围
// 反例:遍历所有K线
double FindHighestHighBad()
  {
   double highest = 0;
   for (int i = 0; i < Bars; i++)     // Bars 可能有几万根
      if (High[i] > highest) highest = High[i];
   return highest;
  }

// 正例:只遍历需要的K线
double FindHighestHighGood(int lookback)
  {
   if (lookback > Bars) lookback = Bars;
   double highest = 0;
   for (int i = 0; i < lookback; i++)  // 只看最近N根
      if (High[i] > highest) highest = High[i];
   return highest;
  }
四、循环优化技巧
// 技巧1:循环外提取不变量
void GoodLoop()
  {
   double point = MarketInfo(_Symbol, MODE_POINT);  // 只调用1次
   for (int i = 0; i < 1000; i++)
      Close[i] = Close[i] * point;
  }

// 技巧2:减少循环内函数调用
// 先批量复制到数组,再计算,比循环内调用iMA快得多

// 技巧3:尽早 break / continue
for (int i = 0; i < OrdersTotal(); i++)
  {
   if (!OrderSelect(i, SELECT_BY_POS)) continue;
   if (OrderMagicNumber() != g_magic) continue;
   if (OrderType() == OP_BUY) { /* 处理 */ break; }  // 找到就退出
  }
  • 减少 Print 调用:频繁打印日志影响性能,生产环境可以加日志级别开关
  • 订单倒序遍历:从后往前(i--)遍历,避免删除订单后索引错位漏项
  • 预分配数组:知道数组大小时一次性 ArrayResize 到位,避免反复扩容
  • 字符串优化:减少拼接次数,大量拼接用 StringConcatenate

调试技巧

MQL 调试没有现代 IDE 那么方便,掌握 Print/Comment/断点调试等方法可以大幅提升排错效率。

一、常用调试方法对比
方法用途输出位置注意事项
Print()输出日志信息Toolbox → Experts支持多参数混合,最常用
Comment()图表实时显示图表左上角实时更新,适合观察动态变量
Alert()弹窗+声音提醒弹出窗口重要事件触发用
GetLastError()获取错误码返回 int交易失败后立即调用
断点调试逐行执行调试器按 F5 启动,F9 设断点
二、Print() 日志调试
// 带标签的日志(方便筛选)
void Log(string msg)
  {
   Print("[MyEA] ", TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS),
         " | ", msg);
  }

// 调试开关(生产环境关闭)
input bool InpDebug = true;

void DebugPrint(string msg)
  {
   if (InpDebug) Print("[DEBUG] ", msg);
  }

// 交易失败时打印详细错误
int ticket = OrderSend(_Symbol, OP_BUY, 0.1, Ask, 30, 0, 0, "test", 12345, 0, clrGreen);
if (ticket < 0)
  {
   int err = GetLastError();
   Print("❌ 下单失败! 错误码: ", err);
   Print("   品种: ", _Symbol, " 手数: 0.1 价格: ", Ask);
  }
三、Comment() 图表输出
// 显示EA运行状态
void UpdateComment()
  {
   double ma20 = iMA(_Symbol, _Period, 20, 0, MODE_SMA, PRICE_CLOSE, 1);
   double rsi  = iRSI(_Symbol, _Period, 14, PRICE_CLOSE, 1);

   Comment(
      "╔══ 晓辉EA 状态 ══╗\n",
      "  时间: ", TimeToString(TimeCurrent(), TIME_SECONDS), "\n",
      "  MA20: ", DoubleToStr(ma20, Digits), "\n",
      "  RSI:  ", DoubleToStr(rsi, 2), "\n",
      "  订单: ", OrdersTotal(), "\n",
      "  余额: ", DoubleToStr(AccountBalance(), 2), "\n",
      "╚══════════════════╝"
   );
  }
四、断点调试步骤
  1. 设置断点

    在代码行号左侧点击(或按 F9),出现红点表示断点已设置。

  2. 启动调试

    按 F5 开始调试,EA 会附加到图表并运行,遇到断点自动暂停。

  3. 调试控制

    F10 单步跳过 / F11 单步进入 / F5 继续运行 / Shift+F5 停止调试。

  4. 查看变量

    鼠标悬停在变量上可查看当前值,Watch 窗口可添加监控。

五、常见错误码速查
错误码常量名说明
5ERR_NOT_ENOUGH_MONEY资金不足
6ERR_PRICE_CHANGED价格已变化
7ERR_OFF_QUOTES没有报价
8ERR_BROKER_BUSY交易商繁忙
128ERR_INVALID_TRADE_VOLUME无效手数
129ERR_INVALID_PRICE无效价格
130ERR_INVALID_STOPS无效止损止盈(太近)
131ERR_INVALID_TRADE_PARAMS无效交易参数
146ERR_TRADE_CONTEXT_BUSY交易上下文繁忙

代码规范

良好的代码风格是团队协作和长期维护的基础。规范的代码不仅好看,更能减少 Bug。

一、命名规范(匈牙利命名法)
类型前缀好的示例不好的示例
inti_i_magicNumbermagic、mn
doubled_d_lotSizelot、ls
boolb_b_useStopLossusl、flag
strings_s_orderCommentcomment
datetimedt_dt_lastBartime
数组类型前缀 + _arrd_ma_arrma
全局g_g_d_balance无前缀的全局变量
常量全大写EA_MAGICmagic
二、变量命名示例
// 全局变量:g_ 前缀
double   g_d_initialBalance;
int      g_i_magicNumber;
bool     g_b_canTrade;
datetime g_dt_lastBarTime;

// input 参数:Inp 前缀
input double InpLotSize    = 0.1;
input int    InpStopLoss   = 50;
input bool   InpUseTrail   = false;

// 局部变量:类型前缀
void OnTick()
  {
   double d_ma20 = iMA(_Symbol, _Period, 20, 0, MODE_SMA, PRICE_CLOSE, 1);
   int    i_total = OrdersTotal();
   bool   b_hasPos = false;
  }

// 常量:全大写 + 下划线
#define MAGIC_NUMBER  12345
#define MAX_DRAWDOWN  20.0
三、函数命名规范
// 布尔函数:Is/Has/Can/Check 开头
bool IsNewBar();
bool HasPosition();
bool CanOpenOrder();

// 动作函数:动词 + 对象
void OpenBuyOrder();
void CloseAllOrders();
void UpdateIndicators();
double CalcLotSize();

// Get 系列:获取某个值
double GetMAValue(int period);
int    GetOrderCount();
四、注释规范
// 文件头注释
//+------------------------------------------------------------------+
//|                                              MyStrategy.mq4      |
//|                        Copyright 2024, 晓辉编程 eafxtech.com     |
//|                                             https://www.eafxtech.com |
//+------------------------------------------------------------------+

// 区块分隔注释
//+------------------------------------------------------------------+
//| 常量定义区                                                       |
//+------------------------------------------------------------------+

// 写"为什么",不写"做什么"
// 从后往前遍历,防止删除订单后索引错位导致漏项
for (int i = OrdersTotal() - 1; i >= 0; i--) { }
五、其他编码规范
  • 缩进:统一使用 3 个空格(MQL 传统风格)
  • 大括号:左大括号单独一行
  • 空格:运算符两侧加空格,逗号后加空格
  • 魔法数字:禁止硬编码数字,用 #define 或 input 参数
  • 错误处理:所有交易操作后检查返回值,打印 GetLastError()
  • 函数长度:单个函数不超过 50 行,过长就拆分
  • 版本号:三段式(主版本.次版本.修订号),每次更新都升级

常用设计模式

成熟的EA开发中常用的架构模式。

🏗 信号-执行分离

信号生成和订单执行解耦,便于独立测试和替换策略

🔄 状态机模式

用枚举管理交易状态:等待信号→挂单→持仓→平仓

🛡 风控中间件

所有开仓请求先过风控层,统一校验手数、止损、风险限额

📊 统计收集器

独立模块收集交易统计,输出CSV用于分析和复盘

常见坑点

新手最容易踩的坑,提前避坑少走弯路。

Top 5 坑点
  1. 1. 数组越界

    访问 iBarShift 或价格数组前,务必检查 Bars 数量是否足够

  2. 2. 滑点设置不当

    实盘点差波动大,滑点设太小会导致频繁下单失败,建议设 30-50 点

  3. 3. 重算 vs 未重算指标

    iMA 等指标第0根K线值会变化,策略信号应使用第1根及以后的K线

  4. 4. 四位数 vs 五位数平台

    Point 在五位数平台是 0.00001,止损计算要用 _Digits 判断

  5. 5. 订单选择状态混乱

    OrderSelect 后必须检查返回值,遍历订单时注意索引变化

📌 常用代码片段 (Cheat Sheet)

高频使用的 MQL4 代码模板,直接复制到你的 EA 中使用,开箱即用。

1. 开仓模板(多单 / 空单)
//+==== 开多单 ====+
int OpenBuy(double lot, double slPoints, double tpPoints, int magic)
  {
   double sl = (slPoints > 0) ? NormalizeDouble(Bid - slPoints * Point, Digits) : 0;
   double tp = (tpPoints > 0) ? NormalizeDouble(Ask + tpPoints * Point, Digits) : 0;

   int ticket = OrderSend(_Symbol, OP_BUY, lot, Ask, 30, sl, tp, "EA", magic, 0, clrGreen);
   if (ticket < 0) Print("开多失败, err=", GetLastError());
   return ticket;
  }

//+==== 开空单 ====+
int OpenSell(double lot, double slPoints, double tpPoints, int magic)
  {
   double sl = (slPoints > 0) ? NormalizeDouble(Ask + slPoints * Point, Digits) : 0;
   double tp = (tpPoints > 0) ? NormalizeDouble(Bid - tpPoints * Point, Digits) : 0;

   int ticket = OrderSend(_Symbol, OP_SELL, lot, Bid, 30, sl, tp, "EA", magic, 0, clrRed);
   if (ticket < 0) Print("开空失败, err=", GetLastError());
   return ticket;
  }
2. 平仓模板
//+==== 平掉指定订单 ====+
bool CloseTicket(int ticket)
  {
   if (!OrderSelect(ticket, SELECT_BY_TICKET)) return false;
   double price = (OrderType() == OP_BUY) ? Bid : Ask;
   return OrderClose(ticket, OrderLots(), price, 30, clrRed);
  }

//+==== 平掉当前品种所有订单 ====+
void CloseAll(string symbol, int magic)
  {
   for (int i = OrdersTotal() - 1; i >= 0; i--)
     {
      if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
      if (OrderSymbol() != symbol || OrderMagicNumber() != magic) continue;

      int type = OrderType();
      if (type == OP_BUY || type == OP_SELL)
        {
         double price = (type == OP_BUY) ? Bid : Ask;
         OrderClose(OrderTicket(), OrderLots(), price, 30, clrRed);
        }
      else { OrderDelete(OrderTicket()); }
     }
  }
3. 止损止盈设置
//+==== 修改订单止损止盈 ====+
bool SetSLTP(int ticket, double sl, double tp)
  {
   if (!OrderSelect(ticket, SELECT_BY_TICKET)) return false;
   return OrderModify(ticket, OrderOpenPrice(),
                     NormalizeDouble(sl, Digits),
                     NormalizeDouble(tp, Digits), 0, clrYellow);
  }

//+==== 移动止损(Trailing Stop)====+
void TrailingStop(int magic, double trailPts)
  {
   for (int i = OrdersTotal() - 1; i >= 0; i--)
     {
      if (!OrderSelect(i, SELECT_BY_POS)) continue;
      if (OrderMagicNumber() != magic || OrderSymbol() != _Symbol) continue;

      if (OrderType() == OP_BUY)
        {
         double newSL = NormalizeDouble(Bid - trailPts * Point, Digits);
         if (newSL > OrderStopLoss() + Point)
           OrderModify(OrderTicket(), OrderOpenPrice(), newSL, OrderTakeProfit(), 0, clrYellow);
        }
      else if (OrderType() == OP_SELL)
        {
         double newSL = NormalizeDouble(Ask + trailPts * Point, Digits);
         if (OrderStopLoss() == 0 || newSL < OrderStopLoss() - Point)
           OrderModify(OrderTicket(), OrderOpenPrice(), newSL, OrderTakeProfit(), 0, clrYellow);
        }
     }
  }
4. 手数计算函数
//+==== 按风险百分比计算手数 ====+
double CalcLot(double riskPercent, double slPoints)
  {
   if (slPoints <= 0) return MarketInfo(_Symbol, MODE_MINLOT);

   double riskAmount = AccountBalance() * riskPercent / 100.0;
   double tickValue  = MarketInfo(_Symbol, MODE_TICKVALUE);
   double tickSize   = MarketInfo(_Symbol, MODE_TICKSIZE);
   double valPerPt   = tickValue / tickSize * Point;
   double lot        = riskAmount / (slPoints * valPerPt);

   double lotStep = MarketInfo(_Symbol, MODE_LOTSTEP);
   lot = MathFloor(lot / lotStep) * lotStep;
   lot = NormalizeDouble(lot, 2);

   double minLot = MarketInfo(_Symbol, MODE_MINLOT);
   double maxLot = MarketInfo(_Symbol, MODE_MAXLOT);
   return MathMax(minLot, MathMin(maxLot, lot));
  }
5. 获取指标值(MA / RSI / MACD)
//+==== 移动平均线 MA ====+
double GetMA(int period, int shift, int method=MODE_SMA, int priceType=PRICE_CLOSE)
  {
   return iMA(_Symbol, _Period, period, 0, method, priceType, shift);
  }

//+==== RSI 指标 ====+
double GetRSI(int period, int shift)
  {
   return iRSI(_Symbol, _Period, period, PRICE_CLOSE, shift);
  }

//+==== MACD 快线 / 信号线 / 柱状图 ====+
double GetMACDMain(int fast, int slow, int shift)
  { return iMACD(_Symbol, _Period, fast, slow, 1, PRICE_CLOSE, MODE_MAIN, shift); }

double GetMACDSignal(int fast, int slow, int signal, int shift)
  { return iMACD(_Symbol, _Period, fast, slow, signal, PRICE_CLOSE, MODE_SIGNAL, shift); }

double GetMACDHist(int fast, int slow, int signal, int shift)
  { return iMACD(_Symbol, _Period, fast, slow, signal, PRICE_CLOSE, MODE_HISTOGRAM, shift); }
6. 时间判断(只在特定时段交易)
//+==== 判断是否在交易时段内 ====+
bool IsTradingTime(int startHour, int endHour)
  {
   int h = Hour();
   if (startHour < endHour)
      return (h >= startHour && h < endHour);
   else  // 跨午夜,比如 22:00 - 06:00
      return (h >= startHour || h < endHour);
  }

//+==== 伦敦/纽约时段判断 ====+
bool IsLondonSession()    { return (Hour() >= 8 && Hour() < 16); }
bool IsNewYorkSession()   { return (Hour() >= 13 && Hour() < 21); }
7. 新 K 线判断
//+==== 全局变量方式 ====+
datetime g_lastBarTime = 0;

bool IsNewBar()
  {
   datetime curTime = iTime(_Symbol, _Period, 0);
   if (curTime != g_lastBarTime)
     { g_lastBarTime = curTime; return true; }
   return false;
  }

//+==== 静态变量方式 ====+
bool IsNewBarStatic()
  {
   static datetime s_lastTime = 0;
   datetime cur = iTime(_Symbol, _Period, 0);
   if (cur != s_lastTime) { s_lastTime = cur; return true; }
   return false;
  }

❓ 常见问题 FAQ

MQL 开发中最常遇到的问题和解答,帮你快速排疑解惑。

MQL4 和 MQL5 可以互相转换吗?

可以手动转换,但无法自动无缝转换。两者主要区别在订单模型:MQL4 用单订单系统(OrderSend/OrderClose),MQL5 用持仓+订单双系统(PositionGetInfo/OrderSend)。 另外 MQL5 是完全面向对象的,有类和继承,MQL4 只支持部分 OOP 特性。 如果策略逻辑简单,转换工作量不大;如果用到了高级特性,可能需要重写交易模块。 我们建议:面向 MT4 用户就用 MQL4,面向 MT5 用户就用 MQL5,不要追求一套代码跑两个平台。

EA 怎么挂到 VPS 上 24 小时运行?

有三种方式:
方式一(推荐新手):租一台 Windows VPS,远程桌面上去安装 MT4,登录账号,把 EA 拖到图表上,保持 VPS 不关机即可。
方式二:使用 MT4 内置的 MQL5 虚拟主机(Virtual Hosting),在 MT 菜单「文件 → 租用虚拟主机」,直接把 EA 同步到云端运行。
方式三:Linux 服务器用 Wine 运行 MT4,但稳定性不如 Windows,不推荐新手尝试。

EA 回测和实盘结果不一样怎么办?

常见原因:
1. 点差差异:回测用固定点差,实盘点差会波动。解决:回测时把点差设大一点。
2. 滑点差异:回测默认 0 滑点,实盘有滑点。解决:回测时加合理滑点(10-20 点)。
3. 指标重绘:第 0 根 K 线的指标值会随价格变化。解决:信号只用第 1 根及以后的已完成 K 线(shift >= 1)。
4. 未来函数:某些指标会用到未来数据。解决:检查是否用到了 future functions。

怎么保护 EA 代码不被破解?

代码保护有几个层次:
1. 编译成 ex4:最基本的保护,但 ex4 可以被反编译。
2. 加壳/混淆:使用 Themida、VMProtect 等工具加壳。
3. DLL 封装核心逻辑:把核心策略写到 C++ DLL 里,MQL 只负责调用。这是商业 EA 常用做法。
4. 在线验证:EA 启动时连接服务器验证授权,绑定账号或硬件 ID。
建议:核心策略放 DLL + 在线验证,这是目前最稳妥的方案。

MT4 最多能同时跑多少个 EA?

理论上没有硬性限制,主要取决于电脑性能和 EA 复杂度:
普通电脑(4核 8G):同时跑 10-20 个简单 EA 没问题。
高配电脑(8核 16G):可以跑 30-50 个 EA。
注意:不是越多越好,跑太多会导致延迟增加,影响下单速度。同一个图表上只能挂 1 个 EA。

EA 为什么有时候不下单?

按以下步骤排查:
1. 检查自动交易开关:工具栏「自动交易」按钮是否亮着。
2. 检查 EA 笑脸/哭脸:图表右上角是笑脸还是哭脸,哭脸说明 EA 被禁用。
3. 检查 DLL 权限:EA 属性里是否勾选了「允许 DLL 导入」。
4. 查看 Experts 日志:按 Ctrl+J 打开工具箱,看有没有错误信息。
5. 检查信号条件:是不是当前行情没有触发信号?
6. 检查交易时间:市场关闭时(周末/节假日)无法下单。

如何让 EA 只在特定品种上运行?
// 在 OnInit 中检查(推荐)
input string AllowedSymbol = "EURUSD";

int OnInit()
  {
   if (_Symbol != AllowedSymbol)
     {
      Print("❌ 此EA只允许在 ", AllowedSymbol, " 上运行");
      return INIT_FAILED;
     }
   return INIT_SUCCEEDED;
  }
学习 MQL 需要编程基础吗?

有编程基础(尤其是 C/C++/Java/C#)学 MQL 会非常快,一周就能上手写简单 EA。
没有编程基础也能学,建议学习路径:
1. 先学基础语法(变量、if、for、函数)— 约 1-2 周
2. 再学交易函数(OrderSend/OrderClose)— 约 1 周
3. 仿写简单策略(均线交叉)— 约 1-2 周
4. 学习风控和进阶技巧 — 持续学习
如果实在不想学编程,可以把策略需求交给我们定制开发,通常 3-7 天交付。

首页 文章 下载 产品 定制
回到顶部 ↑ 搜索 🔍 EA定制 ⚙️
微信咨询 💬
晓辉微信二维码

扫码加微信
XiaoHuiProgramming