跳至主要內容
金融世界

MT5-分享交易程式碼(空單)

250 分鐘0 次瀏覽
MT5-分享交易程式碼(空單)

#property copyright "V16"

#property version "1.08"

#property description "M10808 short: locked winner of 30-experiment batch (layers2 + block h23/Fri + ADX>=20)"

#property strict

#include <Trade/Trade.mqh>

#include <Trade/PositionInfo.mqh>

#define FIXED_LOTS 0.01

#define MAX_EXIT_LOG 32

#define ORDER_COMMENT_MAX 31

#define STAIR_LEVELS 4

input group "Identity"

input long InpMagic = 716045;

input string InpComment = "M10808";

input group "Experiment"

//--- M10808:實驗29配置已永久鎖定於程式內(ApplyExperiment強制id=29),

//--- 此輸入僅保留介面相容性,實際被忽略,杜絕任何set檔污染的可能。

input int InpExperimentId = 29;

//--- V1615校正:1607.html獲勝配置=50,舊原始碼預設400形同虛設

input int InpMaxSpreadPoints = 50;

input group "ConfigGuard"

//--- 參數防呆鎖:false(預設)=底盤13項關鍵參數一律鎖定為基準值運行,

//--- 測試器沿用的舊值不影響實際運行,只在日誌回報矯正清單。

//--- 實驗槓桿(InpMaxGridLayers、TimeFilter群組)不受鎖定,可自由掃描。

input bool InpAllowNonBaseline = false;

input group "TimeFilter"

//--- 進場時段濾網:只擋「開新籃」,加碼與所有出場邏輯完全不受影響。

input bool InpUseEntryTimeFilter = true;

//--- 封鎖開新籃的broker小時(逗號分隔,例如"23"或"15,17,23")。

//--- 預設"23":1607歸因中23點進場4籃平均-28.59、勝率25%(換日結算點差擴大)。

input string InpBlockEntryHours = "23";

//--- 封鎖週五開新籃:1607歸因中週五進場62籃勝率僅41.9%、合計-64.75(週末跳空風險)。

input bool InpBlockFridayEntry = true;

input group "Logging"

input bool InpEnableHeartbeat = true;

input int InpHbRetainHours = 24;

input bool InpEnableTradeLog = true;

input int InpTradeRetainDays = 7;

input group "Infra"

input int InpOrderRetryMax = 3;

input int InpOrderRetryMs = 400;

input bool InpBlockNewOnDisconnect = true;

input group "Grid"

//--- V1614核心改動:1607自身379個籃子的分層統計顯示,勝率隨層數單調崩壞

//--- (L1=49.2%/+1180、L2=45.8%/-254、L3=21.7%/-458、L4=0%/-105),

//--- 全部淨利都來自「從不加碼」的籃子。上限從6砍到2:保留一次攤平的機會

//--- (L2勝率45.8%尚可),砍掉統計上已死的深層地帶(L3+合計-563、勝率約20%)。

input int InpMaxGridLayers = 2;

input double InpGridStepFrac = 0.0015;

input double InpSlFrac = 0.985;

input int InpCoolBars = 3;

input group "HTF"

input ENUM_TIMEFRAMES InpHtf = PERIOD_H1;

input int InpHtfEmaFast = 50;

input int InpHtfEmaSlow = 200;

input bool InpExitOnHtfBull = true;

input bool InpBlockAddOnBull = true;

// HTF bull flatten only if basket profitPct <= this (0=only when flat/red). Protects green runners for stair/spike.

input double InpHtfExitMaxProfitPct = 0.0;

input group "EarlyFlatten"

input ENUM_TIMEFRAMES InpDumpTf = PERIOD_H1;

input bool InpUseFloatExit = true;

input double InpFloatLossPct = 1.10;

input bool InpUseAtrSpikeExit = true;

input int InpAtrPeriod = 14;

input double InpAtrSpikeMult = 2.20;

input int InpAtrSmaPeriod = 20;

input bool InpUseBullBarsExit = false;

input int InpBullBarsNeed = 3;

input bool InpEarlyExitOnlyIfLayers = true;

//--- V1614配套改動:層數上限=2時,count永遠到不了3,原值3會讓

//--- 浮虧/ATR早退「無聲失效」(1610的隱性耦合教訓)。改成2以維持原有保護。

input int InpEarlyExitMinLayers = 2;

input bool InpPauseAddOnAtrSpike = true;

input group "Pause"

input bool InpUseExitPause = true;

input int InpPauseHours = 36;

input group "HighDD"

input bool InpUseHighDrawdownStop = true;

input ENUM_TIMEFRAMES InpHighTf = PERIOD_H4;

input int InpHighLookback = 60;

input double InpHighDrawdownPct = 6.0;

input bool InpFlattenOnHighDD = false;

input group "DailyVeto"

input bool InpUseDailyVeto = true;

input int InpDailyEmaPeriod = 50;

input group "Fuse"

input bool InpUseExitFuse = true;

input int InpFuseWindowDays = 5;

input int InpFuseMaxEarly = 5;

input bool InpFuseNeedsH1BearClear = true;

input group "LossHalt"

input bool InpUseDailyLossHalt = true;

input double InpDailyLossLimitAmt = 300.0;

input bool InpUseWeeklyLossHalt = false;

input double InpWeeklyLossLimitAmt = 800.0;

input group "MarginMagicDD"

input bool InpUseMarginCheck = true;

input double InpMarginSafetyMult = 1.2;

input bool InpUseAccountDdHalt = true;

input double InpAccountDdPct = 30.0;

input int InpMagicDdScanSec = 60;

input group "MultiATR"

input bool InpUseMultiAtrStep = true;

input int InpDynAtrPeriod = 14;

input double InpAtrMultM1 = 8.0;

input double InpAtrMultM5 = 3.5;

input double InpAtrMultM15 = 2.0;

input double InpAtrMultM30 = 1.5;

input double InpAtrMultH1 = 1.0;

input group "Entry"

//--- V1615校正:1607.html獲勝配置=9(當時是在測試器介面手動設定,原始碼預設5從未修正)

input int InpKPeriod = 9;

input int InpDPeriod = 3;

input int InpSlowing = 3;

input double InpKdMax = 38.0;

input int InpBbPeriod = 20;

input double InpBbDev = 2.0;

input bool InpAllowKdOrBb = false;

input group "StaircaseTP"

input bool InpUseStairTp = true;

input bool InpStairLockOnly = true;

input double InpStair1ActPct = 0.12;

input double InpStair1LockPct = 0.00;

input double InpStair2ActPct = 0.25;

input double InpStair2LockPct = 0.08;

input double InpStair3ActPct = 0.45;

input double InpStair3LockPct = 0.20;

input double InpStair4ActPct = 0.70;

input double InpStair4LockPct = 0.40;

input group "SpikeTrail"

input bool InpUseSpikeTrail = true;

input double InpSpikeArmPct = 0.70;

input double InpSpikeTrailPct = 0.30;

input double InpSpikeMinLockPct = 0.40;

input bool InpSpikePauseOnExit = false;

input bool InpSpikeBlockEarly = true;

input bool InpSpikeBlockHtf = true;

input group "BasketRisk"

input bool InpUseBasketLossCap = true;

input double InpBasketLossLimitAmt = 120.0;

//+------------------------------------------------------------------+

//| V1615 ConfigGuard生效值:程式一律使用g_p*而非Inp*。 |

//| InpAllowNonBaseline=false(預設)時,g_p*一律鎖定為V16.15基準值, |

//| 舊set檔怎麼覆蓋輸入參數都不影響實際運行(只會在日誌回報矯正項)。 |

//| InpAllowNonBaseline=true時,g_p*=輸入值(供優化器刻意掃參數用)。 |

//+------------------------------------------------------------------+

long g_pMagic;

string g_pComment;

int g_pMaxSpreadPoints;

int g_pMaxGridLayers;

int g_pEarlyExitMinLayers;

double g_pSlFrac;

int g_pCoolBars;

double g_pFloatLossPct;

int g_pPauseHours;

bool g_pFlattenOnHighDD;

int g_pFuseMaxEarly;

int g_pKPeriod;

double g_pKdMax;

bool g_pAllowKdOrBb;

double g_pBasketLossLimitAmt;

double g_pSpikeArmPct;

double g_pSpikeTrailPct;

//--- V1617 實驗生效值(由ApplyExperiment依InpExperimentId設定)

bool g_eBlockFriday = false;

double g_eAdxMin = 0.0; // >0: H1 ADX低於此值不開新籃

double g_eAdxMax = 0.0; // >0: H1 ADX高於此值不開新籃

double g_eRsiUpper = 0.0; // >0: H1 RSI高於等於此值不開新籃(要求空頭動能)

double g_eRsiLower = 0.0; // >0: H1 RSI低於等於此值不開新籃(要求反彈夠高再空)

bool g_eUseEmaDist = false;// true: 價格需高於 H1 EMA50 + k*ATR(H1) 才開新籃

double g_eEmaDistK = 0.0;

double g_eStairScale = 1.0; // 階梯停利整組縮放倍率

int g_hAdxH1 = INVALID_HANDLE;

int g_hRsiH1 = INVALID_HANDLE;

void ApplyConfigGuard()

