8/23/2026

trade

//+------------------------------------------------------------------+
//|                                                  MagicNine.mq5   |
//|        神奇九转(TD9) + 确认K线 交易策略 (仅限 M15 周期)           |
//+------------------------------------------------------------------+
#property copyright "MagicNine TD9 Strategy"
#property version   "1.00"
#property strict

#include <Trade/Trade.mqh>

//---------------- 可配置参数 ----------------
input double InpLots        = 0.10;     // 交易手数
input double InpStopLoss    = 100.0;    // 止损距离(价格单位, 如黄金 100 = 100美元/盎司)
input int    InpEMAPeriod   = 100;      // EMA 周期(趋势过滤)
input int    InpTDCount     = 9;        // 九转计数(默认9)
input double InpTP1Percent  = 50.0;     // 第一止盈位平仓比例(%)
input int    InpSlippage    = 30;       // 允许滑点(点)
input ulong  InpMagic       = 20260823; // 魔术号

//---------------- 全局变量 ----------------
CTrade   trade;
int      emaHandle = INVALID_HANDLE;
datetime lastBarTime = 0;

enum EState
{
   STATE_IDLE = 0,        // 空仓无信号
   STATE_WAIT_CONFIRM,    // 九转完成, 等待确认K线收盘
   STATE_IN_POSITION      // 持仓中
};

EState   state        = STATE_IDLE;
int      setupDir     = 0;      // +1 做多信号, -1 做空信号
datetime setupBarTime = 0;      // 九转完成的那根K线时间
double   tp1Price     = 0.0;    // 第一止盈: 九转中间(第5转)K线收盘价
double   tp2Price     = 0.0;    // 第二止盈: 九转第1转K线收盘价
bool     tp1Done      = false;  // 第一止盈是否已执行
int      prevBuyCnt   = 0;      // 上一根K线的买入结构计数(用于识别"刚满9")
int      prevSellCnt  = 0;

//+------------------------------------------------------------------+
//| 计算九转结构计数: 从 startShift 开始, 连续满足条件的K线数          |
//| dir=+1 买入结构: 收盘价 < 前第4根收盘价                            |
//| dir=-1 卖出结构: 收盘价 > 前第4根收盘价                            |
//+------------------------------------------------------------------+
int CountSetup(const int dir, const int startShift)
{
   int count = 0;
   int s = startShift;
   while(s < 300)
   {
      double c0 = iClose(_Symbol, PERIOD_M15, s);
      double c4 = iClose(_Symbol, PERIOD_M15, s + 4);
      if(c0 == 0.0 || c4 == 0.0) break;
      if(dir > 0)
      {
         if(c0 < c4) count++;
         else break;
      }
      else
      {
         if(c0 > c4) count++;
         else break;
      }
      s++;
   }
   return count;
}

//+------------------------------------------------------------------+
//| 手数规范化                                                        |
//+------------------------------------------------------------------+
double NormLots(double lots)
{
   double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   double minL = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxL = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   if(step <= 0.0) step = 0.01;
   lots = MathFloor(lots / step) * step;
   return NormalizeDouble(MathMin(maxL, MathMax(minL, lots)), 8);
}

//+------------------------------------------------------------------+
//| 获取本EA当前持仓(返回ticket, 无持仓返回0)                          |
//+------------------------------------------------------------------+
ulong GetMyPosition()
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0) continue;
      if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
         PositionGetInteger(POSITION_MAGIC) == (long)InpMagic)
         return ticket;
   }
   return 0;
}

//+------------------------------------------------------------------+
//| 平仓(全部)                                                        |
//+------------------------------------------------------------------+
void ClosePosition(const string reason)
{
   ulong ticket = GetMyPosition();
   if(ticket == 0) { state = STATE_IDLE; return; }
   if(trade.PositionClose(ticket))
      Print("平仓成功 [", reason, "] ticket=", ticket);
   else
      Print("平仓失败 [", reason, "] error=", GetLastError());
   state = STATE_IDLE;
   setupDir = 0;
}

//+------------------------------------------------------------------+
//| 部分平仓(第一止盈)                                                 |
//+------------------------------------------------------------------+
void PartialClose(const ulong ticket)
{
   double posVol = PositionGetDouble(POSITION_VOLUME);
   double closeVol = NormLots(posVol * InpTP1Percent / 100.0);
   double minL = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double remain = NormalizeDouble(posVol - closeVol, 8);

   // 剩余不足最小手数则全部平仓
   if(remain < minL || closeVol >= posVol)
   {
      ClosePosition("第一止盈(剩余不足最小手数,全部平仓)");
      tp1Done = true;
      return;
   }

   if(trade.PositionClosePartial(ticket, closeVol))
   {
      Print("第一止盈: 部分平仓 ", closeVol, " 手 @ ", tp1Price);
      tp1Done = true;
   }
   else
      Print("部分平仓失败 error=", GetLastError());
}

