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

[MT5] 摸金校尉-MT5【2/3位小数自动适应版】.mq5

[复制链接]

274

主题

6

回帖

1020

积分

管理员

积分
1020
发表于 昨天 11:47 | 显示全部楼层 |阅读模式
//+------------------------------------------------------------------+
//|                                             摸金校尉-MT5【2/3位小数自动适应版】.mq5 |
//|                                                           GSD     |
//|                                                WeChat:Mirootsoft|
//+------------------------------------------------------------------+
#property copyright " Mirootsoft"
#property link      ""
#property version   "1.15"
#property description "免责声明:"
#property description "1.过去的收益不代表未来的收益"
#property description "2.账号产生的盈亏客户需要自己承担"
#property description "3.本EA只作为交易辅助工具,不作任何盈利保证"
#property description "4.EA属于虚拟产品一旦发货正常加载使用不支持退货退款"
#property description "5.任何策略都有一定的风险,不局限于代码问题,BUG,行情问题"

#include <Trade/Trade.mqh>
#include <Trade/OrderInfo.mqh>
#include <Trade/PositionInfo.mqh>
#include <Trade/HistoryOrderInfo.mqh>
#include <Arrays/ArrayInt.mqh>

CTrade Trade;
COrderInfo OrderInfo;
CPositionInfo PositionInfo;
CHistoryOrderInfo HistoryOrder;

// 顺势距离计算模式
enum ENUM_ST_DISTANCE_MODE
{
   ST_DISTANCE_FIXED = 0,    // 固定距离(兼容旧版)
   ST_DISTANCE_PERCENT = 1  // 按价格百分比动态计算
};

// 输入参数
input group "━━━━ 通用设置 ━━━━"
input double      magicNumber = 123;                    // 魔术码
input double      开仓倍数 = 1;                         // 全局开仓倍数
input string      comm = "V:fanfans555";               // 订单注释
input double      FirstStep = 80;                       // 首仓挂单间距
input double      StopLoss = 9999;                      // 强止损金额(ST模式=Basket总止损;原始模式=单方向止损)
input bool        sj = false;                           // 时间过滤
input int         star = 1;                             // 开始时间
input int         end = 23;                             // 结束时间
input bool        allow_sell = true;                    // 允许做空
input bool        allow_buy = true;                     // 允许做多

input group "━━━━ 界面显示 ━━━━"
input bool        启用恒鑫风格界面 = true;              // 深色卡片式交易面板
input bool        启用恒鑫图表配色 = true;              // 黑金/红绿K线配色
input int         界面X = 20;                           // 面板左边距
input int         界面Y = 25;                           // 面板上边距
input int         界面宽度 = 720;                       // 面板宽度
input bool        显示均价止盈线 = true;                // 主图显示持仓均价/当前止盈目标线

input group "━━━━ ST顺势 + 逆势恢复模式 ━━━━"
input bool        启用ST顺势逆势恢复模式 = true;            // 开=ST顺势开新仓+逆势只恢复旧仓;关=V1.07原始逆势模式
input int         Supertrend_ATR周期 = 10;              // ST ATR周期
input double      Supertrend_倍数 = 3.0;                // ST倍数
input int         ST趋势确认K线数 = 1;                  // 连续已收盘K线同方向后确认(1~3)
input bool        显示Supertrend = true;                // K线主图显示ST

input group "━━━━ ST顺势手数与加仓 ━━━━"
input double      顺势首仓手数 = 0.01;                  // ST趋势首仓基础手数
input double      顺势加仓手数递增比例 = 1.00;          // ST顺势加仓递增比例(1.00=不按幂次放大)
input ENUM_ST_DISTANCE_MODE 顺势距离计算模式 = ST_DISTANCE_PERCENT; // 默认按价格百分比动态计算
input double      顺势回调百分比 = 0.05;                // %;基准=当前趋势极值,4000时约2.00价格距离
input double      顺势恢复确认百分比 = 0.02;            // %;基准=本次回调极值,4000时约0.80价格距离
input double      最小同向加仓间距百分比 = 0.04;         // %;基准=最近一次同向成交价,4000时约1.60
input double      固定顺势回调距离 = 200;               // 仅固定模式使用:参数点数
input double      固定顺势恢复确认距离 = 30;            // 仅固定模式使用:参数点数
input double      固定最小同向加仓价格间距 = 1.50;       // 仅固定模式使用:实际价格差
input int         顺势最大层数 = 60;                    // TREND首仓+加仓最大层数(保持60)
input int         顺势最小加仓间隔秒 = 60;              // 防止剧烈行情连续加仓
input bool        启用最小同向加仓间距 = true;           // 顺势/逆势加仓统一启用硬性价格间距保护

input group "━━━━ ST Basket退出 ━━━━"
input double      ST篮子止盈金额 = 4.0;                 // 顺势+逆势同时存在时,所有仓统一净利润止盈(U)
input bool        逆势盈利允许独立退出 = true;           // 逆势恢复组盈利后可单独退出,不必等待顺势组
input double      逆势独立退出净利润 = 0.0;              // U;0=只要逆势整组净利润>0即可退出

input group "━━━━ V1.07原始逆势模式 ━━━━"
input double      原始初始手数 = 0.01;                  // 原始逆势模式首仓基础手数
input double      逆势加仓手数递增比例 = 1.09;          // 原始逆势模式后续加仓手数递增比例
input double      加仓间距变更订单数 = 2;               // 订单数超过此值使用二阶,否则使用一阶
input double      多单_挂单点距_非首单 = 200;
input double      多单_修改挂单起始间距 = 200;
input double      空单_挂单点距_非首单 = 200;
input double      空单_修改挂单起始间距 = 200;
input double      挂单点距_非首单_一阶 = 200;
input double      多单_修改挂单起始间距_一阶 = 200;
input double      挂单点距_非首单_二阶 = 120;
input double      修改挂单起始间距_二阶 = 120;
input int         首单止盈点数 = 150;                   // 原参数保留(当前核心交易未引用)
input double      单边止盈金额 = 50;                    // 原始模式单边止盈金额
input double      双边止盈金额 = 4;                     // 原始模式多空组合止盈金额

// 以下为原源码内部固定配置,不开放优化参数
string   交易设置= "* =============================== *";
string   风控= "* =============================== *";
bool     风控管理 = false;                    // 锁仓触发,停止开仓加仓!
double   浮亏锁仓金额 = -100;
double   止损金额= -300;
string   信息显示= "* =============================== *";
string   dangerHours= "0";                    // 开仓需要过滤的小时
bool     by_bo_9 = true;
bool     by_bo_10 = false;
int      stop挂单改价间距 = 10;
double   by_do_17 = -100000;
double   基准止盈金额 = 3;                    // 基准止盈金额
double   by_do_19 = 0;
int      开仓手数小数位 = 2;
int      显示用_字体大小 = 9;                  // 字体大小
color    by_ui_26 = clrAqua;                   // 字体颜色

// 可修改的变量(非input)
double        当前_多单_挂单点距_非首单;
double        当前_多单_修改挂单起始间距;
double        当前_空单_挂单点距_非首单;
double        当前_空单_修改挂单起始间距;

// 全局变量
double vPoint;
double vRawPoint;
double vPointFactor = 1.0;
double 市场当前最小挂单点距 = 0;   // 兼容显示:当前新建挂单StopLevel(参数点)
double 市场当前StopLevel点距 = 0;
double 市场当前FreezeLevel点距 = 0;
bool runEA = true;
bool runEAG = false;
bool 手动全平 = true;

// 持仓统计变量
double 多单累计利润 = 0, 空单累计利润 = 0;
double 多单累计手数 = 0, 空单累计手数 = 0;
int 多单订单数 = 0, 空单订单数 = 0;
int 多单_挂单订单数 = 0, 空单_挂单订单数 = 0;
double 多单最高价 = 0, 多单最低价 = 0;
double 空单最高价 = 0, 空单最低价 = 0;
double 多单持仓均价 = 0, 空单持仓均价 = 0;
double _FirstStep;
string UI_PREFIX = "MD_UI_";
string UI_BG = "MD_UI_BG";
string LINE_BUY_AVG  = "MD_LINE_BUY_AVG";
string LINE_SELL_AVG = "MD_LINE_SELL_AVG";
string LINE_BUY_TP   = "MD_LINE_BUY_TP";
string LINE_SELL_TP  = "MD_LINE_SELL_TP";
string LINE_BASKET_TP= "MD_LINE_BASKET_TP";

// 当前可视化价格
double g_buyTPPrice=0.0, g_sellTPPrice=0.0, g_basketTPPrice=0.0;
string g_tpModeText="无持仓";

// ===== 统计模块(券商服务器日期) =====
string STAT_UI_PREFIX = "MD_STATUI_";
bool   g_statsPanelOpen = false;
datetime g_statsDay = 0;
double g_todayRealizedProfit = 0.0;
double g_todayOpenedLots = 0.0;
double g_todayMaxEntryLot = 0.0;
int    g_todayCloseTrades = 0;
int    g_todayWins = 0;
int    g_todayLosses = 0;
double g_todayMaxFloatProfit = 0.0;
double g_todayMaxFloatLoss = 0.0;
double g_todayStartBalance = 0.0;
double g_todayLastEquity = 0.0;
datetime g_lastHistoryStatsCalc = 0;
datetime g_lastFloatStatSave = 0;

// 启动周期统计:每次EA重新启动时从0开始,不持久化
datetime g_sessionStartTime = 0;
double   g_sessionStartBalance = 0.0;
double   g_sessionMaxFloatLoss = 0.0;

// ===== ST顺势 + 逆势恢复运行状态 =====
int      g_stAtrHandle = INVALID_HANDLE;
int      g_stDir = 0;                    // 1=多 -1=空 0=未就绪
double   g_stValue = 0.0;
datetime g_stLastBarTime = 0;
int      g_stCandidateDir = 0;
int      g_stCandidateBars = 0;
double   g_trendExtremeBuy = 0.0;        // 多头趋势峰值
double   g_trendExtremeSell = 0.0;       // 空头趋势谷值
bool     g_buyPullbackArmed = false;
bool     g_sellPullbackArmed = false;
double   g_buyPullbackLow = 0.0;
double   g_sellPullbackHigh = 0.0;
datetime g_lastTrendAddTime = 0;
// 顺势生命周期重启控制:某方向顺势仓从有仓变为0后,必须等下一根K线才可重启首仓
int      g_prevTrendCountBuy = -1;
int      g_prevTrendCountSell = -1;
datetime g_trendRestartBlockBarBuy = 0;
datetime g_trendRestartBlockBarSell = 0;
int      g_todaySTFlips = 0;             // 当日ST确认切换次数(券商日)
int      g_todayRecoveryAdds = 0;         // 当日逆势恢复加仓成功次数(券商日)
int      g_recoveryDir = 0;               // 当前逆势恢复方向: 1=BUY -1=SELL 0=无
double   g_recoveryBaseLots = 0.0;        // ST切换瞬间逆势方向总手数快照,作为“虚拟首仓”
int      g_recoveryAddLevel = 0;           // 当前快照之后已成交的逆势恢复加仓次数
datetime g_recoverySnapshotTime = 0;       // 当前逆势基准快照时间
string   ST_LINE_PREFIX = "MJ_ST_";

// 统计事件函数原型(实现位于每日统计模块)
void RecordSTFlipStat();
void RecordRecoveryAddStat();
//+------------------------------------------------------------------+
//| ST顺势 + 逆势恢复辅助函数                                                |
//+------------------------------------------------------------------+
double NormalizeBrokerVolume(double vol)
{
    double vmin=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
    double vmax=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX);
    double step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
    if(step<=0.0) step=0.01;
    vol=MathMax(vmin,MathMin(vmax,vol));
    vol=MathFloor(vol/step+1e-8)*step;
    return NormalizeDouble(vol,8);
}

bool IsOurPositionComment(string key)
{
    string c=PositionInfo.Comment();
    return (StringFind(c,key)>=0);
}

// 获取当前仍持有的同方向仓位中,最近一次真实成交的开仓价格
bool GetLastSameDirectionEntry(int dir,double &lastPrice,datetime &lastTime,ulong &lastTicket)
{
    lastPrice=0.0; lastTime=0; lastTicket=0;
    for(int i=0;i<ositionsTotal();i++)
    {
        if(!PositionInfo.SelectByIndex(i)) continue;
        if(PositionInfo.Symbol()!=_Symbol || PositionInfo.Magic()!=magicNumber) continue;
        ENUM_POSITION_TYPE pt=PositionInfo.PositionType();
        if(dir==1 && pt!=POSITION_TYPE_BUY) continue;
        if(dir==-1 && pt!=POSITION_TYPE_SELL) continue;
        datetime tm=PositionInfo.Time();
        ulong tk=(ulong)PositionInfo.Ticket();
        if(tm>lastTime || (tm==lastTime && tk>lastTicket))
        {
            lastTime=tm; lastTicket=tk; lastPrice=PositionInfo.PriceOpen();
        }
    }
    return (lastPrice>0.0);
}

// 百分比转实际价格距离。参数按“百分数”填写:0.05 表示 0.05%。
double PercentToPriceDistance(double basePrice,double pct)
{
    if(basePrice<=0.0 || pct<=0.0) return 0.0;
    return basePrice*pct/100.0;
}

// 当前顺势回调所需的实际价格距离。百分比模式以趋势极值为基准。
double TrendPullbackDistancePrice(int dir,double currentPrice)
{
    if(顺势距离计算模式==ST_DISTANCE_PERCENT)
    {
        double base=(dir==1?g_trendExtremeBuy:g_trendExtremeSell);
        if(base<=0.0) base=currentPrice;
        return PercentToPriceDistance(base,顺势回调百分比);
    }
    return 固定顺势回调距离*vPoint;
}

// 当前恢复确认所需的实际价格距离。百分比模式以本次回调最低/最高点为基准。
double TrendRecoveryConfirmDistancePrice(int dir,double currentPrice)
{
    if(顺势距离计算模式==ST_DISTANCE_PERCENT)
    {
        double base=(dir==1?g_buyPullbackLow:g_sellPullbackHigh);
        if(base<=0.0) base=currentPrice;
        return PercentToPriceDistance(base,顺势恢复确认百分比);
    }
    return 固定顺势恢复确认距离*vPoint;
}

// 与最近一次同方向真实成交价之间必须保持的实际价格距离。
double MinSameDirectionSpacingPrice(double lastPrice)
{
    if(!启用最小同向加仓间距) return 0.0;
    if(顺势距离计算模式==ST_DISTANCE_PERCENT)
        return PercentToPriceDistance(lastPrice,最小同向加仓间距百分比);
    return MathMax(0.0,固定最小同向加仓价格间距);
}

// 统一加仓硬间距:首仓不受限;所有后续顺势/逆势加仓都使用相同的距离栅栏。
bool PassAddPriceSpacing(int dir,double candidatePrice,double &gapPrice,double &lastPrice)
{
    gapPrice=0.0; lastPrice=0.0;
    if(!启用最小同向加仓间距) return true;
    datetime tm=0; ulong tk=0;
    if(!GetLastSameDirectionEntry(dir,lastPrice,tm,tk)) return true; // 没有同向持仓=首仓,不限制
    double minGap=MinSameDirectionSpacingPrice(lastPrice);
    if(minGap<=0.0) return true;
    gapPrice=MathAbs(candidatePrice-lastPrice);
    double tick=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE);
    if(tick<=0.0) tick=_Point;
    return (gapPrice+tick*0.1 >= minGap);
}

int CountTrendPositions(int dir)
{
    int n=0;
    for(int i=0;i<ositionsTotal();i++)
    {
        if(!PositionInfo.SelectByIndex(i)) continue;
        if(PositionInfo.Symbol()!=_Symbol || PositionInfo.Magic()!=magicNumber) continue;
        if(dir==1 && PositionInfo.PositionType()!=POSITION_TYPE_BUY) continue;
        if(dir==-1 && PositionInfo.PositionType()!=POSITION_TYPE_SELL) continue;
        if(IsOurPositionComment("MJ_TREND")) n++;
    }
    return n;
}

double SumDirectionLots(int dir)
{
    double v=0.0;
    for(int i=0;i<ositionsTotal();i++)
    {
        if(!PositionInfo.SelectByIndex(i)) continue;
        if(PositionInfo.Symbol()!=_Symbol || PositionInfo.Magic()!=magicNumber) continue;
        if(dir==1 && PositionInfo.PositionType()==POSITION_TYPE_BUY) v+=PositionInfo.Volume();
        if(dir==-1 && PositionInfo.PositionType()==POSITION_TYPE_SELL) v+=PositionInfo.Volume();
    }
    return v;
}