{

string dev = "";

if(InpMagic != 716045) dev += " InpMagic=" + (string)InpMagic + "->716045";

if(InpComment != "M10808") dev += " InpComment=" + InpComment + "->M10808";

if(InpMaxSpreadPoints != 50) dev += " InpMaxSpreadPoints=" + (string)InpMaxSpreadPoints + "->50";

if(MathAbs(InpSlFrac - 0.985) > 1e-9) dev += " InpSlFrac=" + DoubleToString(InpSlFrac, 3) + "->0.985";

if(InpCoolBars != 3) dev += " InpCoolBars=" + (string)InpCoolBars + "->3";

if(MathAbs(InpFloatLossPct - 1.10) > 1e-9) dev += " InpFloatLossPct=" + DoubleToString(InpFloatLossPct, 2) + "->1.10";

if(InpPauseHours != 36) dev += " InpPauseHours=" + (string)InpPauseHours + "->36";

if(InpFlattenOnHighDD != false) dev += " InpFlattenOnHighDD=true->false";

if(InpFuseMaxEarly != 5) dev += " InpFuseMaxEarly=" + (string)InpFuseMaxEarly + "->5";

if(InpKPeriod != 9) dev += " InpKPeriod=" + (string)InpKPeriod + "->9";

if(MathAbs(InpKdMax - 38.0) > 1e-9) dev += " InpKdMax=" + DoubleToString(InpKdMax, 1) + "->38.0";

if(InpAllowKdOrBb != false) dev += " InpAllowKdOrBb=true->false";

if(MathAbs(InpBasketLossLimitAmt - 120.0) > 1e-9) dev += " InpBasketLossLimitAmt=" + DoubleToString(InpBasketLossLimitAmt, 0) + "->120";

//--- 實驗槓桿:不受防呆鎖鎖定,永遠採用輸入值(供測試器/優化器直接掃描)。

//--- EarlyExitMinLayers 自動夾在層數上限以內,杜絕「早退無聲失效」的隱性耦合。

g_pMaxGridLayers = InpMaxGridLayers;

g_pEarlyExitMinLayers = MathMin(InpEarlyExitMinLayers, InpMaxGridLayers);

g_pSpikeArmPct = InpSpikeArmPct;

g_pSpikeTrailPct = InpSpikeTrailPct;

if(InpAllowNonBaseline)

{

g_pMagic = InpMagic;

g_pComment = InpComment;

g_pMaxSpreadPoints = InpMaxSpreadPoints;

g_pSlFrac = InpSlFrac;

g_pCoolBars = InpCoolBars;

g_pFloatLossPct = InpFloatLossPct;

g_pPauseHours = InpPauseHours;

g_pFlattenOnHighDD = InpFlattenOnHighDD;

g_pFuseMaxEarly = InpFuseMaxEarly;

g_pKPeriod = InpKPeriod;

g_pKdMax = InpKdMax;

g_pAllowKdOrBb = InpAllowKdOrBb;

g_pBasketLossLimitAmt = InpBasketLossLimitAmt;

if(dev != "")

Print("M10808 CONFIG GUARD: 偵測到偏離基準的輸入值,因InpAllowNonBaseline=true照輸入值運行:", dev);

return;

}

g_pMagic = 716045;

g_pComment = "M10808";

g_pMaxSpreadPoints = 50;

g_pSlFrac = 0.985;

g_pCoolBars = 3;

g_pFloatLossPct = 1.10;

g_pPauseHours = 36;

g_pFlattenOnHighDD = false;

g_pFuseMaxEarly = 5;

g_pKPeriod = 9;

g_pKdMax = 38.0;

g_pAllowKdOrBb = false;

g_pBasketLossLimitAmt = 120.0;

if(dev != "")

Print("M10808 CONFIG GUARD: 偵測到底盤參數被覆蓋(可能是舊set沿用),已自動矯正為基準值:", dev);

else

Print("M10808 CONFIG GUARD: 底盤參數 = 基準值,通過。");

Print("M10808 CONFIG GUARD: 實驗槓桿 layers=", g_pMaxGridLayers,

" earlyMinLayers=", g_pEarlyExitMinLayers,

" timeFilter=", InpUseEntryTimeFilter ? 1 : 0,

" blockHours=", InpBlockEntryHours,

" blockFri=", InpBlockFridayEntry ? 1 : 0);

}

CTrade trade;

CPositionInfo pos;

int g_hEmaFast = INVALID_HANDLE;

int g_hEmaSlow = INVALID_HANDLE;

int g_hStoch = INVALID_HANDLE;

int g_hBb = INVALID_HANDLE;

int g_hAtrDump = INVALID_HANDLE;

int g_hEmaD1 = INVALID_HANDLE;

int g_hAtrM1 = INVALID_HANDLE;

int g_hAtrM5 = INVALID_HANDLE;

int g_hAtrM15 = INVALID_HANDLE;

int g_hAtrM30 = INVALID_HANDLE;

int g_hAtrH1 = INVALID_HANDLE;

datetime g_lastBar = 0;

bool g_blockHour[24];

double g_initPrice = 0.0;

double g_gridStep = 0.0;

datetime g_pauseUntil = 0;

bool g_fuseOn = false;

bool g_fuseSawBull = false;

datetime g_earlyExitLog[MAX_EXIT_LOG];

int g_earlyExitCount = 0;

bool g_closing_in_progress = false;

datetime g_close_lock_start = 0;

string g_pendingCloseReason = "";

bool g_closeDeferredOffline = false;

double g_lastSyncedSl = 0.0;

datetime g_lastSyncSlBar = 0;

int g_stairLevel = 0;

double g_stairLockPrice = 0.0;

bool g_spikeArmed = false;

double g_peakProfitPct = 0.0;

double g_spikeLockPrice = 0.0;

bool g_posDirty = true;

int g_cCount = 0;

double g_cAvg = 0.0, g_cHighest = 0.0;

datetime g_cLastT = 0, g_cOldest = 0;

datetime g_cacheHtfBar = 0;

datetime g_cacheDumpBar = 0;

datetime g_cacheHighBar = 0;

datetime g_cacheD1Bar = 0;

datetime g_cacheM1Bar = 0;

bool g_cH1Bear = false;

bool g_cHtfBullBreak = false;

bool g_cAtrSpike = false;

bool g_cFarLow = false;

bool g_cDailyVeto = false;

double g_cDynStep = 0.0;

datetime g_lastHbMinute = 0;

datetime g_lastPurgeCheck = 0;

bool g_wasConnected = true;

int g_disconnectCount = 0;

datetime g_lastDisconnectLog = 0;

double g_dayRealizedPnl = 0.0;

double g_weekRealizedPnl = 0.0;

datetime g_dayStamp = 0;

datetime g_weekStamp = 0;

bool g_dailyLossHalt = false;

bool g_weeklyLossHalt = false;

datetime g_lastPnlScan = 0;

double g_startEquity = 0.0;

datetime g_eaStartTime = 0;

double g_magicRealizedCached = 0.0;

datetime g_lastMagicRealizedScan = 0;

bool g_magicRealizedDirty = true;

bool g_eaFatalStop = false;

bool g_expertRemovePending = false;

datetime g_lastMarginWarn = 0;

datetime g_lastCloseWarn = 0;

long g_lastMarginMode = -1;

double g_bufF[3], g_bufS[3], g_bufK[3], g_bufD[3], g_bufBb[3], g_bufEmaD1[2];

double g_bufAtrDump[];

double g_bufAtr1[1], g_bufAtr5[1], g_bufAtr15[1], g_bufAtr30[1], g_bufAtrH1[1];

double TpMultByCount(const int count)

{

if(count <= 1) return 1.0020;

if(count == 2) return 1.0015;

if(count == 3) return 1.0012;

if(count == 4) return 1.0009;

if(count == 5) return 1.0006;

if(count == 6) return 1.0003;

return 1.0001;

}

void ResetSpikeTrail()

{

g_spikeArmed = false;

g_peakProfitPct = 0.0;

g_spikeLockPrice = 0.0;

}

void ResetStairLock()

{

g_stairLevel = 0;

g_stairLockPrice = 0.0;

ResetSpikeTrail();

}

double StairActPct(const int level)

{

double v = 0.0;

if(level == 1) v = InpStair1ActPct;

if(level == 2) v = InpStair2ActPct;

if(level == 3) v = InpStair3ActPct;

if(level == 4) v = InpStair4ActPct;

return v * g_eStairScale; // V1617: 實驗23/24整組縮放

}

double StairLockPct(const int level)

{

double v = 0.0;

if(level == 1) v = InpStair1LockPct;

if(level == 2) v = InpStair2LockPct;

if(level == 3) v = InpStair3LockPct;

if(level == 4) v = InpStair4LockPct;

return v * g_eStairScale; // V1617: 實驗23/24整組縮放

}

bool UpdateStairLock(const double avg, const double ask)

{

if(!InpUseStairTp || avg <= 0.0 || ask <= 0.0)

return (g_stairLevel > 0);

// short profit when ask falls below avg

double profitPct = (avg / ask - 1.0) * 100.0;

int reached = 0;

for(int lvl = 1; lvl <= STAIR_LEVELS; ++lvl)

{

double actPct = StairActPct(lvl);

if(actPct <= 0.0) continue;

if(profitPct >= actPct)

reached = lvl;

}

if(reached > g_stairLevel)

{

double lockPct = StairLockPct(reached);

// lock SL below avg (or at avg for BE); ratchet DOWN only

double candidate = NormalizeDouble(avg * (1.0 - lockPct / 100.0), DigitsSym());

if(g_stairLockPrice <= 0.0 || candidate < g_stairLockPrice)

g_stairLockPrice = candidate;

g_stairLevel = reached;

Print(g_pComment, " STAIR_UP level=", g_stairLevel,

" profitPct=", DoubleToString(profitPct, 3),

" lockPct=", DoubleToString(lockPct, 3),

" lockSl=", DoubleToString(g_stairLockPrice, DigitsSym()),

" avg=", DoubleToString(avg, DigitsSym()),

" ask=", DoubleToString(ask, DigitsSym()));

WriteTradeLog("STAIR_UP", 0, g_stairLockPrice, 0, g_cCount, avg, 0,

StringFormat("L%d_%.3f", g_stairLevel, lockPct), 0);

}

return (g_stairLevel > 0);

}

bool UpdateSpikeTrail(const double avg, const double ask)

{

if(!InpUseSpikeTrail || avg <= 0.0 || ask <= 0.0)

return g_spikeArmed;

double profitPct = (avg / ask - 1.0) * 100.0;

if(profitPct > g_peakProfitPct)

g_peakProfitPct = profitPct;

if(!g_spikeArmed && g_pSpikeArmPct > 0.0 && profitPct >= g_pSpikeArmPct)

{

g_spikeArmed = true;

Print(g_pComment, " SPIKE_ARM profitPct=", DoubleToString(profitPct, 3),

" arm=", DoubleToString(g_pSpikeArmPct, 3),

" avg=", DoubleToString(avg, DigitsSym()),

" ask=", DoubleToString(ask, DigitsSym()));

WriteTradeLog("SPIKE_ARM", 0, ask, 0, g_cCount, avg, 0,

StringFormat("peak_%.3f", g_peakProfitPct), 0);

}

if(!g_spikeArmed)

return false;

double lockPct = g_peakProfitPct - g_pSpikeTrailPct;

if(lockPct < InpSpikeMinLockPct)

lockPct = InpSpikeMinLockPct;

if(lockPct < 0.0)

lockPct = 0.0;

double candidate = NormalizeDouble(avg * (1.0 - lockPct / 100.0), DigitsSym());

if(g_spikeLockPrice <= 0.0 || candidate < g_spikeLockPrice)

{

g_spikeLockPrice = candidate;

Print(g_pComment, " SPIKE_TRAIL_UP peakPct=", DoubleToString(g_peakProfitPct, 3),

" lockPct=", DoubleToString(lockPct, 3),

" lockSl=", DoubleToString(g_spikeLockPrice, DigitsSym()),

" avg=", DoubleToString(avg, DigitsSym()),

" ask=", DoubleToString(ask, DigitsSym()));

WriteTradeLog("SPIKE_TRAIL_UP", 0, g_spikeLockPrice, 0, g_cCount, avg, 0,

StringFormat("P%.3f_L%.3f", g_peakProfitPct, lockPct), 0);

}

return true;

}

bool SpreadOk()

{

long spread = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);

if(spread < 0) return false;

return ((int)spread <= g_pMaxSpreadPoints);

}

bool PricesValid(const double bid, const double ask)

{

if(bid <= 0.0 || ask <= 0.0) return false;

if(ask < bid) return false;

return true;

}

bool IsTesterMode()

{

return (MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) ||

MQLInfoInteger(MQL_FORWARD));

}

bool SymbolAllowsNewSell()

{

ENUM_SYMBOL_TRADE_MODE mode = (ENUM_SYMBOL_TRADE_MODE)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_MODE);

return (mode == SYMBOL_TRADE_MODE_FULL || mode == SYMBOL_TRADE_MODE_SHORTONLY);

}

bool TerminalReadyForNewOrders()

