Skip to content

开仓细节与策略逻辑实现

一、开仓核心逻辑总览

OnTick()方法是EA的“心脏”,负责实时行情处理与交易执行。本章节以RSI反转策略为例,解析从信号判断到订单下达的完整开仓流程,重点包括新K线过滤持仓检查条件判断订单参数计算四大模块。

二、新K线过滤:避免实时波动干扰

  • isNewBar()函数
    • 作用:判断是否产生新K线(基于收盘价形成),避免在K线未闭合时执行逻辑(防止实时价格波动误导信号)。
    • 实现原理:通过对比当前K线与前一根K线K线总数,确认新K线已形成。
cpp
bool isNewBar(string symbol, ENUM_TIMEFRAMES tf, int &bTotal) {
  int bars = iBars(_Symbol, tf);
  if (bars == bTotal)
    return false;
  bTotal = bars;
  return true;
}

三、持仓统计:避免重复开仓

1. 持仓计数函数

cpp
// &cntBuy &cntSell 是引用参数, 他们的作用是将函数内部的计算结果传递回函数外部
bool countOpenPositions(string symbol, int &cntBuy, int &cntSell) {
  cntBuy = 0;
  cntSell = 0;
  int total = PositionsTotal();
  // 倒序循环可以避免因关闭订单循环索引变化导致跳过元素的问题
  for (int i = total - 1; i >= 0; i--) {
    // 获取第i个仓位的订单号,并将其赋值给positionTicket,每个订单都有唯一的订单号
    ulong ticket = PositionGetTicket(i);
    if (ticket <= 0) {
      Print("获取ticket失败");
      return false;
    }
    if (!PositionSelectByTicket(ticket)) {
      Print("选中ticket失败");
      return false;
    }
    long magic;
    if (!PositionGetInteger(POSITION_MAGIC, magic)) {
      Print("获取魔术号失败");
      return false;
    }
    if (magic == MagicNum) {
      long type;
      if (!PositionGetInteger(POSITION_TYPE, type)) {
        Print("获取持仓类型失败");
        return false;
      };
      if (type == POSITION_TYPE_BUY) {
        cntBuy++;
      } else if (type == POSITION_TYPE_SELL) {
        cntSell++;
      }
    }
  }
  return true;
}
  • 核心逻辑
    1. 遍历所有持仓,通过MagicNumber筛选属于当前EA的订单。
    2. 区分多单(POSITION_TYPE_BUY)和空单(POSITION_TYPE_SELL),分别计数。
    3. 使用引用传递int &countBuy)将结果返回给调用方。

2. 持仓检查

cpp
int countBuy = 0, countSell = 0;  
CountOpenPositions(MagicNumber, countBuy, countSell);  
if(countBuy > 0 || countSell > 0) return;  // 有持仓则跳过开仓
  • 作用:确保EA同一时间仅持有单方向仓位(避免多空对冲或重复开仓)。

四、开仓条件:

多单开仓条件为例(空单对称):

cpp
  // RSILevel 表示上轨  (100 - RSILevel) 表示下轨
  bool bullSignal = cntBuy == 0 && RsiBuffer[2] >= (100 - RSILevel) &&
                    RsiBuffer[1] < (100 - RSILevel);
  if (UseMAFilter) {
    bullSignal = bullSignal && ask > MABuffer[1];
  }

1. RSI超卖逻辑

  • 核心条件
    • rsiBuffer[2] > 下轨(前前K线RSI高于下轨)
    • rsiBuffer[1] < 下轨(前K线RSI低于下轨)
    • 逻辑:通过两根K线的RSI穿越下轨,判断超卖反转信号(如RSI_Level=70时,下轨=30,形成“RSI穿越下轨”超卖的信号)。
    • 注意: 数组使用下标[1],[2], 而不用[0], 因为[0]表示当前尚未闭合的K线

2. MA趋势过滤

  • 条件ask > MABuffer[1];(现价高于MA,趋势看涨)
  • 作用:仅在价格位于均线上方时开多单,过滤逆趋势信号,提升信号质量。