bool HasRecoveryPending(int dir)
{
    for(int i=0;i<OrdersTotal();i++)
    {
        if(!OrderInfo.SelectByIndex(i)) continue;
        if(OrderInfo.Symbol()!=_Symbol || OrderInfo.Magic()!=magicNumber) continue;
        string c=OrderInfo.Comment();
        if(StringFind(c,"MJ_RECOVERY")<0) continue;
        if(dir==1 && OrderInfo.OrderType()==ORDER_TYPE_BUY_STOP) return true;
        if(dir==-1 && OrderInfo.OrderType()==ORDER_TYPE_SELL_STOP) return true;
    }
    return false;
}

void DeleteRecoveryPending(int dir)
{
    for(int i=OrdersTotal()-1;i>=0;i--)
    {
        if(!OrderInfo.SelectByIndex(i)) continue;
        if(OrderInfo.Symbol()!=_Symbol || OrderInfo.Magic()!=magicNumber) continue;
        string c=OrderInfo.Comment();
        if(StringFind(c,"MJ_RECOVERY")<0) continue;
        bool hit=(dir==1 && OrderInfo.OrderType()==ORDER_TYPE_BUY_STOP) ||
                 (dir==-1 && OrderInfo.OrderType()==ORDER_TYPE_SELL_STOP);
        if(hit) Trade.OrderDelete(OrderInfo.Ticket());
    }
}

bool HasTrendPending(int dir)
{
    for(int i=0;i<OrdersTotal();i++)
    {
        if(!OrderInfo.SelectByIndex(i)) continue;
        if(OrderInfo.Symbol()!=_Symbol || OrderInfo.Magic()!=magicNumber) continue;
        if(dir==1 && OrderInfo.OrderType()==ORDER_TYPE_BUY_STOP && StringFind(OrderInfo.Comment(),"MJ_TREND")>=0) return true;
        if(dir==-1 && OrderInfo.OrderType()==ORDER_TYPE_SELL_STOP && StringFind(OrderInfo.Comment(),"MJ_TREND")>=0) return true;
    }
    return false;
}

void DeleteTrendPending(int dir)
{
    for(int i=OrdersTotal()-1;i>=0;i--)
    {
        if(!OrderInfo.SelectByIndex(i)) continue;
        if(OrderInfo.Symbol()!=_Symbol || OrderInfo.Magic()!=magicNumber) continue;
        string c=OrderInfo.Comment();
        if(StringFind(c,"MJ_TREND")<0) continue;
        bool hit=(dir==1 && OrderInfo.OrderType()==ORDER_TYPE_BUY_STOP) ||
                 (dir==-1 && OrderInfo.OrderType()==ORDER_TYPE_SELL_STOP);
        if(hit) Trade.OrderDelete(OrderInfo.Ticket());
    }
}

void ResetTrendTrackerForDir(int dir)
{
    if(dir==1)
    {
        g_buyPullbackArmed=false;
        g_buyPullbackLow=0.0;
        g_trendExtremeBuy=0.0;
    }
    else if(dir==-1)
    {
        g_sellPullbackArmed=false;
        g_sellPullbackHigh=0.0;
        g_trendExtremeSell=0.0;
    }
}

// 检测“该方向顺势仓刚刚全部退出”。只在真正从 >0 变成 0 时启动下一K线重启门控,
// EA刚启动时本来就是0仓不会被误判,从而保持原来的首次启动行为。
void UpdateTrendCycleRestartState()
{
    int buyCount=CountTrendPositions(1);
    int sellCount=CountTrendPositions(-1);
    datetime curBar=iTime(_Symbol,_Period,0);

    if(g_prevTrendCountBuy>=0 && g_prevTrendCountBuy>0 && buyCount==0)
    {
        DeleteTrendPending(1);
        ResetTrendTrackerForDir(1);
        g_trendRestartBlockBarBuy=curBar;
        Print("【顺势周期完成】BUY顺势仓已全部退出,本K线不重开;下一根K线若ST仍为多则允许重启首仓");
    }
    if(g_prevTrendCountSell>=0 && g_prevTrendCountSell>0 && sellCount==0)
    {
        DeleteTrendPending(-1);
        ResetTrendTrackerForDir(-1);
        g_trendRestartBlockBarSell=curBar;
        Print("【顺势周期完成】SELL顺势仓已全部退出,本K线不重开;下一根K线若ST仍为空则允许重启首仓");
    }

    g_prevTrendCountBuy=buyCount;
    g_prevTrendCountSell=sellCount;
}

bool TrendRestartAllowedNow(int dir)
{
    datetime curBar=iTime(_Symbol,_Period,0);
    datetime blockBar=(dir==1?g_trendRestartBlockBarBuy:g_trendRestartBlockBarSell);
    if(blockBar<=0) return true;
    if(curBar<=blockBar) return false;

    // 已进入下一根K线:解除门控,并从当前价格重新建立这一轮趋势追踪基准。
    if(dir==1) g_trendRestartBlockBarBuy=0;
    else       g_trendRestartBlockBarSell=0;
    ResetTrendTrackerForDir(dir);
    Print("【顺势周期重启】",(dir==1?"BUY":"SELL")," 已进入下一根K线,允许重新建立首仓");
    return true;
}

void DeleteDirectionPending(int dir)
{
    for(int i=OrdersTotal()-1;i>=0;i--)
    {
        if(!OrderInfo.SelectByIndex(i)) continue;
        if(OrderInfo.Symbol()!=_Symbol || OrderInfo.Magic()!=magicNumber) continue;
        bool hit=(dir==1 && OrderInfo.OrderType()==ORDER_TYPE_BUY_STOP) || (dir==-1 && OrderInfo.OrderType()==ORDER_TYPE_SELL_STOP);
        if(hit) Trade.OrderDelete(OrderInfo.Ticket());
    }
}

bool GetSupertrendClosed(int shift,int &direction,double &value)
{
    direction=0; value=0.0;
    if(g_stAtrHandle==INVALID_HANDLE || shift<1) return false;
    int need=MathMax(Supertrend_ATR周期*6+shift+10,140);
    MqlRates rates[]; double atr[];
    ArraySetAsSeries(rates,true); ArraySetAsSeries(atr,true);
    int rc=CopyRates(_Symbol,_Period,0,need,rates);
    int ac=CopyBuffer(g_stAtrHandle,0,0,need,atr);
    int n=MathMin(rc,ac);
    if(n<Supertrend_ATR周期+shift+10) return false;
    int oldest=n-1;
    double mid=(rates[oldest].high+rates[oldest].low)*0.5;
    double fu=mid+Supertrend_倍数*atr[oldest];
    double fl=mid-Supertrend_倍数*atr[oldest];
    int trend=(rates[oldest].close>=mid?1:-1);
    double st=(trend==1?fl:fu);
    for(int i=oldest-1;i>=shift;i--)
    {
        mid=(rates.high+rates.low)*0.5;
        double bu=mid+Supertrend_倍数*atr;
        double bl=mid-Supertrend_倍数*atr;
        double pc=rates[i+1].close;
        double nu=(bu<fu || pc>fu)?bu:fu;
        double nl=(bl>fl || pc<fl)?bl:fl;
        if(rates.close>fu) trend=1;
        else if(rates.close<fl) trend=-1;
        fu=nu; fl=nl; st=(trend==1?fl:fu);
    }
    direction=trend; value=st;
    return true;
}

void DeleteSTLines()
{
    ObjectsDeleteAll(0,ST_LINE_PREFIX);
}

void DrawSupertrendLines()
{
    if(!显示Supertrend || g_stAtrHandle==INVALID_HANDLE) { DeleteSTLines(); return; }
    int need=180;
    MqlRates rates[]; double atr[];
    ArraySetAsSeries(rates,true); ArraySetAsSeries(atr,true);
    int rc=CopyRates(_Symbol,_Period,0,need,rates);
    int ac=CopyBuffer(g_stAtrHandle,0,0,need,atr);
    int n=MathMin(rc,ac);
    if(n<Supertrend_ATR周期+20) return;
    double st[]; int dirs[];
    ArrayResize(st,n); ArrayResize(dirs,n);
    int oldest=n-1;
    double mid=(rates[oldest].high+rates[oldest].low)*0.5;
    double fu=mid+Supertrend_倍数*atr[oldest];
    double fl=mid-Supertrend_倍数*atr[oldest];
    int trend=(rates[oldest].close>=mid?1:-1);
    st[oldest]=(trend==1?fl:fu); dirs[oldest]=trend;
    for(int i=oldest-1;i>=1;i--)
    {
        mid=(rates.high+rates.low)*0.5;
        double bu=mid+Supertrend_倍数*atr;
        double bl=mid-Supertrend_倍数*atr;
        double pc=rates[i+1].close;
        double nu=(bu<fu || pc>fu)?bu:fu;
        double nl=(bl>fl || pc<fl)?bl:fl;
        if(rates.close>fu) trend=1;
        else if(rates.close<fl) trend=-1;
        fu=nu; fl=nl; st=(trend==1?fl:fu); dirs=trend;
    }
    DeleteSTLines();
    int maxDraw=MathMin(120,n-2);
    for(int i=maxDraw;i>=2;i--)
    {
        string name=ST_LINE_PREFIX+IntegerToString(i);
        if(ObjectCreate(0,name,OBJ_TREND,0,rates.time,st,rates[i-1].time,st[i-1]))
        {
            ObjectSetInteger(0,name,OBJPROP_COLOR,(dirs[i-1]==1?clrLime:clrTomato));
            ObjectSetInteger(0,name,OBJPROP_WIDTH,2);
            ObjectSetInteger(0,name,OBJPROP_RAY_LEFT,false);
            ObjectSetInteger(0,name,OBJPROP_RAY_RIGHT,false);
            ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
            ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
        }
    }
}

bool OpenMarketTagged(int dir,double volume,string tag)
{
    volume=NormalizeBrokerVolume(volume);
    if(volume<=0.0) return false;
    Trade.SetExpertMagicNumber(magicNumber);
    bool ok=(dir==1 ? Trade.Buy(volume,_Symbol,0,0,0,tag) : Trade.Sell(volume,_Symbol,0,0,0,tag));
    if(ok) Print("【ST引擎】",tag," 成交 ",DoubleToString(volume,2)," Lot");
    else Print("【ST引擎失败】",tag," retcode=",Trade.ResultRetcode()," ",Trade.ResultRetcodeDescription());
    return ok;
}

void TryOpenTrendFirst(int dir)
{
    if(dir==0 || !runEA) return;
    if(dir==1 && !allow_buy) return;
    if(dir==-1 && !allow_sell) return;
    if(CountTrendPositions(dir)>0 || HasTrendPending(dir)) return;
    // 上一轮同方向顺势仓刚清空时,不允许同K线立即重开;必须等待下一根K线。
    if(!TrendRestartAllowedNow(dir)) return;
    int currentHour=GetCurrentHour();
    if(sj && !(currentHour>star && currentHour<end)) return;
    if(isInDangerTime(dangerHours)) return;

    double p=(dir==1 ? SymbolInfoDouble(_Symbol,SYMBOL_ASK)+_FirstStep*vPoint
                     : SymbolInfoDouble(_Symbol,SYMBOL_BID)-_FirstStep*vPoint);
    ENUM_ORDER_TYPE type=(dir==1?ORDER_TYPE_BUY_STOP:ORDER_TYPE_SELL_STOP);
    p=修正新建挂单价格(type,NormalizeDouble(p,_Digits));
    double v=NormalizeBrokerVolume(顺势首仓手数*开仓倍数);
    MqlTradeRequest req={}; MqlTradeResult res={};
    req.action=TRADE_ACTION_PENDING; req.symbol=_Symbol; req.volume=v; req.price=p;
    req.type=type; req.magic=magicNumber;
    req.comment=(dir==1?"MJ_TREND_FIRST_BUY":"MJ_TREND_FIRST_SELL");
    if(OrderSend(req,res) && 交易返回成功(res.retcode))
        Print("【ST首仓挂单】",(dir==1?"BUY":"SELL")," ",DoubleToString(v,2)," @ ",DoubleToString(p,_Digits));
    else
        Print("【ST首仓挂单失败】retcode=",res.retcode," ",res.comment);
}

void ResetTrendTracker(int allowedDir)
{
    g_buyPullbackArmed=false; g_sellPullbackArmed=false;
    g_buyPullbackLow=0.0; g_sellPullbackHigh=0.0;
    if(allowedDir==1)
    {
        g_trendExtremeBuy=SymbolInfoDouble(_Symbol,SYMBOL_BID);
        g_trendExtremeSell=0.0;
    }
    else if(allowedDir==-1)
    {
        g_trendExtremeSell=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
        g_trendExtremeBuy=0.0;
    }
}

string RecoveryStatePrefix()
{
    return "MJREC_"+IntegerToString((int)AccountInfoInteger(ACCOUNT_LOGIN))+"_"+_Symbol+"_"+IntegerToString((int)magicNumber)+"_";
}

void SaveRecoveryState()
{
    string p=RecoveryStatePrefix();
    GlobalVariableSet(p+"DIR",(double)g_recoveryDir);
    GlobalVariableSet(p+"BASE",g_recoveryBaseLots);
    GlobalVariableSet(p+"LEVEL",(double)g_recoveryAddLevel);
    GlobalVariableSet(p+"TIME",(double)g_recoverySnapshotTime);
}

void ClearRecoveryState()
{
    g_recoveryDir=0;
    g_recoveryBaseLots=0.0;
    g_recoveryAddLevel=0;
    g_recoverySnapshotTime=0;
    SaveRecoveryState();
}

void CaptureRecoverySnapshot(int dir)
{
    double lots=(dir==0?0.0:SumDirectionLots(dir));
    if(dir==0 || lots<=0.0)
    {
        ClearRecoveryState();
        return;
    }
    g_recoveryDir=dir;
    g_recoveryBaseLots=NormalizeBrokerVolume(lots);
    g_recoveryAddLevel=0;
    g_recoverySnapshotTime=TimeCurrent();
    SaveRecoveryState();
    Print("【逆势基准快照】方向=",(dir==1?"BUY":"SELL"),
          " 基准总手数=",DoubleToString(g_recoveryBaseLots,2),
          ",后续按此虚拟首仓×逆势倍数计算");
}

bool LoadRecoveryStateForCurrentTrend()
{
    string p=RecoveryStatePrefix();
    if(!GlobalVariableCheck(p+"DIR") || !GlobalVariableCheck(p+"BASE")) return false;
    int dir=(int)GlobalVariableGet(p+"DIR");
    double base=GlobalVariableGet(p+"BASE");
    int level=(GlobalVariableCheck(p+"LEVEL")?(int)GlobalVariableGet(p+"LEVEL"):0);
    datetime tm=(GlobalVariableCheck(p+"TIME")?(datetime)GlobalVariableGet(p+"TIME"):0);
    if(dir==0 || base<=0.0 || dir!=-g_stDir || SumDirectionLots(dir)<=0.0) return false;
    g_recoveryDir=dir; g_recoveryBaseLots=base; g_recoveryAddLevel=MathMax(0,level); g_recoverySnapshotTime=tm;
    return true;
}

int RecoveryVirtualOrderCount()
{
    if(g_recoveryDir==0 || g_recoveryBaseLots<=0.0) return 0;
    return 1+MathMax(0,g_recoveryAddLevel); // 快照总量视为1个虚拟首仓
}

void GetRecoveryDistances(double &pendingDist,double &modifyDist)
{
    int virtualCount=RecoveryVirtualOrderCount();
    if(virtualCount>加仓间距变更订单数)
    {
        pendingDist=挂单点距_非首单_二阶;
        modifyDist=修改挂单起始间距_二阶;
    }
    else
    {
        pendingDist=挂单点距_非首单_一阶;
        modifyDist=多单_修改挂单起始间距_一阶;
    }
}

double RecoveryNextLot()
{
    if(g_recoveryBaseLots<=0.0) return 0.0;
    int nextLevel=MathMax(1,g_recoveryAddLevel+1);
    double lot=g_recoveryBaseLots*MathPow(逆势加仓手数递增比例,nextLevel);
    // 保留原源码线性附加项语义,但以虚拟层级计数,默认by_do_19=0不产生影响
    lot += nextLevel*by_do_19;
    return NormalizeBrokerVolume(lot);
}

