mql5/Experts/PinbarSignal.mq5
1Morty 50a3adba89
2025-09-08 21:00:32 +08:00

403 lines
17 KiB
MQL5

//+------------------------------------------------------------------+
//| PinbarSignal |
//| Copyright 2025, You |
//| mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025"
#property link "https://www.mql5.com"
#property version "1.00"
//+------------------------------------------------------------------+
//| 输入参数 |
//+------------------------------------------------------------------+
input group "=====信号与运行参数======"
input string SymbolsList = "XAUUSD,EURUSD,GBPUSD,BTCUSD,ETHUSD,XRPUSD,DOGEUSD,BNBUSD,SOLUSD,LTCUSD,LINKUSD,AAVEUSD,DOTUSD"; // 多品种,英文逗号分割
input string TimeframesList = "M15,M30,H1,H4"; // 多周期,示例: "H1, M30, H4"
input string SymbolSuffix = ""; // 品种后缀
input bool EnableEmail = true; // 启用邮件通知
input string MailSubjectPrefix= "信号提醒"; // 邮件标题前缀
input group "=====Pinbar识别参数====="
input double BodyToRangeMax = 0.3; // 实体占总长度最大比例(越小越像针)
input double WickDominanceMin = 0.6; // 主影线占总长度最小比例
input double NoseToTailRatio = 2.0; // 主影线/次影线 最小倍数
input group "=====调试====="
input bool DebugSendTestOnStart = false; // 启用后,EA启动即发送测试邮件
input bool EnableChartArrows = true; // 符合Pinbar时在图表上标记箭头(仅当前图表品种)
// 全局/静态变量
datetime lastCheckedTimes[]; // 每个周期的上次处理收线时间(以首个品种为基准)
string symbols[]; // 解析后的品种数组
ENUM_TIMEFRAMES timeframes[]; // 解析后的周期数组
//+------------------------------------------------------------------+
//| 工具函数:去除字符串首尾空格 |
//+------------------------------------------------------------------+
string Trim(const string text) {
string s = text;
while(StringLen(s)>0 && (ushort)StringGetCharacter(s,0)<=32) s = StringSubstr(s,1);
while(StringLen(s)>0 && (ushort)StringGetCharacter(s,StringLen(s)-1)<=32) s = StringSubstr(s,0,StringLen(s)-1);
return s;
}
//+------------------------------------------------------------------+
//| 解析SymbolsList到数组 |
//+------------------------------------------------------------------+
int ParseSymbols() {
ArrayFree(symbols);
string parts[];
int n = StringSplit(SymbolsList, ',', parts);
for(int i=0;i<n;i++) {
string sym = Trim(parts[i]);
if(SymbolSuffix!="") sym += SymbolSuffix;
if(sym!="") {
int sz = ArraySize(symbols);
ArrayResize(symbols, sz+1);
symbols[sz] = sym;
}
}
if(ArraySize(symbols)==0) {
ArrayResize(symbols,1);
symbols[0] = _Symbol; // 兜底
}
return ArraySize(symbols);
}
//+------------------------------------------------------------------+
//| 解析 TimeframesList 到周期数组 |
//+------------------------------------------------------------------+
bool ParseTimeframes() {
ArrayFree(timeframes);
string parts[];
int n = StringSplit(TimeframesList, ',', parts);
for(int i=0;i<n;i++) {
string tok = Trim(parts[i]);
StringToUpper(tok); // MQL5 方法式:原位大写化,返回bool
ENUM_TIMEFRAMES tf = PERIOD_CURRENT;
if(tok=="M1") tf = PERIOD_M1;
else if(tok=="M2") tf = PERIOD_M2;
else if(tok=="M3") tf = PERIOD_M3;
else if(tok=="M4") tf = PERIOD_M4;
else if(tok=="M5") tf = PERIOD_M5;
else if(tok=="M6") tf = PERIOD_M6;
else if(tok=="M10") tf = PERIOD_M10;
else if(tok=="M12") tf = PERIOD_M12;
else if(tok=="M15") tf = PERIOD_M15;
else if(tok=="M20") tf = PERIOD_M20;
else if(tok=="M30") tf = PERIOD_M30;
else if(tok=="H1") tf = PERIOD_H1;
else if(tok=="H2") tf = PERIOD_H2;
else if(tok=="H3") tf = PERIOD_H3;
else if(tok=="H4") tf = PERIOD_H4;
else if(tok=="H6") tf = PERIOD_H6;
else if(tok=="H8") tf = PERIOD_H8;
else if(tok=="H12") tf = PERIOD_H12;
else if(tok=="D1") tf = PERIOD_D1;
else if(tok=="W1") tf = PERIOD_W1;
else if(tok=="MN1") tf = PERIOD_MN1;
else continue;
int sz = ArraySize(timeframes);
ArrayResize(timeframes, sz+1);
timeframes[sz] = tf;
}
if(ArraySize(timeframes)==0) {
ArrayResize(timeframes, 2);
timeframes[0] = PERIOD_H1;
timeframes[1] = PERIOD_M30;
}
ArrayResize(lastCheckedTimes, ArraySize(timeframes));
for(int k=0;k<ArraySize(lastCheckedTimes);k++) lastCheckedTimes[k]=0;
return(ArraySize(timeframes)>0);
}
//+------------------------------------------------------------------+
//| 读取指定品种与周期的上一根K线(已收线) |
//+------------------------------------------------------------------+
bool GetPrevBar(const string sym, const ENUM_TIMEFRAMES tf, MqlRates &bar) {
// 明确请求上一根已收线K(shift=1,count=1),避免数组顺序或未收线K的歧义
MqlRates rates[1];
int copied = CopyRates(sym, tf, 1, 1, rates);
if(copied<1) return false;
bar = rates[0];
return true;
}
//+------------------------------------------------------------------+
//| 判定Pinbar(返回:0=否, 1=看涨Pinbar, -1=看跌Pinbar) |
//+------------------------------------------------------------------+
int IsPinbar(const MqlRates &b, const int digits) {
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
// 总长度(高低)
double range = b.high - b.low;
if(range <= 0) return 0;
// 实体与影线
double body = MathAbs(b.close - b.open);
double upperWick = b.high - MathMax(b.open, b.close);
double lowerWick = MathMin(b.open, b.close) - b.low;
double bodyRatio = body / range;
if(bodyRatio > BodyToRangeMax) return 0;
// 判断主影线与方向
if(upperWick > lowerWick) {
double dominance = upperWick / range; // 上影占比
double ratio = (lowerWick <= 0 ? 9999.0 : upperWick / lowerWick);
if(dominance >= WickDominanceMin && ratio >= NoseToTailRatio)
return -1; // 看跌Pinbar(长上影)
} else if(lowerWick > upperWick) {
double dominance = lowerWick / range; // 下影占比
double ratio = (upperWick <= 0 ? 9999.0 : lowerWick / upperWick);
if(dominance >= WickDominanceMin && ratio >= NoseToTailRatio)
return 1; // 看涨Pinbar(长下影)
}
return 0;
}
//+------------------------------------------------------------------+
//| 将周期枚举转为短标签(M1/H1/D1等) |
//+------------------------------------------------------------------+
string TfToLabel(const ENUM_TIMEFRAMES tf) {
switch(tf) {
case PERIOD_M1: return "M1";
case PERIOD_M2: return "M2";
case PERIOD_M3: return "M3";
case PERIOD_M4: return "M4";
case PERIOD_M5: return "M5";
case PERIOD_M6: return "M6";
case PERIOD_M10: return "M10";
case PERIOD_M12: return "M12";
case PERIOD_M15: return "M15";
case PERIOD_M20: return "M20";
case PERIOD_M30: return "M30";
case PERIOD_H1: return "H1";
case PERIOD_H2: return "H2";
case PERIOD_H3: return "H3";
case PERIOD_H4: return "H4";
case PERIOD_H6: return "H6";
case PERIOD_H8: return "H8";
case PERIOD_H12: return "H12";
case PERIOD_D1: return "D1";
case PERIOD_W1: return "W1";
case PERIOD_MN1: return "MN1";
default: return EnumToString(tf);
}
}
//+------------------------------------------------------------------+
//| 在图表上标记Pinbar箭头(仅当前图表与主窗口) |
//+------------------------------------------------------------------+
void MarkPinbarOnChart(const string sym, const ENUM_TIMEFRAMES tf, const MqlRates &b, const int dir) {
if(!EnableChartArrows) return;
if(sym != _Symbol) return; // 仅在当前图表品种上绘制
// 生成唯一对象名:Pinbar_<TF>_<time>_<dir>
string name = StringFormat("Pinbar_%s_%I64d_%d", EnumToString(tf), (long)b.time, dir);
// 价格位置:看涨放在低点下方一点,看跌放在高点上方一点
double point = SymbolInfoDouble(sym, SYMBOL_POINT);
double price = (dir>0 ? b.low - 2.0*point : b.high + 2.0*point);
ENUM_OBJECT type = (dir>0 ? OBJ_ARROW_BUY : OBJ_ARROW_SELL);
// 若已存在同名对象则先删除
if(ObjectFind(0, name) >= 0) ObjectDelete(0, name);
bool created = ObjectCreate(0, name, type, 0, b.time, price);
if(!created) {
Print("[Pinbar] 创建箭头对象失败:", name, ", err=", GetLastError());
return;
}
ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, name, OBJPROP_BACK, false);
ObjectSetInteger(0, name, OBJPROP_HIDDEN, false);
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_CENTER);
// 颜色区分方向
color clr = (dir>0 ? clrLime : clrRed);
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
}
//+------------------------------------------------------------------+
//| 在收线时扫描所有品种,返回汇总文本 |
//+------------------------------------------------------------------+
string ScanAllSymbolsAndSummarize(const ENUM_TIMEFRAMES tf, const datetime barTime) {
string lines = "";
for(int i=0;i<ArraySize(symbols);i++) {
string sym = symbols[i];
MqlRates b;
if(!GetPrevBar(sym, tf, b)) {
continue;
}
// 只在同一收线时间的bar上做判断(不同品种会偶发不同步,这里放宽不强制相等)
int digits = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS);
int pinbarType = IsPinbar(b, digits);
if(pinbarType != 0) {
string type = (pinbarType > 0 ? "Bullish" : "Bearish");
// 实时打印每次检测到的Pinbar
Print(StringFormat("[Pinbar] %s | %s | %s", sym, type, TimeToString(b.time, TIME_DATE|TIME_MINUTES)));
// 在当前图表上做箭头标记(仅当前图表品种)
MarkPinbarOnChart(sym, tf, b, pinbarType);
// 邮件正文:<品种><周期>出现↑/↓Pinbar
string arrow = (pinbarType > 0 ? "" : "");
lines += StringFormat("%s %s 出现%sPinbar\n", sym, TfToLabel(tf), arrow);
}
}
return lines;
}
//+------------------------------------------------------------------+
//| 发送测试邮件 |
//+------------------------------------------------------------------+
void SendTestEmailOnStart() {
string subject = MailSubjectPrefix;
string body = "这是一封来自 PinbarSignal 的测试邮件,用于验证终端邮件配置是否正常。";
if(EnableEmail) {
bool ok = SendMail(subject, body);
if(!ok) {
Print("[Pinbar] 测试邮件发送失败,请检查终端邮件设置。");
} else {
Print("[Pinbar] 测试邮件已发送: ", subject);
Print("[Pinbar] 邮件内容:", body);
}
} else {
Print("[Pinbar] 未启用终端邮件,测试主题: ", subject);
Print("[Pinbar] 未启用终端邮件,测试内容:\n", body);
}
}
//+------------------------------------------------------------------+
//| 在新的收线时触发:汇总并发送邮件 |
//+------------------------------------------------------------------+
void OnNewClosedBar(const ENUM_TIMEFRAMES tf, const datetime newBarTime) {
string result = ScanAllSymbolsAndSummarize(tf, newBarTime);
if(result=="") {
Print("[Pinbar] 本次收线未检测到信号。");
return;
}
string subject = MailSubjectPrefix;
string body = result;
if(EnableEmail) {
bool ok = SendMail(subject, body);
if(!ok) {
Print("[Pinbar] 发送邮件失败,请检查终端邮件设置。");
} else {
Print("[Pinbar] 邮件已发送: ", subject);
Print("[Pinbar] 邮件内容:", body);
}
} else {
Print("[Pinbar] 邮件未启用,主题: ", subject);
Print("[Pinbar] 邮件未启用,结果:\n", body);
}
}
//+------------------------------------------------------------------+
//| 计时器:每秒检查一次是否出现新收线 |
//+------------------------------------------------------------------+
void CheckNewBarEvent() {
if(ArraySize(symbols)==0) return;
string base = symbols[0];
// 收集触发的(时间, 周期)
ENUM_TIMEFRAMES triggers_tf[];
datetime triggers_time[];
for(int t=0; t<ArraySize(timeframes); t++) {
ENUM_TIMEFRAMES tf = timeframes[t];
datetime times[2];
int copied = CopyTime(base, tf, 0, 2, times);
if(copied<2) continue;
datetime prevClosedTime = times[1];
if(prevClosedTime<=0) continue;
if(lastCheckedTimes[t] == 0) {
lastCheckedTimes[t] = prevClosedTime;
continue;
}
if(prevClosedTime != lastCheckedTimes[t]) {
lastCheckedTimes[t] = prevClosedTime;
int sz = ArraySize(triggers_tf);
ArrayResize(triggers_tf, sz+1);
ArrayResize(triggers_time, sz+1);
triggers_tf[sz] = tf;
triggers_time[sz] = prevClosedTime;
}
}
// 将同一时间的所有周期合并为一次发送
for(int i=0;i<ArraySize(triggers_time);i++) {
if(triggers_time[i]==0) continue; // 已处理
datetime tkey = triggers_time[i];
string combined = "";
// 包含当前索引
combined += ScanAllSymbolsAndSummarize(triggers_tf[i], tkey);
// 收集同一时间的其余周期
for(int j=i+1;j<ArraySize(triggers_time);j++) {
if(triggers_time[j]==tkey) {
combined += ScanAllSymbolsAndSummarize(triggers_tf[j], tkey);
triggers_time[j] = 0; // 标记已并入
}
}
// 发送一次(若有结果)
if(combined!="") {
string subject = MailSubjectPrefix;
string body = combined;
if(EnableEmail) {
bool ok = SendMail(subject, body);
if(!ok) {
Print("[Pinbar] 发送邮件失败,请检查终端邮件设置。");
} else {
Print("[Pinbar] 邮件已发送: ", subject);
Print("[Pinbar] 邮件内容:", body);
}
} else {
Print("[Pinbar] 邮件未启用,主题: ", subject);
Print("[Pinbar] 邮件未启用,结果:\n", body);
}
} else {
Print("[Pinbar] 本次收线未检测到信号。");
}
triggers_time[i] = 0; // 标记自身
}
}
//+------------------------------------------------------------------+
//| Expert initialization |
//+------------------------------------------------------------------+
int OnInit() {
ParseSymbols();
ParseTimeframes();
EventSetTimer(1); // 每秒检查一次
Print("=== PinbarSignal 初始化 ===");
string tfs = "";
for(int i=0;i<ArraySize(timeframes);i++) {
if(i>0) tfs += ", ";
tfs += EnumToString(timeframes[i]);
}
Print("监控周期:", tfs);
Print("监控品种:", SymbolsList);
if(DebugSendTestOnStart) {
SendTestEmailOnStart();
}
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
EventKillTimer();
}
//+------------------------------------------------------------------+
//| OnTick 保持轻量(主要逻辑在OnTimer) |
//+------------------------------------------------------------------+
void OnTick() {
// 仅保证活性,不做重逻辑
}
//+------------------------------------------------------------------+
//| OnTimer:定时检查 |
//+------------------------------------------------------------------+
void OnTimer() {
CheckNewBarEvent();
}
//+------------------------------------------------------------------+