//+------------------------------------------------------------------+
//| 初始化                                                            |
//+------------------------------------------------------------------+
int OnInit()
{
   if(Period() != PERIOD_M15)
   {
      Alert("MagicNine: 本策略仅限 15分钟(M15) 周期使用, 请切换周期后再加载!");
      return INIT_FAILED;
   }

   emaHandle = iMA(_Symbol, PERIOD_M15, InpEMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
   if(emaHandle == INVALID_HANDLE)
   {
      Print("EMA 句柄创建失败 error=", GetLastError());
      return INIT_FAILED;
   }

   trade.SetExpertMagicNumber(InpMagic);
   trade.SetDeviationInPoints(InpSlippage);

   Print("MagicNine 初始化完成: ", _Symbol, " M15, 手数=", InpLots,
         ", 止损距离=", InpStopLoss, ", EMA", InpEMAPeriod);
   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| 反初始化                                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   if(emaHandle != INVALID_HANDLE) IndicatorRelease(emaHandle);
}

//+------------------------------------------------------------------+
//| 新K线判断                                                         |
//+------------------------------------------------------------------+
bool IsNewBar()
{
   datetime t = iTime(_Symbol, PERIOD_M15, 0);
   if(t != lastBarTime)
   {
      lastBarTime = t;
      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| 检测九转信号(在刚收盘的K线上, shift=1)                             |
//+------------------------------------------------------------------+
void DetectSignal()
{
   int buyCnt  = CountSetup(+1, 1);
   int sellCnt = CountSetup(-1, 1);

   int newDir = 0;
   if(buyCnt >= InpTDCount && prevBuyCnt < InpTDCount)  newDir = +1;
   if(sellCnt >= InpTDCount && prevSellCnt < InpTDCount) newDir = -1;

   prevBuyCnt  = buyCnt;
   prevSellCnt = sellCnt;

   if(newDir == 0) return;

   // 持仓中出现反向九转信号 -> 先平仓
   ulong ticket = GetMyPosition();
   if(ticket != 0)
   {
      long posType = PositionGetInteger(POSITION_TYPE);
      if((posType == POSITION_TYPE_BUY  && newDir == -1) ||
         (posType == POSITION_TYPE_SELL && newDir == +1))
      {
         ClosePosition("出现反向九转信号");
      }
   }

   // 记录九转关键价位
   // shift=1 是第9转(最新); 第K转K线 shift = 1 + (InpTDCount - K)
   int midK     = (InpTDCount + 1) / 2;              // 中间转(9转时为第5转)
   int midShift = 1 + (InpTDCount - midK);
   int fstShift = 1 + (InpTDCount - 1);              // 第1转K线

   setupDir     = newDir;
   setupBarTime = iTime(_Symbol, PERIOD_M15, 1);
   tp1Price     = iClose(_Symbol, PERIOD_M15, midShift);
   tp2Price     = iClose(_Symbol, PERIOD_M15, fstShift);
   tp1Done      = false;
   state        = STATE_WAIT_CONFIRM;

   Print("九转信号: ", (newDir > 0 ? "做多结构" : "做空结构"),
         " 完成 @ ", setupBarTime,
         " | TP1(第", midK, "转)=", tp1Price, " TP2(第1转)=", tp2Price);
}

//+------------------------------------------------------------------+
//| 确认K线收盘判断(信号后的下一根K线, shift=1)                        |
//+------------------------------------------------------------------+
void CheckConfirm()
{
   // 确认K线必须是信号K线的下一根
   datetime sigTime = iTime(_Symbol, PERIOD_M15, 2);
   if(sigTime != setupBarTime)
   {
      // 已超时(理论上不会发生), 丢弃信号
      state = STATE_IDLE;
      setupDir = 0;
      return;
   }

   double c = iClose(_Symbol, PERIOD_M15, 1);
   double o = iOpen(_Symbol, PERIOD_M15, 1);

   // 前三根K线(含确认K线, shift 1..3)的最高价/最低价
   double hh = MathMax(iHigh(_Symbol, PERIOD_M15, 1),
               MathMax(iHigh(_Symbol, PERIOD_M15, 2),
                       iHigh(_Symbol, PERIOD_M15, 3)));
   double ll = MathMin(iLow(_Symbol, PERIOD_M15, 1),
               MathMin(iLow(_Symbol, PERIOD_M15, 2),
                       iLow(_Symbol, PERIOD_M15, 3)));

   double emaBuf[];
   if(CopyBuffer(emaHandle, 0, 1, 1, emaBuf) <= 0)
   {
      Print("EMA 数据读取失败, 本次信号放弃");
      state = STATE_IDLE;
      setupDir = 0;
      return;
   }
   double ema = emaBuf[0];

   bool confirmed = false;

   if(setupDir > 0)
   {
      // 做多确认: 阳线 + 收盘价未超过前三根K线最高价 + 收盘价高于EMA100
      bool bull      = (c > o);
      bool notExceed = (c <= hh);
      bool aboveEma  = (c > ema);
      confirmed = bull && notExceed && aboveEma;
      Print("做多确认检查: 阳线=", bull, " 未破前三高(", hh, ")=", notExceed,
            " 高于EMA(", ema, ")=", aboveEma);
   }
   else
   {
      // 做空确认: 阴线 + 收盘价未跌破前三根K线最低价 + 收盘价低于EMA100
      bool bear      = (c < o);
      bool notBelow  = (c >= ll);
      bool belowEma  = (c < ema);
      confirmed = bear && notBelow && belowEma;
      Print("做空确认检查: 阴线=", bear, " 未破前三低(", ll, ")=", notBelow,
            " 低于EMA(", ema, ")=", belowEma);
   }

   if(!confirmed)
   {
      Print("确认失败, 信号丢弃");
      state = STATE_IDLE;
      setupDir = 0;
      return;
   }

   // 已有持仓则不再开仓(单仓策略)
   if(GetMyPosition() != 0)
   {
      state = STATE_IN_POSITION;
      return;
   }

   double lots = NormLots(InpLots);
   bool ok = false;
   if(setupDir > 0)
   {
      double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      double sl  = NormalizeDouble(ask - InpStopLoss, _Digits);
      ok = trade.Buy(lots, _Symbol, ask, sl, 0.0, "MagicNine Buy");
   }
   else
   {
      double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      double sl  = NormalizeDouble(bid + InpStopLoss, _Digits);
      ok = trade.Sell(lots, _Symbol, bid, sl, 0.0, "MagicNine Sell");
   }

   if(ok)
   {
      Print("开仓成功: ", (setupDir > 0 ? "BUY" : "SELL"), " ", lots, " 手",
            " | SL距离=", InpStopLoss, " | TP1=", tp1Price, " TP2=", tp2Price);
      state = STATE_IN_POSITION;
      // 若TP1价格不在有利方向(异常数据), 直接跳过TP1
      double entry = trade.ResultPrice();
      if((setupDir > 0 && tp1Price <= entry) || (setupDir < 0 && tp1Price >= entry))
         tp1Done = true;
   }
   else
   {
      Print("开仓失败 error=", GetLastError());
      state = STATE_IDLE;
      setupDir = 0;
   }
}

//+------------------------------------------------------------------+
//| 持仓管理: 移动止盈(逐Tick检查)                                     |
//+------------------------------------------------------------------+
void ManagePosition()
{
   ulong ticket = GetMyPosition();
   if(ticket == 0)
   {
      state = STATE_IDLE;
      setupDir = 0;
      return;
   }

   long posType = PositionGetInteger(POSITION_TYPE);
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

   if(posType == POSITION_TYPE_BUY)
   {
      // 第二止盈: 反弹到第1转价格 -> 全部平仓
      if(bid >= tp2Price)
      {
         ClosePosition("第二止盈(第1转价格)");
         return;
      }
      // 第一止盈: 反弹到中间转价格 -> 平仓50%
      if(!tp1Done && bid >= tp1Price)
         PartialClose(ticket);
   }
   else if(posType == POSITION_TYPE_SELL)
   {
      if(ask <= tp2Price)
      {
         ClosePosition("第二止盈(第1转价格)");
         return;
      }
      if(!tp1Done && ask <= tp1Price)
         PartialClose(ticket);
   }
}

//+------------------------------------------------------------------+
//| 主循环                                                            |
//+------------------------------------------------------------------+
void OnTick()
{
   if(Period() != PERIOD_M15) return;

   // 逐Tick管理持仓止盈
   if(state == STATE_IN_POSITION || GetMyPosition() != 0)
      ManagePosition();

   // 信号与确认只在新K线时处理
   if(!IsNewBar()) return;

   if(iBars(_Symbol, PERIOD_M15) < InpTDCount + 10) return;

   // 1. 先处理等待确认的信号
   if(state == STATE_WAIT_CONFIRM)
      CheckConfirm();

   // 2. 再检测新的九转信号(含反向信号平仓)
   DetectSignal();
}
//+------------------------------------------------------------------+