void HandleConfirmedSTFlip(int oldDir,int newDir)
{
    Print("【ST趋势切换】",(oldDir==1?"多":"空")," -> ",(newDir==1?"多":"空"));
    RecordSTFlipStat();

    // 新趋势方向允许开新仓;旧趋势方向立即转为“逆势恢复组”。
    // 先删除旧方向遗留挂单,再把切换瞬间旧方向全部持仓总量视为一个虚拟首仓快照。
    DeleteDirectionPending(oldDir);
    CaptureRecoverySnapshot(oldDir);
    // ST真正翻转本身就是一个全新的趋势生命周期,不继承该方向旧周期的“等下一根K线”门控。
    if(newDir==1) g_trendRestartBlockBarBuy=0;
    else if(newDir==-1) g_trendRestartBlockBarSell=0;
    ResetTrendTracker(newDir);
}

void UpdateSTDirectionOnClosedBar()
{
    datetime bar=iTime(_Symbol,_Period,0);
    if(bar<=0 || bar==g_stLastBarTime) return;
    g_stLastBarTime=bar;
    int raw=0; double val=0.0;
    if(!GetSupertrendClosed(1,raw,val)) return;
    g_stValue=val;
    if(raw==g_stCandidateDir) g_stCandidateBars++;
    else { g_stCandidateDir=raw; g_stCandidateBars=1; }
    int need=MathMax(1,MathMin(3,ST趋势确认K线数));
    if(g_stCandidateBars<need) return;
    if(g_stDir==0)
    {
        g_stDir=raw;
        ResetTrendTracker(g_stDir);
        if(!LoadRecoveryStateForCurrentTrend())
            CaptureRecoverySnapshot(-g_stDir);
        Print("【ST初始化】当前趋势=",(g_stDir==1?"多":"空"));
    }
    else if(raw!=g_stDir)
    {
        int old=g_stDir; g_stDir=raw;
        HandleConfirmedSTFlip(old,g_stDir);
    }
    DrawSupertrendLines();
}

void ProcessTrendPullbackAdd()
{
    if(!启用ST顺势逆势恢复模式 || g_stDir==0 || !runEA) return;
    int trendCount=CountTrendPositions(g_stDir);
    if(trendCount<=0) { TryOpenTrendFirst(g_stDir); return; }
    if(trendCount>=顺势最大层数) return;
    if(g_lastTrendAddTime>0 && TimeCurrent()-g_lastTrendAddTime<顺势最小加仓间隔秒) return;

    double bid=SymbolInfoDouble(_Symbol,SYMBOL_BID);
    double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
    double pull=TrendPullbackDistancePrice(g_stDir,(g_stDir==1?bid:ask));
    double confirm=TrendRecoveryConfirmDistancePrice(g_stDir,(g_stDir==1?bid:ask));

    if(g_stDir==1)
    {
        if(g_trendExtremeBuy<=0.0 || bid>g_trendExtremeBuy)
        {
            g_trendExtremeBuy=bid;
            if(!g_buyPullbackArmed) g_buyPullbackLow=0.0;
        }
        if(!g_buyPullbackArmed && g_trendExtremeBuy-bid>=pull)
        {
            g_buyPullbackArmed=true; g_buyPullbackLow=bid;
            Print("【顺势多回调】已达到回调距离,开始等待恢复确认");
        }
        if(g_buyPullbackArmed)
        {
            if(g_buyPullbackLow<=0.0 || bid<g_buyPullbackLow) g_buyPullbackLow=bid;
            if(bid-g_buyPullbackLow>=TrendRecoveryConfirmDistancePrice(1,bid))
            {
                double gap=0.0,lastPrice=0.0;
                double candidate=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
                if(!PassAddPriceSpacing(1,candidate,gap,lastPrice))
                {
                    Print("【顺势多加仓拦截】与上一笔BUY间距=",DoubleToString(gap,_Digits),
                          " < 最小间距=",DoubleToString(MinSameDirectionSpacingPrice(lastPrice),_Digits),
                          " 上一笔=",DoubleToString(lastPrice,_Digits));
                    return;
                }
                double v=NormalizeDouble(trendCount*by_do_19+顺势首仓手数*MathPow(顺势加仓手数递增比例,trendCount),开仓手数小数位)*开仓倍数;
                if(OpenMarketTagged(1,v,"MJ_TREND_ADD_BUY"))
                {
                    g_lastTrendAddTime=TimeCurrent();
                    g_buyPullbackArmed=false; g_buyPullbackLow=0.0; g_trendExtremeBuy=bid;
                }
            }
        }
    }
    else
    {
        if(g_trendExtremeSell<=0.0 || ask<g_trendExtremeSell)
        {
            g_trendExtremeSell=ask;
            if(!g_sellPullbackArmed) g_sellPullbackHigh=0.0;
        }
        if(!g_sellPullbackArmed && ask-g_trendExtremeSell>=pull)
        {
            g_sellPullbackArmed=true; g_sellPullbackHigh=ask;
            Print("【顺势空回调】已达到反弹距离,开始等待恢复确认");
        }
        if(g_sellPullbackArmed)
        {
            if(g_sellPullbackHigh<=0.0 || ask>g_sellPullbackHigh) g_sellPullbackHigh=ask;
            if(g_sellPullbackHigh-ask>=TrendRecoveryConfirmDistancePrice(-1,ask))
            {
                double gap=0.0,lastPrice=0.0;
                double candidate=SymbolInfoDouble(_Symbol,SYMBOL_BID);
                if(!PassAddPriceSpacing(-1,candidate,gap,lastPrice))
                {
                    Print("【顺势空加仓拦截】与上一笔SELL间距=",DoubleToString(gap,_Digits),
                          " < 最小间距=",DoubleToString(MinSameDirectionSpacingPrice(lastPrice),_Digits),
                          " 上一笔=",DoubleToString(lastPrice,_Digits));
                    return;
                }
                double v=NormalizeDouble(trendCount*by_do_19+顺势首仓手数*MathPow(顺势加仓手数递增比例,trendCount),开仓手数小数位)*开仓倍数;
                if(OpenMarketTagged(-1,v,"MJ_TREND_ADD_SELL"))
                {
                    g_lastTrendAddTime=TimeCurrent();
                    g_sellPullbackArmed=false; g_sellPullbackHigh=0.0; g_trendExtremeSell=ask;
                }
            }
        }
    }
}

//+------------------------------------------------------------------+
//| ST模式下:反趋势方向只允许在已有持仓基础上按V1.07规则继续加仓     |
//| 严禁在反趋势方向从0开始建立新仓                                  |
//+------------------------------------------------------------------+
bool PlaceRecoveryPending(int dir)
{
    if(dir==0 || !runEA) return false;
    if(dir==1 && !allow_buy) return false;
    if(dir==-1 && !allow_sell) return false;

    int posCount=(dir==1?多单订单数:空单订单数);
    if(posCount<=0) return false; // 核心约束:逆势绝不新开首仓
    if(dir!=g_recoveryDir || g_recoveryBaseLots<=0.0) return false;
    if(HasRecoveryPending(dir)) return false;

    int currentHour=GetCurrentHour();
    if(sj && !(currentHour>star && currentHour<end)) return false;

    double trade_price=0.0;
    double pendingDist=0.0, modifyDist=0.0;
    GetRecoveryDistances(pendingDist,modifyDist);

    if(dir==1)
    {
        trade_price=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK)+pendingDist*vPoint,_Digits);
        bool allowed=((多单最高价!=0 && trade_price>=NormalizeDouble(多单最高价+modifyDist*vPoint,_Digits) && by_bo_10) ||
                      (多单最低价!=0 && trade_price<=NormalizeDouble(多单最低价-modifyDist*vPoint,_Digits)));
        if(!allowed) return false;
    }
    else
    {
        trade_price=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID)-pendingDist*vPoint,_Digits);
        bool allowed=((空单最低价!=0 && trade_price<=NormalizeDouble(空单最低价-modifyDist*vPoint,_Digits) && by_bo_10) ||
                      (空单最高价!=0 && trade_price>=NormalizeDouble(空单最高价+modifyDist*vPoint,_Digits)));
        if(!allowed) return false;
    }

    double trade_lot=RecoveryNextLot();
    if(!((trade_lot < AccountInfoDouble(ACCOUNT_FREEMARGIN)/SymbolInfoDouble(_Symbol,SYMBOL_TRADE_CONTRACT_SIZE) && posCount>0) || by_bo_9))
        return false;

    ENUM_ORDER_TYPE type=(dir==1?ORDER_TYPE_BUY_STOP:ORDER_TYPE_SELL_STOP);
    trade_price=修正新建挂单价格(type,trade_price);
    double gap=0.0,lastPrice=0.0;
    if(!PassAddPriceSpacing(dir,trade_price,gap,lastPrice))
    {
        Print("【逆势恢复挂单拦截】",(dir==1?"BUY":"SELL")," 候选价=",DoubleToString(trade_price,_Digits),
              " 与上一笔间距=",DoubleToString(gap,_Digits)," < 最小间距=",
              DoubleToString(MinSameDirectionSpacingPrice(lastPrice),_Digits));
        return false;
    }
    MqlTradeRequest request={}; MqlTradeResult result={};
    request.action=TRADE_ACTION_PENDING;
    request.symbol=_Symbol; request.volume=trade_lot; request.price=trade_price;
    request.sl=0; request.tp=0; request.type=type; request.magic=magicNumber;
    request.comment=(dir==1?"MJ_RECOVERY_BUY":"MJ_RECOVERY_SELL");
    ResetLastError();
    bool sent=OrderSend(request,result);
    if(sent && 交易返回成功(result.retcode))
    {
        Print("【逆势恢复挂单】",(dir==1?"BUY":"SELL")," 虚拟层=",RecoveryVirtualOrderCount(),
              " 基准=",DoubleToString(g_recoveryBaseLots,2)," 下单手数=",DoubleToString(trade_lot,2),
              " @ ",DoubleToString(trade_price,_Digits));
        return true;
    }
    Print("【逆势恢复挂单失败】sent=",sent," retcode=",result.retcode," ",result.comment," err=",GetLastError());
    return false;
}

void MoveRecoveryPending(int dir)
{
    int posCount=(dir==1?多单订单数:空单订单数);
    if(posCount<=0) { DeleteRecoveryPending(dir); return; }

    for(int i=OrdersTotal()-1;i>=0;i--)
    {
        if(!OrderInfo.SelectByIndex(i)) continue;
        if(OrderInfo.Symbol()!=_Symbol || OrderInfo.Magic()!=magicNumber) continue;
        string c=OrderInfo.Comment();
        if(StringFind(c,"MJ_RECOVERY")<0) continue;
        ENUM_ORDER_TYPE typ=OrderInfo.OrderType();
        if(dir==1 && typ!=ORDER_TYPE_BUY_STOP) continue;
        if(dir==-1 && typ!=ORDER_TYPE_SELL_STOP) continue;

        double currentPrice=OrderInfo.PriceOpen();
        double existingGap=0.0,existingLast=0.0;
        if(!PassAddPriceSpacing(dir,currentPrice,existingGap,existingLast))
        {
            ulong badTicket=OrderInfo.Ticket();
            if(Trade.OrderDelete(badTicket))
                Print("【逆势恢复挂单撤销】ticket=",badTicket," 与上一笔同向成交间距不足 ",
                      DoubleToString(existingGap,_Digits)," < ",DoubleToString(MinSameDirectionSpacingPrice(existingLast),_Digits));
            continue;
        }
        double newPrice=0.0;
        bool shouldModify=false;
        double pendingDist=0.0, modifyDist=0.0;
        GetRecoveryDistances(pendingDist,modifyDist);
        if(dir==1)
        {
            newPrice=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK)+pendingDist*vPoint,_Digits);
            if(NormalizeDouble(currentPrice-stop挂单改价间距*vPoint,_Digits)>newPrice &&
               (newPrice<=NormalizeDouble(多单最低价-modifyDist*vPoint,_Digits) || 多单最低价==0 || by_bo_10) &&
               (newPrice>=NormalizeDouble(modifyDist*vPoint+多单最高价,_Digits) ||
                newPrice<=NormalizeDouble(多单最低价-modifyDist*vPoint,_Digits)))
                shouldModify=true;
        }
        else
        {
            newPrice=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID)-pendingDist*vPoint,_Digits);
            if(NormalizeDouble(currentPrice+stop挂单改价间距*vPoint,_Digits)<newPrice &&
               (newPrice>=NormalizeDouble(modifyDist*vPoint+空单最高价,_Digits) || 空单最高价==0 || by_bo_10) &&
               (newPrice<=NormalizeDouble(空单最低价-modifyDist*vPoint,_Digits) ||
                newPrice>=NormalizeDouble(modifyDist*vPoint+空单最高价,_Digits)))
                shouldModify=true;
        }
        if(!shouldModify || newPrice<=0) continue;

        double legalPrice=newPrice;
        if(!准备修改挂单价格(typ,currentPrice,newPrice,legalPrice)) continue;
        double moveGap=0.0,moveLast=0.0;
        if(!PassAddPriceSpacing(dir,legalPrice,moveGap,moveLast)) continue;
        MqlTradeRequest req={}; MqlTradeResult res={};
        req.action=TRADE_ACTION_MODIFY; req.order=OrderInfo.Ticket(); req.price=legalPrice;
        req.sl=OrderInfo.StopLoss(); req.tp=OrderInfo.TakeProfit();
        if(!OrderSend(req,res) || !交易返回成功(res.retcode))
            Print("【逆势恢复改单失败】ticket=",OrderInfo.Ticket()," retcode=",res.retcode," ",res.comment);
    }
}

void ProcessInverseRecoveryAdd()
{
    if(!启用ST顺势逆势恢复模式 || g_stDir==0 || !runEA) return;
    int inverseDir=-g_stDir;
    int inverseCount=(inverseDir==1?多单订单数:空单订单数);

    // 反趋势方向没有旧仓:删除残留恢复挂单,清除本轮基准;绝不建立新首仓。
    if(inverseCount<=0)
    {
        DeleteRecoveryPending(inverseDir);
        if(g_recoveryDir==inverseDir) ClearRecoveryState();
        return;
    }

    // 兼容EA重启/状态缺失:当前反趋势持仓总量只在没有有效快照时抓取一次。
    if(g_recoveryDir!=inverseDir || g_recoveryBaseLots<=0.0)
        CaptureRecoverySnapshot(inverseDir);

    PlaceRecoveryPending(inverseDir);
    MoveRecoveryPending(inverseDir);
}

//+------------------------------------------------------------------+
//| 统计模块:券商服务器自然日                                       |
//+------------------------------------------------------------------+
datetime BrokerDayStart(datetime t)
{
    MqlDateTime dt;
    TimeToStruct(t,dt);
    dt.hour=0; dt.min=0; dt.sec=0;
    return StructToTime(dt);
}

int SymbolStatHash()
{
    uint h=2166136261;
    for(int i=0;i<StringLen(_Symbol);i++)
    {
        h ^= (uint)StringGetCharacter(_Symbol,i);
        h *= 16777619;
    }
    return (int)(h & 0x7FFFFFFF);
}

string DailyStatPrefix(datetime day)
{
    MqlDateTime dt; TimeToStruct(day,dt);
    return "MDSTAT_"+IntegerToString((int)AccountInfoInteger(ACCOUNT_LOGIN))+"_"
           +IntegerToString((int)magicNumber)+"_"+IntegerToString(SymbolStatHash())+"_"
           +StringFormat("%04d%02d%02d_",dt.year,dt.mon,dt.day);
}

void LoadTodaySTEventStats(datetime day)
{
    string k=DailyStatPrefix(day);
    g_todaySTFlips=(GlobalVariableCheck(k+"stFlips")?(int)GlobalVariableGet(k+"stFlips"):0);
    g_todayRecoveryAdds=(GlobalVariableCheck(k+"recoveryCount")?(int)GlobalVariableGet(k+"recoveryCount"):0);
}

void RecordSTFlipStat()
{
    datetime day=BrokerDayStart(TimeCurrent());
    string k=DailyStatPrefix(day);
    double v=GlobalVariableCheck(k+"stFlips")?GlobalVariableGet(k+"stFlips"):0.0;
    v+=1.0;
    GlobalVariableSet(k+"stFlips",v);
    if(g_statsDay==day) g_todaySTFlips=(int)v;
}

void RecordRecoveryAddStat()
{
    datetime day=BrokerDayStart(TimeCurrent());
    string k=DailyStatPrefix(day);
    double v=GlobalVariableCheck(k+"recoveryCount")?GlobalVariableGet(k+"recoveryCount"):0.0;
    v+=1.0;
    GlobalVariableSet(k+"recoveryCount",v);
    if(g_statsDay==day) g_todayRecoveryAdds=(int)v;
}

void DeleteStatsPanelObjects()
{
    ObjectsDeleteAll(0,STAT_UI_PREFIX);
}