{

if(IsStopped()) return false;

// Tester: skip live terminal/AutoTrading gates (common "跑不動/不下單" cause)

if(!IsTesterMode())

{

if(InpBlockNewOnDisconnect && !TerminalInfoInteger(TERMINAL_CONNECTED))

return false;

if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) return false;

if(!AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)) return false;

if(!AccountInfoInteger(ACCOUNT_TRADE_EXPERT)) return false;

}

if(!MQLInfoInteger(MQL_TRADE_ALLOWED)) return false;

if(!SymbolAllowsNewSell()) return false;

return true;

}

bool TerminalReadyForClose()

{

if(IsStopped()) return false;

if(!IsTesterMode())

{

if(!TerminalInfoInteger(TERMINAL_CONNECTED)) return false;

if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) return false;

if(!AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)) return false;

if(!AccountInfoInteger(ACCOUNT_TRADE_EXPERT)) return false;

}

if(!MQLInfoInteger(MQL_TRADE_ALLOWED)) return false;

ENUM_SYMBOL_TRADE_MODE mode = (ENUM_SYMBOL_TRADE_MODE)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_MODE);

if(mode == SYMBOL_TRADE_MODE_DISABLED) return false;

return true;

}

void SafeSleep(const int ms)

{

if(IsTesterMode()) return; // Sleep stalls Strategy Tester badly

int wait = ms;

if(wait < 0) wait = 0;

if(wait > 2000) wait = 2000;

if(wait > 0) Sleep(wait);

}

string StartEquityGvName()

{

return "V1602_SE_" + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN))

+ "_" + _Symbol

+ "_" + IntegerToString(g_pMagic);

}

string StartTimeGvName()

{

return "V1602_ST_" + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN))

+ "_" + _Symbol

+ "_" + IntegerToString(g_pMagic);

}

void PersistStartBaseline()

{

string kEq = StartEquityGvName();

string kT = StartTimeGvName();

if(g_startEquity > 0.0)

GlobalVariableSet(kEq, g_startEquity);

if(g_eaStartTime > 0)

GlobalVariableSet(kT, (double)g_eaStartTime);

GlobalVariablesFlush();

}

void LoadOrInitStartEquity()

{

string kEq = StartEquityGvName();

string kT = StartTimeGvName();

bool haveEq = false;

if(GlobalVariableCheck(kEq))

{

double v = GlobalVariableGet(kEq);

if(v > 0.0)

{

g_startEquity = v;

haveEq = true;

}

}

if(GlobalVariableCheck(kT))

{

datetime t = (datetime)GlobalVariableGet(kT);

if(t > 0) g_eaStartTime = t;

}

if(!haveEq)

{

g_startEquity = AccountInfoDouble(ACCOUNT_EQUITY);

if(g_startEquity <= 0.0)

g_startEquity = AccountInfoDouble(ACCOUNT_BALANCE);

}

if(g_eaStartTime <= 0)

g_eaStartTime = TimeCurrent();

PersistStartBaseline();

}

void ClearStartEquityGv()

{

string kEq = StartEquityGvName();

string kT = StartTimeGvName();

if(GlobalVariableCheck(kEq)) GlobalVariableDel(kEq);

if(GlobalVariableCheck(kT)) GlobalVariableDel(kT);

GlobalVariablesFlush();

}

string LogInstanceTag()

{

string sym = _Symbol;

StringReplace(sym, ".", "_");

StringReplace(sym, "#", "_");

StringReplace(sym, " ", "_");

return sym + "_"

+ IntegerToString(g_pMagic) + "_"

+ IntegerToString(ChartID()) + "_"

+ IntegerToString((int)_Period);

}

void WarnIfNettingAccount(const bool forcePrint)

{

long mode = AccountInfoInteger(ACCOUNT_MARGIN_MODE);

if(!forcePrint && mode == g_lastMarginMode) return;

g_lastMarginMode = mode;

if(mode == ACCOUNT_MARGIN_MODE_RETAIL_NETTING)

Print(g_pComment, " WARN: netting account — grid layer count may not match hedging design");

}

string DateStamp(const datetime t)

{

MqlDateTime dt;

TimeToStruct(t, dt);

return StringFormat("%04d%02d%02d", dt.year, dt.mon, dt.day);

}

datetime MidnightOfStamp(const string yyyymmdd)

{

if(StringLen(yyyymmdd) != 8) return 0;

MqlDateTime dt;

dt.year = (int)StringToInteger(StringSubstr(yyyymmdd, 0, 4));

dt.mon = (int)StringToInteger(StringSubstr(yyyymmdd, 4, 2));

dt.day = (int)StringToInteger(StringSubstr(yyyymmdd, 6, 2));

dt.hour = 0; dt.min = 0; dt.sec = 0;

return StructToTime(dt);

}

void PurgeOldDayFiles(const string prefix, const string ext, const int retainHours)

{

if(retainHours <= 0) return;

string fname = "";

long h = FileFindFirst(prefix + "*." + ext, fname);

if(h == INVALID_HANDLE) return;

do

{

int p = StringLen(prefix);

int e = StringFind(fname, "." + ext);

if(e > p + 7)

{

string stamp = StringSubstr(fname, p, 8);

datetime day0 = MidnightOfStamp(stamp);

if(day0 > 0)

{

datetime dayEnd = day0 + 86400;

if((TimeCurrent() - dayEnd) > (datetime)retainHours * 3600)

{

if(FileIsExist(fname))

{

FileDelete(fname);

Print(g_pComment, " PURGE ", fname);

}

}

}

}

}

while(FileFindNext(h, fname));

FileFindClose(h);

}

void MaybePurgeLogs()

{

if(g_lastPurgeCheck > 0 && (TimeCurrent() - g_lastPurgeCheck) < 3600) return;

g_lastPurgeCheck = TimeCurrent();

if(InpEnableHeartbeat)

PurgeOldDayFiles("V1602_HB_", "log", InpHbRetainHours);

if(InpEnableTradeLog)

PurgeOldDayFiles("V1602_Trade_", "csv", InpTradeRetainDays * 24);

}

void WriteHeartbeatLine()

{

if(!InpEnableHeartbeat) return;

if(IsTesterMode()) return; // file I/O kills M1 backtests

datetime now = TimeCurrent();

MqlDateTime dt;

TimeToStruct(now, dt);

datetime minuteKey = now - dt.sec;

if(minuteKey == g_lastHbMinute) return;

g_lastHbMinute = minuteKey;

string fname = "V1602_HB_" + DateStamp(now) + "_" + LogInstanceTag() + ".log";

int fh = FileOpen(fname, FILE_READ | FILE_WRITE | FILE_TXT | FILE_ANSI | FILE_SHARE_READ);

if(fh == INVALID_HANDLE)

{

Print(g_pComment, " HB open fail ", GetLastError());

return;

}

FileSeek(fh, 0, SEEK_END);

bool connected = (bool)TerminalInfoInteger(TERMINAL_CONNECTED);

int spread = (int)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);

double avg, hi; datetime lt, ol;

int cnt = CountSells(avg, hi, lt, ol);

double eq = AccountInfoDouble(ACCOUNT_EQUITY);

double bal = AccountInfoDouble(ACCOUNT_BALANCE);

double magicPnl = 0.0;

MagicBasketPnL(magicPnl);

string line = StringFormat(

"%s | sym=%s | conn=%d | spread=%d | pos=%d | avg=%.5f | eq=%.2f | bal=%.2f | startEq=%.2f | magicPnl=%.2f | pause=%d | fuse=%d | dayHalt=%d | weekHalt=%d | dayPnl=%.2f | weekPnl=%.2f | fatal=%d | closeLock=%d | discCnt=%d | step=%.5f | stair=%d | stairSl=%.5f | spike=%d | peak=%.3f | spikeSl=%.5f",

TimeToString(now, TIME_DATE | TIME_SECONDS),

_Symbol,

connected ? 1 : 0,

spread,

cnt,

avg,

eq,

bal,

g_startEquity,

magicPnl,

PauseActive() ? 1 : 0,

g_fuseOn ? 1 : 0,

g_dailyLossHalt ? 1 : 0,

g_weeklyLossHalt ? 1 : 0,

g_dayRealizedPnl,

g_weekRealizedPnl,

g_eaFatalStop ? 1 : 0,

g_closing_in_progress ? 1 : 0,

g_disconnectCount,

g_cDynStep,

g_stairLevel,

g_stairLockPrice,

g_spikeArmed ? 1 : 0,

g_peakProfitPct,

g_spikeLockPrice

);

FileWriteString(fh, line + "\n");

FileFlush(fh);

FileClose(fh);

MaybePurgeLogs();

}

void EnsureTradeCsvHeader(const int fh)

{

if(FileSize(fh) > 0) return;

FileWriteString(fh,

"timestamp,symbol,event,ticket,price,lots,basket_count,basket_avg,pnl,reason,spread,bid,ask,retcode,comment\n");

}

void WriteTradeLog(const string event,

const ulong ticket,

const double price,

const double lots,

const int basketCount,

const double basketAvg,

const double pnl,

const string reason,

const uint retcode)

{

if(!InpEnableTradeLog) return;

if(IsTesterMode()) return; // file I/O kills M1 backtests

datetime now = TimeCurrent();

string fname = "V1602_Trade_" + DateStamp(now) + "_" + LogInstanceTag() + ".csv";

int fh = FileOpen(fname, FILE_READ | FILE_WRITE | FILE_TXT | FILE_ANSI | FILE_SHARE_READ);

if(fh == INVALID_HANDLE)

{

Print(g_pComment, " TradeLog open fail ", GetLastError());

return;

}

FileSeek(fh, 0, SEEK_END);

EnsureTradeCsvHeader(fh);

double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

int spread = (int)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);

string safeReason = reason;

StringReplace(safeReason, ",", ";");

string line = StringFormat(

"%s,%s,%s,%s,%.5f,%.2f,%d,%.5f,%.2f,%s,%d,%.5f,%.5f,%u,%s",

TimeToString(now, TIME_DATE | TIME_SECONDS),

_Symbol,

event,

IntegerToString((long)ticket),

price,

lots,

basketCount,

basketAvg,

pnl,

safeReason,

spread,

bid,

ask,

retcode,

g_pComment

);

FileWriteString(fh, line + "\n");

FileFlush(fh);

FileClose(fh);

}

bool IsTransientTradeError(const uint rc)

{

return (

rc == TRADE_RETCODE_REQUOTE ||

rc == TRADE_RETCODE_PRICE_CHANGED ||

rc == TRADE_RETCODE_PRICE_OFF ||

rc == TRADE_RETCODE_CONNECTION ||

rc == TRADE_RETCODE_TIMEOUT ||

rc == TRADE_RETCODE_TOO_MANY_REQUESTS ||

rc == TRADE_RETCODE_LOCKED

);

}

void CheckConnectionState()

