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();
}
//+------------------------------------------------------------------+