// 从历史成交重建某一天的“已实现类”统计。
// 只统计当前品种 + 当前Magic;盈亏包含 profit/swap/commission/fee。
bool CalcDailyHistoryStats(datetime day,double &pnl,double &openedLots,double &maxEntryLot,
                           int &closeTrades,int &wins,int &losses)
{
    pnl=0.0; openedLots=0.0; maxEntryLot=0.0;
    closeTrades=0; wins=0; losses=0;
    datetime from=BrokerDayStart(day);
    datetime now=TimeCurrent();
    datetime to=from+86399;
    if(BrokerDayStart(now)==from) to=now;
    if(!HistorySelect(from,to)) return false;

    int total=HistoryDealsTotal();
    for(int i=0;i<total;i++)
    {
        ulong ticket=HistoryDealGetTicket(i);
        if(ticket==0) continue;
        if(HistoryDealGetString(ticket,DEAL_SYMBOL)!=_Symbol) continue;
        if((long)HistoryDealGetInteger(ticket,DEAL_MAGIC)!=(long)magicNumber) continue;

        double dealPnl=HistoryDealGetDouble(ticket,DEAL_PROFIT)
                      +HistoryDealGetDouble(ticket,DEAL_SWAP)
                      +HistoryDealGetDouble(ticket,DEAL_COMMISSION)
                      +HistoryDealGetDouble(ticket,DEAL_FEE);
        pnl += dealPnl;

        long entry=HistoryDealGetInteger(ticket,DEAL_ENTRY);
        double vol=HistoryDealGetDouble(ticket,DEAL_VOLUME);
        if(entry==DEAL_ENTRY_IN || entry==DEAL_ENTRY_INOUT)
        {
            openedLots += vol;
            if(vol>maxEntryLot) maxEntryLot=vol;
        }
        if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_OUT_BY || entry==DEAL_ENTRY_INOUT)
        {
            closeTrades++;
            if(dealPnl>0.0000001) wins++;
            else if(dealPnl<-0.0000001) losses++;
        }
    }
    return true;
}

void SaveDailyFloatStats(datetime day)
{
    if(day<=0) return;
    string k=DailyStatPrefix(day);
    GlobalVariableSet(k+"maxFloat",g_todayMaxFloatProfit);
    GlobalVariableSet(k+"minFloat",g_todayMaxFloatLoss);
    GlobalVariableSet(k+"startBal",g_todayStartBalance);
    GlobalVariableSet(k+"lastEq",g_todayLastEquity);
}

void LoadDailyFloatStats(datetime day)
{
    string k=DailyStatPrefix(day);
    g_todayMaxFloatProfit=GlobalVariableCheck(k+"maxFloat")?GlobalVariableGet(k+"maxFloat"):0.0;
    g_todayMaxFloatLoss=GlobalVariableCheck(k+"minFloat")?GlobalVariableGet(k+"minFloat"):0.0;
    g_todayStartBalance=GlobalVariableCheck(k+"startBal")?GlobalVariableGet(k+"startBal"):AccountInfoDouble(ACCOUNT_BALANCE);
    g_todayLastEquity=GlobalVariableCheck(k+"lastEq")?GlobalVariableGet(k+"lastEq"):AccountInfoDouble(ACCOUNT_EQUITY);
}

void RefreshTodayHistoryStats(bool force=false)
{
    datetime now=TimeCurrent();
    datetime day=BrokerDayStart(now);

    if(g_statsDay==0)
    {
        g_statsDay=day;
        LoadDailyFloatStats(day);
        LoadTodaySTEventStats(day);
    }
    else if(day!=g_statsDay)
    {
        // 券商服务器跨日:先保存旧日极值,再切换到新日
        SaveDailyFloatStats(g_statsDay);
        g_statsDay=day;
        g_todayRealizedProfit=0.0;
        g_todayOpenedLots=0.0;
        g_todayMaxEntryLot=0.0;
        g_todayCloseTrades=0;
        g_todayWins=0;
        g_todayLosses=0;
        g_todaySTFlips=0;
        g_todayRecoveryAdds=0;
        LoadDailyFloatStats(day);
        LoadTodaySTEventStats(day);
        Print("【统计】券商服务器跨日,开始新一天: ",TimeToString(day,TIME_DATE));
        force=true;
    }

    // 历史扫描最多每秒一次,避免高频tick反复扫描历史库
    if(!force && g_lastHistoryStatsCalc==now) return;
    g_lastHistoryStatsCalc=now;
    CalcDailyHistoryStats(day,g_todayRealizedProfit,g_todayOpenedLots,g_todayMaxEntryLot,
                          g_todayCloseTrades,g_todayWins,g_todayLosses);
}

void UpdateFloatingStats()
{
    double fp=多单累计利润+空单累计利润;
    bool extremeChanged=false;
    if(fp>g_todayMaxFloatProfit) { g_todayMaxFloatProfit=fp; extremeChanged=true; }
    if(fp<g_todayMaxFloatLoss)   { g_todayMaxFloatLoss=fp; extremeChanged=true; }
    if(fp<g_sessionMaxFloatLoss) g_sessionMaxFloatLoss=fp;
    g_todayLastEquity=AccountInfoDouble(ACCOUNT_EQUITY);

    datetime now=TimeCurrent();
    if(extremeChanged || g_lastFloatStatSave==0 || now-g_lastFloatStatSave>=30)
    {
        SaveDailyFloatStats(g_statsDay);
        g_lastFloatStatSave=now;
    }
}

//+------------------------------------------------------------------+
//| 获取当前时间的小时数                                             |
//+------------------------------------------------------------------+

// 参数点数统一按2位/4位报价习惯:
// 2位/3位品种:参数100点都代表1.00价格
// 4位/5位品种:参数100点都代表0.0100价格
void 刷新券商距离限制()
{
    long stopLevelRaw = (long)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
    long freezeLevelRaw = (long)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);

    市场当前StopLevel点距   = MathCeil((double)stopLevelRaw / vPointFactor);
    市场当前FreezeLevel点距 = MathCeil((double)freezeLevelRaw / vPointFactor);
    // 保留旧变量名供兼容/显示,但新建挂单只按StopLevel约束
    市场当前最小挂单点距 = 市场当前StopLevel点距;
}

void 初始化点数自适应()
{
    vRawPoint = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
    if(vRawPoint <= 0.0)
        vRawPoint = Point();

    if(_Digits == 3 || _Digits == 5)
        vPointFactor = 10.0;
    else
        vPointFactor = 1.0;

    vPoint = vRawPoint * vPointFactor;
    刷新券商距离限制();

    long stopLevelRaw = (long)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
    long freezeLevelRaw = (long)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);

    Print("【点数自适应】", _Symbol,
          " Digits=", _Digits,
          " RawPoint=", DoubleToString(vRawPoint, _Digits),
          " 参数1点价格=", DoubleToString(vPoint, _Digits),
          " 倍数=", DoubleToString(vPointFactor, 0),
          " BrokerStop=", stopLevelRaw,
          " BrokerFreeze=", freezeLevelRaw,
          " StopLevel参数点=", DoubleToString(市场当前StopLevel点距, 0),
          " FreezeLevel参数点=", DoubleToString(市场当前FreezeLevel点距, 0));
}

// 新建挂单:只按StopLevel把价格修正到合法区域
// 返回修正后的挂单价格
// type仅支持BUY_STOP / SELL_STOP
double 修正新建挂单价格(ENUM_ORDER_TYPE type, double wantedPrice)
{
    刷新券商距离限制();
    double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
    double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    double minDist = 市场当前StopLevel点距 * vPoint;
    double fixedPrice = wantedPrice;

    if(type == ORDER_TYPE_BUY_STOP)
    {
        double minPrice = ask + minDist;
        if(fixedPrice < minPrice)
            fixedPrice = minPrice;
    }
    else if(type == ORDER_TYPE_SELL_STOP)
    {
        double maxPrice = bid - minDist;
        if(fixedPrice > maxPrice)
            fixedPrice = maxPrice;
    }
    return NormalizeDouble(fixedPrice, _Digits);
}

// 修改挂单:StopLevel约束目标价;FreezeLevel约束当前挂单是否允许修改。
// 返回false表示当前处于冻结区,本tick跳过修改。
bool 准备修改挂单价格(ENUM_ORDER_TYPE type, double currentOrderPrice, double wantedPrice, double &fixedPrice)
{
    刷新券商距离限制();
    double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
    double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    double stopDist = 市场当前StopLevel点距 * vPoint;
    double freezeDist = 市场当前FreezeLevel点距 * vPoint;

    // 订单已进入FreezeLevel范围时,券商通常禁止修改/删除,直接等待下一tick。
    if(freezeDist > 0.0)
    {
        if(type == ORDER_TYPE_BUY_STOP && (currentOrderPrice - ask) <= freezeDist)
            return false;
        if(type == ORDER_TYPE_SELL_STOP && (bid - currentOrderPrice) <= freezeDist)
            return false;
    }

    fixedPrice = wantedPrice;
    if(type == ORDER_TYPE_BUY_STOP)
    {
        double minPrice = ask + stopDist;
        if(fixedPrice < minPrice)
            fixedPrice = minPrice;
    }
    else if(type == ORDER_TYPE_SELL_STOP)
    {
        double maxPrice = bid - stopDist;
        if(fixedPrice > maxPrice)
            fixedPrice = maxPrice;
    }

    fixedPrice = NormalizeDouble(fixedPrice, _Digits);
    return true;
}

bool 交易返回成功(uint retcode)
{
    return (retcode == TRADE_RETCODE_DONE ||
            retcode == TRADE_RETCODE_PLACED ||
            retcode == TRADE_RETCODE_DONE_PARTIAL ||
            retcode == TRADE_RETCODE_NO_CHANGES);
}

ulong 用户点转原生点(double userPoints)
{
    if(userPoints <= 0.0)
        return 0;
    return (ulong)MathRound(userPoints * vPointFactor);
}

int GetCurrentHour()
{
    MqlDateTime dt;
    TimeCurrent(dt);
    return dt.hour;
}

//+------------------------------------------------------------------+
//| 获取当前时间的分钟数                                             |
//+------------------------------------------------------------------+
int GetCurrentMinute()
{
    MqlDateTime dt;
    TimeCurrent(dt);
    return dt.min;
}

//+------------------------------------------------------------------+
//| 获取当前时间的秒数                                               |
//+------------------------------------------------------------------+
int GetCurrentSecond()
{
    MqlDateTime dt;
    TimeCurrent(dt);
    return dt.sec;
}

//+------------------------------------------------------------------+
//| 均价/止盈目标可视化                                               |
//+------------------------------------------------------------------+
void 删除均价止盈线()
{
    ObjectDelete(0,LINE_BUY_AVG);
    ObjectDelete(0,LINE_SELL_AVG);
    ObjectDelete(0,LINE_BUY_TP);
    ObjectDelete(0,LINE_SELL_TP);
    ObjectDelete(0,LINE_BASKET_TP);
}

void 设置价格线(string name,double price,color clr,ENUM_LINE_STYLE style,int width,string tip)
{
    if(price<=0.0)
    {
        ObjectDelete(0,name);
        return;
    }
    if(ObjectFind(0,name)<0)
        ObjectCreate(0,name,OBJ_HLINE,0,0,price);
    ObjectSetDouble(0,name,OBJPROP_PRICE,NormalizeDouble(price,_Digits));
    ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
    ObjectSetInteger(0,name,OBJPROP_STYLE,style);
    ObjectSetInteger(0,name,OBJPROP_WIDTH,width);
    ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
    ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
    ObjectSetInteger(0,name,OBJPROP_HIDDEN,false);
    ObjectSetString(0,name,OBJPROP_TOOLTIP,tip+"  "+DoubleToString(price,_Digits));
}

// 估算价格每移动1.0时,该方向持仓浮盈变化金额。
// 使用券商SYMBOL_TRADE_TICK_VALUE_PROFIT / TICK_SIZE,适配黄金/外汇/指数。
double 每价格单位盈亏斜率(double lots)
{
    if(lots<=0.0) return 0.0;
    double tickSize=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE);
    double tickValue=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_VALUE_PROFIT);
    if(tickValue<=0.0) tickValue=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_VALUE);
    if(tickSize<=0.0 || tickValue<=0.0) return 0.0;
    return lots*tickValue/tickSize;
}

void 计算当前止盈价格()
{
    g_buyTPPrice=0.0;
    g_sellTPPrice=0.0;
    g_basketTPPrice=0.0;
    g_tpModeText="无持仓";

    double bid=SymbolInfoDouble(_Symbol,SYMBOL_BID);
    double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
    double targetSingle=单边止盈金额*开仓倍数;
    double targetBasket=(启用ST顺势逆势恢复模式?ST篮子止盈金额:双边止盈金额)*开仓倍数;

    // ST模式始终按全部多空净敞口反算Basket目标价,绝不显示单边TP以免误导拆锁。
    if(启用ST顺势逆势恢复模式)
    {
        if(多单累计手数>0.0 || 空单累计手数>0.0)
        {
            g_tpModeText="ST Basket止盈";
            double buySlope=每价格单位盈亏斜率(多单累计手数);
            double sellSlope=每价格单位盈亏斜率(空单累计手数);
            double netSlope=buySlope-sellSlope;
            double totalProfit=多单累计利润+空单累计利润;
            if(MathAbs(netSlope)>0.0000001)
            {
                double anchor=(netSlope>0.0?bid:ask);
                g_basketTPPrice=NormalizeDouble(anchor+(targetBasket-totalProfit)/netSlope,_Digits);
            }
            else
                g_tpModeText="ST Basket·净敞口≈0";
        }
        return;
    }

    // 与EA真实平仓逻辑保持同一模式判断。
    bool singleMode=(多单累计利润>基准止盈金额 && 空单累计利润>基准止盈金额);

    if(singleMode)
    {
        g_tpModeText="单边止盈";
        if(多单累计手数>0.0)
        {
            double slope=每价格单位盈亏斜率(多单累计手数);
            if(slope>0.0)
                g_buyTPPrice=NormalizeDouble(bid+(targetSingle-多单累计利润)/slope,_Digits);
        }
        if(空单累计手数>0.0)
        {
            double slope=每价格单位盈亏斜率(空单累计手数);
            if(slope>0.0)
                g_sellTPPrice=NormalizeDouble(ask-(targetSingle-空单累计利润)/slope,_Digits);
        }
    }
    else if(多单累计手数>0.0 || 空单累计手数>0.0)
    {
        g_tpModeText="双边总止盈";
        double buySlope=每价格单位盈亏斜率(多单累计手数);
        double sellSlope=每价格单位盈亏斜率(空单累计手数);
        double netSlope=buySlope-sellSlope;
        double totalProfit=多单累计利润+空单累计利润;
        // 多空手数完全对锁时,价格变化几乎无法改变组合利润,因此不存在唯一价格止盈线。
        if(MathAbs(netSlope)>0.0000001)
        {
            double anchor=(netSlope>0.0?bid:ask);
            g_basketTPPrice=NormalizeDouble(anchor+(targetBasket-totalProfit)/netSlope,_Digits);
        }
        else
            g_tpModeText="双边止盈·净敞口≈0";
    }
}

void 绘制均价止盈线()
{
    计算当前止盈价格();
    if(!显示均价止盈线)
    {
        删除均价止盈线();
        return;
    }

    if(多单累计手数>0.0)
        设置价格线(LINE_BUY_AVG,多单持仓均价,clrDeepSkyBlue,STYLE_DASH,1,"多单持仓均价");
    else ObjectDelete(0,LINE_BUY_AVG);

    if(空单累计手数>0.0)
        设置价格线(LINE_SELL_AVG,空单持仓均价,clrOrangeRed,STYLE_DASH,1,"空单持仓均价");
    else ObjectDelete(0,LINE_SELL_AVG);

    if(g_buyTPPrice>0.0)
        设置价格线(LINE_BUY_TP,g_buyTPPrice,clrLime,STYLE_SOLID,2,"多单止盈目标");
    else ObjectDelete(0,LINE_BUY_TP);

    if(g_sellTPPrice>0.0)
        设置价格线(LINE_SELL_TP,g_sellTPPrice,clrTomato,STYLE_SOLID,2,"空单止盈目标");
    else ObjectDelete(0,LINE_SELL_TP);

    if(g_basketTPPrice>0.0)
        设置价格线(LINE_BASKET_TP,g_basketTPPrice,clrGold,STYLE_SOLID,2,"双边组合止盈目标");
    else ObjectDelete(0,LINE_BASKET_TP);
}

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   _FirstStep = FirstStep;
    if(magicNumber == 0)
    {
        Alert("请自定义魔术码");
        return(INIT_PARAMETERS_INCORRECT);
    }

    // 2/3位、4/5位报价自动适应
    初始化点数自适应();