{

bool connected = (bool)TerminalInfoInteger(TERMINAL_CONNECTED);

if(!connected && g_wasConnected)

{

g_wasConnected = false;

g_disconnectCount++;

if(g_lastDisconnectLog == 0 || (TimeCurrent() - g_lastDisconnectLog) >= 60)

{

g_lastDisconnectLog = TimeCurrent();

InvalidatePosCache();

double avg, hi; datetime lt, ol;

int cnt = CountSellsRaw(avg, hi, lt, ol);

Print(g_pComment, " DISCONNECTED #", g_disconnectCount);

WriteTradeLog("DISCONNECT", 0, 0, 0, cnt, avg, 0, "TERMINAL", 0);

}

}

else if(connected && !g_wasConnected)

{

g_wasConnected = true;

InvalidatePosCache();

double avg, hi; datetime lt, ol;

int cnt = CountSellsRaw(avg, hi, lt, ol);

Print(g_pComment, " RECONNECTED");

WriteTradeLog("RECONNECT", 0, 0, 0, cnt, avg, 0, "TERMINAL", 0);

g_cacheHtfBar = g_cacheDumpBar = g_cacheHighBar = g_cacheD1Bar = g_cacheM1Bar = 0;

WarnIfNettingAccount(false);

}

}

double NormLot(double lot)

{

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;

if(lot < vmin) lot = vmin;

if(lot > vmax) lot = vmax;

lot = MathFloor(lot / step + 1e-8) * step;

return NormalizeDouble(lot, 2);

}

int DigitsSym() { return (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); }

string MakeOrderComment(const bool isAdd)

{

string suffix = isAdd ? "_G" : "_0";

string base = g_pComment;

int keep = ORDER_COMMENT_MAX - StringLen(suffix);

if(keep < 1) keep = 1;

if(StringLen(base) > keep)

base = StringSubstr(base, 0, keep);

return base + suffix;

}

datetime DayStart(const datetime t)

{

MqlDateTime dt;

TimeToStruct(t, dt);

dt.hour = 0; dt.min = 0; dt.sec = 0;

return StructToTime(dt);

}

datetime WeekStartMonday(const datetime t)

{

MqlDateTime dt;

TimeToStruct(t, dt);

dt.hour = 0; dt.min = 0; dt.sec = 0;

datetime mid = StructToTime(dt);

int dow = dt.day_of_week;

int sinceMon = (dow == 0 ? 6 : dow - 1);

return mid - (datetime)sinceMon * 86400;

}

bool SumRealizedPnlFrom(const datetime fromTime, double &sumOut)

{

sumOut = 0.0;

if(fromTime <= 0) return false;

if(!HistorySelect(fromTime, TimeCurrent())) return false;

double sum = 0.0;

int total = HistoryDealsTotal();

for(int i = 0; i < total; ++i)

{

ulong deal = HistoryDealGetTicket(i);

if(deal == 0) continue;

if(HistoryDealGetString(deal, DEAL_SYMBOL) != _Symbol) continue;

if((long)HistoryDealGetInteger(deal, DEAL_MAGIC) != g_pMagic) continue;

long entry = HistoryDealGetInteger(deal, DEAL_ENTRY);

if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_INOUT && entry != DEAL_ENTRY_OUT_BY)

continue;

sum += HistoryDealGetDouble(deal, DEAL_PROFIT)

+ HistoryDealGetDouble(deal, DEAL_SWAP)

+ HistoryDealGetDouble(deal, DEAL_COMMISSION);

}

sumOut = sum;

return true;

}

void RefreshLossHaltState(const bool force = false)

{

datetime now = TimeCurrent();

datetime day0 = DayStart(now);

datetime week0 = WeekStartMonday(now);

bool newDay = (day0 != g_dayStamp);

bool newWeek = (week0 != g_weekStamp);

bool dueScan = (g_lastPnlScan == 0 || (now - g_lastPnlScan) >= 60);

if(newDay)

{

g_dayStamp = day0;

if(g_dailyLossHalt)

{

g_dailyLossHalt = false;

Print(g_pComment, " DAILY_LOSS_HALT OFF (new day)");

WriteTradeLog("DAILY_HALT_OFF", 0, 0, 0, g_cCount, g_cAvg, 0, "NEW_DAY", 0);

}

}

if(newWeek)

{

g_weekStamp = week0;

if(g_weeklyLossHalt)

{

g_weeklyLossHalt = false;

Print(g_pComment, " WEEKLY_LOSS_HALT OFF (new week)");

WriteTradeLog("WEEKLY_HALT_OFF", 0, 0, 0, g_cCount, g_cAvg, 0, "NEW_WEEK", 0);

}

}

if(force || newDay || newWeek || dueScan)

{

double dayPnl = g_dayRealizedPnl;

double weekPnl = g_weekRealizedPnl;

bool dayOk = SumRealizedPnlFrom(day0, dayPnl);

bool weekOk = SumRealizedPnlFrom(week0, weekPnl);

if(dayOk) g_dayRealizedPnl = dayPnl;

if(weekOk) g_weekRealizedPnl = weekPnl;

if(dayOk || weekOk)

g_lastPnlScan = now;

}

if(InpUseDailyLossHalt && InpDailyLossLimitAmt > 0.0)

{

if(!g_dailyLossHalt && g_dayRealizedPnl <= -InpDailyLossLimitAmt)

{

g_dailyLossHalt = true;

Print(g_pComment, " DAILY_LOSS_HALT ON dayPnl=", g_dayRealizedPnl,

" limit=", InpDailyLossLimitAmt);

WriteTradeLog("DAILY_HALT_ON", 0, 0, 0, g_cCount, g_cAvg, g_dayRealizedPnl,

"DAY_LOSS", 0);

}

}

if(InpUseWeeklyLossHalt && InpWeeklyLossLimitAmt > 0.0)

{

if(!g_weeklyLossHalt && g_weekRealizedPnl <= -InpWeeklyLossLimitAmt)

{

g_weeklyLossHalt = true;

Print(g_pComment, " WEEKLY_LOSS_HALT ON weekPnl=", g_weekRealizedPnl,

" limit=", InpWeeklyLossLimitAmt);

WriteTradeLog("WEEKLY_HALT_ON", 0, 0, 0, g_cCount, g_cAvg, g_weekRealizedPnl,

"WEEK_LOSS", 0);

}

}

}

bool LossHaltActive()

{

return (g_dailyLossHalt || g_weeklyLossHalt);

}

bool MarginOkForLot(const double lot, const double price)

{

if(!InpUseMarginCheck) return true;

double need = 0.0;

if(!OrderCalcMargin(ORDER_TYPE_SELL, _Symbol, lot, price, need))

{

if(g_lastMarginWarn == 0 || (TimeCurrent() - g_lastMarginWarn) >= 60)

{

g_lastMarginWarn = TimeCurrent();

Print(g_pComment, " MARGIN calc fail err=", GetLastError());

}

return false;

}

double freeM = AccountInfoDouble(ACCOUNT_MARGIN_FREE);

double require = need * InpMarginSafetyMult;

if(freeM < require)

{

if(g_lastMarginWarn == 0 || (TimeCurrent() - g_lastMarginWarn) >= 60)

{

g_lastMarginWarn = TimeCurrent();

Print(g_pComment, " MARGIN BLOCK free=", freeM,

" need=", require, " (raw=", need, ")");

WriteTradeLog("MARGIN_BLOCK", 0, price, lot, g_cCount, g_cAvg, 0,

"INSUFFICIENT", 0);

}

return false;

}

return true;

}

void InvalidateMagicRealizedCache()

{

g_magicRealizedDirty = true;

}

double MagicFloatingPnL()

{

double sum = 0.0;

for(int i = PositionsTotal() - 1; i >= 0; --i)

{

if(!pos.SelectByIndex(i)) continue;

if(pos.Symbol() != Symbol || pos.Magic() != gpMagic) continue;

if(pos.PositionType() != POSITION_TYPE_SELL) continue;

sum += pos.Profit() + pos.Swap();

}

return sum;

}

bool RefreshMagicRealizedCache(const bool force)

{

int scanSec = InpMagicDdScanSec;

if(scanSec < 1) scanSec = 1;

bool due = (g_lastMagicRealizedScan == 0 ||

(TimeCurrent() - g_lastMagicRealizedScan) >= scanSec);

if(!force && !g_magicRealizedDirty && !due)

return true;

double realized = 0.0;

if(!SumRealizedPnlFrom(g_eaStartTime, realized))

return false;

g_magicRealizedCached = realized;

g_magicRealizedDirty = false;

g_lastMagicRealizedScan = TimeCurrent();

return true;

}

bool MagicBasketPnL(double &pnlOut, const bool forceRealized = false)

{

RefreshMagicRealizedCache(forceRealized || g_magicRealizedDirty);

pnlOut = g_magicRealizedCached + MagicFloatingPnL();

return true;

}

void CheckAccountDrawdownHalt()

{

if(!InpUseAccountDdHalt || InpAccountDdPct <= 0.0) return;

if(g_expertRemovePending) return;

if(g_eaFatalStop) return;

if(g_startEquity <= 0.0) return;

double magicPnl = 0.0;

MagicBasketPnL(magicPnl, false);

double lossLimit = g_startEquity * (InpAccountDdPct / 100.0);

if(magicPnl > -lossLimit) return;

MagicBasketPnL(magicPnl, true);

if(magicPnl > -lossLimit) return;

g_eaFatalStop = true;

Print(g_pComment, " MAGIC_DD_HALT magicPnl=", magicPnl,

" startEq=", g_startEquity, " lossLimit=", -lossLimit,

" ddPct=", InpAccountDdPct);

WriteTradeLog("MAGIC_DD_HALT", 0, 0, 0, g_cCount, g_cAvg, magicPnl,

"MAGIC_DD", 0);

}

void TryFatalStopExit()

{

if(!g_eaFatalStop || g_expertRemovePending) return;

InvalidatePosCache();

double a, h; datetime lt, ol;

int left = CountSellsRaw(a, h, lt, ol);

if(left > 0)

{

if(!g_closing_in_progress)

CloseAllSells("MAGIC_DD_HALT");

else if(g_close_lock_start > 0 && (TimeCurrent() - g_close_lock_start) >= 30)

{

g_closing_in_progress = false;

CloseAllSells("MAGIC_DD_HALT");

}

return;

}

g_closing_in_progress = false;

g_close_lock_start = 0;

g_expertRemovePending = true;

ClearStartEquityGv();

Print(g_pComment, " MAGIC_DD_HALT → ExpertRemove left=", left);

WriteTradeLog("MAGIC_DD_REMOVE", 0, 0, 0, left, 0, 0, "REMOVE", 0);

ExpertRemove();

}

bool HtfBear()

{

ArraySetAsSeries(g_bufF, true);

ArraySetAsSeries(g_bufS, true);

if(CopyBuffer(g_hEmaFast, 0, 0, 3, g_bufF) < 3) return false;

if(CopyBuffer(g_hEmaSlow, 0, 0, 3, g_bufS) < 3) return false;

double c1 = iClose(_Symbol, InpHtf, 1);

return (c1 < g_bufF[1] && g_bufF[1] < g_bufS[1]);

}

bool HtfBullBreak()

{

ArraySetAsSeries(g_bufF, true);

ArraySetAsSeries(g_bufS, true);

if(CopyBuffer(g_hEmaFast, 0, 0, 3, g_bufF) < 3) return false;

if(CopyBuffer(g_hEmaSlow, 0, 0, 3, g_bufS) < 3) return false;

double c1 = iClose(_Symbol, InpHtf, 1);

bool golden = (g_bufF[2] <= g_bufS[2] && g_bufF[1] > g_bufS[1]);

bool above = (c1 > g_bufS[1]);

return (golden || above);

}