五、止损止盈与仓位计算

1. 价格计算(三元表达式简化逻辑)

cpp
    double sl = StopLoss == 0 ? 0 : ask - StopLoss * _Point; // 多单止损价
    double tp = TakeProfit == 0 ? 0 : ask + TakeProfit * _Point; // 多单止盈价
  • 三元表达式:等价于if-else,简化代码结构,提升可读性。

2. 价格规范化

cpp
NormalizePrice(_Symbol, sl);  // 确保价格符合品种最小报价单位  
NormalizePrice(_Symbol, tp);
  • NormalizePrice()函数
    • 作用: 将品种的价格规范化为最小变动单位的整数倍。
cpp
bool NormalizePrice(string symbol, double &price) {
  // 获取 交易品种symbol最小交易单位大小tickSize
  double tickSize = 0;
  if (!SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE, tickSize)) {
    Print("SYMBOL_TRADE_TICK_SIZE 获取异常");
    return false;
  }
  // 获取交易品种symbol的小数点位数digits
  int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
  // 将价格price除以tickSize取整,再乘以tickSize的到规范化后的价格
  // 使用NormalizeDouble函数将价格舍入到指定的小鼠点位数digits
  price = NormalizeDouble(MathRound(price / tickSize) * tickSize, digits);
  return true;
}

3. 仓位计算

cpp
double slp = SlParam;
if (SlType == 4) {
  slp = calculateEachEaBalance(MagicNum, initBalance); // 子账户百分之一
}
double lotSize = calcLots(_Symbol, bid, sl, SlType, slp);

六、反向信号平仓

  • CloseAllPositions()函数
    • 作用:根据信号方向,平掉反向持仓(如开多单前平空单),确保单一方向持仓。
    • 实现:遍历持仓,按类型(多/空)平仓,避免多空同时持仓增加风险。
cpp
// 平仓
bool closePositions(long magicNum, int posType) {
  int total = PositionsTotal();
  for (int i = total - 1; i >= 0; i--) {
    ulong ticket = PositionGetTicket(i);
    if (ticket < 0) {
      Print("获取ticket失败");
      return false;
    }
    if (!PositionSelectByTicket(ticket)) {
      Print("选中ticket失败");
      return false;
    }
    long magic;
    if (PositionGetInteger(POSITION_MAGIC, magic)) {
      Print("获取magic失败");
      return false;
    }
    if (magicNum == magic) {
      long type;
      if (!PositionGetInteger(POSITION_TYPE, type)) {
        Print("获取持仓类型失败");
        return false;
      }
      //如果要平的仓位是多单,但是当前选择的仓位类型是空单,就跳出本次循环,继续下一个仓位的处理
      if (posType == POSITION_TYPE_BUY && type == POSITION_TYPE_SELL)
        continue;
      //如果要平的仓位是空单,但是当前选择的仓位类型是多单,就跳出本次循环,继续下一个仓位的处理  
      if (posType == POSITION_TYPE_SELL && type == POSITION_TYPE_BUY)
        continue;
      trade.PositionClose(ticket);
      if (trade.ResultRetcode() != TRADE_RETCODE_DONE) {
        Print("平仓失败", ticket, " 异常:", (string)trade.ResultRetcode(), " :",
              trade.ResultRetcodeDescription());
      }
    }
  }
  return true;
}

七、自定义NormalizePrice函数和 直接使用NormalizeDouble()的区别

  • 自定义NormalizePrice()函数:
    例如 tickSize = 0.01,digits = 2 , 将100.123规范成100.12
  • 直接使用NormalizeDouble(price,_Digits)函数:
    如果 tickSize 不是0.01(例如 0.05),输出的 100.12 可能不是有效的价格,因为它不是0.05的整数倍。
  • 跨品种兼容性: 不同交易品种的 tickSize 和 digits 可能不同(例如,外汇是 0.00001,股票可能是 0.01),NormalizePrice 能动态适配。