// 初始化可修改变量
    当前_多单_挂单点距_非首单 = 挂单点距_非首单_一阶;
    当前_多单_修改挂单起始间距 = 多单_修改挂单起始间距_一阶;
    当前_空单_挂单点距_非首单 = 挂单点距_非首单_一阶;
    当前_空单_修改挂单起始间距 = 多单_修改挂单起始间距_一阶;

    // FirstStep仅受新建挂单StopLevel约束;FreezeLevel不再误用于首挂单距离。
    if(_FirstStep < 市场当前StopLevel点距)
    {
        Print("【挂单距离自动修正】FirstStep=", DoubleToString(_FirstStep,0),
              " 小于券商StopLevel=", DoubleToString(市场当前StopLevel点距,0),
              ",运行时已自动调整。FreezeLevel仅用于修改/删除挂单检查。");
        _FirstStep = 市场当前StopLevel点距;
    }

    Trade.SetExpertMagicNumber(magicNumber);
    Trade.SetDeviationInPoints(用户点转原生点(30));

    if(启用ST顺势逆势恢复模式)
    {
        // 本模式需要保留反趋势旧仓,同时允许当前趋势方向建立新仓,因此必须使用Hedging账户。
        if((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
        {
            Print("【ST双向管理模式无法启动】当前账户不是Hedging对冲账户;净额账户无法同时保留多空持仓。");
            return(INIT_FAILED);
        }
        g_stAtrHandle=iATR(_Symbol,_Period,Supertrend_ATR周期);
        if(g_stAtrHandle==INVALID_HANDLE)
        {
            Print("【错误】Supertrend ATR句柄创建失败");
            return(INIT_FAILED);
        }
        g_stLastBarTime=0;
        UpdateSTDirectionOnClosedBar();
    }

    // 统计初始化:日期完全采用券商服务器时间 TimeCurrent()
    g_sessionStartTime=TimeCurrent();
    g_sessionStartBalance=AccountInfoDouble(ACCOUNT_BALANCE);
    g_sessionMaxFloatLoss=0.0;
    g_statsDay=BrokerDayStart(TimeCurrent());
    LoadDailyFloatStats(g_statsDay);
    RefreshTodayHistoryStats(true);

    设置恒鑫图表主题();
    统计持仓信息();
    UpdateFloatingStats();
    绘制均价止盈线();
    Information();
    ChartRedraw(0);

    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    SaveDailyFloatStats(g_statsDay);
    DeleteStatsPanelObjects();
    ObjectsDeleteAll(0, UI_PREFIX);
    ObjectDelete(0, "Close button");
    删除均价止盈线();
    DeleteSTLines();
    if(g_stAtrHandle!=INVALID_HANDLE) { IndicatorRelease(g_stAtrHandle); g_stAtrHandle=INVALID_HANDLE; }
    Comment("");
    ChartRedraw(0);
}

//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| 成交事件:记录实际成交的逆势恢复加仓                              |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
                        const MqlTradeRequest &request,
                        const MqlTradeResult &result)
{
    if(trans.type!=TRADE_TRANSACTION_DEAL_ADD || trans.deal==0) return;
    if(!HistoryDealSelect(trans.deal)) return;
    if(HistoryDealGetString(trans.deal,DEAL_SYMBOL)!=_Symbol) return;
    if((long)HistoryDealGetInteger(trans.deal,DEAL_MAGIC)!=(long)magicNumber) return;
    if((ENUM_DEAL_ENTRY)HistoryDealGetInteger(trans.deal,DEAL_ENTRY)!=DEAL_ENTRY_IN) return;
    string c=HistoryDealGetString(trans.deal,DEAL_COMMENT);
    if(StringFind(c,"MJ_RECOVERY")>=0)
    {
        RecordRecoveryAddStat();
        g_recoveryAddLevel++;
        SaveRecoveryState();
        Print("【逆势恢复成交】当前逆势加仓层级=",g_recoveryAddLevel,
              " 基准手数=",DoubleToString(g_recoveryBaseLots,2));
    }
}

void OnTick()
{
   int runok = 0;
    //int adn[] = {618727, 9317310, 50165780,82002507,20949963, 50166849, 50153572, 1900336531};
    datetime expiry_time = D'2999.10.15 00:00';

     //检查账号是否在允许列表中
    //for(int i = 0; i < ArraySize(adn); i++)
    {
        //if(AccountInfoInteger(ACCOUNT_LOGIN) == adn)
        {
            runok = 1;
           // break;
        }
    }

    if(runok == 0)
    {
        Alert("非法账号,请联系v:17630708259");
        ExpertRemove();
        return;
    }

    // 检查是否超过有效期
    if(TimeCurrent() > expiry_time)
    {
        Alert("软件已过期,请联系v:17630708259");
        ExpertRemove();
        return;
    }

    if(风控管理)
    {
        亏损达到锁仓();
        suo();
    }

    统计持仓信息();
    if(启用ST顺势逆势恢复模式)
    {
        UpdateSTDirectionOnClosedBar();
        UpdateTrendCycleRestartState();
    }
    RefreshTodayHistoryStats();
    UpdateFloatingStats();
    绘制均价止盈线();
    Information();

    // 调整挂单间距 - 使用可修改的变量
    if(多单订单数 > 加仓间距变更订单数)
    {
        当前_多单_挂单点距_非首单 = 挂单点距_非首单_二阶;
        当前_多单_修改挂单起始间距 = 修改挂单起始间距_二阶;
    }
    else
    {
        当前_多单_挂单点距_非首单 = 挂单点距_非首单_一阶;
        当前_多单_修改挂单起始间距 = 多单_修改挂单起始间距_一阶;
    }

    if(空单订单数 > 加仓间距变更订单数)
    {
        当前_空单_挂单点距_非首单 = 挂单点距_非首单_二阶;
        当前_空单_修改挂单起始间距 = 修改挂单起始间距_二阶;
    }
    else
    {
        当前_空单_挂单点距_非首单 = 挂单点距_非首单_一阶;
        当前_空单_修改挂单起始间距 = 多单_修改挂单起始间距_一阶;
    }

    // 止盈止损逻辑
    if(启用ST顺势逆势恢复模式)
    {
        // 非对称退出规则:
        // 1) 顺势+逆势整体达到Basket目标 -> 全部退出;
        // 2) 逆势恢复组自己转为盈利 -> 允许逆势方向独立退出;
        // 3) 只要逆势组还存在,顺势组没有独立盈利退出权;
        // 4) 逆势组已退出后,剩余顺势仓达到同一ST止盈目标即可正常退出。
        double basketProfit=多单累计利润+空单累计利润;
        double basketTarget=ST篮子止盈金额*开仓倍数;
        int inverseDir=(g_stDir==0?0:-g_stDir);
        double inverseLots=(inverseDir==1?多单累计手数inverseDir==-1?空单累计手数:0.0));
        double inverseProfit=(inverseDir==1?多单累计利润inverseDir==-1?空单累计利润:0.0));

        if(basketProfit>=basketTarget)
        {
            Print("【ST Basket止盈】总净利润=",DoubleToString(basketProfit,2));
            平仓及删除订单(0);
            ClearRecoveryState();
            return;
        }

        double inverseExitTarget=逆势独立退出净利润*开仓倍数;
        bool inverseProfitable=(inverseLots>0.0 &&
                                ((inverseExitTarget<=0.0 && inverseProfit>0.0) ||
                                 (inverseExitTarget>0.0 && inverseProfit>=inverseExitTarget)));
        if(逆势盈利允许独立退出 && inverseProfitable)
        {
            Print("【逆势独立盈利退出】方向=",(inverseDir==1?"BUY":"SELL"),
                  " 净利润=",DoubleToString(inverseProfit,2),
                  ";顺势仓保留继续运行");
            DeleteRecoveryPending(inverseDir);
            平仓及删除订单(inverseDir);
            ClearRecoveryState();
            return;
        }

        if(basketProfit<=-StopLoss*开仓倍数)
        {
            Print("【ST Basket止损】",DoubleToString(basketProfit,2));
            平仓及删除订单(0);
            ClearRecoveryState();
            return;
        }
    }
    else
    {
    if(多单累计利润 > 基准止盈金额 && 空单累计利润 > 基准止盈金额)
    {
        if(多单累计利润 >= 单边止盈金额 * 开仓倍数)
        {
            Print("Buy Profit ", 多单累计利润);
            平仓及删除订单(1);
            return;
        }
        if(空单累计利润 >= 单边止盈金额 * 开仓倍数)
        {
            Print("Sell Profit ", 空单累计利润);
            平仓及删除订单(-1);
            return;
        }
    }
    else
    {
        double 多空总利润 = 多单累计利润 + 空单累计利润;
        if(多空总利润 >= 双边止盈金额 * 开仓倍数)
        {
            Print("CloseAll ", 多空总利润);
            平仓及删除订单(0);
            return;
        }
    }

    if(多单累计利润 <= -StopLoss * 开仓倍数)
    {
        Print("Buy Loss ", 多单累计利润);
        平仓及删除订单(1);
        return;
    }

    if(空单累计利润 <= -StopLoss * 开仓倍数)
    {
        Print("Sell Loss ", 空单累计利润);
        平仓及删除订单(-1);
        return;
    }
    }
    // 获取当前时间
    int currentHour = GetCurrentHour();

    if(!启用ST顺势逆势恢复模式)
    {
    // 开单逻辑
    //Print(多单_挂单订单数 == 0 ,"||", 多单累计利润 > by_do_17 ,"||",  allow_buy); ExpertRemove();
    if(多单_挂单订单数 == 0 && 多单累计利润 > by_do_17 && allow_buy)
    {
        double trade_price, trade_lot;
        bool isFirst = (多单订单数 == 0);
        //问题在这里
        if(isFirst)
            trade_price = NormalizeDouble(_FirstStep * vPoint + SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits);
        else
            trade_price = NormalizeDouble(当前_多单_挂单点距_非首单 * vPoint + SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits);

        if(多单订单数 == 0 || (多单最高价 != 0 && trade_price >= NormalizeDouble(当前_多单_修改挂单起始间距 * vPoint + 多单最高价, _Digits) && by_bo_10) ||
           (多单最低价 != 0 && trade_price <= NormalizeDouble(多单最低价 - 当前_多单_修改挂单起始间距 * vPoint, _Digits)))
        {
            if(多单订单数 == 0)
                trade_lot = 原始初始手数 * 开仓倍数;
            else
                trade_lot = NormalizeDouble(多单订单数 * by_do_19 + 原始初始手数 * MathPow(逆势加仓手数递增比例, 多单订单数), 开仓手数小数位) * 开仓倍数;

            if(((trade_lot < AccountInfoDouble(ACCOUNT_FREEMARGIN) / SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE) && 多单订单数 > 0) || by_bo_9))
            {
                if((sj == false || (sj == true && currentHour > star && currentHour < end)) && runEA == true)
                {
                    if(!isFirst || (isFirst && !isInDangerTime(dangerHours)))
                    {
                        double wantedPrice = trade_price;
                        trade_price = 修正新建挂单价格(ORDER_TYPE_BUY_STOP, trade_price);
                        if(MathAbs(trade_price - wantedPrice) >= vRawPoint * 0.5)
                            Print("【BuyStop合法性修正】目标价=", DoubleToString(wantedPrice,_Digits),
                                  " 修正价=", DoubleToString(trade_price,_Digits),
                                  " StopLevel参数点=", DoubleToString(市场当前StopLevel点距,0));

                        MqlTradeRequest request = {};
                        MqlTradeResult result = {};

                        request.action = TRADE_ACTION_PENDING;
                        request.symbol = _Symbol;
                        request.volume = trade_lot;
                        request.price = trade_price;
                        request.sl = 0;
                        request.tp = 0;
                        request.type = ORDER_TYPE_BUY_STOP;
                        request.magic = magicNumber;
                        request.comment = comm + ":" + magicNumber;

                        ResetLastError();
                        bool sent = OrderSend(request, result);
                        if(sent && 交易返回成功(result.retcode))
                            Print("【BuyStop成功】ticket=", result.order, " retcode=", result.retcode, " ", result.comment);
                        else
                            Print("【BuyStop失败】sent=", sent, " retcode=", result.retcode,
                                  " comment=", result.comment, " lastError=", GetLastError(),
                                  " price=", DoubleToString(trade_price,_Digits));
                    }
                }
            }
        }
    }

    // 空单开单逻辑
    if(空单_挂单订单数 == 0 && 空单累计利润 > by_do_17 && allow_sell)
    {
        double trade_price, trade_lot;
        bool isFirst = (空单订单数 == 0);

        if(isFirst)
            trade_price = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - _FirstStep * vPoint, _Digits);
        else
            trade_price = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - 当前_空单_挂单点距_非首单 * vPoint, _Digits);

        if(空单订单数 == 0 || (空单最低价 != 0 && trade_price <= NormalizeDouble(空单最低价 - 当前_空单_修改挂单起始间距 * vPoint, _Digits) && by_bo_10) ||
           (空单最高价 != 0 && trade_price >= NormalizeDouble(当前_空单_修改挂单起始间距 * vPoint + 空单最高价, _Digits)))
        {
            if(空单订单数 == 0)
                trade_lot = 原始初始手数 * 开仓倍数;
            else
                trade_lot = NormalizeDouble(空单订单数 * by_do_19 + 原始初始手数 * MathPow(逆势加仓手数递增比例, 空单订单数), 开仓手数小数位) * 开仓倍数;

            if(((trade_lot < AccountInfoDouble(ACCOUNT_FREEMARGIN) / SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE) && 空单订单数 > 0) || by_bo_9))
            {
                if((sj == false || (sj == true && currentHour > star && currentHour < end)) && runEA == true)
                {
                    if(!isFirst || (isFirst && !isInDangerTime(dangerHours)))
                    {
                        double wantedPrice = trade_price;
                        trade_price = 修正新建挂单价格(ORDER_TYPE_SELL_STOP, trade_price);
                        if(MathAbs(trade_price - wantedPrice) >= vRawPoint * 0.5)
                            Print("【SellStop合法性修正】目标价=", DoubleToString(wantedPrice,_Digits),
                                  " 修正价=", DoubleToString(trade_price,_Digits),
                                  " StopLevel参数点=", DoubleToString(市场当前StopLevel点距,0));

                        MqlTradeRequest request = {};
                        MqlTradeResult result = {};

                        request.action = TRADE_ACTION_PENDING;
                        request.symbol = _Symbol;
                        request.volume = trade_lot;
                        request.price = trade_price;
                        request.sl = 0;
                        request.tp = 0;
                        request.type = ORDER_TYPE_SELL_STOP;
                        request.magic = magicNumber;
                        request.comment = comm + ":" + magicNumber;

                        ResetLastError();
                        bool sent = OrderSend(request, result);
                        if(sent && 交易返回成功(result.retcode))
                            Print("【SellStop成功】ticket=", result.order, " retcode=", result.retcode, " ", result.comment);
                        else
                            Print("【SellStop失败】sent=", sent, " retcode=", result.retcode,
                                  " comment=", result.comment, " lastError=", GetLastError(),
                                  " price=", DoubleToString(trade_price,_Digits));
                    }
                }
            }
        }
    }

    // 移动挂单逻辑
    移动挂单订单();
    }
    else
    {
        // ST决定“新周期”方向:趋势方向可开首仓并顺势回调加仓;
        // 反趋势方向禁止从0开首仓,但已有旧仓允许按V1.07原始逆势规则继续恢复加仓。
        ProcessTrendPullbackAdd();
        ProcessInverseRecoveryAdd();
    }
}