//+------------------------------------------------------------------+

//| 解析封鎖小時字串(如"23"或"15,17,23")到 g_blockHour[]。 |

//+------------------------------------------------------------------+

void ParseBlockHoursStr(const string hoursCsv)

{

for(int i = 0; i < 24; i++) g_blockHour[i] = false;

if(StringLen(hoursCsv) == 0) return;

string parts[];

int n = StringSplit(hoursCsv, ',', parts);

for(int i = 0; i < n; i++)

{

string p = parts[i];

StringTrimLeft(p);

StringTrimRight(p);

if(StringLen(p) == 0) continue;

int h = (int)StringToInteger(p);

if(h >= 0 && h < 24) g_blockHour[h] = true;

}

}

//+------------------------------------------------------------------+

//| V1617 實驗定義表:InpExperimentId → 完整配置(寫死,零污染)。 |

//| 於OnInit在ApplyConfigGuard之後呼叫。-1=手動模式(照輸入參數)。 |

//+------------------------------------------------------------------+

void ApplyExperiment()

{

g_eAdxMin = 0.0; g_eAdxMax = 0.0;

g_eRsiUpper = 0.0; g_eRsiLower = 0.0;

g_eUseEmaDist = false; g_eEmaDistK = 0.0;

g_eStairScale = 1.0;

// M10808:永久鎖定實驗29(30.xml批次優勝配置),忽略InpExperimentId輸入,

// 杜絕任何set檔污染的可能。

int id = 29;

if(InpExperimentId != 29)

Print(g_pComment, " NOTE: InpExperimentId=", InpExperimentId,

" 已被忽略,M10808固定使用實驗29配置");

// 預定義實驗一律從乾淨的1607底盤出發(層數6、無濾網),再套上該實驗的改動

ParseBlockHoursStr("");

g_eBlockFriday = false;

g_pMaxGridLayers = 6;

g_pEarlyExitMinLayers = 3;

string desc = "1607 baseline control";

switch(id)

{

// --- A組:網格深度(1607歸因:勝率隨層數49%→46%→22%→0%崩壞) ---

case 0: break;

case 1: g_pMaxGridLayers = 1; g_pEarlyExitMinLayers = 1; desc = "layers=1 完全不加碼"; break;

case 2: g_pMaxGridLayers = 2; g_pEarlyExitMinLayers = 2; desc = "layers=2"; break;

case 3: g_pMaxGridLayers = 3; g_pEarlyExitMinLayers = 3; desc = "layers=3"; break;

case 4: g_pMaxGridLayers = 4; g_pEarlyExitMinLayers = 3; desc = "layers=4"; break;

// --- B組:時段/星期濾網(1607歸因:23點-28.59/籃、15/17點大虧、週五勝率41.9%) ---

case 5: ParseBlockHoursStr("23"); desc = "封鎖23點開新籃"; break;

case 6: ParseBlockHoursStr("15,17,23"); desc = "封鎖15,17,23點"; break;

case 7: g_eBlockFriday = true; desc = "封鎖週五開新籃"; break;

case 8: ParseBlockHoursStr("23"); g_eBlockFriday = true; desc = "封鎖23點+週五"; break;

case 9: ParseBlockHoursStr("15,16,17,18,19,20,21,22,23"); desc = "只在0-14點開新籃(亞洲+倫敦早盤)"; break;

case 10: ParseBlockHoursStr("15,16,17,18"); desc = "封鎖紐約數據窗15-18點"; break;

// --- C組:H1 ADX(14) 趨勢強度濾網(1612/1613舊結論已作廢,重新乾淨測試) ---

case 11: g_eAdxMin = 20.0; desc = "ADX>=20才開新籃(要求趨勢動能)"; break;

case 12: g_eAdxMin = 25.0; desc = "ADX>=25才開新籃"; break;

case 13: g_eAdxMax = 25.0; desc = "ADX<=25才開新籃(避開噴出)"; break;

// --- D組:H1 RSI(14) 動能濾網(TradingView黃金常用:門檻要比一般市場極端) ---

case 14: g_eRsiUpper = 45.0; desc = "H1 RSI<45才開新籃(空頭動能確認)"; break;

case 15: g_eRsiUpper = 50.0; desc = "H1 RSI<50才開新籃"; break;

case 16: g_eRsiLower = 55.0; desc = "H1 RSI>55才開新籃(反彈夠高再空)"; break;

// --- E組:均線距離濾網(只空「反彈到H1 EMA50上方」的過度延伸) ---

case 17: g_eUseEmaDist = true; g_eEmaDistK = 0.3; desc = "價格>=EMA50+0.3*ATR才開新籃"; break;

case 18: g_eUseEmaDist = true; g_eEmaDistK = 0.0; desc = "價格>=EMA50才開新籃"; break;

// --- F組:KD門檻變體(進場觸發的嚴格度) ---

case 19: g_pKdMax = 30.0; desc = "KdMax=30(更嚴格超買)"; break;

case 20: g_pKdMax = 45.0; desc = "KdMax=45(較寬鬆)"; break;

// --- G組:停利/追蹤變體 ---

case 21: g_pSpikeArmPct = 0.5; g_pSpikeTrailPct = 0.25; desc = "spike追蹤更早啟動(0.5/0.25)"; break;

case 22: g_pSpikeArmPct = 1.0; g_pSpikeTrailPct = 0.40; desc = "spike追蹤更晚更寬(1.0/0.40)"; break;

case 23: g_eStairScale = 1.5; desc = "階梯停利整組x1.5(讓利潤跑更遠)"; break;

case 24: g_eStairScale = 0.7; desc = "階梯停利整組x0.7(更早鎖利)"; break;

// --- H組:風控變體 ---

case 25: g_pBasketLossLimitAmt = 80.0; desc = "籃子虧損上限$80"; break;

case 26: g_pBasketLossLimitAmt = 200.0; desc = "籃子虧損上限$200"; break;

case 27: g_pFloatLossPct = 0.8; desc = "浮虧砍倉0.8%"; break;

case 28: g_pPauseHours = 12; desc = "出場後暫停12h(縮短冷卻)"; break;

// --- I組:組合拳(歸因數據最看好的疊加) ---

case 29: g_pMaxGridLayers = 2; g_pEarlyExitMinLayers = 2; ParseBlockHoursStr("23");

g_eBlockFriday = true; g_eAdxMin = 20.0; desc = "組合:layers2+封23點+封週五+ADX>=20"; break;

default: desc = "未定義ID,等同baseline"; break;

}

// M10808:註解固定,不加實驗編號後綴(g_pComment已由防呆鎖設為"M10808")

Print(g_pComment, " EXPERIMENT id=", id, " => ", desc,

" | layers=", g_pMaxGridLayers,

" minLayers=", g_pEarlyExitMinLayers,

" blockFri=", g_eBlockFriday ? 1 : 0,

" adxMin=", g_eAdxMin, " adxMax=", g_eAdxMax,

" rsiUp=", g_eRsiUpper, " rsiLo=", g_eRsiLower,

" emaDist=", g_eUseEmaDist ? DoubleToString(g_eEmaDistK, 2) : "off",

" kdMax=", g_pKdMax,

" spike=", g_pSpikeArmPct, "/", g_pSpikeTrailPct,

" stairX=", g_eStairScale,

" cap=", g_pBasketLossLimitAmt,

" floatLoss=", g_pFloatLossPct,

" pauseH=", g_pPauseHours);

}

//+------------------------------------------------------------------+

//| V1617:目前broker時間是否禁止「開新籃」。只擋首單。 |

//+------------------------------------------------------------------+

bool EntryBlockedByTime()

{

MqlDateTime dt;

TimeToStruct(TimeCurrent(), dt);

if(g_blockHour[dt.hour]) return true;

if(g_eBlockFriday && dt.day_of_week == 5) return true;

return false;

}

//+------------------------------------------------------------------+

//| V1617:H1 ADX/RSI/均線距離濾網。只擋「開新籃」,讀不到資料時 |

//| 保守擋單。全部關閉時零成本直接放行。 |

//+------------------------------------------------------------------+

bool EntryBlockedByIndicators()

{

if(g_eAdxMin > 0.0 || g_eAdxMax > 0.0)

{

double adx[1];

if(CopyBuffer(g_hAdxH1, MAIN_LINE, 0, 1, adx) < 1) return true;

if(g_eAdxMin > 0.0 && adx[0] < g_eAdxMin) return true;

if(g_eAdxMax > 0.0 && adx[0] > g_eAdxMax) return true;

}

if(g_eRsiUpper > 0.0 || g_eRsiLower > 0.0)

{

double rsi[1];

if(CopyBuffer(g_hRsiH1, 0, 0, 1, rsi) < 1) return true;

if(g_eRsiUpper > 0.0 && rsi[0] >= g_eRsiUpper) return true;

if(g_eRsiLower > 0.0 && rsi[0] <= g_eRsiLower) return true;

}

if(g_eUseEmaDist)

{

double ema[1], atr[1];

if(CopyBuffer(g_hEmaFast, 0, 0, 1, ema) < 1) return true;

if(CopyBuffer(g_hAtrH1, 0, 0, 1, atr) < 1) return true;

double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

if(bid < ema[0] + g_eEmaDistK * atr[0]) return true;

}

return false;

}

// Entry trigger (M1 bar close): KD death in overbought OR BB upper touch.

// Gate (caller): H1 bear + !DailyBullVeto + !RiskOffNewEntry — see OnTick.

bool FirstEntrySignal()

{

ArraySetAsSeries(g_bufK, true);

ArraySetAsSeries(g_bufD, true);

ArraySetAsSeries(g_bufBb, true);

if(CopyBuffer(g_hStoch, 0, 0, 3, g_bufK) < 3) return false;

if(CopyBuffer(g_hStoch, 1, 0, 3, g_bufD) < 3) return false;

if(CopyBuffer(g_hBb, 1, 0, 3, g_bufBb) < 3) return false; // upper band

double c1 = iClose(_Symbol, PERIOD_CURRENT, 1);

double h1 = iHigh(_Symbol, PERIOD_CURRENT, 1);

bool kdDeath = (g_bufK[2] >= g_bufD[2] && g_bufK[1] < g_bufD[1]);

double kdMin = 100.0 - g_pKdMax; // g_pKdMax=35 → overbought floor 65

bool kdOb = (g_bufK[1] >= kdMin || g_bufK[2] >= kdMin);

bool kdOk = (kdDeath && kdOb);

bool touchBb = (h1 >= g_bufBb[1] || c1 >= g_bufBb[1]);

if(g_pAllowKdOrBb) return (kdOk || touchBb);

return (kdOk && touchBb);

}

bool FloatLossHit(const double avg, const double ask)

{

if(!InpUseFloatExit || avg <= 0.0) return false;

return (ask >= avg * (1.0 + g_pFloatLossPct / 100.0));

}

bool AtrSpikeHit()

{

if(!InpUseAtrSpikeExit || g_hAtrDump == INVALID_HANDLE) return false;

int need = InpAtrSmaPeriod;

if(need < 1) need = 1;

ArrayResize(g_bufAtrDump, need);

ArraySetAsSeries(g_bufAtrDump, true);

if(CopyBuffer(g_hAtrDump, 0, 1, need, g_bufAtrDump) < need) return false;

double sum = 0.0;

for(int i = 0; i < InpAtrSmaPeriod; ++i) sum += g_bufAtrDump[i];

double sma = sum / (double)InpAtrSmaPeriod;

if(sma <= 0.0) return false;

return (g_bufAtrDump[0] >= sma * InpAtrSpikeMult);

}

