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);
}