//+------------------------------------------------------------------+
//| 统计持仓信息                                                     |
//+------------------------------------------------------------------+
void 统计持仓信息()
{
    多单累计利润 = 0; 空单累计利润 = 0;
    多单累计手数 = 0; 空单累计手数 = 0;
    多单订单数 = 0; 空单订单数 = 0;
    多单_挂单订单数 = 0; 空单_挂单订单数 = 0;
    多单最高价 = 0; 多单最低价 = 0;
    空单最高价 = 0; 空单最低价 = 0;
    多单持仓均价 = 0; 空单持仓均价 = 0;
    double 多单面值累计 = 0, 空单面值累计 = 0;

    // 统计持仓
    for(int i = 0; i < PositionsTotal(); i++)
    {
        if(PositionInfo.SelectByIndex(i))
        {
            if(PositionInfo.Symbol() == _Symbol && PositionInfo.Magic() == magicNumber)
            {
                double profit = PositionInfo.Profit() + PositionInfo.Swap() + PositionInfo.Commission();
                double volume = PositionInfo.Volume();
                double openPrice = PositionInfo.PriceOpen();

                if(PositionInfo.PositionType() == POSITION_TYPE_BUY)
                {
                    多单订单数++;
                    多单累计手数 += volume;
                    多单面值累计 += openPrice * volume;
                    多单累计利润 += profit;

                    if(多单最高价 < openPrice || 多单最高价 == 0)
                        多单最高价 = openPrice;
                    if(多单最低价 > openPrice || 多单最低价 == 0)
                        多单最低价 = openPrice;
                }
                else if(PositionInfo.PositionType() == POSITION_TYPE_SELL)
                {
                    空单订单数++;
                    空单累计手数 += volume;
                    空单面值累计 += openPrice * volume;
                    空单累计利润 += profit;

                    if(空单最低价 > openPrice || 空单最低价 == 0)
                        空单最低价 = openPrice;
                    if(空单最高价 < openPrice || 空单最高价 == 0)
                        空单最高价 = openPrice;
                }
            }
        }
    }

    // 统计挂单
    for(int i = 0; i < OrdersTotal(); i++)
    {
        if(OrderInfo.SelectByIndex(i))
        {
            if(OrderInfo.Symbol() == _Symbol && OrderInfo.Magic() == magicNumber)
            {
                double openPrice = OrderInfo.PriceOpen();

                if(OrderInfo.OrderType() == ORDER_TYPE_BUY_STOP)
                {
                    多单_挂单订单数++;
                    if(多单最高价 < openPrice || 多单最高价 == 0)
                        多单最高价 = openPrice;
                }
                else if(OrderInfo.OrderType() == ORDER_TYPE_SELL_STOP)
                {
                    空单_挂单订单数++;
                    if(空单最低价 > openPrice || 空单最低价 == 0)
                        空单最低价 = openPrice;
                }
            }
        }
    }

    // 计算持仓均价
    if(多单累计手数 > 0)
        多单持仓均价 = NormalizeDouble(多单面值累计 / 多单累计手数, _Digits);
    if(空单累计手数 > 0)
        空单持仓均价 = NormalizeDouble(空单面值累计 / 空单累计手数, _Digits);
}

//+------------------------------------------------------------------+
//| 平仓及删除订单                                                   |
//+------------------------------------------------------------------+
int 平仓及删除订单(int direction)
{
    int closed = 0;

    // 平仓持仓
    for(int i = PositionsTotal() - 1; i >= 0; i--)
    {
        if(PositionInfo.SelectByIndex(i))
        {
            if(PositionInfo.Symbol() == _Symbol && PositionInfo.Magic() == magicNumber)
            {
                bool shouldClose = false;

                if((direction == 1 || direction == 0) && PositionInfo.PositionType() == POSITION_TYPE_BUY)
                    shouldClose = true;
                else if((direction == -1 || direction == 0) && PositionInfo.PositionType() == POSITION_TYPE_SELL)
                    shouldClose = true;

                if(shouldClose)
                {
                    if(Trade.PositionClose(PositionInfo.Ticket()))
                        closed++;
                    else
                        Print("Close position failed: ", GetLastError());
                }
            }
        }
    }

    // 删除挂单
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
        if(OrderInfo.SelectByIndex(i))
        {
            if(OrderInfo.Symbol() == _Symbol && OrderInfo.Magic() == magicNumber)
            {
                bool shouldDelete = false;

                if((direction == 1 || direction == 0) && OrderInfo.OrderType() == ORDER_TYPE_BUY_STOP)
                    shouldDelete = true;
                else if((direction == -1 || direction == 0) && OrderInfo.OrderType() == ORDER_TYPE_SELL_STOP)
                    shouldDelete = true;

                if(shouldDelete)
                {
                    if(Trade.OrderDelete(OrderInfo.Ticket()))
                        closed++;
                    else
                        Print("Delete order failed: ", GetLastError());
                }
            }
        }
    }

    return closed;
}

//+------------------------------------------------------------------+
//| 是否当前时间是危险时间段                                         |
//+------------------------------------------------------------------+
bool isInDangerTime(string skipHours)
{
    int hour = GetCurrentHour();
    string sep = ",";
    ushort u_sep = StringGetCharacter(sep, 0);
    string skipHoursArr[];

    int k = StringSplit(skipHours, u_sep, skipHoursArr);
    if(k > 0)
    {
        for(int i = 0; i < ArraySize(skipHoursArr); i++)
        {
            if(hour == (int)StringToInteger(skipHoursArr))
                return true;
        }
    }
    return false;
}

//+------------------------------------------------------------------+
//| 移动挂单订单逻辑                                                 |
//+------------------------------------------------------------------+
void 移动挂单订单()
{
    // 遍历所有挂单
    for(int i = 0; i < OrdersTotal(); i++)
    {
        if(OrderInfo.SelectByIndex(i))
        {
            if(OrderInfo.Symbol() == _Symbol && OrderInfo.Magic() == magicNumber)
            {
                ulong orderTicket = OrderInfo.Ticket();
                double currentPrice = OrderInfo.PriceOpen();
                double newPrice = 0;
                bool shouldModify = false;

                if(OrderInfo.OrderType() == ORDER_TYPE_BUY_STOP && allow_buy)
                {
                    if(多单订单数 == 0)
                        newPrice = NormalizeDouble(_FirstStep * vPoint + SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits);
                    else
                        newPrice = NormalizeDouble(当前_多单_挂单点距_非首单 * vPoint + SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits);

                    if(NormalizeDouble(currentPrice - stop挂单改价间距 * vPoint, _Digits) > newPrice &&
                       (newPrice <= NormalizeDouble(多单最低价 - 当前_多单_修改挂单起始间距 * vPoint, _Digits) || 多单最低价 == 0 || by_bo_10) &&
                       (多单订单数 == 0 || newPrice >= NormalizeDouble(当前_多单_修改挂单起始间距 * vPoint + 多单最高价, _Digits) ||
                        newPrice <= NormalizeDouble(多单最低价 - 当前_多单_修改挂单起始间距 * vPoint, _Digits)))
                    {
                        shouldModify = true;
                    }
                }
                else if(OrderInfo.OrderType() == ORDER_TYPE_SELL_STOP && allow_sell)
                {
                    if(空单订单数 == 0)
                        newPrice = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - _FirstStep * vPoint, _Digits);
                    else
                        newPrice = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - 当前_空单_挂单点距_非首单 * vPoint, _Digits);

                    if(NormalizeDouble(currentPrice + stop挂单改价间距 * vPoint, _Digits) < newPrice &&
                       (newPrice >= NormalizeDouble(当前_空单_修改挂单起始间距 * vPoint + 空单最高价, _Digits) || 空单最高价 == 0 || by_bo_10) &&
                       (空单订单数 == 0 || newPrice <= NormalizeDouble(空单最低价 - 当前_空单_修改挂单起始间距 * vPoint, _Digits) ||
                        newPrice >= NormalizeDouble(当前_空单_修改挂单起始间距 * vPoint + 空单最高价, _Digits)))
                    {
                        shouldModify = true;
                    }
                }

                if(shouldModify && newPrice > 0)
                {
                    ENUM_ORDER_TYPE ordType = OrderInfo.OrderType();
                    double wantedPrice = newPrice;
                    double legalPrice = newPrice;
                    if(!准备修改挂单价格(ordType, currentPrice, wantedPrice, legalPrice))
                    {
                        Print("【挂单修改跳过】ticket=", orderTicket,
                              " 已进入FreezeLevel范围 Freeze参数点=", DoubleToString(市场当前FreezeLevel点距,0));
                        continue;
                    }
                    newPrice = legalPrice;
                    if(MathAbs(newPrice - wantedPrice) >= vRawPoint * 0.5)
                        Print("【挂单修改合法性修正】ticket=", orderTicket,
                              " 目标价=", DoubleToString(wantedPrice,_Digits),
                              " 修正价=", DoubleToString(newPrice,_Digits),
                              " StopLevel参数点=", DoubleToString(市场当前StopLevel点距,0));

                    MqlTradeRequest request = {};
                    MqlTradeResult result = {};

                    request.action = TRADE_ACTION_MODIFY;
                    request.order = orderTicket;
                    request.price = newPrice;
                    request.sl = OrderInfo.StopLoss();
                    request.tp = OrderInfo.TakeProfit();

                    ResetLastError();
                    bool sent = OrderSend(request, result);
                    if(sent && 交易返回成功(result.retcode))
                        Print("【挂单修改成功】ticket=", orderTicket, " newPrice=", DoubleToString(newPrice,_Digits),
                              " retcode=", result.retcode, " ", result.comment);
                    else
                        Print("【挂单修改失败】ticket=", orderTicket, " sent=", sent,
                              " retcode=", result.retcode, " comment=", result.comment,
                              " lastError=", GetLastError(), " price=", DoubleToString(newPrice,_Digits));
                }
            }
        }
    }
}

//+------------------------------------------------------------------+
//| 恒鑫风格图表主题                                                   |
//+------------------------------------------------------------------+
void 设置恒鑫图表主题()
{
    if(!启用恒鑫图表配色) return;
    ChartSetInteger(0, CHART_COLOR_BACKGROUND, C'6,6,8');
    ChartSetInteger(0, CHART_COLOR_FOREGROUND, C'212,175,55');
    ChartSetInteger(0, CHART_COLOR_GRID, C'26,22,12');
    ChartSetInteger(0, CHART_COLOR_CHART_LINE, C'212,175,55');
    ChartSetInteger(0, CHART_COLOR_CANDLE_BULL, C'46,204,113');
    ChartSetInteger(0, CHART_COLOR_CANDLE_BEAR, C'231,76,60');
    ChartSetInteger(0, CHART_COLOR_CHART_UP, C'46,204,113');
    ChartSetInteger(0, CHART_COLOR_CHART_DOWN, C'231,76,60');
    ChartSetInteger(0, CHART_COLOR_VOLUME, C'160,128,48');
    ChartSetInteger(0, CHART_COLOR_BID, C'231,76,60');
    ChartSetInteger(0, CHART_COLOR_ASK, C'46,204,113');
    ChartSetInteger(0, CHART_FOREGROUND, false);
}

void UI_Rect(string name,int x,int y,int w,int h,color bg,color border)
{
    if(ObjectFind(0,name)<0) ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,0,0,0);
    ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
    ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
    ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
    ObjectSetInteger(0,name,OBJPROP_XSIZE,w);
    ObjectSetInteger(0,name,OBJPROP_YSIZE,h);
    ObjectSetInteger(0,name,OBJPROP_BGCOLOR,bg);
    ObjectSetInteger(0,name,OBJPROP_COLOR,border);
    ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
    ObjectSetInteger(0,name,OBJPROP_BACK,false);
    ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
    ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
    ObjectSetInteger(0,name,OBJPROP_ZORDER,5);
}

void UI_Label(string name,int x,int y,string text,color clr,int size=10,string font="Microsoft YaHei")
{
    if(ObjectFind(0,name)<0)
    {
        ObjectCreate(0,name,OBJ_LABEL,0,0,0);
        ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
        ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
        ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
        ObjectSetInteger(0,name,OBJPROP_FONTSIZE,size);
        ObjectSetString(0,name,OBJPROP_FONT,font);
        ObjectSetInteger(0,name,OBJPROP_BACK,false);
        ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
        ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
        ObjectSetInteger(0,name,OBJPROP_ZORDER,20);
    }
    ObjectSetString(0,name,OBJPROP_TEXT,text);
    ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
}

void UI_Value(string name,int rightX,int y,string text,color clr,int size=10,string font="Consolas")
{
    if(ObjectFind(0,name)<0)
    {
        ObjectCreate(0,name,OBJ_LABEL,0,0,0);
        ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
        ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER);
        ObjectSetInteger(0,name,OBJPROP_XDISTANCE,rightX);
        ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
        ObjectSetInteger(0,name,OBJPROP_FONTSIZE,size);
        ObjectSetString(0,name,OBJPROP_FONT,font);
        ObjectSetInteger(0,name,OBJPROP_BACK,false);
        ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
        ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
        ObjectSetInteger(0,name,OBJPROP_ZORDER,20);
    }
    ObjectSetString(0,name,OBJPROP_TEXT,text);
    ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
}

void UI_Button(string name,string text,int x,int y,int w,int h,color bg,color fg)
{
    if(ObjectFind(0,name)<0) ObjectCreate(0,name,OBJ_BUTTON,0,0,0);
    ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
    ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
    ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
    ObjectSetInteger(0,name,OBJPROP_XSIZE,w);
    ObjectSetInteger(0,name,OBJPROP_YSIZE,h);
    ObjectSetInteger(0,name,OBJPROP_BGCOLOR,bg);
    ObjectSetInteger(0,name,OBJPROP_COLOR,fg);
    ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,C'90,100,115');
    ObjectSetInteger(0,name,OBJPROP_FONTSIZE,11);
    ObjectSetString(0,name,OBJPROP_FONT,"Microsoft YaHei");
    ObjectSetString(0,name,OBJPROP_TEXT,text);
    ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
    ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
    ObjectSetInteger(0,name,OBJPROP_ZORDER,30);
}

//+------------------------------------------------------------------+
//| ST界面仓位角色统计(只读,不参与交易计算)                        |
//+------------------------------------------------------------------+
void GetPositionRoleStats(double &buyLockLots,double &sellLockLots,
                          double &buyTrendLots,double &sellTrendLots,
                          double &buyOtherLots,double &sellOtherLots,
                          double &buyLockProfit,double &sellLockProfit,
                          double &buyTrendProfit,double &sellTrendProfit,
                          double &buyOtherProfit,double &sellOtherProfit)
{
    buyLockLots=sellLockLots=buyTrendLots=sellTrendLots=buyOtherLots=sellOtherLots=0.0;
    buyLockProfit=sellLockProfit=buyTrendProfit=sellTrendProfit=buyOtherProfit=sellOtherProfit=0.0;
    for(int i=0;i<ositionsTotal();i++)
    {
        if(!PositionInfo.SelectByIndex(i)) continue;
        if(PositionInfo.Symbol()!=_Symbol || PositionInfo.Magic()!=magicNumber) continue;
        double vol=PositionInfo.Volume();
        double pnl=PositionInfo.Profit()+PositionInfo.Swap()+PositionInfo.Commission();
        string c=PositionInfo.Comment();
        bool isLock=(StringFind(c,"MJ_RECOVERY")>=0);
        bool isTrend=(StringFind(c,"MJ_TREND")>=0);
        if(PositionInfo.PositionType()==POSITION_TYPE_BUY)
        {
            if(isLock){ buyLockLots+=vol; buyLockProfit+=pnl; }
            else if(isTrend){ buyTrendLots+=vol; buyTrendProfit+=pnl; }
            else { buyOtherLots+=vol; buyOtherProfit+=pnl; }
        }
        else if(PositionInfo.PositionType()==POSITION_TYPE_SELL)
        {
            if(isLock){ sellLockLots+=vol; sellLockProfit+=pnl; }
            else if(isTrend){ sellTrendLots+=vol; sellTrendProfit+=pnl; }
            else { sellOtherLots+=vol; sellOtherProfit+=pnl; }
        }
    }
}

