|
|
//+------------------------------------------------------------------+
//| Bollinger & RSI Close Only EA |
//| |
//| |
//+------------------------------------------------------------------+
#property copyright "QQ群:788238290"
#property link "QQ群:788238290"
#property version "投资有风险,入市需谨慎"
#property strict
#property description "布林RSI一单一结EA - 黄金量化交易程序"
#property description "开仓信号: 布林带 + RSI 组合策略"
#property description " - 做多: 价格触及布林带下轨 + RSI超卖"
#property description " - 做空: 价格触及布林带上轨 + RSI超买"
#property description "功能特性: 固定止盈止损 + 移动止损 + 冷却时间"
#property description "周期适配: 1M/5M/15M/30M/H1 多周期适用"
#include <Trade/Trade.mqh>
#include <Trade/PositionInfo.mqh>
//+------------------------------------------------------------------+
//| 版本历史记录 |
//+------------------------------------------------------------------+
//| 1.00 (2026.06.02) 基于 GOLD PULSE EA V8.2 重构
//| - 【核心修复】周期硬编码→支持多周期(1M/5M/15M/H1等)
//| - 【核心修复】信号逻辑优化,使用上一根K线收盘价避免未来函数
//| - 【核心修复】一单一结逻辑修复,支持反向开仓
//| - 【新增功能】完整交易时间控制(时区/每日时间/周五时间/午间休市)
//| - 【稳健性】添加连接状态检查和详细错误日志
//| - 【性能优化】减少重复指标更新,代码清理
//| - 【参数调整】优化默认参数,适配黄金交易
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| 硬编码过期日期 |
//+------------------------------------------------------------------+
#define USE_DATE_LIMIT true
#define EXPIRY_DATE "2026.08.18"
//+------------------------------------------------------------------+
//| 输入参数 |
//+------------------------------------------------------------------+
input string Inp_General = "========= 常规设置 =========";
input string Inp_QQGroup = "QQ群:1054681521";
input double InpInitialLot = 0.01; // 初始开仓手数
input double InpMaxTotalLossUSD = 0; // 最大总亏损限制(美元),0为禁用
input int InpMagicNumber = 666666; // 魔术号
input int InpCooldownMinutes = 10; // 冷却时间(分钟)
input string Inp_TP_SL = "========= 止盈止损设置 =========";
input int InpStopLossPoints = 700; // 固定止损点数
input int InpTakeProfitPoints = 1500; // 固定止盈点数
input string Inp_Trailing = "========= 移动止损设置 =========";
input int InpTrailingStartPoints = 150; // 移动止损激活点数
input int InpTrailingStepPoints = 200; // 移动止损步长
input int InpTrailingStopPoints = 100; // 移动止损距离
input string Inp_Bollinger = "========= 布林带设置 =========";
input int InpBollingerPeriod = 5; // 布林带周期
input double InpBollingerDeviation = 2.0; // 布林带标准差倍数
input string Inp_RSI = "========= RSI设置 =========";
input int InpRSIPeriod = 5; // RSI周期
input int InpRSIOverbought = 66; // RSI超买阈值
input int InpRSIOversold = 33; // RSI超卖阈值
//+------------------------------------------------------------------+
//| 交易时间设置 |
//+------------------------------------------------------------------+
input string Inp_TradingTime = "========= 交易时间设置 =========";
input string Inp_TimeNote = "说明:以下所有时间均为【北京时间】";
input int InpTimeZoneOffset = 5; // 时区偏移(北京时间=平台时间+偏移小时)
input string Inp_DailyTime = "------ 每日交易时间 ------";
input int InpTradeStartHour = 9; // 交易开始小时(北京时间)
input int InpTradeStartMinute = 0; // 交易开始分钟
input int InpTradeEndHour = 18; // 交易结束小时(北京时间)
input int InpTradeEndMinute = 0; // 交易结束分钟
input string Inp_FridayTime = "------ 周五交易时间 ------";
input int InpFriTradeStartHour = 9; // 周五交易开始小时
input int InpFriTradeStartMinute = 0; // 周五交易开始分钟
input int InpFriTradeEndHour = 18; // 周五交易结束小时
input int InpFriTradeEndMinute = 0; // 周五交易结束分钟
input string Inp_NoonBreak = "------ 午间休市 ------";
input bool InpEnableNoonBreak = true; // 是否启用午间休市
input int InpNoonBreakStartHour = 13; // 休市开始小时
input int InpNoonBreakStartMinute= 30; // 休市开始分钟
input int InpNoonBreakEndHour = 14; // 休市结束小时
input int InpNoonBreakEndMinute = 30; // 休市结束分钟
//+------------------------------------------------------------------+
//| 全局变量 |
//+------------------------------------------------------------------+
CTrade m_trade;
CPositionInfo m_position;
string g_symbol;
int g_magic;
double g_point;
double g_tickValue;
bool g_isPaused = false;
bool g_maxLossReached = false;
double g_totalClosedProfit = 0; // 累计已平仓盈亏
// 冷却时间变量
datetime g_lastCloseTime = 0;
bool g_inCooldown = false;
// 布林带指标句柄
int g_bollingerHandle = INVALID_HANDLE;
double g_upperBand[], g_middleBand[], g_lowerBand[];
// RSI指标句柄
int g_rsiHandle = INVALID_HANDLE;
double g_rsiBuffer[];
// UI组件
string g_prefix = "ONESHOT_EV_";
string g_btnCloseAll = "btnCloseAll";
string g_btnPause = "btnPause";
bool g_buttonsCreated = false;
// 移动止损追踪
struct TrailingData
{
ulong ticket;
double lastProfitPoints;
};
TrailingData g_trailingData;
//+------------------------------------------------------------------+
//| 专家初始化函数 |
//+------------------------------------------------------------------+
int OnInit()
{
// 过期检查
if(USE_DATE_LIMIT)
{
datetime expiry = StringToTime(EXPIRY_DATE);
if(expiry == 0)
{
Print("错误: 无效的过期日期格式: ", EXPIRY_DATE);
return(INIT_FAILED);
}
if(TimeCurrent() > expiry)
{
MessageBox("EA已过期。\n过期日期: " + EXPIRY_DATE, "ONESHOT_GOLD_EV - 已过期", MB_ICONSTOP);
return(INIT_FAILED);
}
}
// 连接状态检查
if(TerminalInfoInteger(TERMINAL_CONNECTED) != 1)
{
Print("警告: 未连接到交易服务器");
}
if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) != 1)
{
Print("警告: 交易功能未启用");
}
g_symbol = Symbol();
g_magic = InpMagicNumber;
g_point = SymbolInfoDouble(g_symbol, SYMBOL_POINT);
g_tickValue = SymbolInfoDouble(g_symbol, SYMBOL_TRADE_TICK_VALUE);
if(g_tickValue <= 0)
{
Print("错误: 获取品种点值失败。错误码: ", GetLastError());
return(INIT_FAILED);
}
m_trade.SetExpertMagicNumber(g_magic);
// 初始化布林带指标
g_bollingerHandle = iBands(g_symbol, _Period, InpBollingerPeriod, 0, InpBollingerDeviation, PRICE_CLOSE);
if(g_bollingerHandle == INVALID_HANDLE)
{
Print("错误: 创建布林带指标失败。错误码: ", GetLastError());
return(INIT_FAILED);
}
// 初始化RSI指标
g_rsiHandle = iRSI(g_symbol, _Period, InpRSIPeriod, PRICE_CLOSE);
if(g_rsiHandle == INVALID_HANDLE)
{
Print("错误: 创建RSI指标失败。错误码: ", GetLastError());
return(INIT_FAILED);
}
// 初始化变量
g_isPaused = false;
g_maxLossReached = false;
g_lastCloseTime = 0;
g_inCooldown = false;
g_trailingData.ticket = 0;
g_trailingData.lastProfitPoints = 0;
// 创建面板
CreateDashboard();
Print("========== 布林RSI一单一结EA 初始化完成 | QQ群:1054681521 ==========");
Print("魔术号: ", g_magic);
Print("品种: ", g_symbol, " 点值: ", DoubleToString(g_tickValue, 2));
Print("当前周期: ", EnumToString(_Period), " (多周期适用)");
Print("开仓信号: 布林带 + RSI 组合策略");
Print(" - 做多: 上一根K线收盘价触及布林带下轨 + RSI < ", InpRSIOversold);
Print(" - 做空: 上一根K线收盘价触及布林带上轨 + RSI > ", InpRSIOverbought);
Print("布林带: 周期=", InpBollingerPeriod, " 标准差=", InpBollingerDeviation);
Print("RSI: 周期=", InpRSIPeriod, " 超买=", InpRSIOverbought, " 超卖=", InpRSIOversold);
Print("止损: ", InpStopLossPoints, " 点");
Print("止盈: ", InpTakeProfitPoints, " 点");
Print("移动止损: 激活=", InpTrailingStartPoints, " 步长=", InpTrailingStepPoints, " 距离=", InpTrailingStopPoints);
Print("冷却时间: ", InpCooldownMinutes, " 分钟");
Print("最大总亏损: ", InpMaxTotalLossUSD > 0 ? DoubleToString(InpMaxTotalLossUSD,2) + " 美元" : "禁用");
Print("====================================================");
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| 专家反初始化函数 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
if(g_bollingerHandle != INVALID_HANDLE)
IndicatorRelease(g_bollingerHandle);
if(g_rsiHandle != INVALID_HANDLE)
IndicatorRelease(g_rsiHandle);
ObjectsDeleteAll(0, g_prefix);
Print("布林RSI一单一结EA 已移除。QQ群:1054681521");
}
//+------------------------------------------------------------------+
//| 将平台时间转换为北京时间 |
//+------------------------------------------------------------------+
datetime GetBeijingTime()
{
return TimeCurrent() + InpTimeZoneOffset * 3600;
}
//+------------------------------------------------------------------+
//| 检查是否在交易时间内 |
//+------------------------------------------------------------------+
bool IsInTradingTime()
{
datetime beijingTime = GetBeijingTime();
MqlDateTime dt;
TimeToStruct(beijingTime, dt);
int dayOfWeek = dt.day_of_week;
int hour = dt.hour;
int minute = dt.min;
// 周末不交易
if(dayOfWeek == 0 || dayOfWeek == 6)
return false;
// 获取今日交易时间
int startHour, startMinute, endHour, endMinute;
if(dayOfWeek == 5)
{
startHour = InpFriTradeStartHour;
startMinute = InpFriTradeStartMinute;
endHour = InpFriTradeEndHour;
endMinute = InpFriTradeEndMinute;
}
else
{
startHour = InpTradeStartHour;
startMinute = InpTradeStartMinute;
endHour = InpTradeEndHour;
endMinute = InpTradeEndMinute;
}
// 转换为分钟数比较
int currentMinutes = hour * 60 + minute;
int startMinutes = startHour * 60 + startMinute;
int endMinutes = endHour * 60 + endMinute;
// 检查是否在交易时段内
if(currentMinutes < startMinutes || currentMinutes >= endMinutes + 1)
return false;
// 检查午间休市
if(InpEnableNoonBreak)
{
int noonStartMinutes = InpNoonBreakStartHour * 60 + InpNoonBreakStartMinute;
int noonEndMinutes = InpNoonBreakEndHour * 60 + InpNoonBreakEndMinute;
if(currentMinutes >= noonStartMinutes && currentMinutes <= noonEndMinutes)
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| 获取当前交易时间状态描述 |
//+------------------------------------------------------------------+
string GetTradingTimeStatus()
{
if(!IsInTradingTime())
{
datetime beijingTime = GetBeijingTime();
MqlDateTime dt2;
TimeToStruct(beijingTime, dt2);
int dayOfWeek = dt2.day_of_week;
if(dayOfWeek == 0 || dayOfWeek == 6)
return "时间: 周末休市";
if(InpEnableNoonBreak)
{
MqlDateTime dt3;
TimeToStruct(beijingTime, dt3);
int hour = dt3.hour;
int minute = dt3.min;
int noonStartMinutes = InpNoonBreakStartHour * 60 + InpNoonBreakStartMinute;
int noonEndMinutes = InpNoonBreakEndHour * 60 + InpNoonBreakEndMinute;
int currentMinutes = hour * 60 + minute;
if(currentMinutes >= noonStartMinutes && currentMinutes <= noonEndMinutes)
return "时间: 午间休市";
}
return "时间: 非交易时段";
}
return "时间: 交易时间";
}
//+------------------------------------------------------------------+
//| 更新指标数据 |
//+------------------------------------------------------------------+
bool UpdateIndicatorData()
{
// 获取布林带数据
if(CopyBuffer(g_bollingerHandle, 0, 1, 1, g_middleBand) != 1)
{
Print("错误: 获取布林带中轨失败。错误码: ", GetLastError());
return false;
}
if(CopyBuffer(g_bollingerHandle, 1, 1, 1, g_upperBand) != 1)
{
Print("错误: 获取布林带上轨失败。错误码: ", GetLastError());
return false;
}
if(CopyBuffer(g_bollingerHandle, 2, 1, 1, g_lowerBand) != 1)
{
Print("错误: 获取布林带下轨失败。错误码: ", GetLastError());
return false;
}
// 获取RSI数据
if(CopyBuffer(g_rsiHandle, 0, 1, 1, g_rsiBuffer) != 1)
{
Print("错误: 获取RSI数据失败。错误码: ", GetLastError());
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| 检查是否应该开仓 - 布林带+RSI组合信号 |
//| 做多: 价格触及/突破布林带下轨 + RSI < 超卖阈值 |
//| 做空: 价格触及/突破布林带上轨 + RSI > 超买阈值 |
//+------------------------------------------------------------------+
int CheckSignal()
{
if(!UpdateIndicatorData())
return 0;
// 使用上一根K线收盘价,与指标数据时间维度一致
double lastClose = iClose(g_symbol, _Period, 1);
double lowerBand = g_lowerBand[0];
double upperBand = g_upperBand[0];
double rsi = g_rsiBuffer[0];
// 做多信号: 上一根K线收盘价触及或跌破布林带下轨 + RSI低于超卖阈值
// 使用一个小偏差来确保信号触发,避免因价格非常接近但未触及而错过
double tolerance = g_point * 2;
bool buySignal = (lastClose <= lowerBand + tolerance) && (rsi < InpRSIOversold);
if(buySignal)
{
Print("[信号] 做多信号触发!");
Print(" 上一根K线收盘价: ", DoubleToString(lastClose, _Digits));
Print(" 布林带下轨: ", DoubleToString(lowerBand, _Digits));
Print(" RSI: ", DoubleToString(rsi, 1));
Print(" 条件: 收盘价 ≤ 下轨 + 容差 + RSI < ", InpRSIOversold);
return 1;
}
// 做空信号: 上一根K线收盘价触及或突破布林带上轨 + RSI高于超买阈值
bool sellSignal = (lastClose >= upperBand - tolerance) && (rsi > InpRSIOverbought);
if(sellSignal)
{
Print("[信号] 做空信号触发!");
Print(" 上一根K线收盘价: ", DoubleToString(lastClose, _Digits));
Print(" 布林带上轨: ", DoubleToString(upperBand, _Digits));
Print(" RSI: ", DoubleToString(rsi, 1));
Print(" 条件: 收盘价 ≥ 上轨 - 容差 + RSI > ", InpRSIOverbought);
return -1;
}
return 0;
}
//+------------------------------------------------------------------+
//| 计算止损价格 |
//+------------------------------------------------------------------+
double CalculateStopLoss(ENUM_POSITION_TYPE type, double entryPrice, int points)
{
double slPrice;
if(type == POSITION_TYPE_BUY)
slPrice = entryPrice - points * g_point;
else
slPrice = entryPrice + points * g_point;
// 确保止损不低于经纪商要求的最小距离
long stopLevel = SymbolInfoInteger(g_symbol, SYMBOL_TRADE_STOPS_LEVEL);
double minDistance = stopLevel * g_point;
if(type == POSITION_TYPE_BUY)
slPrice = MathMin(slPrice, entryPrice - minDistance);
else
slPrice = MathMax(slPrice, entryPrice + minDistance);
return NormalizeDouble(slPrice, (int)SymbolInfoInteger(g_symbol, SYMBOL_DIGITS));
}
//+------------------------------------------------------------------+
//| 计算止盈价格 |
//+------------------------------------------------------------------+
double CalculateTakeProfit(ENUM_POSITION_TYPE type, double entryPrice, int points)
{
double tpPrice;
if(type == POSITION_TYPE_BUY)
tpPrice = entryPrice + points * g_point;
else
tpPrice = entryPrice - points * g_point;
return NormalizeDouble(tpPrice, (int)SymbolInfoInteger(g_symbol, SYMBOL_DIGITS));
}
//+------------------------------------------------------------------+
//| 开仓 - 一单一结: 先平反方向单,再开新单 |
//+------------------------------------------------------------------+
void OpenPosition(ENUM_POSITION_TYPE type)
{
// 一单一结: 先平掉所有现有仓位
if(HasPosition())
{
Print("[一单一结] 检测到现有仓位,先平仓...");
CloseAllPositions();
// 平仓后重置移动止损追踪
g_trailingData.ticket = 0;
g_trailingData.lastProfitPoints = 0;
}
double price = (type == POSITION_TYPE_BUY) ? SymbolInfoDouble(g_symbol, SYMBOL_ASK) : SymbolInfoDouble(g_symbol, SYMBOL_BID);
double slPrice = CalculateStopLoss(type, price, InpStopLossPoints);
double tpPrice = CalculateTakeProfit(type, price, InpTakeProfitPoints);
long stopLevel = SymbolInfoInteger(g_symbol, SYMBOL_TRADE_STOPS_LEVEL);
double actualSLPoints = (type == POSITION_TYPE_BUY) ? (price - slPrice) / g_point : (slPrice - price) / g_point;
Print("========== 开仓 ", EnumToString(type), " ==========");
Print("入场: ", DoubleToString(price, _Digits));
Print("止损: ", DoubleToString(slPrice, _Digits), " (", DoubleToString(actualSLPoints, 0), "点)");
Print("止盈: ", DoubleToString(tpPrice, _Digits));
if(actualSLPoints < stopLevel)
{
Print("警告: 止损距离低于经纪商最小要求,自动调整...");
if(type == POSITION_TYPE_BUY)
slPrice = price - stopLevel * g_point;
else
slPrice = price + stopLevel * g_point;
slPrice = NormalizeDouble(slPrice, (int)SymbolInfoInteger(g_symbol, SYMBOL_DIGITS));
}
string comment = "OS_EV_" + IntegerToString(g_magic);
if(m_trade.PositionOpen(g_symbol, (type == POSITION_TYPE_BUY) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL,
InpInitialLot, price, slPrice, tpPrice, comment))
{
Print("✓ 开仓成功!");
// ====== 修复:正确获取持仓ticket ======
bool foundPosition = false;
for(int i = PositionsTotal() - 1; i >= 0 && !foundPosition; i--)
{
if(m_position.SelectByIndex(i))
{
if(m_position.Symbol() == g_symbol && m_position.Magic() == g_magic)
{
g_trailingData.ticket = m_position.Ticket();
g_trailingData.lastProfitPoints = 0;
foundPosition = true;
}
}
}
// ===================================
Print("===============================================");
}
else
{
int error = GetLastError();
Print("✗ 开仓失败. 错误码: ", error, " - ", m_trade.ResultRetcodeDescription());
Print("===============================================");
}
}
//+------------------------------------------------------------------+
//| 检查是否有持仓 |
//+------------------------------------------------------------------+
bool HasPosition()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(m_position.SelectByIndex(i) && m_position.Symbol() == g_symbol && m_position.Magic() == g_magic)
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| 获取持仓信息(如果有) |
//+------------------------------------------------------------------+
bool GetCurrentPosition(CPositionInfo &pos)
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(m_position.SelectByIndex(i) && m_position.Symbol() == g_symbol && m_position.Magic() == g_magic)
{
pos = m_position;
return true;
}
}
return false;
}
//+------------------------------------------------------------------+
//| 利润美元转点数 |
//+------------------------------------------------------------------+
double ProfitUSDToPoints(double profitUSD, double lot)
{
if(lot <= 0 || g_tickValue <= 0) return 0;
return profitUSD / (lot * g_tickValue);
}
//+------------------------------------------------------------------+
//| 管理移动止损 |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
CPositionInfo pos;
if(!GetCurrentPosition(pos))
{
if(g_trailingData.ticket != 0)
{
g_trailingData.ticket = 0;
g_trailingData.lastProfitPoints = 0;
}
return;
}
ulong ticket = pos.Ticket();
double profitUSD = pos.Profit();
double currentSL = pos.StopLoss();
ENUM_POSITION_TYPE posType = pos.PositionType();
double volume = pos.Volume();
if(g_trailingData.ticket != ticket)
{
g_trailingData.ticket = ticket;
g_trailingData.lastProfitPoints = 0;
}
double profitPoints = ProfitUSDToPoints(profitUSD, volume);
if(profitPoints < InpTrailingStartPoints)
return;
double newStopLoss;
double currentPrice = (posType == POSITION_TYPE_BUY) ? SymbolInfoDouble(g_symbol, SYMBOL_BID) : SymbolInfoDouble(g_symbol, SYMBOL_ASK);
if(posType == POSITION_TYPE_BUY)
newStopLoss = currentPrice - InpTrailingStopPoints * g_point;
else
newStopLoss = currentPrice + InpTrailingStopPoints * g_point;
long stopLevel = SymbolInfoInteger(g_symbol, SYMBOL_TRADE_STOPS_LEVEL);
double minDistance = stopLevel * g_point;
if(posType == POSITION_TYPE_BUY)
newStopLoss = MathMax(newStopLoss, pos.PriceOpen() + minDistance);
else
newStopLoss = MathMin(newStopLoss, pos.PriceOpen() - minDistance);
newStopLoss = NormalizeDouble(newStopLoss, (int)SymbolInfoInteger(g_symbol, SYMBOL_DIGITS));
if(currentSL != 0)
{
if(posType == POSITION_TYPE_BUY && newStopLoss <= currentSL)
return;
if(posType == POSITION_TYPE_SELL && newStopLoss >= currentSL)
return;
}
double targetProfitLevel = MathFloor(profitPoints / InpTrailingStepPoints) * InpTrailingStepPoints;
if(targetProfitLevel > g_trailingData.lastProfitPoints)
{
if(m_trade.PositionModify(ticket, newStopLoss, pos.TakeProfit()))
{
g_trailingData.lastProfitPoints = targetProfitLevel;
Print("[移动止损] 已更新 | 新止损: ", DoubleToString(newStopLoss, _Digits), " | 当前利润: ",
DoubleToString(profitPoints, 0), "点 | 触发水平: ", DoubleToString(targetProfitLevel, 0), "点");
}
}
}
//+------------------------------------------------------------------+
//| 平仓所有持仓 |
//+------------------------------------------------------------------+
void CloseAllPositions()
{
bool hadPositions = false;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(m_position.SelectByIndex(i) && m_position.Symbol() == g_symbol && m_position.Magic() == g_magic)
{
hadPositions = true;
double closeProfit = m_position.Profit(); // 记录平仓前的盈亏
ulong ticket = m_position.Ticket();
if(m_trade.PositionClose(ticket))
{
g_totalClosedProfit += closeProfit; // 累加已平仓盈亏
Print("平仓成功: Ticket #", ticket, " | 盈亏: ", DoubleToString(closeProfit, 2), " 美元 | 累计盈亏: ", DoubleToString(g_totalClosedProfit, 2), " 美元");
}
}
}
if(hadPositions)
{
g_lastCloseTime = TimeCurrent();
g_inCooldown = true;
Print("[冷却] 开始于 ", TimeToString(g_lastCloseTime), ",冷却时间 ", InpCooldownMinutes, " 分钟");
}
}
//+------------------------------------------------------------------+
//| 检查是否在冷却期 |
//+------------------------------------------------------------------+
bool IsInCooldown()
{
if(InpCooldownMinutes <= 0)
return false;
if(g_lastCloseTime == 0)
return false;
datetime currentTime = TimeCurrent();
int minutesPassed = (int)((currentTime - g_lastCloseTime) / 60);
if(minutesPassed < InpCooldownMinutes)
{
if(!g_inCooldown)
g_inCooldown = true;
return true;
}
else
{
if(g_inCooldown)
{
g_inCooldown = false;
Print("[冷却] 已结束");
}
return false;
}
}
//+------------------------------------------------------------------+
//| 获取剩余冷却时间(秒) |
//+------------------------------------------------------------------+
int GetRemainingCooldownSeconds()
{
if(InpCooldownMinutes <= 0 || g_lastCloseTime == 0)
return 0;
int elapsedSeconds = (int)(TimeCurrent() - g_lastCloseTime);
int cooldownSeconds = InpCooldownMinutes * 60;
if(elapsedSeconds >= cooldownSeconds)
return 0;
return cooldownSeconds - elapsedSeconds;
}
//+------------------------------------------------------------------+
//| 获取总盈亏(美元) |
//+------------------------------------------------------------------+
double GetTotalProfitUSD()
{
double profit = g_totalClosedProfit; // 包含历史已平仓盈亏
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(m_position.SelectByIndex(i) && m_position.Symbol() == g_symbol && m_position.Magic() == g_magic)
profit += m_position.Profit();
}
return profit;
}
//+------------------------------------------------------------------+
//| 检查是否过期 |
//+------------------------------------------------------------------+
bool IsExpired()
{
datetime expiry = StringToTime(EXPIRY_DATE);
if(expiry == 0) return false;
return (TimeCurrent() > expiry);
}
//+------------------------------------------------------------------+
//| 获取当前指标值字符串用于显示 |
//+------------------------------------------------------------------+
string GetIndicatorDisplay()
{
string result = "";
if(g_bollingerHandle != INVALID_HANDLE && g_rsiHandle != INVALID_HANDLE)
{
result = StringFormat("布林: %.2f/%.2f/%.2f | RSI: %.1f",
(double)g_upperBand[0], (double)g_middleBand[0], (double)g_lowerBand[0], (double)g_rsiBuffer[0]);
}
else
{
result = "指标: 加载中...";
}
return result;
}
//+------------------------------------------------------------------+
//| 创建信息面板 |
//+------------------------------------------------------------------+
void CreateDashboard()
{
ObjectsDeleteAll(0, g_prefix);
// 动态计算位置,适应不同图表尺寸
int chartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
int chartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
// 面板尺寸根据屏幕自适应:最小800,最大1600
int panelW = MathMin(1600, MathMax(800, chartWidth - 100));
int panelH = 88;
// 确保面板不超出屏幕
int x = MathMax(3, (chartWidth - panelW) / 2);
int y = MathMax(2, chartHeight - panelH - 20);
// 背景
ObjectCreate(0, g_prefix + "bg", OBJ_RECTANGLE_LABEL, 0, 0, 0);
ObjectSetInteger(0, g_prefix + "bg", OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, g_prefix + "bg", OBJPROP_YDISTANCE, y + 5);
ObjectSetInteger(0, g_prefix + "bg", OBJPROP_XSIZE, panelW);
ObjectSetInteger(0, g_prefix + "bg", OBJPROP_YSIZE, panelH);
ObjectSetInteger(0, g_prefix + "bg", OBJPROP_BGCOLOR, (long)clrBlack);
ObjectSetInteger(0, g_prefix + "bg", OBJPROP_COLOR, (long)clrGoldenrod);
ObjectSetInteger(0, g_prefix + "bg", OBJPROP_BORDER_TYPE, (long)BORDER_RAISED);
ObjectSetInteger(0, g_prefix + "bg", OBJPROP_BACK, true);
// 标题(居中)
ObjectCreate(0, g_prefix + "title", OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, g_prefix + "title", OBJPROP_XDISTANCE, x + panelW / 2 - 120);
ObjectSetInteger(0, g_prefix + "title", OBJPROP_YDISTANCE, y + 10);
ObjectSetString(0, g_prefix + "title", OBJPROP_TEXT, "布林RSI一单一结EA QQ群:1054681521");
ObjectSetInteger(0, g_prefix + "title", OBJPROP_FONTSIZE, 12);
ObjectSetInteger(0, g_prefix + "title", OBJPROP_COLOR, (long)clrGold);
// 副标题
ObjectCreate(0, g_prefix + "subtitle", OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, g_prefix + "subtitle", OBJPROP_XDISTANCE, x + panelW / 2 - 10);
ObjectSetInteger(0, g_prefix + "subtitle", OBJPROP_YDISTANCE, y + 38);
ObjectSetString(0, g_prefix + "subtitle", OBJPROP_TEXT, " ");
ObjectSetInteger(0, g_prefix + "subtitle", OBJPROP_FONTSIZE, 8);
ObjectSetInteger(0, g_prefix + "subtitle", OBJPROP_COLOR, (long)clrBisque);
// 信息行:使用相对偏移,从左到右均匀分布
int rowY = y + 55;
int colWidth = panelW / 7; // 7列均匀分布
// 第1列: 状态
ObjectCreate(0, g_prefix + "status", OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, g_prefix + "status", OBJPROP_XDISTANCE, x + 10);
ObjectSetInteger(0, g_prefix + "status", OBJPROP_YDISTANCE, rowY);
ObjectSetInteger(0, g_prefix + "status", OBJPROP_FONTSIZE, 10);
ObjectSetInteger(0, g_prefix + "status", OBJPROP_COLOR, (long)clrYellow);
ObjectSetString(0, g_prefix + "status", OBJPROP_FONT, "Consolas");
// 第2列: 信号
ObjectCreate(0, g_prefix + "signal", OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, g_prefix + "signal", OBJPROP_XDISTANCE, x + colWidth);
ObjectSetInteger(0, g_prefix + "signal", OBJPROP_YDISTANCE, rowY);
ObjectSetInteger(0, g_prefix + "signal", OBJPROP_FONTSIZE, 10);
ObjectSetInteger(0, g_prefix + "signal", OBJPROP_COLOR, (long)clrYellow);
ObjectSetString(0, g_prefix + "signal", OBJPROP_FONT, "Consolas");
// 第3列: 冷却
ObjectCreate(0, g_prefix + "cooldown", OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, g_prefix + "cooldown", OBJPROP_XDISTANCE, x + colWidth * 2);
ObjectSetInteger(0, g_prefix + "cooldown", OBJPROP_YDISTANCE, rowY);
ObjectSetInteger(0, g_prefix + "cooldown", OBJPROP_FONTSIZE, 10);
ObjectSetInteger(0, g_prefix + "cooldown", OBJPROP_COLOR, (long)clrOrange);
ObjectSetString(0, g_prefix + "cooldown", OBJPROP_FONT, "Consolas");
// 第4列: 点差
ObjectCreate(0, g_prefix + "spread", OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, g_prefix + "spread", OBJPROP_XDISTANCE, x + colWidth * 3);
ObjectSetInteger(0, g_prefix + "spread", OBJPROP_YDISTANCE, rowY);
ObjectSetInteger(0, g_prefix + "spread", OBJPROP_FONTSIZE, 10);
ObjectSetInteger(0, g_prefix + "spread", OBJPROP_COLOR, (long)clrWhite);
ObjectSetString(0, g_prefix + "spread", OBJPROP_FONT, "Consolas");
// 第5列: 盈亏
ObjectCreate(0, g_prefix + "profit", OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, g_prefix + "profit", OBJPROP_XDISTANCE, x + colWidth * 4);
ObjectSetInteger(0, g_prefix + "profit", OBJPROP_YDISTANCE, rowY);
ObjectSetInteger(0, g_prefix + "profit", OBJPROP_FONTSIZE, 10);
ObjectSetInteger(0, g_prefix + "profit", OBJPROP_COLOR, (long)clrCyan);
ObjectSetString(0, g_prefix + "profit", OBJPROP_FONT, "Consolas");
// 第6列: 持仓
ObjectCreate(0, g_prefix + "position", OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, g_prefix + "position", OBJPROP_XDISTANCE, x + colWidth * 5);
ObjectSetInteger(0, g_prefix + "position", OBJPROP_YDISTANCE, rowY);
ObjectSetInteger(0, g_prefix + "position", OBJPROP_FONTSIZE, 10);
ObjectSetInteger(0, g_prefix + "position", OBJPROP_COLOR, (long)clrLime);
ObjectSetString(0, g_prefix + "position", OBJPROP_FONT, "Consolas");
// 第7列: 交易时间
ObjectCreate(0, g_prefix + "timeStatus", OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, g_prefix + "timeStatus", OBJPROP_XDISTANCE, x + colWidth * 6);
ObjectSetInteger(0, g_prefix + "timeStatus", OBJPROP_YDISTANCE, rowY);
ObjectSetInteger(0, g_prefix + "timeStatus", OBJPROP_FONTSIZE, 10);
ObjectSetInteger(0, g_prefix + "timeStatus", OBJPROP_COLOR, (long)clrYellow);
ObjectSetString(0, g_prefix + "timeStatus", OBJPROP_FONT, "Consolas");
// 指标信息行(在信息行下方)
ObjectCreate(0, g_prefix + "indicators", OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, g_prefix + "indicators", OBJPROP_XDISTANCE, x + 10);
ObjectSetInteger(0, g_prefix + "indicators", OBJPROP_YDISTANCE, rowY + 20);
ObjectSetInteger(0, g_prefix + "indicators", OBJPROP_FONTSIZE, 9);
ObjectSetInteger(0, g_prefix + "indicators", OBJPROP_COLOR, (long)clrLightSteelBlue);
ObjectSetString(0, g_prefix + "indicators", OBJPROP_FONT, "Consolas");
// 按钮区域(右上角,相对面板右边缘定位)
int btnW = 50, btnH = 20;
int btnX = x + panelW - btnW - 15;
ObjectCreate(0, g_prefix + g_btnCloseAll, OBJ_BUTTON, 0, 0, 0);
ObjectSetInteger(0, g_prefix + g_btnCloseAll, OBJPROP_XDISTANCE, btnX);
ObjectSetInteger(0, g_prefix + g_btnCloseAll, OBJPROP_YDISTANCE, y + 10);
ObjectSetInteger(0, g_prefix + g_btnCloseAll, OBJPROP_XSIZE, btnW + 3);
ObjectSetInteger(0, g_prefix + g_btnCloseAll, OBJPROP_YSIZE, btnH + 5);
ObjectSetString(0, g_prefix + g_btnCloseAll, OBJPROP_TEXT, "平仓");
ObjectSetInteger(0, g_prefix + g_btnCloseAll, OBJPROP_BGCOLOR, (long)clrMaroon);
ObjectSetInteger(0, g_prefix + g_btnCloseAll, OBJPROP_COLOR, (long)clrWhite);
ObjectSetInteger(0, g_prefix + g_btnCloseAll, OBJPROP_FONTSIZE, 10);
ObjectCreate(0, g_prefix + g_btnPause, OBJ_BUTTON, 0, 0, 0);
ObjectSetInteger(0, g_prefix + g_btnPause, OBJPROP_XDISTANCE, btnX - btnW - 10);
ObjectSetInteger(0, g_prefix + g_btnPause, OBJPROP_YDISTANCE, y + 10);
ObjectSetInteger(0, g_prefix + g_btnPause, OBJPROP_XSIZE, btnW + 3);
ObjectSetInteger(0, g_prefix + g_btnPause, OBJPROP_YSIZE, btnH + 5);
ObjectSetString(0, g_prefix + g_btnPause, OBJPROP_TEXT, "暂停");
ObjectSetInteger(0, g_prefix + g_btnPause, OBJPROP_BGCOLOR, (long)clrDarkSlateGray);
ObjectSetInteger(0, g_prefix + g_btnPause, OBJPROP_COLOR, (long)clrWhite);
ObjectSetInteger(0, g_prefix + g_btnPause, OBJPROP_FONTSIZE, 10);
// 过期日期(按钮下方)
ObjectCreate(0, g_prefix + "expiry", OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, g_prefix + "expiry", OBJPROP_XDISTANCE, btnX - 60);
ObjectSetInteger(0, g_prefix + "expiry", OBJPROP_YDISTANCE, rowY);
ObjectSetString(0, g_prefix + "expiry", OBJPROP_TEXT, "到期: " + EXPIRY_DATE);
ObjectSetInteger(0, g_prefix + "expiry", OBJPROP_FONTSIZE, 8);
ObjectSetInteger(0, g_prefix + "expiry", OBJPROP_COLOR, (long)clrRosyBrown);
g_buttonsCreated = true;
}
//+------------------------------------------------------------------+
//| 更新面板 |
//+------------------------------------------------------------------+
void UpdateDashboard(int cachedSignal = -999)
{
if(!g_buttonsCreated) return;
// 先更新一次指标数据(后续所有显示都使用这组数据)
UpdateIndicatorData();
// 信号状态:优先使用缓存的信号,避免重复调用CheckSignal
int signal = (cachedSignal != -999) ? cachedSignal : CheckSignal();
string signalDisplay;
if(signal == 1)
signalDisplay = "信号: ▲ 做多信号 (触及下轨+RSI超卖)";
else if(signal == -1)
signalDisplay = "信号: ▼ 做空信号 (触及上轨+RSI超买)";
else
signalDisplay = "信号: ● 等待信号";
// 指标显示
string indicatorDisplay = GetIndicatorDisplay();
// 持仓信息
string positionDisplay = "持仓: 无";
CPositionInfo pos;
if(GetCurrentPosition(pos))
{
string posType = (pos.PositionType() == POSITION_TYPE_BUY) ? "多单" : "空单";
positionDisplay = StringFormat("持仓: %s | 手数=%.2f | 入场=%.2f",
posType, pos.Volume(), pos.PriceOpen());
}
// 盈亏
double profitUSD = GetTotalProfitUSD();
string profitDisplay = StringFormat("盈亏: %.2f 美元", profitUSD);
// 点差
long spread = SymbolInfoInteger(g_symbol, SYMBOL_SPREAD);
string spreadDisplay = StringFormat("点差: %d 点", spread);
// 冷却时间
string cooldownDisplay = "";
int remainingSec = GetRemainingCooldownSeconds();
if(IsInCooldown() && remainingSec > 0)
{
int minutes = remainingSec / 60;
int seconds = remainingSec % 60;
cooldownDisplay = StringFormat("冷却: %02d:%02d", minutes, seconds);
}
else if(InpCooldownMinutes > 0)
{
cooldownDisplay = StringFormat("冷却: %d 分钟 (等待中)", InpCooldownMinutes);
}
else
{
cooldownDisplay = "冷却: 关闭";
}
// 状态
string status = "";
if(g_maxLossReached)
status = "状态: 已达最大亏损限制";
else if(USE_DATE_LIMIT && IsExpired())
status = "状态: 已过期";
else if(g_isPaused)
status = "状态: 已暂停";
else if(IsInCooldown())
status = "状态: 冷却中 - 等待开仓";
else
status = "状态: 运行中";
// 交易时间状态
string timeStatus = GetTradingTimeStatus();
// 更新显示
ObjectSetString(0, g_prefix + "signal", OBJPROP_TEXT, signalDisplay);
ObjectSetString(0, g_prefix + "indicators", OBJPROP_TEXT, indicatorDisplay);
ObjectSetString(0, g_prefix + "position", OBJPROP_TEXT, positionDisplay);
ObjectSetString(0, g_prefix + "profit", OBJPROP_TEXT, profitDisplay);
ObjectSetString(0, g_prefix + "spread", OBJPROP_TEXT, spreadDisplay);
ObjectSetString(0, g_prefix + "cooldown", OBJPROP_TEXT, cooldownDisplay);
ObjectSetString(0, g_prefix + "status", OBJPROP_TEXT, status);
ObjectSetString(0, g_prefix + "timeStatus", OBJPROP_TEXT, timeStatus);
// 颜色
if(signal == 1)
ObjectSetInteger(0, g_prefix + "signal", OBJPROP_COLOR, (long)clrLime);
else if(signal == -1)
ObjectSetInteger(0, g_prefix + "signal", OBJPROP_COLOR, (long)clrRed);
else
ObjectSetInteger(0, g_prefix + "signal", OBJPROP_COLOR, (long)clrYellow);
if(profitUSD >= 0)
ObjectSetInteger(0, g_prefix + "profit", OBJPROP_COLOR, (long)clrLime);
else
ObjectSetInteger(0, g_prefix + "profit", OBJPROP_COLOR, (long)clrRed);
if(IsInCooldown())
{
ObjectSetInteger(0, g_prefix + "cooldown", OBJPROP_COLOR, (long)clrOrangeRed);
ObjectSetInteger(0, g_prefix + "cooldown", OBJPROP_FONTSIZE, 11);
}
else
{
ObjectSetInteger(0, g_prefix + "cooldown", OBJPROP_COLOR, (long)clrOrange);
ObjectSetInteger(0, g_prefix + "cooldown", OBJPROP_FONTSIZE, 10);
}
// 按钮状态
if(g_maxLossReached || IsInCooldown() || g_isPaused)
ObjectSetInteger(0, g_prefix + g_btnPause, OBJPROP_BGCOLOR, (long)clrGray);
else
ObjectSetInteger(0, g_prefix + g_btnPause, OBJPROP_BGCOLOR, (long)clrDarkSlateGray);
// 交易时间状态颜色
if(StringFind(timeStatus, "交易时间") != -1)
ObjectSetInteger(0, g_prefix + "timeStatus", OBJPROP_COLOR, (long)clrLime);
else
ObjectSetInteger(0, g_prefix + "timeStatus", OBJPROP_COLOR, (long)clrOrange);
ChartRedraw();
}
//+------------------------------------------------------------------+
//| 图表事件处理 |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
if(id == CHARTEVENT_OBJECT_CLICK)
{
if(sparam == g_prefix + g_btnCloseAll)
{
Print("按钮: 平仓所有持仓");
CloseAllPositions();
UpdateDashboard();
}
else if(sparam == g_prefix + g_btnPause)
{
if(!g_maxLossReached && !(USE_DATE_LIMIT && IsExpired()))
{
g_isPaused = !g_isPaused;
Print("按钮: ", g_isPaused ? "暂停" : "恢复");
if(!g_isPaused)
{
// 恢复时不重置冷却时间,保持原有冷却状态
Print("交易已恢复,", g_inCooldown ? "仍在冷却期" : "可立即开仓");
}
UpdateDashboard();
}
}
}
}
//+------------------------------------------------------------------+
//| 主Tick函数 |
//+------------------------------------------------------------------+
void OnTick()
{
// 连接状态检查(放在最前面,避免断连时浪费资源)
if(TerminalInfoInteger(TERMINAL_CONNECTED) != 1)
{
Print("警告: 未连接到交易服务器,跳过本轮处理");
return;
}
if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) != 1)
{
Print("警告: 交易功能未启用,跳过本轮处理");
return;
}
// 最大亏损检查
if(g_maxLossReached)
{
UpdateDashboard(0); // 传入0表示无信号
return;
}
double totalProfitUSD = GetTotalProfitUSD();
if(InpMaxTotalLossUSD > 0 && totalProfitUSD <= -InpMaxTotalLossUSD)
{
Print("[风控] 已达最大亏损限制: ", totalProfitUSD, " 美元 (限制: -", InpMaxTotalLossUSD, ")。平仓所有持仓。");
CloseAllPositions();
g_maxLossReached = true;
g_isPaused = true;
UpdateDashboard(0);
return;
}
// 过期检查
if(USE_DATE_LIMIT && IsExpired())
{
UpdateDashboard(0);
return;
}
// 暂停检查
if(g_isPaused)
{
UpdateDashboard(0);
return;
}
// 移动止损管理(任何时间都执行)
ManageTrailingStop();
// 冷却检查
if(IsInCooldown())
{
UpdateDashboard(0);
return;
}
// 交易时间检查(只阻止开仓,不影响止损)
if(!IsInTradingTime())
{
UpdateDashboard(0);
return;
}
// 新K线确认:仅在新K线开始时检查信号
static datetime lastBarTime = 0;
datetime currentBarTime = iTime(g_symbol, _Period, 0);
// 缓存信号(每Tick只计算一次)
int signal = 0;
if(currentBarTime != lastBarTime)
{
lastBarTime = currentBarTime;
signal = CheckSignal();
if(signal != 0)
{
if(signal == 1)
{
Print("========== 检测到做多信号 ==========");
Print("开仓条件: 上一根K线收盘价触及布林带下轨 + RSI超卖");
OpenPosition(POSITION_TYPE_BUY);
}
else if(signal == -1)
{
Print("========== 检测到做空信号 ==========");
Print("开仓条件: 上一根K线收盘价触及布林带上轨 + RSI超买");
OpenPosition(POSITION_TYPE_SELL);
}
}
}
// 更新面板(使用缓存的信号,不再重复调用CheckSignal)
UpdateDashboard(signal);
}
//+------------------------------------------------------------------+
|
|