bool BullBarsHit()

{

if(!InpUseBullBarsExit || InpBullBarsNeed <= 0) return false;

for(int i = 1; i <= InpBullBarsNeed; ++i)

{

if(iClose(_Symbol, InpDumpTf, i) <= iOpen(_Symbol, InpDumpTf, i))

return false;

}

return true;

}

bool EarlyFlattenTrigger(const int count, const double avg, const double ask, const bool atrSpike)

{

if(InpEarlyExitOnlyIfLayers && count < g_pEarlyExitMinLayers) return false;

if(FloatLossHit(avg, ask)) return true;

if(atrSpike) return true;

if(BullBarsHit()) return true;

return false;

}

bool FarAboveRecentLow()

{

if(!InpUseHighDrawdownStop) return false;

int ll = iLowest(_Symbol, InpHighTf, MODE_LOW, InpHighLookback, 1);

if(ll < 0) return false;

double trough = iLow(_Symbol, InpHighTf, ll);

if(trough <= 0.0) return false;

double c = iClose(_Symbol, InpHighTf, 1);

double uu = (c - trough) / trough * 100.0;

return (uu >= InpHighDrawdownPct);

}

bool DailyBullVeto()

{

if(!InpUseDailyVeto || g_hEmaD1 == INVALID_HANDLE) return false;

ArraySetAsSeries(g_bufEmaD1, true);

if(CopyBuffer(g_hEmaD1, 0, 0, 2, g_bufEmaD1) < 2) return false;

return (iClose(_Symbol, PERIOD_D1, 1) > g_bufEmaD1[1]);

}

bool PauseActive()

{

if(!InpUseExitPause) return false;

return (g_pauseUntil > 0 && TimeCurrent() < g_pauseUntil);

}

void ArmPause(const string reason)

{

if(!InpUseExitPause || g_pPauseHours <= 0) return;

g_pauseUntil = TimeCurrent() + (datetime)g_pPauseHours * 3600;

Print(g_pComment, " PAUSE until ", TimeToString(g_pauseUntil), " after ", reason);

}

void LogEarlyExit()

{

if(g_earlyExitCount < MAX_EXIT_LOG)

g_earlyExitLog[g_earlyExitCount++] = TimeCurrent();

else

{

for(int i = 1; i < MAX_EXIT_LOG; ++i)

g_earlyExitLog[i - 1] = g_earlyExitLog[i];

g_earlyExitLog[MAX_EXIT_LOG - 1] = TimeCurrent();

}

}

int CountEarlyInWindow()

{

if(!InpUseExitFuse) return 0;

datetime cut = TimeCurrent() - (datetime)InpFuseWindowDays * 86400;

int n = 0;

for(int i = 0; i < g_earlyExitCount; ++i)

if(g_earlyExitLog[i] >= cut) n++;

return n;

}

void UpdateFuse(const bool h1Bear)

{

if(!InpUseExitFuse) { g_fuseOn = false; return; }

if(!g_fuseOn && CountEarlyInWindow() >= g_pFuseMaxEarly)

{

g_fuseOn = true;

g_fuseSawBull = !h1Bear;

Print(g_pComment, " FUSE ON earlyCount=", CountEarlyInWindow(),

" windowDays=", InpFuseWindowDays);

}

if(g_fuseOn)

{

if(!h1Bear) g_fuseSawBull = true;

if(InpFuseNeedsH1BearClear)

{

if(g_fuseSawBull && h1Bear)

{

g_fuseOn = false;

g_fuseSawBull = false;

Print(g_pComment, " FUSE OFF — H1 bear restored");

}

}

else if(h1Bear)

{

g_fuseOn = false;

Print(g_pComment, " FUSE OFF");

}

}

}

// First-entry gate: fatal/pause/fuse/loss-halt/HighDD/DailyBull

bool RiskOffNewEntry()

{

if(g_eaFatalStop) return true;

if(PauseActive()) return true;

if(g_fuseOn) return true;

if(LossHaltActive()) return true;

if(g_cFarLow) return true;

if(g_cDailyVeto) return true;

return false;

}

bool RiskOffAdds()

{

if(g_eaFatalStop) return true;

if(g_fuseOn) return true;

if(LossHaltActive()) return true;

if(g_cFarLow) return true;

if(g_cDailyVeto) return true;

return false;

}

int CountSellsRaw(double &avg, double &highest, datetime &lastT, datetime &oldest)

{

int n = 0;

double cost = 0.0, vol = 0.0;

avg = 0.0; highest = 0.0; lastT = 0; oldest = 0;

bool have = false;

for(int i = PositionsTotal() - 1; i >= 0; --i)

{

if(!pos.SelectByIndex(i)) continue;

if(pos.Symbol() != Symbol || pos.Magic() != gpMagic) continue;

if(pos.PositionType() != POSITION_TYPE_SELL) continue;

n++;

double p = pos.PriceOpen();

double v = pos.Volume();

cost += p * v; vol += v;

if(!have || p > highest) { highest = p; have = true; }

if(pos.Time() > lastT) lastT = pos.Time();

if(oldest == 0 || pos.Time() < oldest) oldest = pos.Time();

}

avg = (vol > 0.0 ? cost / vol : 0.0);

return n;

}

void InvalidatePosCache() { g_posDirty = true; }

int CountSells(double &avg, double &highest, datetime &lastT, datetime &oldest)

{

if(g_posDirty)

{

g_cCount = CountSellsRaw(g_cAvg, g_cHighest, g_cLastT, g_cOldest);

g_posDirty = false;

}

avg = g_cAvg;

highest = g_cHighest;

lastT = g_cLastT;

oldest = g_cOldest;

return g_cCount;

}

bool CloseAllSells(const string reason)

{

if(g_closing_in_progress)

{

if(g_close_lock_start > 0 && (TimeCurrent() - g_close_lock_start) < 30)

{

if(StringLen(reason) > 0) g_pendingCloseReason = reason;

return false;

}

Print(g_pComment, " CLOSE LOCK TIMEOUT retry ", reason);

}

if(!TerminalReadyForClose())

{

g_closing_in_progress = true;

if(g_close_lock_start == 0) g_close_lock_start = TimeCurrent();

g_pendingCloseReason = reason;

g_closeDeferredOffline = true;

if(g_lastCloseWarn == 0 || (TimeCurrent() - g_lastCloseWarn) >= 60)

{

g_lastCloseWarn = TimeCurrent();

Print(g_pComment, " CLOSE deferred (terminal/trade not ready) ", reason);

}

return false;

}

g_closing_in_progress = true;

g_close_lock_start = TimeCurrent();

g_pendingCloseReason = reason;

g_closeDeferredOffline = false;

trade.SetExpertMagicNumber(g_pMagic);

trade.SetDeviationInPoints(40);

trade.SetTypeFillingBySymbol(_Symbol);

trade.SetAsyncMode(false);

bool ok = true;

for(int i = PositionsTotal() - 1; i >= 0; --i)

{

if(!pos.SelectByIndex(i)) continue;

if(pos.Symbol() != Symbol || pos.Magic() != gpMagic) continue;

if(pos.PositionType() != POSITION_TYPE_SELL) continue;

ulong ticket = pos.Ticket();

double px = pos.PriceOpen();

double lots = pos.Volume();

double pnl = pos.Profit() + pos.Swap();

int cntBefore = g_cCount;

double avgBefore = g_cAvg;

bool closed = false;

uint lastRc = 0;

for(int attempt = 0; attempt <= InpOrderRetryMax; ++attempt)

{

if(!TerminalReadyForClose())

{

lastRc = TRADE_RETCODE_CONNECTION;

break;

}

if(trade.PositionClose(ticket))

{

closed = true;

break;

}

lastRc = trade.ResultRetcode();

if(!PositionSelectByTicket(ticket))

{

closed = true;

break;

}

if(!IsTransientTradeError(lastRc) || attempt >= InpOrderRetryMax)

break;

SafeSleep(InpOrderRetryMs);

}

if(closed)

{

WriteTradeLog("CLOSE", ticket, px, lots, cntBefore, avgBefore, pnl, reason, 0);

}

else

{

ok = false;

Print(g_pComment, " CLOSE FAIL ", reason, " ticket=", ticket,

" rc=", lastRc, " ", trade.ResultRetcodeDescription());

WriteTradeLog("CLOSE_FAIL", ticket, px, lots, cntBefore, avgBefore, pnl, reason, lastRc);

}

}

InvalidatePosCache();

double a, h; datetime lt, ol;

int left = CountSellsRaw(a, h, lt, ol);

if(left == 0)

{

g_closing_in_progress = false;

g_close_lock_start = 0;

g_pendingCloseReason = "";

g_closeDeferredOffline = false;

g_initPrice = 0.0;

g_gridStep = 0.0;

g_lastSyncedSl = 0.0;

ResetStairLock();

Print(g_pComment, " CLOSE_ALL ", reason);

WriteTradeLog("CLOSE_ALL", 0, 0, 0, 0, 0, 0, reason, 0);

InvalidateMagicRealizedCache();

RefreshLossHaltState(true);

bool isEarly = (StringFind(reason, "EARLY") == 0);

bool isSl = (reason == "SL_0.976");

bool isSpike = (reason == "SPIKE_TRAIL");

if(isEarly)

{

LogEarlyExit();

ArmPause(reason);

}

else if(isSl)

ArmPause(reason);

else if(isSpike && InpSpikePauseOnExit)

ArmPause(reason);

}

return ok;

}

void MaintainCloseLock()

{

if(!g_closing_in_progress) return;

InvalidatePosCache();

double a, h; datetime lt, ol;

int left = CountSellsRaw(a, h, lt, ol);

if(left == 0)

{

g_closing_in_progress = false;

g_close_lock_start = 0;

g_pendingCloseReason = "";

g_closeDeferredOffline = false;

g_initPrice = 0.0;

g_gridStep = 0.0;

ResetStairLock();

return;

}

if(!TerminalReadyForClose())

{

g_closeDeferredOffline = true;

return;

}

bool timedOut = (g_close_lock_start > 0 && (TimeCurrent() - g_close_lock_start) >= 30);

if(timedOut || g_closeDeferredOffline)

{

string why = (StringLen(g_pendingCloseReason) > 0 ? g_pendingCloseReason : "CLOSE_RETRY");

Print(g_pComment, " CLOSE LOCK retry remaining=", left, " reason=", why);

g_closeDeferredOffline = false;

g_closing_in_progress = false;

CloseAllSells(why);

}

}

void SyncSl(const double slPrice, const bool force = false)

{

if(g_eaFatalStop || g_closing_in_progress) return;

datetime bar = iTime(_Symbol, PERIOD_CURRENT, 0);

if(!force && bar != 0 && bar == g_lastSyncSlBar &&

MathAbs(g_lastSyncedSl - slPrice) <= _Point)

return;

int digits = DigitsSym();

double sl = NormalizeDouble(slPrice, digits);

long stops = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);