//+------------------------------------------------------------------+
//| 展开式每日统计:券商服务器日期,最近7天                           |
//+------------------------------------------------------------------+
void DrawDailyStatsPanel(int x0,int y0,int mainW)
{
    if(!g_statsPanelOpen)
    {
        DeleteStatsPanelObjects();
        return;
    }

    const color cBG=C'13,17,23', cCard=C'20,26,34', cText=C'225,231,239', cSub=C'143,155,171';
    const color cGreen=C'46,204,113', cRed=C'231,76,60', cBlue=C'52,164,235', cOrange=C'240,173,78';
    string uiFont="Microsoft YaHei", mono="Consolas";
    int W=1290,H=350,x=x0+mainW+12,y=y0;

    UI_Rect(STAT_UI_PREFIX+"BG",x,y,W,H,cBG,C'76,86,101');
    UI_Rect(STAT_UI_PREFIX+"Top",x+1,y+1,W-2,3,cBlue,cBlue);
    UI_Label(STAT_UI_PREFIX+"Title",x+16,y+14,"统计 · 最近7天(券商时间)",cText,15,uiFont);
    UI_Label(STAT_UI_PREFIX+"Note",x+16,y+44,"成交类数据从历史重建;浮盈/浮亏及ST切换、逆势加仓次数按券商自然日记录",cSub,9,uiFont);

    string heads[12]={"日期","已实现","开仓手数","最大单笔","平仓","盈利/亏损","胜率","最大浮盈","最大浮亏","ST切换","逆势加仓","净值"};
    int widths[12]={76,100,96,92,72,105,72,100,100,82,72,108};
    int colX[12]; colX[0]=x+14;
    for(int i=1;i<12;i++) colX=colX[i-1]+widths[i-1];
    int hy=y+84;
    UI_Rect(STAT_UI_PREFIX+"Header",x+10,hy-7,W-20,28,C'24,30,39',C'24,30,39');
    for(int c=0;c<12;c++) UI_Label(STAT_UI_PREFIX+"H"+IntegerToString(c),colX[c],hy,heads[c],cSub,9,uiFont);

    datetime today=BrokerDayStart(TimeCurrent());
    for(int r=0;r<7;r++)
    {
        datetime d=today-r*86400;
        int yy=hy+34+r*31;
        if(r%2==1) UI_Rect(STAT_UI_PREFIX+"RowBG"+IntegerToString(r),x+10,yy-6,W-20,27,cCard,cCard);

        double pnl=0,lots=0,maxLot=0; int trades=0,wins=0,losses=0;
        CalcDailyHistoryStats(d,pnl,lots,maxLot,trades,wins,losses);
        string k=DailyStatPrefix(d);
        double maxF=GlobalVariableCheck(k+"maxFloat")?GlobalVariableGet(k+"maxFloat"):0.0;
        double minF=GlobalVariableCheck(k+"minFloat")?GlobalVariableGet(k+"minFloat"):0.0;
        double eq=GlobalVariableCheck(k+"lastEq")?GlobalVariableGet(k+"lastEq"):0.0;
        int stFlips=GlobalVariableCheck(k+"stFlips")?(int)GlobalVariableGet(k+"stFlips"):0;
        int recoveryAdds=GlobalVariableCheck(k+"recoveryCount")?(int)GlobalVariableGet(k+"recoveryCount"):0;
        if(d==g_statsDay)
        {
            pnl=g_todayRealizedProfit; lots=g_todayOpenedLots; maxLot=g_todayMaxEntryLot;
            trades=g_todayCloseTrades; wins=g_todayWins; losses=g_todayLosses;
            maxF=g_todayMaxFloatProfit; minF=g_todayMaxFloatLoss;
            stFlips=g_todaySTFlips; recoveryAdds=g_todayRecoveryAdds;
            eq=AccountInfoDouble(ACCOUNT_EQUITY);
        }
        int decided=wins+losses;
        double wr=(decided>0?100.0*wins/decided:0.0);
        MqlDateTime dd; TimeToStruct(d,dd);
        string vals[12];
        vals[0]=StringFormat("%02d.%02d",dd.mon,dd.day);
        vals[1]=DoubleToString(pnl,2);
        vals[2]=DoubleToString(lots,2);
        vals[3]=(maxLot>0?DoubleToString(maxLot,2):"-");
        vals[4]=IntegerToString(trades);
        vals[5]=IntegerToString(wins)+" / "+IntegerToString(losses);
        vals[6]=DoubleToString(wr,1)+"%";
        vals[7]=DoubleToString(maxF,2);
        vals[8]=DoubleToString(minF,2);
        vals[9]=IntegerToString(stFlips);
        vals[10]=IntegerToString(recoveryAdds);
        vals[11]=(eq>0?DoubleToString(eq,2):"-");

        for(int c=0;c<12;c++)
        {
            color cc=cText;
            if(c==1) cc=(pnl>=0?cGreen:cRed);
            else if(c==7) cc=cGreen;
            else if(c==8) cc=cRed;
            else if(c==9) cc=cBlue;
            else if(c==10) cc=cOrange;
            UI_Label(STAT_UI_PREFIX+"R"+IntegerToString(r)+"C"+IntegerToString(c),colX[c],yy,vals[c],cc,9,(c==0?uiFont:mono));
        }
    }
}

//+------------------------------------------------------------------+
//| 显示界面信息:恒鑫风格卡片面板                                    |
//+------------------------------------------------------------------+
void Information()
{
    if(!启用恒鑫风格界面)
    {
        DeleteStatsPanelObjects();
        ObjectsDeleteAll(0, UI_PREFIX);
        ObjectDelete(0,"Close button");
        Comment("");
        return;
    }

    const color cBG=C'13,17,23', cCard=C'20,26,34', cCard2=C'17,22,29';
    const color cBorder=C'58,67,80', cText=C'225,231,239', cSub=C'143,155,171';
    const color cBlue=C'52,164,235', cGreen=C'46,204,113', cRed=C'231,76,60', cOrange=C'240,173,78';
    string uiFont="Microsoft YaHei", mono="Consolas";

    int x0=界面X, y0=界面Y, W=MathMax(界面宽度,780), H=(启用ST顺势逆势恢复模式?684:520);
    int pad=12, gap=8, innerW=W-pad*2;
    int colW=(innerW-gap*2)/3;
    int x1=x0+pad, x2=x1+colW+gap, x3=x2+colW+gap;
    int ldx=14, line=22;

    UI_Rect(UI_BG,x0,y0,W,H,cBG,C'76,86,101');
    UI_Rect(UI_PREFIX+"TopLine",x0+1,y0+1,W-2,3,cBlue,cBlue);

    double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
    double bid=SymbolInfoDouble(_Symbol,SYMBOL_BID);
    double spreadPts=(vPoint>0 ? (ask-bid)/vPoint : 0.0);
    double totalProfit=多单累计利润+空单累计利润;
    double totalLots=多单累计手数+空单累计手数;
    int totalPos=多单订单数+空单订单数;
    int totalPending=多单_挂单订单数+空单_挂单订单数;
    double balance=AccountInfoDouble(ACCOUNT_BALANCE);
    double equity=AccountInfoDouble(ACCOUNT_EQUITY);

    double buyLockLots,sellLockLots,buyTrendLots,sellTrendLots,buyOtherLots,sellOtherLots;
    double buyLockProfit,sellLockProfit,buyTrendProfit,sellTrendProfit,buyOtherProfit,sellOtherProfit;
    GetPositionRoleStats(buyLockLots,sellLockLots,buyTrendLots,sellTrendLots,buyOtherLots,sellOtherLots,
                         buyLockProfit,sellLockProfit,buyTrendProfit,sellTrendProfit,buyOtherProfit,sellOtherProfit);

    int headerY=y0+13;
    UI_Label(UI_PREFIX+"Title",x1,headerY,"摸金校尉 · MT5 V1.14",cText,16,uiFont);
    UI_Label(UI_PREFIX+"Symbol",x0+270,headerY+2,_Symbol,cOrange,12,mono);
    UI_Label(UI_PREFIX+"Spread",x0+430,headerY+3,"Spread "+DoubleToString(spreadPts,1),cSub,10,mono);
    UI_Value(UI_PREFIX+"State",x0+W-170,headerY+2,(runEA?"● 运行中":"● 暂停"),(runEA?cGreen:cOrange),10,uiFont);
    UI_Button(UI_PREFIX+"StatsBtn","统计",x0+W-76,y0+9,62,30,C'48,56,68',clrWhite);

    // ===== 第一排:BUY / SELL / BASKET =====
    int row1Y=y0+52, row1H=142, vy=row1Y+43;
    UI_Rect(UI_PREFIX+"BuyCard",x1,row1Y,colW,row1H,cCard,cBorder);
    UI_Rect(UI_PREFIX+"SellCard",x2,row1Y,colW,row1H,cCard,cBorder);
    UI_Rect(UI_PREFIX+"TotalCard",x3,row1Y,colW,row1H,cCard2,cBorder);

    UI_Label(UI_PREFIX+"BuyH",x1+ldx,row1Y+12,"多单 BUY",cGreen,13,uiFont);
    UI_Label(UI_PREFIX+"BuyL1",x1+ldx,vy,"持仓 / 手数",cSub,10,uiFont); UI_Value(UI_PREFIX+"BuyV1",x1+colW-14,vy,IntegerToString(多单订单数)+" / "+DoubleToString(多单累计手数,2),cText,10,mono);
    UI_Label(UI_PREFIX+"BuyL2",x1+ldx,vy+line,"趋势仓",cSub,10,uiFont); UI_Value(UI_PREFIX+"BuyV2",x1+colW-14,vy+line,DoubleToString(buyTrendLots,2)+" Lot",cGreen,10,mono);
    UI_Label(UI_PREFIX+"BuyL3",x1+ldx,vy+line*2,"逆势加仓",cSub,10,uiFont); UI_Value(UI_PREFIX+"BuyV3",x1+colW-14,vy+line*2,DoubleToString(buyLockLots,2)+" Lot",cOrange,10,mono);
    UI_Label(UI_PREFIX+"BuyL4",x1+ldx,vy+line*3,"浮盈",cSub,10,uiFont); UI_Value(UI_PREFIX+"BuyV4",x1+colW-14,vy+line*3,"$"+DoubleToString(多单累计利润,2),(多单累计利润>=0?cGreen:cRed),11,mono);

    UI_Label(UI_PREFIX+"SellH",x2+ldx,row1Y+12,"空单 SELL",cRed,13,uiFont);
    UI_Label(UI_PREFIX+"SellL1",x2+ldx,vy,"持仓 / 手数",cSub,10,uiFont); UI_Value(UI_PREFIX+"SellV1",x2+colW-14,vy,IntegerToString(空单订单数)+" / "+DoubleToString(空单累计手数,2),cText,10,mono);
    UI_Label(UI_PREFIX+"SellL2",x2+ldx,vy+line,"趋势仓",cSub,10,uiFont); UI_Value(UI_PREFIX+"SellV2",x2+colW-14,vy+line,DoubleToString(sellTrendLots,2)+" Lot",cRed,10,mono);
    UI_Label(UI_PREFIX+"SellL3",x2+ldx,vy+line*2,"逆势加仓",cSub,10,uiFont); UI_Value(UI_PREFIX+"SellV3",x2+colW-14,vy+line*2,DoubleToString(sellLockLots,2)+" Lot",cOrange,10,mono);
    UI_Label(UI_PREFIX+"SellL4",x2+ldx,vy+line*3,"浮盈",cSub,10,uiFont); UI_Value(UI_PREFIX+"SellV4",x2+colW-14,vy+line*3,"$"+DoubleToString(空单累计利润,2),(空单累计利润>=0?cGreen:cRed),11,mono);

    double netLots=多单累计手数-空单累计手数;
    string netText=(MathAbs(netLots)<0.000001?"0.00"netLots>0?"BUY ":"SELL ")+DoubleToString(MathAbs(netLots),2));
    double basketTarget=(启用ST顺势逆势恢复模式?ST篮子止盈金额:双边止盈金额)*开仓倍数;
    double remain=basketTarget-totalProfit;
    UI_Label(UI_PREFIX+"TotalH",x3+ldx,row1Y+12,"BASKET 一篮子",cText,13,uiFont);
    UI_Label(UI_PREFIX+"TotalL1",x3+ldx,vy,"仓位 / 挂单",cSub,10,uiFont); UI_Value(UI_PREFIX+"TotalV1",x3+colW-14,vy,IntegerToString(totalPos)+" / "+IntegerToString(totalPending),cText,10,mono);
    UI_Label(UI_PREFIX+"TotalL2",x3+ldx,vy+line,"当前盈亏",cSub,10,uiFont); UI_Value(UI_PREFIX+"TotalV2",x3+colW-14,vy+line,"$"+DoubleToString(totalProfit,2),(totalProfit>=0?cGreen:cRed),11,mono);
    UI_Label(UI_PREFIX+"TotalL3",x3+ldx,vy+line*2,"净敞口",cSub,10,uiFont); UI_Value(UI_PREFIX+"TotalV3",x3+colW-14,vy+line,netText,(netLots>=0?cGreen:cRed),10,mono);
    UI_Label(UI_PREFIX+"TotalL4",x3+ldx,vy+line*3,"距止盈",cSub,10,uiFont); UI_Value(UI_PREFIX+"TotalV4",x3+colW-14,vy+line*3,"$"+DoubleToString(remain,2),(remain<=0?cGreen:cOrange),10,mono);

    // ===== 第二排:账户 / 趋势 / 锁仓 =====
    int row2Y=row1Y+row1H+gap, row2H=(启用ST顺势逆势恢复模式?164:126), rvy=row2Y+43;
    UI_Rect(UI_PREFIX+"AccCard",x1,row2Y,colW,row2H,cCard,cBorder);
    UI_Rect(UI_PREFIX+"TrendCard",x2,row2Y,colW,row2H,cCard,cBorder);
    UI_Rect(UI_PREFIX+"LockCard",x3,row2Y,colW,row2H,cCard,cBorder);

    UI_Label(UI_PREFIX+"AccH",x1+ldx,row2Y+12,"账户 / 今日",cText,13,uiFont);
    UI_Label(UI_PREFIX+"AccL1",x1+ldx,rvy,"余额",cSub,10,uiFont); UI_Value(UI_PREFIX+"AccV1",x1+colW-14,rvy,"$"+DoubleToString(balance,2),cText,10,mono);
    UI_Label(UI_PREFIX+"AccL2",x1+ldx,rvy+line,"净值",cSub,10,uiFont); UI_Value(UI_PREFIX+"AccV2",x1+colW-14,rvy+line,"$"+DoubleToString(equity,2),cText,10,mono);
    UI_Label(UI_PREFIX+"AccL3",x1+ldx,rvy+line*2,"今日利润",cSub,10,uiFont); UI_Value(UI_PREFIX+"AccV3",x1+colW-14,rvy+line*2,"$"+DoubleToString(g_todayRealizedProfit,2),(g_todayRealizedProfit>=0?cGreen:cRed),10,mono);
    UI_Label(UI_PREFIX+"AccL4",x1+ldx,rvy+line*3,"当前浮亏",cSub,10,uiFont); UI_Value(UI_PREFIX+"AccV4",x1+colW-14,rvy+line*3,"$"+DoubleToString((totalProfit<0?totalProfit:0.0),2),(totalProfit<0?cRed:cSub),10,mono);
    UI_Label(UI_PREFIX+"AccL5",x1+ldx,rvy+line*4,"启动最大浮亏",cSub,10,uiFont); UI_Value(UI_PREFIX+"AccV5",x1+colW-14,rvy+line*4,"$"+DoubleToString(g_sessionMaxFloatLoss,2),cRed,9,mono);

    string stText=(g_stDir==1?"多头 ↑"g_stDir==-1?"空头 ↓":"未确认"));
    string allowText=(g_stDir==1?"只加 BUY"g_stDir==-1?"只加 SELL":"暂停新增"));
    int trendLayers=(g_stDir==0?0:CountTrendPositions(g_stDir));
    UI_Label(UI_PREFIX+"TrendH",x2+ldx,row2Y+12,"趋势状态",cText,13,uiFont);
    UI_Label(UI_PREFIX+"TrendL1",x2+ldx,rvy,"Supertrend",cSub,10,uiFont); UI_Value(UI_PREFIX+"TrendV1",x2+colW-14,rvy,stText,(g_stDir==1?cGreeng_stDir==-1?cRed:cSub)),10,uiFont);
    UI_Label(UI_PREFIX+"TrendL2",x2+ldx,rvy+line,"允许新增",cSub,10,uiFont); UI_Value(UI_PREFIX+"TrendV2",x2+colW-14,rvy+line,allowText,(g_stDir==1?cGreeng_stDir==-1?cRed:cSub)),10,uiFont);
    UI_Label(UI_PREFIX+"TrendL3",x2+ldx,rvy+line*2,"趋势层数",cSub,10,uiFont); UI_Value(UI_PREFIX+"TrendV3",x2+colW-14,rvy+line*2,IntegerToString(trendLayers)+" / "+IntegerToString(顺势最大层数),cText,10,mono);
    UI_Label(UI_PREFIX+"TrendL4",x2+ldx,rvy+line*3,"今日ST切换",cSub,10,uiFont); UI_Value(UI_PREFIX+"TrendV4",x2+colW-14,rvy+line*3,IntegerToString(g_todaySTFlips)+" 次",cBlue,10,mono);
    UI_Label(UI_PREFIX+"TrendL5",x2+ldx,rvy+line*4,"ST数值",cSub,10,uiFont); UI_Value(UI_PREFIX+"TrendV5",x2+colW-14,rvy+line*4,(g_stValue>0?DoubleToString(g_stValue,_Digits):"-"),cSub,9,mono);

    double oldLots=0,oldProfit=0,recoveryLots=0,recoveryProfit=0;
    string oldSide="-";
    int recoveryDir=0;
    if(g_stDir==1)
    {
        oldSide="SELL"; oldLots=空单累计手数; oldProfit=空单累计利润; recoveryDir=-1;
        recoveryLots=sellLockLots; recoveryProfit=sellLockProfit;
    }
    else if(g_stDir==-1)
    {
        oldSide="BUY"; oldLots=多单累计手数; oldProfit=多单累计利润; recoveryDir=1;
        recoveryLots=buyLockLots; recoveryProfit=buyLockProfit;
    }
    bool hasOld=(oldLots>0.0);
    bool recPending=(recoveryDir!=0 && HasRecoveryPending(recoveryDir));
    double nextDist=0.0, nextModifyDist=0.0;
    if(recoveryDir!=0) GetRecoveryDistances(nextDist,nextModifyDist);
    UI_Label(UI_PREFIX+"LockH",x3+ldx,row2Y+12,"逆势恢复",cText,13,uiFont);
    UI_Label(UI_PREFIX+"LockL1",x3+ldx,rvy,"旧方向",cSub,10,uiFont); UI_Value(UI_PREFIX+"LockV1",x3+colW-14,rvy,(hasOld?oldSide+" "+DoubleToString(oldLots,2):"无旧仓"),(oldSide=="BUY"?cGreenoldSide=="SELL"?cRed:cSub)),10,mono);
    UI_Label(UI_PREFIX+"LockL2",x3+ldx,rvy+line,"恢复挂单",cSub,10,uiFont); UI_Value(UI_PREFIX+"LockV2",x3+colW-14,rvy+line,(recPending?"等待成交":"未挂"),(recPending?cOrange:cSub),10,uiFont);
    UI_Label(UI_PREFIX+"LockL3",x3+ldx,rvy+line*2,"基准 / 层级",cSub,10,uiFont); UI_Value(UI_PREFIX+"LockV3",x3+colW-14,rvy+line*2,DoubleToString(g_recoveryBaseLots,2)+" / "+IntegerToString(g_recoveryAddLevel),cOrange,10,mono);
    UI_Label(UI_PREFIX+"LockL4",x3+ldx,rvy+line*3,"下一档距离",cSub,10,uiFont); UI_Value(UI_PREFIX+"LockV4",x3+colW-14,rvy+line*3,DoubleToString(nextDist,0)+" pt",cText,10,mono);
    UI_Label(UI_PREFIX+"LockL5",x3+ldx,rvy+line*4,"逆势组盈亏",cSub,9,uiFont); UI_Value(UI_PREFIX+"LockV5",x3+colW-14,rvy+line*4,"$"+DoubleToString(oldProfit,2),(oldProfit>=0?cGreen:cRed),9,mono);

    if(启用ST顺势逆势恢复模式)
    {
        // ===== 第三排:顺势加仓状态 =====
        int addY=row2Y+row2H+gap, addH=92;
        UI_Rect(UI_PREFIX+"AddCard",x1,addY,innerW,addH,cCard2,cBorder);
        string addDir=(g_stDir==1?"BUY"g_stDir==-1?"SELL":"-"));
        string addState="等待趋势确认";
        double extreme=0.0, pullPts=0.0, recoverPts=0.0;
        if(g_stDir==1)
        {
            extreme=g_trendExtremeBuy;
            pullPts=(extreme>0&&bid<extreme?(extreme-bid)/vPoint:0.0);
            recoverPts=(g_buyPullbackArmed&&g_buyPullbackLow>0?(bid-g_buyPullbackLow)/vPoint:0.0);
            if(trendLayers<=0)
                addState=(g_trendRestartBlockBarBuy>0 && iTime(_Symbol,_Period,0)<=g_trendRestartBlockBarBuy ? "等待下一根K线重启" : "等待首多挂单/成交");
            else if(trendLayers>=顺势最大层数) addState="趋势层数已满";
            else if(g_buyPullbackArmed) addState="等待反弹确认";
            else addState="等待回调";
        }
        else if(g_stDir==-1)
        {
            extreme=g_trendExtremeSell;
            pullPts=(extreme>0&&ask>extreme?(ask-extreme)/vPoint:0.0);
            recoverPts=(g_sellPullbackArmed&&g_sellPullbackHigh>0?(g_sellPullbackHigh-ask)/vPoint:0.0);
            if(trendLayers<=0)
                addState=(g_trendRestartBlockBarSell>0 && iTime(_Symbol,_Period,0)<=g_trendRestartBlockBarSell ? "等待下一根K线重启" : "等待首空挂单/成交");
            else if(trendLayers>=顺势最大层数) addState="趋势层数已满";
            else if(g_sellPullbackArmed) addState="等待回落确认";
            else addState="等待反弹";
        }
        double lastSamePrice=0.0, currentGap=0.0;
        datetime lastSameTime=0; ulong lastSameTicket=0;
        bool hasLastSame=(g_stDir!=0 && GetLastSameDirectionEntry(g_stDir,lastSamePrice,lastSameTime,lastSameTicket));
        double spacingCandidate=(g_stDir==1?askg_stDir==-1?bid:0.0));
        if(hasLastSame && spacingCandidate>0.0) currentGap=MathAbs(spacingCandidate-lastSamePrice);
        double currentMinGap=(hasLastSame?MinSameDirectionSpacingPrice(lastSamePrice):0.0);
        bool spacingOK=(!启用最小同向加仓间距 || !hasLastSame || currentGap>=currentMinGap);

        double pullNeedPrice=TrendPullbackDistancePrice(g_stDir,(g_stDir==1?bid:ask));
        double confirmNeedPrice=TrendRecoveryConfirmDistancePrice(g_stDir,(g_stDir==1?bid:ask));
        double pullNowPrice=(g_stDir==1?(extreme>0&&bid<extreme?extreme-bid:0.0):(g_stDir==-1?(extreme>0&&ask>extreme?ask-extreme:0.0):0.0));
        double confirmNowPrice=(g_stDir==1?(g_buyPullbackArmed&&g_buyPullbackLow>0?bid-g_buyPullbackLow:0.0):(g_stDir==-1?(g_sellPullbackArmed&&g_sellPullbackHigh>0?g_sellPullbackHigh-ask:0.0):0.0));
        if(trendLayers>0 && !spacingOK && (g_buyPullbackArmed || g_sellPullbackArmed))
            addState="等待最小价格间距";

        UI_Label(UI_PREFIX+"AddH",x1+14,addY+10,"顺势加仓状态",cText,13,uiFont);
        UI_Label(UI_PREFIX+"AddDir",x1+160,addY+12,"方向  "+addDir,(g_stDir==1?cGreen:(g_stDir==-1?cRed:cSub)),10,mono);
        UI_Label(UI_PREFIX+"AddState",x1+300,addY+12,"状态  "+addState,cOrange,10,uiFont);
        UI_Label(UI_PREFIX+"AddExtreme",x1+14,addY+42,"趋势极值  "+(extreme>0?DoubleToString(extreme,_Digits):"-"),cSub,10,mono);
        string pullTxt=(顺势距离计算模式==ST_DISTANCE_PERCENT ? DoubleToString(pullNowPrice,2)+" / "+DoubleToString(pullNeedPrice,2)+" ("+DoubleToString(顺势回调百分比,3)+"%)" : DoubleToString(pullPts,0)+" / "+DoubleToString(固定顺势回调距离,0)+" pt");
        string confirmTxt=(顺势距离计算模式==ST_DISTANCE_PERCENT ? DoubleToString(confirmNowPrice,2)+" / "+DoubleToString(confirmNeedPrice,2)+" ("+DoubleToString(顺势恢复确认百分比,3)+"%)" : DoubleToString(recoverPts,0)+" / "+DoubleToString(固定顺势恢复确认距离,0)+" pt");
        UI_Label(UI_PREFIX+"AddPull",x1+245,addY+42,"回调  "+pullTxt,cText,9,mono);
        UI_Label(UI_PREFIX+"AddConfirm",x1+470,addY+42,"恢复  "+confirmTxt,cText,9,mono);
        UI_Label(UI_PREFIX+"AddToday",x1+14,addY+66,"今日逆势加仓  "+IntegerToString(g_todayRecoveryAdds)+" 次",cOrange,9,mono);
        UI_Label(UI_PREFIX+"AddInterval",x1+245,addY+66,"时间间隔  "+IntegerToString(顺势最小加仓间隔秒)+" 秒",cSub,9,mono);
        string gapTxt=(hasLastSame?DoubleToString(currentGap,2)+" / "+DoubleToString(currentMinGap,2)+(顺势距离计算模式==ST_DISTANCE_PERCENT?" ("+DoubleToString(最小同向加仓间距百分比,3)+"%)":""):"首仓不限制");
        UI_Label(UI_PREFIX+"AddGap",x1+470,addY+66,"价格间距  "+gapTxt,(spacingOK?cGreen:cOrange),9,mono);

        // ===== 第四排:均价 / Basket目标 =====
        int priceY=addY+addH+gap, priceH=72;
        UI_Rect(UI_PREFIX+"riceBar",x1,priceY,innerW,priceH,cCard2,cBorder);
        string buyAvgTxt=(多单累计手数>0.0?DoubleToString(多单持仓均价,_Digits):"-");
        string sellAvgTxt=(空单累计手数>0.0?DoubleToString(空单持仓均价,_Digits):"-");
        string basketTpTxt=(g_basketTPPrice>0?DoubleToString(g_basketTPPrice,_Digits):"无唯一价格");
        UI_Label(UI_PREFIX+"riceTitle",x1+14,priceY+8,"均价 / Basket退出",cText,12,uiFont);
        UI_Label(UI_PREFIX+"BuyAvg",x1+14,priceY+35,"多均价  "+buyAvgTxt,cBlue,10,mono);
        UI_Label(UI_PREFIX+"SellAvg",x1+220,priceY+35,"空均价  "+sellAvgTxt,cOrange,10,mono);
        UI_Label(UI_PREFIX+"BasketTP",x1+430,priceY+35,"Basket TP价  "+basketTpTxt,cGreen,10,mono);
        UI_Value(UI_PREFIX+"TPMode",x1+innerW-14,priceY+8,"目标 $"+DoubleToString(basketTarget,2),cSub,9,mono);

        int statusY=priceY+priceH+gap, statusH=50;
        UI_Rect(UI_PREFIX+"StatusBar",x1,statusY,innerW,statusH,cCard2,cBorder);
        string status=(顺势距离计算模式==ST_DISTANCE_PERCENT
                       ? "ST顺势 + 逆势快照恢复 | 百分比距离 回调 "+DoubleToString(顺势回调百分比,3)+"% | 恢复 "+DoubleToString(顺势恢复确认百分比,3)+"% | 硬间距 "+DoubleToString(最小同向加仓间距百分比,3)+"% | V:fanfans555"
                       : "ST顺势 + 逆势快照恢复 | 固定距离 回调 "+DoubleToString(固定顺势回调距离,0)+"pt | 恢复 "+DoubleToString(固定顺势恢复确认距离,0)+"pt | 硬间距 "+DoubleToString(固定最小同向加仓价格间距,2)+" | V:fanfans555");
        UI_Label(UI_PREFIX+"Bottom",x1+14,statusY+16,status,cSub,9,uiFont);
        if(手动全平) UI_Button("Close button","全部平仓",x0+W-134,statusY+8,120,34,C'132,41,45',clrWhite);
    }
    else
    {
        // 原V1.07模式保持紧凑显示
        int priceY=row2Y+row2H+gap, priceH=68;
        UI_Rect(UI_PREFIX+"riceBar",x1,priceY,innerW,priceH,cCard2,cBorder);
        string buyAvgTxt=(多单累计手数>0.0?DoubleToString(多单持仓均价,_Digits):"-");
        string sellAvgTxt=(空单累计手数>0.0?DoubleToString(空单持仓均价,_Digits):"-");
        UI_Label(UI_PREFIX+"riceTitle",x1+14,priceY+8,"持仓均价 / 当前止盈位置",cText,11,uiFont);
        UI_Label(UI_PREFIX+"BuyAvg",x1+14,priceY+36,"多均价  "+buyAvgTxt,cBlue,10,mono);
        UI_Label(UI_PREFIX+"SellAvg",x1+220,priceY+36,"空均价  "+sellAvgTxt,cOrange,10,mono);
        int statusY=priceY+priceH+gap;
        UI_Rect(UI_PREFIX+"StatusBar",x1,statusY,innerW,50,cCard2,cBorder);
        UI_Label(UI_PREFIX+"Bottom",x1+14,statusY+16,"V1.07原始逆势模式 | V:fanfans555",cSub,9,uiFont);
        if(手动全平) UI_Button("Close button","全部平仓",x0+W-134,statusY+8,120,34,C'132,41,45',clrWhite);
    }

    DrawDailyStatsPanel(x0,y0,W);
    Comment("");
}

