找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 4|回复: 0

[MT5] 布林RSI一单一结EA - 黄金量化交易程序

[复制链接]

269

主题

6

回帖

1001

积分

管理员

积分
1001
发表于 昨天 17:31 | 显示全部楼层 |阅读模式
//+------------------------------------------------------------------+
//|                                       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);
}
//+------------------------------------------------------------------+


您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|量化魔方 ( 陕ICP备2025062059号-3 )|网站地图

GMT+8, 2026-9-8 02:08 , Processed in 0.145306 second(s), 5 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表