long freeze = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);

double minD = (double)MathMax(stops, freeze) * _Point;

double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

if(ask <= 0.0) return;

// sell SL must stay above ask + stops

if(sl <= ask + minD)

sl = NormalizeDouble(ask + MathMax(minD, 10 * _Point), digits);

trade.SetExpertMagicNumber(g_pMagic);

trade.SetDeviationInPoints(40);

for(int i = PositionsTotal() - 1; i >= 0; --i)

{

if(!pos.SelectByIndex(i)) continue;

if(pos.Symbol() != Symbol || pos.Magic() != gpMagic) continue;

if(pos.PositionType() != POSITION_TYPE_SELL) continue;

if(MathAbs(pos.StopLoss() - sl) > _Point)

trade.PositionModify(pos.Ticket(), sl, 0.0);

}

g_lastSyncedSl = sl;

g_lastSyncSlBar = bar;

}

double GetMultiAtrStep(double basePrice)

{

double step = basePrice * InpGridStepFrac;

if(step <= 0.0) step = 10 * _Point;

if(!InpUseMultiAtrStep) return step;

if(CopyBuffer(g_hAtrM1, 0, 1, 1, g_bufAtr1) < 1) return step;

if(CopyBuffer(g_hAtrM5, 0, 1, 1, g_bufAtr5) < 1) return step;

if(CopyBuffer(g_hAtrM15, 0, 1, 1, g_bufAtr15) < 1) return step;

if(CopyBuffer(g_hAtrM30, 0, 1, 1, g_bufAtr30) < 1) return step;

if(CopyBuffer(g_hAtrH1, 0, 1, 1, g_bufAtrH1) < 1) return step;

double sM1 = g_bufAtr1[0] * InpAtrMultM1;

double sM5 = g_bufAtr5[0] * InpAtrMultM5;

double sM15 = g_bufAtr15[0] * InpAtrMultM15;

double sM30 = g_bufAtr30[0] * InpAtrMultM30;

double sH1 = g_bufAtrH1[0] * InpAtrMultH1;

step = MathMax(step, sM1);

step = MathMax(step, sM5);

step = MathMax(step, sM15);

step = MathMax(step, sM30);

step = MathMax(step, sH1);

return step;

}

void RefreshCaches()

{

datetime htfBar = iTime(_Symbol, InpHtf, 0);

if(htfBar != 0 && htfBar != g_cacheHtfBar)

{

g_cacheHtfBar = htfBar;

g_cH1Bear = HtfBear();

g_cHtfBullBreak = HtfBullBreak();

}

datetime dumpBar = iTime(_Symbol, InpDumpTf, 0);

if(dumpBar != 0 && dumpBar != g_cacheDumpBar)

{

g_cacheDumpBar = dumpBar;

g_cAtrSpike = AtrSpikeHit();

}

datetime highBar = iTime(_Symbol, InpHighTf, 0);

if(highBar != 0 && highBar != g_cacheHighBar)

{

g_cacheHighBar = highBar;

g_cFarLow = FarAboveRecentLow();

}

datetime d1Bar = iTime(_Symbol, PERIOD_D1, 0);

if(d1Bar != 0 && d1Bar != g_cacheD1Bar)

{

g_cacheD1Bar = d1Bar;

g_cDailyVeto = DailyBullVeto();

}

datetime m1Bar = iTime(_Symbol, PERIOD_M1, 0);

if(m1Bar != 0 && m1Bar != g_cacheM1Bar)

{

g_cacheM1Bar = m1Bar;

double base = (g_initPrice > 0.0 ? g_initPrice : SymbolInfoDouble(_Symbol, SYMBOL_BID));

g_cDynStep = GetMultiAtrStep(base);

}

}

bool OpenSell(const bool isAdd)

{

if(g_eaFatalStop || g_expertRemovePending) return false;

if(!SpreadOk()) return false;

if(!TerminalReadyForNewOrders()) return false;

if(g_closing_in_progress) return false;

trade.SetExpertMagicNumber(g_pMagic);

trade.SetDeviationInPoints(40);

trade.SetTypeFillingBySymbol(_Symbol);

trade.SetAsyncMode(false);

double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

if(!PricesValid(bid, ask)) return false;

double lot = NormLot(FIXED_LOTS);

if(lot <= 0.0) return false;

if(!MarginOkForLot(lot, bid)) return false;

double prevInit = g_initPrice;

double prevStep = g_gridStep;

if(!isAdd)

{

g_initPrice = bid;

g_gridStep = GetMultiAtrStep(g_initPrice);

g_cDynStep = g_gridStep;

}

// SL=0 at send, SyncSl after fill (avoids invalid-stops rejects in tester)

string cmt = MakeOrderComment(isAdd);

bool ok = false;

uint lastRc = 0;

for(int attempt = 0; attempt <= InpOrderRetryMax; ++attempt)

{

ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

if(!PricesValid(bid, ask))

{

lastRc = TRADE_RETCODE_PRICE_OFF;

break;

}

if(attempt > 0)

{

if(!isAdd && g_initPrice <= 0.0) g_initPrice = bid;

}

if(!MarginOkForLot(lot, bid))

{

lastRc = TRADE_RETCODE_NO_MONEY;

break;

}

if(trade.Sell(lot, _Symbol, bid, 0.0, 0.0, cmt))

{

ok = true;

break;

}

lastRc = trade.ResultRetcode();

if(!IsTransientTradeError(lastRc) || attempt >= InpOrderRetryMax)

break;

Print(g_pComment, " SELL retry ", attempt + 1, "/", InpOrderRetryMax,

" rc=", lastRc);

SafeSleep(InpOrderRetryMs);

}

if(ok)

{

InvalidatePosCache();

double a, h; datetime lt, ol;

int cnt = CountSellsRaw(a, h, lt, ol);

double prStep = isAdd ? GetMultiAtrStep(g_initPrice) : g_gridStep;

Print(g_pComment, isAdd ? " GRID" : " OPEN0",

" bid=", bid, " step=", prStep);

WriteTradeLog(isAdd ? "ADD" : "OPEN", trade.ResultOrder(), bid, lot, cnt, a, 0,

isAdd ? "GRID" : "FIRST", 0);

SyncSl(g_initPrice / g_pSlFrac, true);

}

else

{

if(!isAdd)

{

g_initPrice = prevInit;

g_gridStep = prevStep;

}

Print(g_pComment, " SELL FAIL ", lastRc, " ", trade.ResultRetcodeDescription());

WriteTradeLog(isAdd ? "ADD_FAIL" : "OPEN_FAIL", 0, bid, lot, g_cCount, g_cAvg, 0,

"SELL_FAIL", lastRc);

}

return ok;

}

void RecoverInitFromPositions()

{

double lo = 0.0;

bool have = false;

for(int i = PositionsTotal() - 1; i >= 0; --i)

{

if(!pos.SelectByIndex(i)) continue;

if(pos.Symbol() != Symbol || pos.Magic() != gpMagic) continue;

if(pos.PositionType() != POSITION_TYPE_SELL) continue;

if(!have || pos.PriceOpen() < lo) { lo = pos.PriceOpen(); have = true; }

}

if(have && lo > 0.0)

{

g_initPrice = lo;

g_gridStep = GetMultiAtrStep(g_initPrice);

g_cDynStep = g_gridStep;

}

}

int OnInit()

{

ApplyConfigGuard();

ApplyExperiment();

trade.SetExpertMagicNumber(g_pMagic);

trade.SetAsyncMode(false);

if(InpMarginSafetyMult < 1.0)

Print(g_pComment, " WARN: InpMarginSafetyMult < 1.0 — using unsafe margin buffer");

if(InpAccountDdPct > 0.0 && InpAccountDdPct < 1.0)

Print(g_pComment, " WARN: InpAccountDdPct < 1% — very tight DD halt");

if(InpOrderRetryMax < 0)

Print(g_pComment, " WARN: InpOrderRetryMax < 0");

g_lastMarginMode = -1;

WarnIfNettingAccount(true);

g_hEmaFast = iMA(_Symbol, InpHtf, InpHtfEmaFast, 0, MODE_EMA, PRICE_CLOSE);

g_hEmaSlow = iMA(_Symbol, InpHtf, InpHtfEmaSlow, 0, MODE_EMA, PRICE_CLOSE);

g_hStoch = iStochastic(_Symbol, PERIOD_CURRENT, g_pKPeriod, InpDPeriod, InpSlowing,

MODE_SMA, STO_LOWHIGH);

g_hBb = iBands(_Symbol, PERIOD_CURRENT, InpBbPeriod, 0, InpBbDev, PRICE_CLOSE);

g_hAtrDump = iATR(_Symbol, InpDumpTf, InpAtrPeriod);

g_hEmaD1 = iMA(_Symbol, PERIOD_D1, InpDailyEmaPeriod, 0, MODE_EMA, PRICE_CLOSE);

g_hAtrM1 = iATR(_Symbol, PERIOD_M1, InpDynAtrPeriod);

g_hAtrM5 = iATR(_Symbol, PERIOD_M5, InpDynAtrPeriod);

g_hAtrM15 = iATR(_Symbol, PERIOD_M15, InpDynAtrPeriod);

g_hAtrM30 = iATR(_Symbol, PERIOD_M30, InpDynAtrPeriod);

g_hAtrH1 = iATR(_Symbol, PERIOD_H1, InpDynAtrPeriod);

g_hAdxH1 = iADX(_Symbol, PERIOD_H1, 14);

g_hRsiH1 = iRSI(_Symbol, PERIOD_H1, 14, PRICE_CLOSE);

if(g_hEmaFast == INVALID_HANDLE || g_hEmaSlow == INVALID_HANDLE ||

g_hStoch == INVALID_HANDLE || g_hBb == INVALID_HANDLE ||

g_hAtrDump == INVALID_HANDLE || g_hEmaD1 == INVALID_HANDLE ||

g_hAtrM1 == INVALID_HANDLE || g_hAtrM5 == INVALID_HANDLE ||

g_hAtrM15 == INVALID_HANDLE || g_hAtrM30 == INVALID_HANDLE ||

g_hAtrH1 == INVALID_HANDLE ||

g_hAdxH1 == INVALID_HANDLE || g_hRsiH1 == INVALID_HANDLE)

return INIT_FAILED;

ArrayInitialize(g_earlyExitLog, 0);

g_earlyExitCount = 0;

g_pauseUntil = 0;

g_fuseOn = false;

g_fuseSawBull = false;

g_closing_in_progress = false;

g_posDirty = true;

g_pendingCloseReason = "";

g_closeDeferredOffline = false;

g_cacheHtfBar = g_cacheDumpBar = g_cacheHighBar = g_cacheD1Bar = g_cacheM1Bar = 0;

g_lastHbMinute = 0;

g_lastPurgeCheck = 0;

g_wasConnected = (bool)TerminalInfoInteger(TERMINAL_CONNECTED);

g_disconnectCount = 0;

g_dayStamp = 0;

g_weekStamp = 0;

g_dailyLossHalt = false;

g_weeklyLossHalt = false;

g_eaFatalStop = false;

g_expertRemovePending = false;

g_lastCloseWarn = 0;

g_eaStartTime = 0;

g_magicRealizedCached = 0.0;

g_lastMagicRealizedScan = 0;

g_magicRealizedDirty = true;

ResetStairLock();

if(InpUseStairTp)

{

for(int s = 2; s <= STAIR_LEVELS; ++s)

{

double prevAct = StairActPct(s - 1);

double curAct = StairActPct(s);

if(prevAct > 0.0 && curAct > 0.0 && curAct <= prevAct)

Print(g_pComment, " WARN: Stair act pct not ascending at level ", s);

}

}

LoadOrInitStartEquity();

RefreshMagicRealizedCache(true);

RefreshLossHaltState(true);

double a0, h0; datetime lt0, ol0;

if(CountSellsRaw(a0, h0, lt0, ol0) > 0 && g_initPrice <= 0.0)

RecoverInitFromPositions();

if(StringLen(g_pComment) + 2 > ORDER_COMMENT_MAX)

Print(g_pComment, " WARN: g_pComment too long; will truncate for order comment (max ",

ORDER_COMMENT_MAX, ")");

if(!IsTesterMode())

EventSetTimer(1);

Print(g_pComment, " V16.17 SHORT batch-experiment expId=", InpExperimentId,

" blockFri=", g_eBlockFriday ? 1 : 0,

" layers=", g_pMaxGridLayers,

" earlyMinLayers=", g_pEarlyExitMinLayers,

" kPeriod=", g_pKPeriod,

" maxSpread=", g_pMaxSpreadPoints,

" slFrac=", g_pSlFrac,

" floatLoss%=", g_pFloatLossPct,

" basketCap=", g_pBasketLossLimitAmt,

" htfMaxProfit%=", InpHtfExitMaxProfitPct,

" flatHighDD=", g_pFlattenOnHighDD ? 1 : 0,

" pauseH=", g_pPauseHours,

" spikeTrail=", g_pSpikeTrailPct,

" | startEq=", g_startEquity,

" startTime=", TimeToString(g_eaStartTime),

" magicDdPct=", InpAccountDdPct,

" magicDdScanSec=", InpMagicDdScanSec,

" marginCheck=", InpUseMarginCheck,

" blockDisc=", InpBlockNewOnDisconnect,

" dayLimit=", InpDailyLossLimitAmt,

" tester=", IsTesterMode() ? 1 : 0,

" logTag=", LogInstanceTag());

return INIT_SUCCEEDED;

}