//+------------------------------------------------------------------+
//| 创建按钮(保留兼容旧调用)                                        |
//+------------------------------------------------------------------+
void sub_CreateButton(string obj_name, string object_text, int object_x_distance, int object_y_distance,
                     color object_BGcolor, color object_OPcolor, int object_corner_value, int object_XSIZE, int object_YSIZE)
{
    UI_Button(obj_name,object_text,object_x_distance,object_y_distance,object_XSIZE,object_YSIZE,object_BGcolor,object_OPcolor);
}

//+------------------------------------------------------------------+
//| 图表事件处理                                                     |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
    if(id == CHARTEVENT_OBJECT_CLICK && sparam == UI_PREFIX+"StatsBtn")
    {
        g_statsPanelOpen=!g_statsPanelOpen;
        if(!g_statsPanelOpen) DeleteStatsPanelObjects();
        ObjectSetInteger(0,sparam,OBJPROP_STATE,false);
        Information();
        ChartRedraw(0);
        return;
    }

    if(id == CHARTEVENT_OBJECT_CLICK && sparam == "Close button")
    {
        runEA = false;
        ExitAllTrades(clrCrimson, "手动平仓");
        if(CountTrades("buy") == 0 && CountTrades("sell") == 0)
        {
            runEA = true;
            Print("【手动全平】", Symbol(), " 全部持仓及挂单已处理");
        }
        ObjectSetInteger(0,"Close button",OBJPROP_STATE,false);
        Information();
        ChartRedraw(0);
    }
}

//+------------------------------------------------------------------+
//| 平仓所有订单,且删除挂单                                         |
//+------------------------------------------------------------------+
void ExitAllTrades(color Color, string reason)
{
    // 平仓所有持仓
    for(int i = PositionsTotal() - 1; i >= 0; i--)
    {
        if(PositionInfo.SelectByIndex(i))
        {
            if(PositionInfo.Symbol() == _Symbol && PositionInfo.Magic() == magicNumber)
            {
                Trade.PositionClose(PositionInfo.Ticket());
            }
        }
    }

    // 删除所有挂单
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
        if(OrderInfo.SelectByIndex(i))
        {
            if(OrderInfo.Symbol() == _Symbol && OrderInfo.Magic() == magicNumber)
            {
                Trade.OrderDelete(OrderInfo.Ticket());
            }
        }
    }

    Print("Closed all positions because ", reason);
}

//+------------------------------------------------------------------+
//| 计算当前订单数                                                   |
//+------------------------------------------------------------------+
int CountTrades(string type)
{
    int count = 0;

    if(type == "buy")
    {
        for(int i = 0; i < PositionsTotal(); i++)
        {
            if(PositionInfo.SelectByIndex(i))
            {
                if(PositionInfo.Symbol() == _Symbol && PositionInfo.Magic() == magicNumber &&
                   PositionInfo.PositionType() == POSITION_TYPE_BUY)
                    count++;
            }
        }
    }
    else if(type == "sell")
    {
        for(int i = 0; i < PositionsTotal(); i++)
        {
            if(PositionInfo.SelectByIndex(i))
            {
                if(PositionInfo.Symbol() == _Symbol && PositionInfo.Magic() == magicNumber &&
                   PositionInfo.PositionType() == POSITION_TYPE_SELL)
                    count++;
            }
        }
    }

    return count;
}

//+------------------------------------------------------------------+
//| 亏损达到锁仓                                                     |
//+------------------------------------------------------------------+
void 亏损达到锁仓()
{
    double currentProfit = 多单累计利润 + 空单累计利润;
    if(currentProfit <= 浮亏锁仓金额)
    {
        runEA = false;
        runEAG = true;
        Alert(Symbol() + "锁仓停止交易");
    }
}

//+------------------------------------------------------------------+
//| 解锁                                                             |
//+------------------------------------------------------------------+
void suo()
{
    double currentProfit = 多单累计利润 + 空单累计利润;
    if(runEAG)
    {
        if(currentProfit >= 0)
        {
            ExitAllTrades(clrRed, "解锁成功继续交易!");
            runEA = true;
            runEAG = false;
        }
        else if(currentProfit < 止损金额)
        {
            ExitAllTrades(clrRed, "触发止损!");
            runEA = true;
            runEAG = false;
        }
    }
}
//+------------------------------------------------------------------+


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

本版积分规则

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

GMT+8, 2026-9-9 01:06 , Processed in 0.179355 second(s), 5 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

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