void OnDeinit(const int reason)

{

EventKillTimer();

if(g_hEmaFast != INVALID_HANDLE) IndicatorRelease(g_hEmaFast);

if(g_hEmaSlow != INVALID_HANDLE) IndicatorRelease(g_hEmaSlow);

if(g_hStoch != INVALID_HANDLE) IndicatorRelease(g_hStoch);

if(g_hBb != INVALID_HANDLE) IndicatorRelease(g_hBb);

if(g_hAtrDump != INVALID_HANDLE) IndicatorRelease(g_hAtrDump);

if(g_hEmaD1 != INVALID_HANDLE) IndicatorRelease(g_hEmaD1);

if(g_hAtrM1 != INVALID_HANDLE) IndicatorRelease(g_hAtrM1);

if(g_hAtrM5 != INVALID_HANDLE) IndicatorRelease(g_hAtrM5);

if(g_hAtrM15 != INVALID_HANDLE) IndicatorRelease(g_hAtrM15);

if(g_hAtrM30 != INVALID_HANDLE) IndicatorRelease(g_hAtrM30);

if(g_hAtrH1 != INVALID_HANDLE) IndicatorRelease(g_hAtrH1);

if(g_hAdxH1 != INVALID_HANDLE) IndicatorRelease(g_hAdxH1);

if(g_hRsiH1 != INVALID_HANDLE) IndicatorRelease(g_hRsiH1);

}

void OnTimer()

{

if(g_expertRemovePending) return;

CheckConnectionState();

WarnIfNettingAccount(false);

RefreshLossHaltState();

CheckAccountDrawdownHalt();

if(g_eaFatalStop)

{

MaintainCloseLock();

TryFatalStopExit();

return;

}

WriteHeartbeatLine();

}

void OnTradeTransaction(const MqlTradeTransaction &trans,

const MqlTradeRequest &request,

const MqlTradeResult &result)

{

if(trans.type == TRADE_TRANSACTION_DEAL_ADD ||

trans.type == TRADE_TRANSACTION_HISTORY_ADD ||

trans.type == TRADE_TRANSACTION_POSITION)

{

InvalidatePosCache();

if(trans.type == TRADE_TRANSACTION_DEAL_ADD && trans.deal != 0 &&

HistoryDealSelect(trans.deal))

{

if(HistoryDealGetString(trans.deal, DEAL_SYMBOL) == _Symbol &&

(long)HistoryDealGetInteger(trans.deal, DEAL_MAGIC) == g_pMagic)

{

long entry = HistoryDealGetInteger(trans.deal, DEAL_ENTRY);

if(entry == DEAL_ENTRY_OUT || entry == DEAL_ENTRY_INOUT ||

entry == DEAL_ENTRY_OUT_BY)

{

InvalidateMagicRealizedCache();

RefreshLossHaltState(true);

}

}

}

}

}

void OnTick()

{

if(g_expertRemovePending) return;

CheckConnectionState();

RefreshLossHaltState();

CheckAccountDrawdownHalt();

if(g_eaFatalStop)

{

MaintainCloseLock();

TryFatalStopExit();

return;

}

if(IsStopped()) return;

MaintainCloseLock();

if(g_closing_in_progress) return;

RefreshCaches();

double avg = 0.0, highest = 0.0;

datetime lastT = 0, oldest = 0;

int count = CountSells(avg, highest, lastT, oldest);

if(count == 0)

{

g_initPrice = 0.0;

g_gridStep = 0.0;

ResetStairLock();

}

else if(g_initPrice <= 0.0)

RecoverInitFromPositions();

UpdateFuse(g_cH1Bear);

double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

if(!PricesValid(bid, ask)) return;

if(count > 0 && g_pFlattenOnHighDD && g_cFarLow)

{

CloseAllSells("RISKOFF_FROM_LOW");

ArmPause("RISKOFF_FROM_LOW");

return;

}

if(count > 0 && InpUseBasketLossCap && g_pBasketLossLimitAmt > 0.0)

{

double basketFloat = 0.0;

for(int bi = PositionsTotal() - 1; bi >= 0; --bi)

{

if(!pos.SelectByIndex(bi)) continue;

if(pos.Symbol() != Symbol || pos.Magic() != gpMagic) continue;

if(pos.PositionType() != POSITION_TYPE_SELL) continue;

basketFloat += pos.Profit() + pos.Swap();

}

if(basketFloat <= -g_pBasketLossLimitAmt)

{

CloseAllSells("BASKET_LOSS_CAP");

ArmPause("BASKET_LOSS_CAP");

return;

}

}

if(count > 0 && avg > 0.0)

{

bool spikeGreen = (g_spikeArmed && ask < avg);

if(!(InpSpikeBlockEarly && spikeGreen) &&

EarlyFlattenTrigger(count, avg, ask, g_cAtrSpike))

{

string why = "EARLY";

if(FloatLossHit(avg, ask)) why = "EARLY_FLOAT";

else if(g_cAtrSpike) why = "EARLY_ATR_H1";

else if(BullBarsHit()) why = "EARLY_BULL_H1";

CloseAllSells(why);

return;

}

}

if(count > 0 && InpExitOnHtfBull && g_cHtfBullBreak)

{

bool spikeGreen = (g_spikeArmed && ask < avg);

double profitPctHtf = (avg > 0.0 && ask > 0.0) ? ((avg / ask - 1.0) * 100.0) : 0.0;

bool tooGreenForHtf = (profitPctHtf > InpHtfExitMaxProfitPct);

// Skip HTF flatten when spiked-green OR already past small profit — leave to stair/spike trail

if(!(InpSpikeBlockHtf && spikeGreen) && !tooGreenForHtf)

{

CloseAllSells("HTF_BULL_BREAK");

ArmPause("HTF_BULL_BREAK");

return;

}

}

if(count > 0 && avg > 0.0 && g_initPrice > 0.0)

{

double baseSl = g_initPrice / g_pSlFrac;

double effectiveSl = baseSl;

bool spikeLockActive = false;

if(InpUseStairTp)

{

UpdateStairLock(avg, ask);

if(g_stairLevel > 0 && g_stairLockPrice > 0.0)

effectiveSl = MathMin(baseSl, g_stairLockPrice);

}

if(UpdateSpikeTrail(avg, ask))

{

if(g_spikeLockPrice > 0.0 && g_spikeLockPrice < effectiveSl)

{

effectiveSl = g_spikeLockPrice;

spikeLockActive = true;

}

}

SyncSl(effectiveSl, false);

if(ask >= effectiveSl)

{

if(spikeLockActive && g_spikeArmed)

CloseAllSells("SPIKE_TRAIL");

else if(g_stairLevel > 0)

CloseAllSells("STAIR_LOCK");

else

CloseAllSells("SL_0.976");

return;

}

if(!InpUseStairTp || !InpStairLockOnly)

{

double tpPrice = avg / TpMultByCount(count);

if(ask <= tpPrice)

{

Print(g_pComment, " TP count=", count, " avg=", avg);

CloseAllSells("BASKET_TP");

return;

}

}

bool allowAdd = true;

if(InpBlockAddOnBull && !g_cH1Bear) allowAdd = false;

if(InpPauseAddOnAtrSpike && g_cAtrSpike) allowAdd = false;

if(InpUseBullBarsExit && BullBarsHit()) allowAdd = false;

if(RiskOffAdds()) allowAdd = false;

if(InpUseBasketLossCap && g_pBasketLossLimitAmt > 0.0)

{

double bleed = 0.0;

for(int ai = PositionsTotal() - 1; ai >= 0; --ai)

{

if(!pos.SelectByIndex(ai)) continue;

if(pos.Symbol() != Symbol || pos.Magic() != gpMagic) continue;

if(pos.PositionType() != POSITION_TYPE_SELL) continue;

bleed += pos.Profit() + pos.Swap();

}

if(bleed <= -0.5 * g_pBasketLossLimitAmt)

allowAdd = false;

}

double dynStep = (g_cDynStep > 0.0 ? g_cDynStep : GetMultiAtrStep(g_initPrice));

if(allowAdd && count < g_pMaxGridLayers && dynStep > 0.0)

{

int barsPassed = (lastT > 0 ? iBarShift(_Symbol, PERIOD_CURRENT, lastT) : 0);

if(barsPassed < 0) barsPassed = 0;

if(barsPassed >= g_pCoolBars && bid >= highest + dynStep)

OpenSell(true);

}

}

datetime t = iTime(_Symbol, PERIOD_CURRENT, 0);

if(t == 0 || t == g_lastBar) return;

g_lastBar = t;

if(count != 0) return;

// --- V16.17: experiment filters (time / ADX / RSI / EMA-distance), first-entry only ---

if(EntryBlockedByTime()) return;

if(EntryBlockedByIndicators()) return;

// --- V16.02 first-entry: gate then M1 KD/BB trigger ---

if(RiskOffNewEntry()) return;

if(!g_cH1Bear) return;

if(!FirstEntrySignal()) return;

OpenSell(false);

}