nyao_scalper_mt5/nyao_scalper.mq5

5868 lines
No EOL
251 KiB
MQL5

// +------------------------------------------------------------------+
// | Nyao Scalper v43.0 |
// | Indicator-Based Signal Strength EA with Comprehensive Features |
// | © Copyright Nyao Scalper by Elriz Wiraswara |
// +------------------------------------------------------------------+
#property copyright "© Copyright Nyao Scalper by Elriz Wiraswara"
#property version "43.0"
#property description "Auto Trading EA Robot with Comprehensive Features"
#property description ""
#property description "This is an open-source project for educational and experimental purposes only"
#property description "Source: https://github.com/elrizwiraswara/nyao_scalper_mt5 [BSD-3-Clause]"
#property description ""
#property description "No guarantee of profitability. Use at your own risk. Past performance ≠ future results"
#property description "Built with significant effort, please use and share respectfully"
#property description "I do not sell this EA myself. If sold under my name, treat it as a scam and report it"
#property description "Named after my cat MaoMao, he says 'Nyao!' when spotting good trades"
#property strict
// Windows API for Algo Trading Button Control
#define MT_WMCMD_EXPERTS 32851
#define WM_COMMAND 0x0111
#define GA_ROOT 2
#include <WinAPI\winapi.mqh>
// Dialog Controls for Password Input
#include <Controls\Dialog.mqh>
#include <Controls\Edit.mqh>
#include <Controls\Button.mqh>
#include <Controls\Label.mqh>
// Print wrapper with logging control
#define LogPrint if(EnableLogging) Print
enum ENUM_INPUT_TYPE
{
INPUT_DOLLAR, // Dollar Amount
INPUT_PERCENT, // Percent of Equity
INPUT_POINTS // Points
};
enum ENUM_RR_RISK_MODE
{
RR_RISK_MANUAL, // Manual Distance
RR_RISK_ATR // Auto (ATR-Based)
};
enum ENUM_LIMIT_ANCHOR
{
LIMIT_ANCHOR_FIXED_ATR, // Fixed ATR Fraction (flat depth)
LIMIT_ANCHOR_EMA, // Fast EMA
LIMIT_ANCHOR_SWING, // Swing Level (structure)
LIMIT_ANCHOR_SMART // Nearer of Swing/EMA (ATR-capped)
};
input group "+-----------------------------------------+"
input group " Nyao Scalper v43.0"
input group " © Copyright Nyao Scalper by Elriz Wiraswara"
input group "+-----------------------------------------+"
// +------------------------------------------------------------------+
// | Input Parameters |
// +------------------------------------------------------------------+
input group "📊 Indicator Settings"
input int DirectionalBodyLookback = 10; // Lookback for directional body analysis
input int EMAFastPeriod = 5; // EMA Fast Period
input int EMASlowPeriod = 12; // EMA Slow Period
input int SlopeLookback = 3; // EMA Slope Lookback Bars (noise reduction)
input int RSIPeriod = 8; // RSI Period
input int ATRPeriod = 8; // ATR Period
input int ATRAvgLookback = 10; // ATR Average Lookback
input double MinVolRatioToTrade = 0.6; // Min ATR/AvgATR Ratio to Trade (0 = Disabled, blocks dead market)
input int ImpulseLookback = 3; // Impulse Lookback
input double ImpulseBoostWeight = 1.0; // Impulse Boost Weight
input int SignalSmoothingCandles = 2; // Closed Candles for Weighted Average (1-10)
input double CurrentCandleBlend = 0.40; // Current Candle Blend Factor (0.0-1.0)
input double VelocityWindow = 2.0; // Velocity Window (Score Delta)
input int RSIOverbought = 80; // RSI Overbought Level (Max Buy)
input int RSIOversold = 20; // RSI Oversold Level (Min Sell)
input int RSIMomentumBuy = 60; // RSI Momentum Buy Trigger
input int RSIMomentumSell = 40; // RSI Momentum Sell Trigger
input group "⚖️ Score Weight Settings"
input double TrendWeight = 1.5; // Trend Alignment Initial Weight
input double SlopeWeight = 1.5; // Trend Slope Confirmation Weight
input double MomentumBaseWeight = 1.0; // Momentum Base Weight (RSI Sweet Spot)
input double MomentumTriggerWeight = 0.5; // Momentum Trigger Weight (RSI Breakout)
input double BodyMomentumWeight = 1.5; // Body Momentum Weight
input double ChopScoreHigh = 2.0; // Chop Score High (Strong Trend)
input double ChopScoreMed = 1.0; // Chop Score Med (Weak Trend)
input double ChopScoreLow = 0.0; // Chop Score Low (Chop Risk - no free points)
input double VolatilityScoreHigh = 1.0; // Volatility Score High
input double VolatilityScoreLow = 0.0; // Volatility Score Low (no free points)
input double PeakScoreWeight = 1.0; // Peak Breakout Score Weight
input double WickRejectionWeight = 1.0; // Wick Rejection Penalty Weight
input double MinBodyRatio = 1.5; // Min Body Ratio for Wick Calculation
input group "📝 Order & Position Settings"
input bool EnableBuyOrders = true; // Enable Buy Orders
input bool EnableSellOrders = true; // Enable Sell Orders
input bool EnableNewBarEntryOnly = true; // Evaluate/Open Entries Only on New Bar (stable signals)
input bool EnableMaxSpreadFilter = true; // Block New Entries When Spread Too Wide
input double MaxSpreadPoints = 0; // Max Spread in Points (0 = Auto: ATR-based)
input double MaxSpreadATRRatio = 0.25; // Auto Spread Cap as Fraction of ATR (when MaxSpreadPoints = 0)
input double BaseLotSize = 0.01; // Base Lot Size
input int MaxOpenOrders = 8; // Max Consecutive Open Orders
input int MaxTradesPerCandle = 1; // Max Trades Per Candle (0 = Unlimited)
input double ConsecutiveCandleThresholdBoost = 1.0; // Signal Threshold Boost Per Consecutive Trading Candle
input int MaxConsecutiveCandleBoosts = 3; // Max Consecutive Candle Boosts (0 = Unlimited)
input double ZonePoints = 500; // Zone Points to Avoid Duplicate Signals
input double BuyDuplicateMultiplier = 1.5; // Min Distance Multiplier to Avoid Duplicate Buy Signals
input double SellDuplicateMultiplier = 1.5; // Min Distance Multiplier to Avoid Duplicate Sell Signals
input double MinBreakEvenProfit = 0.5; // Min Profit to Trigger Break-Even ($ | 0 = Disabled)
input double ProfitThresholdMultiplier = 1.5; // Threshold Multiplier for Min Break-Even Profit
input double LossThresholdMultiplier = 2.0; // Threshold Multiplier for Max Break-Even Loss
input double MinBuySignalScore = 4.5; // Min Signal Strength Score to Buy (0.0 - 10.0)
input double MinSellSignalScore = 4.5; // Min Signal Strength Score to Sell (0.0 - 10.0)
input group "🎯 Limit Entry Settings"
input bool EnableLimitEntry = false; // Fresh Entries Use Pending Limit (pullback) Instead of Market
input ENUM_LIMIT_ANCHOR LimitEntryAnchor = LIMIT_ANCHOR_FIXED_ATR; // Pullback Anchor (Smart = nearer of Swing/EMA)
input double LimitEntryATRFraction = 0.25; // Pullback Depth / ATR Cap as Fraction of ATR (below Ask / above Bid)
input int LimitEntryExpiryBars = 1; // Cancel Unfilled Limit After N Bars (0 = no expiry)
input bool LimitEntryCancelOnFlip = true; // Cancel Pending When Directional Signal Drops Below Threshold
input group "🛡️ Signal Dampening Settings"
input bool EnableSignalDampening = true; // Enable Position-Aware Signal Dampening
input int MaxLosingPositionsSameDir = 2; // Max Losing Positions in Same Direction Before Block
input double LosingPosScorePenalty = 1.5; // Score Penalty Per Losing Same-Direction Position
input double DrawdownThresholdPct = 3.0; // Equity Drawdown % to Raise Signal Threshold
input double DrawdownScoreBoost = 2.0; // Extra Score Required During Drawdown
input int ConsecutiveLossesBeforeCooldown = 3; // Consecutive Losses Before Cooldown Activates
input int ConsecutiveLossCooldownBars = 3; // Cooldown Duration (Bars) After Threshold Reached
input group "🩺 Loss Management Settings"
input bool EnableLossManagement = true; // Enable Adaptive Loss Management
input int MaxHoldingLossPositions = 2; // Max Losing Positions to Hold
input double MinHealthScore = 0.40; // Min Health Score to Hold Position (0.0 - 1.0)
input double MaxAdverseATR = 1.5; // Max Adverse Movement in ATR Multiples
input double HealthTrendWeight = 0.40; // Health Weight: Trend Alignment
input double HealthRSIWeight = 0.25; // Health Weight: RSI Zone
input double HealthATRWeight = 0.25; // Health Weight: Adverse Excursion
input double HealthSwingWeight = 0.10; // Health Weight: Swing Level
input double HealthRSIBuyMin = 40.0; // Health RSI Min for Buy Position
input double HealthRSISellMax = 60.0; // Health RSI Max for Sell Position
input int HealthSwingLookback = 20; // Swing Level Lookback Bars
input int HealthGraceBars = 2; // Grace Period (Bars Before Health Check)
input bool EnablePartialClose = true; // Enable Scaled Partial Close on Signal Decay
input double PartialClose75Pct = 0.25; // Close % When Signal Drops to 75% of Initial
input double PartialClose50Pct = 0.50; // Close % When Signal Drops to 50% of Initial
input double PartialClose25Pct = 1.00; // Close % When Signal Drops to 25% of Initial (Remaining)
input bool EnableHealthSLTightening = true; // Tighten SL as Health Weakens
input double SLTightenATRMultiplier = 2.0; // ATR Multiplier for Tightened SL
input double SLTightenMinHealthPct = 0.50; // Start Tightening Below This Health %
input bool EnableBreakEvenOnSpread = true; // Lock SL to Entry After Profit > Spread Cost
input double BreakEvenSpreadMultiplier = 1.5; // Spread Multiplier for Break-Even Lock Trigger
input bool EnableVirtualSLReentry = true; // Close at Threshold Then Re-evaluate & Re-enter
input bool ReentryRespectsNewBarGate = false; // Re-entry Obeys New-Bar Entry Gate (no intrabar re-entry)
input double ReentryMinSignalPct = 0.75; // Min % of Entry Signal Required to Re-enter
input bool EnableProfitOffsetSL = true; // Tighten SL of Losing Pos by Consecutive Closed Profits
input int ConsecutiveWinsRequired = 3; // Min Consecutive Wins Before Offset Applies
input double MinOffsetProfit = 1.0; // Min Accumulated Profit ($) to Trigger SL Offset
input group "🔀 Hedge Chain (Rolling Martingale Recovery) Settings"
input bool EnableHedgeChain = true; // Enable Hedge Chain (MARTINGALE - high risk)
input double HedgeTriggerATR = 1.5; // Adverse Move (ATR) to Start the Chain
input bool HedgeRequireSignal = true; // Only Hedge if Reverse Signal Confirms (anti-spike)
input double HedgeMinSignalScore = 4.5; // Min Reverse-Direction Score to Open Hedge
input bool HedgeAutoLot = true; // Auto-size Hedge Lot to Recover (else Multiplier)
input double HedgeRecoveryATR = 1.0; // Favorable Move (ATR) to Recover Within
input double HedgeLotMultiplier = 2.0; // Fixed Hedge Lot Multiplier (Auto-size OFF)
input double HedgeMaxLot = 0.10; // Hard Lot Ceiling Per Hedge Leg
input double HedgeRecoveryPct = 110.0; // Close Older Leg When Hedge Covers This % Loss
input double HedgeRollMinProfit = 0.5; // Min Older-Leg Profit ($) to Roll
input int HedgeCycleLevels = 2; // Max Hedge Levels Per Cycle Before Reseed
input bool EnableHedgeCycleReset = false; // Reseed New Cycle at Limit (else Close Chain)
input double HedgeCyclePartialPct = 50.0; // % of Deepest Hedge to Close at Reseed
input int HedgeMaxCycles = 3; // Max Cycles Before Closing Chain (0 = Unlimited)
input double HedgeMaxChainLossUSD = 0.0; // Close Chain if Loss >= this $ (0 = Off)
input double HedgeMaxChainLossPct = 0.0; // Close Chain if Loss >= this % Equity (0 = Off)
input bool HedgeClearRootSL = true; // Clear First Position SL on Chain Start
input double HedgeTrailATR = 0.5; // Graduated Hedge Trail Distance (ATR; 0 = normal trailing)
input group "🧮 Dynamic Lot Sizing Settings"
input bool EnableDynamicLots = true; // Enable Dynamic Lot Sizing
input double EquityDropPercent = 5.0; // Equity Drop % per Lot Step
input int MaxEquityDropLotSteps = 2; // Max Drawdown-Based Lot Steps (0 = Unlimited)
input double MinSignalStrengthForLot = 8.0; // Min Signal Score for Lot Increase
input double LotStepSize = 0.01; // Lot Increase Step Size
input double MaxLotSize = 0.05; // Max Lot Size
input group "🏦 Equity Settings"
input bool EnableBasketStop = true; // Close All When Total Floating Loss Exceeds Limit
input double MaxBasketLossPct = 8.0; // Max Total Floating Loss as % of Equity (0 = Disabled)
input double MinEquityPercent = 70.0; // Min Equity % from Peak - Pause Trading when Reached
input double MaxDrawdownFromPeak = 0; // Max Equity $ Drawdown - Pause Trading when Reached (0 = Disabled)
input int PauseMinutes = 5; // Pause Duration (Minutes)
input double PauseMinutesMultiplier = 1.5; // Multiply Pause Duration on Each Trigger
input int MaxPauseMinutes = 120; // Max Pause Duration Minutes (0 = Max 24,855 days)
input int MaxMinEquityTriggers = 0; // Max Times Trigger - Stop Trading when Reached (0 = Unlimited)
input bool ResetOnNewPeak = true; // Reset Min Equity Triggers on New Peak Equity
input double TargetEquity = 0; // Target Equity - Stop Trading when Reached (0 = Disabled)
input double MinimumEquity = 20; // Min Equity - Stop Trading when Reached (0 = Disabled)
input group "📈 Take Profit Settings"
input bool EnableTakeProfit = false; // Enable Take Profit
input ENUM_INPUT_TYPE TPInputType = INPUT_DOLLAR; // TP Input Type
input double TPValue = 10.0; // TP Value
input group "📉 Stop Loss Settings"
input bool EnableStopLoss = true; // Enable Stop Loss
input ENUM_INPUT_TYPE SLInputType = INPUT_PERCENT; // SL Input Type
input double SLValue = 10.0; // SL Value
input group "⚖️ Risk:Reward Settings"
input bool EnableRiskReward = false; // Enable Independent R:R SL/TP (overrides manual SL & TP)
input ENUM_RR_RISK_MODE RRRiskMode = RR_RISK_ATR; // Risk (SL) Sizing: Manual or Auto ATR
input ENUM_INPUT_TYPE RRRiskInputType = INPUT_POINTS; // Manual Risk Input Type (when Mode = Manual)
input double RRRiskValue = 200.0; // Manual Risk Distance (SL leg, when Mode = Manual)
input double RRAtrMultiplier = 1.5; // Auto Risk: SL = ATR × this (when Mode = ATR)
input double RiskRewardRatio = 1.5; // Reward : Risk (TP distance = SL distance × this)
input group "💸 Trailing TP/SL Settings"
input bool EnableTrailing = true; // Enable Trailing TP/SL
input bool TrailingEnableBreakEvenLock = true; // Enable Trailing Break-Even Lock
input bool TrailingSLOnProfitableOnly = true; // Trailing SL on Profitable Position Only
input bool EnableAdaptiveTP = true; // Enable Adaptive TP
input bool EnableAdaptiveSL = true; // Enable Adaptive SL
input ENUM_INPUT_TYPE TSInputType = INPUT_DOLLAR; // Trailing Distance Input Type
input double TrailingDistanceValue = 0.2; // Trailing Distance Value
input double TrailingValueMultiplier = 0.2; // Trailing Value Multiplier
input group "🤖 Robot Settings"
input int MagicNumber = 6926268; // Magic Number
input bool EnableDiscordAlerts = false; // Enable Discord Alerts
input string DiscordWebhookURL = ""; // Discord Webhook URL
input bool EnableTradingHours = false; // Enable Trading Hours
input string TradingStartTime = "00:00"; // Trading Start Time (HH:MM)
input string TradingEndTime = "23:59"; // Trading End Time (HH:MM)
input bool EnableReports = true; // Enable Trading Reports
input int SendReportEveryHour = 1; // Send Report Every (n) Hours
input bool EnableMarketCloseFilter = true; // Stop Opening New Positions Near Market Close Hour
input int MinutesBeforeClose = 30; // Stop Opening Minutes Before Market Close
input bool EnableNewsFilter = true; // Enable News Filter (Pause Trading During News)
input int NewsMinutesBefore = 30; // Minutes Before News Event
input int NewsMinutesAfter = 30; // Minutes After News Event
input bool EnableLeveragePause = true; // Pause Trading When Leverage Changed
input bool EnableLogging = false; // Enable EA Logging (May cause lag)
// +------------------------------------------------------------------+
// | Global Variables |
// +------------------------------------------------------------------+
// EMBEDDED PASSWORD - Change this to your desired password (leave empty to disable)
// const string EA_PASSWORD = "maomao chou kawaii";
const string EA_PASSWORD = "";
// Password Dialog Controls
CDialog passwordDialog;
CEdit passwordEdit;
CButton passwordSubmitBtn;
bool passwordVerified = false;
bool passwordDialogActive = false;
double initialBalance = 0; // Initial Account Balance
double peakEquity = 0; // Peak Equity Recorded
double lastPeakEquity = 0; // Last recorded peak equity for drawdown calculations
bool targetEquityReached = false; // Flags for target/minimum equity reached
bool minimumEquityReached = false; // Flags for target/minimum equity reached
bool minEquityTriggersExceeded = false; // Flag when max triggers exceeded
int minEquityTriggerCount = 0; // Counter for MinEquityPercent triggers
bool isPaused = false; // Trading pause state
int currentPauseDuration = 0; // Current pause duration in minutes
datetime pauseStartTime = 0; // Pause start time
bool isOutsideTradingHours = false; // Flag when outside trading hours
bool isLeverageDiffFromInitial = false; // Flag for leverage changed
bool isNearMarketClose = false; // Flag for near market close time
ulong lastProcessedNewsEventID = 0; // Last processed news event ID
string symbolBaseCurrency = ""; // Base currency of the symbol
string symbolQuoteCurrency = ""; // Quote currency of the symbol
long initialLeverage = 0; // Initial Account Leverage
bool isOrderSendLocked = false; // Flag for locking OrderSend execution
bool marketCloseAlertSent = false; // Flag for near market close time
bool algoTradingStatus = false; // Flag for algo trading status
// Normalized Health Weights
double normHealthTrendWeight = 0;
double normHealthRSIWeight = 0;
double normHealthATRWeight = 0;
double normHealthSwingWeight = 0;
// Duplicate Signal Filter Variables
datetime startTime = 0; // EA Start Time
datetime lastDailyReportTime = 0; // Last time daily report was sent
double lastReportEquity = 0; // Equity at last report
// Pause Tracking
int totalPauseCount = 0; // Total number of times trading was paused
double totalPauseDurationMinutes = 0; // Total duration of pauses in minutes
int emaFastHandle = INVALID_HANDLE; // Handle for Fast EMA
int emaSlowHandle = INVALID_HANDLE; // Handle for Slow EMA
int rsiHandle = INVALID_HANDLE; // Handle for RSI
int atrSignalHandle = INVALID_HANDLE; // Handle for Signal ATR
// Signal Strength Structure - Indicator-Based Scoring System
// Weights are adjustable via Score Weight Settings inputs
struct SignalStrength
{
double avgBody; // Average body size of matching candles
double bodySignal; // Body size of signal candle
double ratio; // Ratio of bodySignal / avgBody
double upperWick; // Upper wick size
double lowerWick; // Lower wick size
double rejection; // Wick to body ratio
double penaltyBody; // Penalty from body ratio
double penaltyWick; // Penalty from wick rejection
double finalScore; // 0.00-10.00 Score
double trendScore; // Trend Component (0-3)
double momentumScore; // Momentum Component (0-3)
double chopScore; // Chop Component (0-2)
double peakScore; // Peak Component (0-1)
double volatilityScore; // Volatility Component (0-1)
double impulseStrength; // 0.0-1.0 Impulse Strength
double velocity; // Current Score - Previous Score
double normalizedVelocity; // 0.0-1.0 Normalized Velocity
string reasoning; // Detailed explanation
};
// Position Health Structure - Measurement-Based Revalidation
// Evaluates whether a position's trade thesis is still valid
struct PositionHealth
{
double healthScore; // 0.0 (dead) to 1.0 (fully healthy)
bool trendValid; // EMA still aligned with position direction?
bool momentumValid; // RSI still in favorable zone?
double adverseATR; // How many ATRs moved against position
bool swingValid; // Price hasn't broken swing level?
bool inGracePeriod; // Position too new for health check?
string reason; // Human-readable invalidation reason
};
// Managed Position Structure - For Position Tracking
// Stores position info to avoid repeated MQL function calls
struct ManagedPosition
{
ulong ticket; // Position ticket ID
ENUM_POSITION_TYPE type; // Buy or Sell
double signalScore; // Initial signal score
double entryPrice; // Entry price for adverse excursion calc
int partialCloseLevel; // 0=none, 1=75% triggered, 2=50% triggered, 3=fully closed
bool breakEvenLocked; // Whether SL has been moved to break-even by loss mgmt
int profitOffsetConsecWins; // Consecutive winning trades closed since this position opened
double profitOffsetAccumulated; // Accumulated profit from consecutive wins (USD)
double profitOffsetOriginalSL; // Original SL price when position was opened
ulong chainId; // Rolling-hedge chain id (current cycle's root ticket); 0 = standalone
int hedgeLevel; // Level within the cycle: 0 = root, 1+ = each successive hedge
double chainAnchorLoss; // Cycle start loss ($, positive) carried on every leg of the cycle
int cycleNum; // Which cycle this leg belongs to (0 = first; +1 on each reseed)
bool noRehedge; // true = exhausted chain released to loss mgmt; never start a new chain on it
bool hedgeGraduated; // true = former chain leg; trail with HedgeTrailATR (lot-independent) not the $ distance
double hedgeLockProfit; // min profit ($) to keep locked on a graduated hedge (recovery floor); 0 = none
};
// Managed positions array
ManagedPosition managedPositions[];
int managedPositionCount = 0;
// Candle-based Position Counters
datetime currentBarTime = 0;
int buysOnCurrentBar = 0;
int sellsOnCurrentBar = 0;
// New-Bar Entry Gate (only evaluate entries once per closed bar when enabled)
datetime lastEntryBarTime = 0;
// Consecutive Trading Candle Tracker (for threshold escalation)
int consecutiveBuyCandles = 0; // How many consecutive candles opened buy positions
int consecutiveSellCandles = 0; // How many consecutive candles opened sell positions
bool prevBarHadBuys = false; // Whether the previous bar opened buy positions
bool prevBarHadSells = false; // Whether the previous bar opened sell positions
// Signal Dampening Globals
int consecutiveLossCount = 0; // Track consecutive closing losses
datetime cooldownUntilBarTime = 0; // Bar time after which cooldown expires
// Last Position Tracking
datetime lastBuyTime = 0;
double lastBuyPrice = 0;
datetime lastSellTime = 0;
double lastSellPrice = 0;
// Last signal tracking per candle
double lastBuySignalScore = 0;
double lastBuySignalScorePrev = 0;
double lastBuyVelocity = 0;
double lastBuyNormalizedVelocity = 0;
double lastSellSignalScore = 0;
double lastSellSignalScorePrev = 0;
double lastSellVelocity = 0;
double lastSellNormalizedVelocity = 0;
// Per-tick signal cache (invalidated each tick)
bool _buyStrengthValid = false;
bool _sellStrengthValid = false;
SignalStrength _cachedBuyStrength;
SignalStrength _cachedSellStrength;
// Trade Statistics Structure
struct TradeStats
{
int count;
int won;
int lost;
double profit; // Total net profit
double loss; // Total net loss (sum of negative profits)
double avgProfit; // Average of winning trades
double maxProfit; // Largest single profit
double minProfit; // Smallest single profit
double avgLoss; // Average of losing trades
double maxLoss; // Largest single loss (most negative)
double minLoss; // Smallest single loss (closest to 0)
};
// +------------------------------------------------------------------+
// | Create Password Dialog |
// +------------------------------------------------------------------+
bool CreatePasswordDialog()
{
if(!passwordDialog.Create(0, "PasswordDialog", 0, 10, 10, 324, 120))
return false;
passwordDialog.Caption("Enter Password to Use Nyao Scalper EA");
if(!passwordEdit.Create(0, "PasswordEdit", 0, 5, 10, 300, 35))
return false;
passwordEdit.Text("");
if(!passwordDialog.Add(passwordEdit))
return false;
if(!passwordSubmitBtn.Create(0, "PasswordSubmit", 0, 5, 45, 100, 75))
return false;
passwordSubmitBtn.Text("Submit");
if(!passwordDialog.Add(passwordSubmitBtn))
return false;
return true;
}
// +------------------------------------------------------------------+
// | Expert Initialization Function |
// +------------------------------------------------------------------+
int OnInit()
{
// Password protection - show dialog if password is set
if(EA_PASSWORD != "")
{
passwordVerified = false;
passwordDialogActive = true;
if(!CreatePasswordDialog())
{
Alert("ERROR: Failed to create password dialog!");
return(INIT_FAILED);
}
Print("🔐 Password required. Please enter password in the dialog on chart.");
return(INIT_SUCCEEDED);
}
else
{
passwordVerified = true;
passwordDialogActive = false;
}
// Continue with normal initialization
return(InitializeEA());
}
// +------------------------------------------------------------------+
// | Full EA Initialization |
// +------------------------------------------------------------------+
int InitializeEA()
{
if(BaseLotSize <= 0)
{
Alert("ERROR: BaseLotSize must be greater than 0");
return(INIT_PARAMETERS_INCORRECT);
}
if(MaxLotSize < BaseLotSize)
{
Alert("ERROR: MaxLotSize must be >= BaseLotSize");
return(INIT_PARAMETERS_INCORRECT);
}
if(!EnableBuyOrders && !EnableSellOrders)
{
Alert("ERROR: Both Buy and Sell orders are disabled! EA will not trade!");
return(INIT_PARAMETERS_INCORRECT);
}
string tradingHoursTestParts[];
if(StringSplit(TradingStartTime, ':', tradingHoursTestParts) != 2)
{
Alert("ERROR: Invalid TradingStartTime format. Use HH:MM");
return(INIT_PARAMETERS_INCORRECT);
}
if(StringSplit(TradingEndTime, ':', tradingHoursTestParts) != 2)
{
Alert("ERROR: Invalid TradingEndTime format. Use HH:MM");
return(INIT_PARAMETERS_INCORRECT);
}
// Normalize health weights to sum to 1.0
double healthWeightSum = HealthTrendWeight + HealthRSIWeight + HealthATRWeight + HealthSwingWeight;
if(healthWeightSum > 0)
{
normHealthTrendWeight = HealthTrendWeight / healthWeightSum;
normHealthRSIWeight = HealthRSIWeight / healthWeightSum;
normHealthATRWeight = HealthATRWeight / healthWeightSum;
normHealthSwingWeight = HealthSwingWeight / healthWeightSum;
if(MathAbs(healthWeightSum - 1.0) > 0.001)
{
Print("⚠️ Health weights sum to ", DoubleToString(healthWeightSum, 3), ", normalized to 1.0");
}
}
else
{
// Fallback: equal weights
normHealthTrendWeight = 0.25;
normHealthRSIWeight = 0.25;
normHealthATRWeight = 0.25;
normHealthSwingWeight = 0.25;
Print("⚠️ All health weights are 0, defaulting to equal weights (0.25 each)");
}
// Initialize Signal Indicators
emaFastHandle = iMA(_Symbol, _Period, EMAFastPeriod, 0, MODE_EMA, PRICE_CLOSE);
if(emaFastHandle == INVALID_HANDLE)
{
Print("Error creating Fast EMA handle!");
return(INIT_FAILED);
}
emaSlowHandle = iMA(_Symbol, _Period, EMASlowPeriod, 0, MODE_EMA, PRICE_CLOSE);
if(emaSlowHandle == INVALID_HANDLE)
{
Print("Error creating Slow EMA handle!");
return(INIT_FAILED);
}
rsiHandle = iRSI(_Symbol, _Period, RSIPeriod, PRICE_CLOSE);
if(rsiHandle == INVALID_HANDLE)
{
Print("Error creating RSI handle!");
return(INIT_FAILED);
}
atrSignalHandle = iATR(_Symbol, _Period, ATRPeriod);
if(atrSignalHandle == INVALID_HANDLE)
{
Print("Error creating Signal ATR handle!");
return(INIT_FAILED);
}
initialBalance = AccountInfoDouble(ACCOUNT_BALANCE);
peakEquity = AccountInfoDouble(ACCOUNT_EQUITY);
lastPeakEquity = AccountInfoDouble(ACCOUNT_EQUITY);
targetEquityReached = false;
minimumEquityReached = false;
minEquityTriggersExceeded = false;
minEquityTriggerCount = 0;
isPaused = false;
pauseStartTime = 0;
lastProcessedNewsEventID = 0;
startTime = TimeCurrent();
lastDailyReportTime = 0;
lastReportEquity = AccountInfoDouble(ACCOUNT_EQUITY);
totalPauseCount = 0;
totalPauseDurationMinutes = 0;
symbolBaseCurrency = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_BASE);
symbolQuoteCurrency = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_PROFIT);
initialLeverage = AccountInfoInteger(ACCOUNT_LEVERAGE);
isOrderSendLocked = false;
algoTradingStatus = TerminalInfoInteger(TERMINAL_TRADE_ALLOWED);
// Initialize managed positions array
ArrayResize(managedPositions, 0);
managedPositionCount = 0;
// Scan and register existing positions
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(!PositionSelectByTicket(ticket)) continue;
if(PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
double posEntryPrice = PositionGetDouble(POSITION_PRICE_OPEN);
// For existing positions, try to calculate current signal strength as baseline
// If calculation fails or returns 0, use a default safe value (MinBuySignalScore)
double initialScore = 0;
// We can't easily get the signal at open time, so we use current as baseline
// This effectively "resets" the signal tracking for this position
SignalStrength strength;
if(type == POSITION_TYPE_BUY) strength = GetSignalStrength(ORDER_TYPE_BUY);
else strength = GetSignalStrength(ORDER_TYPE_SELL);
initialScore = strength.finalScore;
if(initialScore <= 0) initialScore = (type == POSITION_TYPE_BUY) ? MinBuySignalScore : MinSellSignalScore;
RegisterManagedPosition(ticket, type, initialScore, posEntryPrice);
// Update global last position tracking
datetime posTime = (datetime)PositionGetInteger(POSITION_TIME);
double posPrice = PositionGetDouble(POSITION_PRICE_OPEN);
// Reconstruct Candle Counters for existing positions
datetime posBarTime = (posTime / PeriodSeconds(_Period)) * PeriodSeconds(_Period);
datetime curBarTime = iTime(_Symbol, _Period, 0);
// Initialize current bar time if needed
if(currentBarTime == 0) currentBarTime = curBarTime;
if(posBarTime == currentBarTime)
{
if(type == POSITION_TYPE_BUY) buysOnCurrentBar++;
else sellsOnCurrentBar++;
}
if(type == POSITION_TYPE_BUY)
{
if(posTime > lastBuyTime)
{
lastBuyTime = posTime;
lastBuyPrice = posPrice;
}
}
else if(type == POSITION_TYPE_SELL)
{
if(posTime > lastSellTime)
{
lastSellTime = posTime;
lastSellPrice = posPrice;
}
}
}
Print("+-----------------------------------------+");
Print("Nyao Scalper v43.0 Initialized Successfully");
Print("+-----------------------------------------+");
if(EnableDiscordAlerts) CheckDiscordAlert();
return(INIT_SUCCEEDED);
}
// +------------------------------------------------------------------+
// | Expert Deinitialization Function |
// +------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// Cleanup password dialog if active
if(passwordDialogActive)
{
passwordDialog.Destroy();
passwordDialogActive = false;
}
// Cleanup Dashboard Objects
ObjectsDeleteAll(0, "NyaoDash_");
Comment("");
// Release ATR Handle
IndicatorRelease(emaFastHandle);
IndicatorRelease(emaSlowHandle);
IndicatorRelease(rsiHandle);
IndicatorRelease(atrSignalHandle);
Print("Nyao Scalper v43.0 Deinitialized");
}
// +------------------------------------------------------------------+
// | Expert Tick Function |
// +------------------------------------------------------------------+
void OnTick()
{
// Block trading until password is verified
if(!passwordVerified) return;
// Invalidate per-tick signal cache
_buyStrengthValid = false;
_sellStrengthValid = false;
// Check Algo Trading status
CheckAlgoTradingStatus();
// Check and update peak equity
CheckPeakEquity();
// Check if target equity reached
CheckTargetEquity();
// Check if minimum equity reached
CheckMinTradeableEquity();
// Check equity drawdawn
CheckEquityDrawdawn();
// Aggregate (basket) floating-loss protection
CheckBasketStop();
if(targetEquityReached || minimumEquityReached || minEquityTriggersExceeded)
{
// Close all positions and completely stop the EA
CloseAllPositions();
DisableAlgoTrading();
LogPrint("[STOPPED] Trading stopped.");
UpdateDashboard();
return;
}
// Check if current time is within allowed trading hours
CheckTradingHours();
// Check for leverage changes
CheckLeverageChange();
// Check for market close time
CheckMarketClose();
// Update Signal Globals on New Bar (for Velocity Tracking)
datetime currBarTime = iTime(_Symbol, _Period, 0);
if(currentBarTime != currBarTime)
{
// Update History Scores
// Recalculate Score(1) which is the just-closed candle
// We can't trust the live variable, so we re-calc
lastBuySignalScorePrev = lastBuySignalScore;
// Update Buy Stats
SignalStrength buyStr = GetSignalStrength(ORDER_TYPE_BUY);
lastBuySignalScore = buyStr.finalScore;
// Update Sell Stats
SignalStrength sellStr = GetSignalStrength(ORDER_TYPE_SELL);
lastSellSignalScore = sellStr.finalScore;
// Track consecutive trading candles for threshold escalation
// If the just-closed bar had trades, increment consecutive counter
// Otherwise reset it (the streak is broken)
if(buysOnCurrentBar > 0)
{
consecutiveBuyCandles++;
prevBarHadBuys = true;
}
else
{
consecutiveBuyCandles = 0;
prevBarHadBuys = false;
}
if(sellsOnCurrentBar > 0)
{
consecutiveSellCandles++;
prevBarHadSells = true;
}
else
{
consecutiveSellCandles = 0;
prevBarHadSells = false;
}
// Update Bar Time
currentBarTime = currBarTime;
buysOnCurrentBar = 0;
sellsOnCurrentBar = 0;
}
if (isOutsideTradingHours || isLeverageDiffFromInitial || isNearMarketClose)
{
// Don't open new positions, but continue managing existing ones
ManagePositions();
// LogPrint("[PAUSED] Trading paused."); // Prevent LogPrint spam on every tick
UpdateDashboard();
return;
}
// Check for high-impact news events
CheckHighImpactNews();
// Check pause duration
if (isPaused)
{
datetime currentTime = TimeTradeServer();
int elapsedSeconds = (int)(currentTime - pauseStartTime);
int pauseDurationSeconds = currentPauseDuration * 60;
if(currentPauseDuration == 0 || elapsedSeconds < pauseDurationSeconds)
{
// Don't open new positions, but continue managing existing ones
ManagePositions();
// LogPrint("[PAUSED] Paused. Time remaining: ", (pauseDurationSeconds - elapsedSeconds) / 60, " minute(s)"); // Prevent LogPrint spam on every tick
UpdateDashboard();
return; // EXIT - prevent all new orders while paused
}
else
{
// Pause period ended - reset flag and resume trading
isPaused = false;
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
LogPrint("+-----------------------------------------+");
LogPrint("PAUSE PERIOD ENDED");
LogPrint("Trading RESUMED after ", currentPauseDuration, " minutes");
LogPrint("Current Equity: $", currentEquity);
LogPrint("+-----------------------------------------+");
// Send Discord alert for trading resumed
if(EnableDiscordAlerts)
{
string alertMsg = "**Instrument:** " + _Symbol + "\n";
alertMsg += "**Timeframe:** " + EnumToString(_Period) + "\n";
alertMsg += "**Server Time:** " + TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS) + "\n";
alertMsg += "**Pause Duration:** " + IntegerToString(currentPauseDuration) + " minutes\n";
alertMsg += "**Current Equity:** $" + DoubleToString(currentEquity, 2) + "\n";
alertMsg += "**Peak Equity:** $" + DoubleToString(peakEquity, 2) + "\n";
alertMsg += "**Action:** Trading Resumed";
SendDiscordAlert("▶️ TRADING RESUMED!", alertMsg, 3066993); // Blue color
}
}
}
// Manage existing positions
ManagePositions();
// Check for trading signals
CheckForTradingSignal();
// Check for Trade Report
CheckTradeReport();
// Update On-Chart Dashboard
UpdateDashboard();
}
// +------------------------------------------------------------------+
// | Chart Event Handler - Password Dialog |
// +------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
if(passwordDialogActive)
{
passwordDialog.OnEvent(id, lparam, dparam, sparam);
// Check for submit button click
if(id == CHARTEVENT_OBJECT_CLICK && sparam == "PasswordSubmit")
{
string enteredPassword = passwordEdit.Text();
if(enteredPassword == EA_PASSWORD)
{
// Password correct - close dialog and initialize EA
passwordDialog.Destroy();
passwordDialogActive = false;
passwordVerified = true;
Print("Password verified! EA is now active.");
// Complete initialization
if(InitializeEA() != INIT_SUCCEEDED)
{
Alert("EA initialization failed!");
}
}
else
{
Alert("Invalid password! Please try again.");
passwordEdit.Text("");
}
}
}
}
// +------------------------------------------------------------------+
// | Trade Transaction Handler - Primary Close Detection |
// | Fires when a deal is added to history. We account for a fully- |
// | closed managed position here (event-driven) instead of relying |
// | solely on per-tick polling, which can miss closes that bunch up |
// | on a single tick. SyncManagedPositions stays as a reconciliation |
// | fallback; ProcessClosedPosition is idempotent so there is no |
// | double counting between the two paths. |
// +------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
// Only react to a deal being added to history
if(trans.type != TRADE_TRANSACTION_DEAL_ADD) return;
ulong dealTicket = trans.deal;
if(dealTicket == 0) return;
if(!HistoryDealSelect(dealTicket)) return;
// Only our symbol + magic
if(HistoryDealGetString(dealTicket, DEAL_SYMBOL) != _Symbol) return;
if(HistoryDealGetInteger(dealTicket, DEAL_MAGIC) != MagicNumber) return;
long dealEntry = HistoryDealGetInteger(dealTicket, DEAL_ENTRY);
ulong posID = (ulong)HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID);
if(posID == 0) return;
// NEW POSITION OPENED
// Registers fills here so pending-limit entries get tracked. Market entries are
// already registered inline in OpenPosition, so the index guard below skips them.
if(dealEntry == DEAL_ENTRY_IN)
{
if(GetManagedPositionIndex(posID) != -1) return; // already tracked (market path)
ENUM_POSITION_TYPE ptype;
double entryPrice;
string posComment = "";
if(PositionSelectByTicket(posID))
{
ptype = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
entryPrice = PositionGetDouble(POSITION_PRICE_OPEN);
posComment = PositionGetString(POSITION_COMMENT);
}
else
{
// Fallback to deal data if the position can't be selected
ptype = (HistoryDealGetInteger(dealTicket, DEAL_TYPE) == DEAL_TYPE_BUY) ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
entryPrice = HistoryDealGetDouble(dealTicket, DEAL_PRICE);
}
// Recover the entry-thesis score from the limit-order comment; fall back to the
// direction's min threshold if absent (e.g. EA restarted before the fill).
double score = ParseLimitEntryScore(posComment);
if(score <= 0) score = (ptype == POSITION_TYPE_BUY) ? MinBuySignalScore : MinSellSignalScore;
RegisterManagedPosition(posID, ptype, score, entryPrice);
// Mirror OpenPosition's candle-counter + last-position bookkeeping for the fill bar
datetime currBarTime = iTime(_Symbol, _Period, 0);
if(currentBarTime != currBarTime)
{
currentBarTime = currBarTime;
buysOnCurrentBar = 0;
sellsOnCurrentBar = 0;
}
if(ptype == POSITION_TYPE_BUY)
{
buysOnCurrentBar++;
lastBuyTime = TimeCurrent();
lastBuyPrice = entryPrice;
}
else
{
sellsOnCurrentBar++;
lastSellTime = TimeCurrent();
lastSellPrice = entryPrice;
}
LogPrint("[LIMIT FILL] Position ", posID, " registered. Type: ",
ptype == POSITION_TYPE_BUY ? "BUY" : "SELL",
" | Entry: ", entryPrice, " | Score: ", DoubleToString(score, 1));
return;
}
// POSITION CLOSED (full close accounting)
if(dealEntry != DEAL_ENTRY_OUT && dealEntry != DEAL_ENTRY_INOUT) return;
// Partial close — the position is still open (reduced volume); no full-close accounting
if(PositionSelectByTicket(posID)) return;
// Only act on positions we manage (also guards against double accounting)
if(GetManagedPositionIndex(posID) == -1) return;
double closedProfit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT)
+ HistoryDealGetDouble(dealTicket, DEAL_SWAP)
+ HistoryDealGetDouble(dealTicket, DEAL_COMMISSION);
ProcessClosedPosition(posID, closedProfit);
}
// +------------------------------------------------------------------+
// | Position Loss State - Aggregate loss metrics for a direction |
// +------------------------------------------------------------------+
struct PositionLossState
{
int losingCount; // Number of losing positions in this direction
int totalCount; // Total positions in this direction
double totalUnrealizedLoss; // Sum of unrealized losses (negative = loss)
double worstLossPct; // Worst single position loss as % of entry
};
// +------------------------------------------------------------------+
// | Get Open Position Loss State for a Direction |
// | Scans all open managed positions and returns aggregate loss info |
// +------------------------------------------------------------------+
PositionLossState GetOpenPositionLossState(ENUM_POSITION_TYPE direction)
{
PositionLossState state;
state.losingCount = 0;
state.totalCount = 0;
state.totalUnrealizedLoss = 0;
state.worstLossPct = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(!PositionSelectByTicket(ticket)) continue;
if(PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
if(posType != direction) continue;
state.totalCount++;
double profit = PositionGetDouble(POSITION_PROFIT);
if(profit < 0)
{
state.losingCount++;
state.totalUnrealizedLoss += profit; // Accumulate negative value
// Calculate loss as % of entry for worst-case tracking
double entryPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double volume = PositionGetDouble(POSITION_VOLUME);
double contractSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE);
if(entryPrice > 0 && volume > 0 && contractSize > 0)
{
double lossPct = MathAbs(profit) / (entryPrice * volume * contractSize) * 100.0;
if(lossPct > state.worstLossPct)
state.worstLossPct = lossPct;
}
}
}
return state;
}
// +------------------------------------------------------------------+
// | Get Total Floating P/L for our positions on this symbol |
// +------------------------------------------------------------------+
double GetTotalFloatingPL()
{
double total = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(!PositionSelectByTicket(ticket)) continue;
if(PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
total += PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
}
return total;
}
// +------------------------------------------------------------------+
// | Floating P/L of NON hedge-chain positions only |
// | A hedge chain intentionally carries a transient drawdown while it |
// | recovers; including its legs here would let the basket stop close |
// | the chain prematurely. Active chain legs are bounded by their own |
// | HedgeMaxChainLossPct/USD instead. Falls back to the full total |
// | when the hedge feature is disabled. |
// +------------------------------------------------------------------+
double GetBasketFloatingPL()
{
if(!EnableHedgeChain) return GetTotalFloatingPL();
double total = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(!PositionSelectByTicket(ticket)) continue;
if(PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
int idx = GetManagedPositionIndex(ticket);
if(idx != -1 && managedPositions[idx].chainId != 0) continue; // skip active chain legs
total += PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
}
return total;
}
// +------------------------------------------------------------------+
// | Basket Stop - Close all when aggregate floating loss exceeds cap |
// | Per-position management protects single trades; this is a hard |
// | portfolio-level backstop against compounding stacked drawdown. |
// +------------------------------------------------------------------+
void CheckBasketStop()
{
if(!EnableBasketStop || MaxBasketLossPct <= 0) return;
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
if(equity <= 0) return;
// Exclude active hedge-chain legs: they are bounded by HedgeMaxChainLossPct/USD,
// not by the basket stop (a chain's transient drawdown must not trip the basket).
double floatingPL = GetBasketFloatingPL();
if(floatingPL >= 0) return; // only acts on net floating loss
double lossPct = (-floatingPL / equity) * 100.0;
if(lossPct < MaxBasketLossPct) return;
LogPrint("+-----------------------------------------+");
LogPrint("BASKET STOP TRIGGERED!");
LogPrint("Floating Loss (excl. hedge chains): $", DoubleToString(floatingPL, 2),
" (", DoubleToString(lossPct, 2), "% of equity >= ", DoubleToString(MaxBasketLossPct, 2), "%)");
LogPrint("Closing all non-chain positions and pausing.");
LogPrint("+-----------------------------------------+");
CloseAllPositions(false, true); // skip active hedge-chain legs
// Reuse the existing pause machinery
if(!isPaused)
{
isPaused = true;
pauseStartTime = TimeTradeServer();
currentPauseDuration = (MaxPauseMinutes > 0) ? MathMin(PauseMinutes, MaxPauseMinutes) : PauseMinutes;
totalPauseCount++;
totalPauseDurationMinutes += currentPauseDuration;
}
if(EnableDiscordAlerts)
{
string alertMsg = "**Instrument:** " + _Symbol + "\n";
alertMsg += "**Timeframe:** " + EnumToString(_Period) + "\n";
alertMsg += "**Server Time:** " + TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS) + "\n";
alertMsg += "**Floating Loss:** $" + DoubleToString(floatingPL, 2) + " (" + DoubleToString(lossPct, 2) + "%)\n";
alertMsg += "**Limit:** " + DoubleToString(MaxBasketLossPct, 2) + "% of equity\n";
alertMsg += "**Pause Duration:** " + IntegerToString(currentPauseDuration) + " minutes\n";
alertMsg += "**Action:** All Positions Closed, Trading Paused";
SendDiscordAlert("🧺 BASKET STOP TRIGGERED", alertMsg, 15158332); // Red color
}
}
// +------------------------------------------------------------------+
// | Check For Trading Signals |
// +------------------------------------------------------------------+
void CheckForTradingSignal()
{
// NEW-BAR ENTRY GATE
// When enabled, evaluate/open entries only once per newly closed bar.
// Position management still runs every tick (ManagePositions in OnTick).
// This stabilizes signals (no intrabar repaint) and makes "Open prices"/
// "1-minute OHLC" backtests representative of live behavior.
if(EnableNewBarEntryOnly)
{
datetime currBarTime = iTime(_Symbol, _Period, 0);
if(lastEntryBarTime == currBarTime) return;
lastEntryBarTime = currBarTime;
}
// Check Signals
double buySignal = BuySignal();
double sellSignal = SellSignal();
// Process signals
// Fresh entries use a pending pullback limit when EnableLimitEntry is on, otherwise
// a market order. (Virtual-SL re-entries always stay market — they exist to restore
// exposure immediately and must not risk going unfilled. See TryVirtualSLReentry.)
if (buySignal > sellSignal)
{
if (!EnableBuyOrders) return;
if (EnableLimitEntry)
PlaceLimitEntry(ORDER_TYPE_BUY, buySignal);
else
OpenPosition(ORDER_TYPE_BUY, buySignal);
}
else if (buySignal < sellSignal)
{
if (!EnableSellOrders) return;
if (EnableLimitEntry)
PlaceLimitEntry(ORDER_TYPE_SELL, sellSignal);
else
OpenPosition(ORDER_TYPE_SELL, sellSignal);
}
}
// Buy Signal
double BuySignal()
{
double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
// Check strict conditions (limits & distance) first
if(!CheckEntryConditions(POSITION_TYPE_BUY, currentPrice)) return 0;
// Calculate smoothed signal strength (blended weighted average)
SignalStrength strength = GetSignalStrength(ORDER_TYPE_BUY);
double adjustedScore = strength.finalScore;
double adjustedThreshold = MinBuySignalScore;
// CONSECUTIVE CANDLE THRESHOLD ESCALATION
// When previous candles opened buy positions, raise the threshold
// to prevent chasing moves and opening at the peak
if(consecutiveBuyCandles > 0 && ConsecutiveCandleThresholdBoost > 0)
{
int boostCount = consecutiveBuyCandles;
if(MaxConsecutiveCandleBoosts > 0 && boostCount > MaxConsecutiveCandleBoosts)
boostCount = MaxConsecutiveCandleBoosts;
double candleBoost = boostCount * ConsecutiveCandleThresholdBoost;
adjustedThreshold += candleBoost;
LogPrint("[CANDLE ESCALATION] Buy threshold boosted by ", DoubleToString(candleBoost, 1),
" (", boostCount, " consecutive trading candles). Threshold: ",
DoubleToString(adjustedThreshold, 1));
}
// SIGNAL DAMPENING: Apply score penalty and drawdown gating
if(EnableSignalDampening)
{
// A. Score Penalty: reduce score based on losing same-direction positions
PositionLossState lossState = GetOpenPositionLossState(POSITION_TYPE_BUY);
if(lossState.losingCount > 0)
{
double penalty = lossState.losingCount * LosingPosScorePenalty;
adjustedScore -= penalty;
LogPrint("[DAMPENED] Buy score reduced by ", DoubleToString(penalty, 1),
" (", lossState.losingCount, " losing buys). Raw: ",
DoubleToString(strength.finalScore, 1), " -> Adjusted: ",
DoubleToString(adjustedScore, 1));
}
// B. Drawdown Gate: raise threshold when account in drawdown
if(peakEquity > 0)
{
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
double drawdownPct = ((peakEquity - currentEquity) / peakEquity) * 100.0;
if(drawdownPct >= DrawdownThresholdPct)
{
adjustedThreshold += DrawdownScoreBoost;
LogPrint("[DRAWDOWN GATE] Equity drawdown ", DoubleToString(drawdownPct, 1),
"% >= ", DoubleToString(DrawdownThresholdPct, 1),
"%. Buy threshold raised to ", DoubleToString(adjustedThreshold, 1));
}
}
}
if (adjustedScore >= adjustedThreshold)
{
LogPrint("BUY SIGNAL RECEIVED (Score: ", DoubleToString(strength.finalScore, 1),
" | Adjusted: ", DoubleToString(adjustedScore, 1),
" / Threshold: ", DoubleToString(adjustedThreshold, 1), ")");
LogPrint("Details: Body=", DoubleToString(strength.bodySignal, _Digits),
", AvgBody=", DoubleToString(strength.avgBody, _Digits),
", Ratio=", DoubleToString(strength.ratio, 2),
", PenBody=", DoubleToString(strength.penaltyBody, 1),
", PenWick=", DoubleToString(strength.penaltyWick, 1));
LogPrint("Reasoning: ", strength.reasoning);
LogPrint("Price: ", currentPrice);
return adjustedScore;
}
return 0;
}
// Sell Signal
double SellSignal()
{
double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
// Check strict conditions (limits & distance) first
if(!CheckEntryConditions(POSITION_TYPE_SELL, currentPrice)) return 0;
// Calculate smoothed signal strength (blended weighted average)
SignalStrength strength = GetSignalStrength(ORDER_TYPE_SELL);
double adjustedScore = strength.finalScore;
double adjustedThreshold = MinSellSignalScore;
// CONSECUTIVE CANDLE THRESHOLD ESCALATION
// When previous candles opened sell positions, raise the threshold
// to prevent chasing moves and opening at the peak
if(consecutiveSellCandles > 0 && ConsecutiveCandleThresholdBoost > 0)
{
int boostCount = consecutiveSellCandles;
if(MaxConsecutiveCandleBoosts > 0 && boostCount > MaxConsecutiveCandleBoosts)
boostCount = MaxConsecutiveCandleBoosts;
double candleBoost = boostCount * ConsecutiveCandleThresholdBoost;
adjustedThreshold += candleBoost;
LogPrint("[CANDLE ESCALATION] Sell threshold boosted by ", DoubleToString(candleBoost, 1),
" (", boostCount, " consecutive trading candles). Threshold: ",
DoubleToString(adjustedThreshold, 1));
}
// SIGNAL DAMPENING: Apply score penalty and drawdown gating
if(EnableSignalDampening)
{
// A. Score Penalty: reduce score based on losing same-direction positions
PositionLossState lossState = GetOpenPositionLossState(POSITION_TYPE_SELL);
if(lossState.losingCount > 0)
{
double penalty = lossState.losingCount * LosingPosScorePenalty;
adjustedScore -= penalty;
LogPrint("[DAMPENED] Sell score reduced by ", DoubleToString(penalty, 1),
" (", lossState.losingCount, " losing sells). Raw: ",
DoubleToString(strength.finalScore, 1), " -> Adjusted: ",
DoubleToString(adjustedScore, 1));
}
// B. Drawdown Gate: raise threshold when account in drawdown
if(peakEquity > 0)
{
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
double drawdownPct = ((peakEquity - currentEquity) / peakEquity) * 100.0;
if(drawdownPct >= DrawdownThresholdPct)
{
adjustedThreshold += DrawdownScoreBoost;
LogPrint("[DRAWDOWN GATE] Equity drawdown ", DoubleToString(drawdownPct, 1),
"% >= ", DoubleToString(DrawdownThresholdPct, 1),
"%. Sell threshold raised to ", DoubleToString(adjustedThreshold, 1));
}
}
}
if (adjustedScore >= adjustedThreshold)
{
LogPrint("SELL SIGNAL RECEIVED (Score: ", DoubleToString(strength.finalScore, 1),
" | Adjusted: ", DoubleToString(adjustedScore, 1),
" / Threshold: ", DoubleToString(adjustedThreshold, 1), ")");
LogPrint("Details: Body=", DoubleToString(strength.bodySignal, _Digits),
", AvgBody=", DoubleToString(strength.avgBody, _Digits),
", Ratio=", DoubleToString(strength.ratio, 2),
", PenBody=", DoubleToString(strength.penaltyBody, 1),
", PenWick=", DoubleToString(strength.penaltyWick, 1));
LogPrint("Reasoning: ", strength.reasoning);
LogPrint("Price: ", currentPrice);
return adjustedScore;
}
return 0;
}
// Duplicate Buy Filter
// +------------------------------------------------------------------+
// | Unified Entry Conditions (limits, dampening, cooldown, duplicate)|
// | Direction-driven: replaces the former CheckBuy/SellConditions |
// +------------------------------------------------------------------+
bool CheckEntryConditions(ENUM_POSITION_TYPE dir, double price)
{
datetime currBarTime = iTime(_Symbol, _Period, 0);
bool isBuy = (dir == POSITION_TYPE_BUY);
string dirName = isBuy ? "Buy" : "Sell";
int sameOnBar = isBuy ? buysOnCurrentBar : sellsOnCurrentBar;
int oppOnBar = isBuy ? sellsOnCurrentBar : buysOnCurrentBar;
// Per-candle trade limit
if(MaxTradesPerCandle > 0)
{
int onCandle = (currentBarTime == currBarTime) ? sameOnBar : 0;
if(onCandle >= MaxTradesPerCandle)
{
return false;
}
}
// Prevent opposite direction trades on the same candle
if(oppOnBar > 0)
{
return false;
}
// SIGNAL DAMPENING: Hard block when too many losing same-dir positions are open
if(EnableSignalDampening)
{
PositionLossState lossState = GetOpenPositionLossState(dir);
if(lossState.losingCount >= MaxLosingPositionsSameDir)
{
LogPrint("[DAMPENED] ", dirName, " BLOCKED: ", lossState.losingCount,
" losing ", dirName, "s >= max ", MaxLosingPositionsSameDir);
return false;
}
}
// SIGNAL DAMPENING: Cooldown after consecutive losses
if(EnableSignalDampening && cooldownUntilBarTime > 0)
{
if(currBarTime < cooldownUntilBarTime)
{
LogPrint("[COOLDOWN] ", dirName, " BLOCKED: cooldown active until ",
TimeToString(cooldownUntilBarTime));
return false;
}
else
{
cooldownUntilBarTime = 0; // Cooldown expired
}
}
// Check minimum distance from last same-dir entry (duplicate signal filter)
ulong lastTicket = GetLastPositionTicket(dir);
datetime lastTime = isBuy ? lastBuyTime : lastSellTime;
double lastPrice = isBuy ? lastBuyPrice : lastSellPrice;
double dupMult = isBuy ? BuyDuplicateMultiplier : SellDuplicateMultiplier;
if(lastTime > 0 && lastTicket > 0)
{
double minDistance = ZonePoints * _Point * dupMult;
double distance = MathAbs(price - lastPrice);
if(distance < minDistance)
{
return false;
}
}
return true;
}
// +------------------------------------------------------------------+
// +------------------------------------------------------------------+
// | Manage Positions |
// +------------------------------------------------------------------+
void ManagePositions()
{
// Sync managed positions with broker (remove closed ones)
SyncManagedPositions();
// Hedge chain recovery: manage existing chains (resolve / stop / extend) and start
// new chains for losing positions. Runs before trailing/loss management so chain
// legs are correctly frozen/skipped by those routines.
ManageHedgeChains();
// Manage trailing stops for all positions
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(!PositionSelectByTicket(ticket)) continue;
if(PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
// Manage Trailing TP & SL for all positions
ManageTrailingTPSL(ticket);
}
// Manage losing positions
ManageLosingPositions();
// Expire / cancel stale pending limit entries (no-op when EnableLimitEntry is off)
ManagePendingOrders();
}
// +------------------------------------------------------------------+
// | Compute Raw Score - Internal Helper |
// | Computes the raw signal score for a given candle index |
// | signalIndex: 0 = current forming candle, 1+ = closed candles |
// +------------------------------------------------------------------+
double ComputeRawScore(ENUM_ORDER_TYPE orderType, int signalIndex)
{
SignalStrength dummy;
return ComputeRawScore(orderType, signalIndex, dummy, false);
}
double ComputeRawScore(ENUM_ORDER_TYPE orderType, int signalIndex, SignalStrength &components, bool fillComponents)
{
bool isBuy = (orderType == ORDER_TYPE_BUY);
bool isSell = (orderType == ORDER_TYPE_SELL);
double bufEMA_Fast[], bufEMA_Slow[], bufRSI[], bufATR[];
ArraySetAsSeries(bufEMA_Fast, true);
ArraySetAsSeries(bufEMA_Slow, true);
ArraySetAsSeries(bufRSI, true);
ArraySetAsSeries(bufATR, true);
// Copy minimal buffers
int needed = MathMax(ImpulseLookback, MathMax(DirectionalBodyLookback, ATRAvgLookback)) + 5;
// Fast EMA needs enough history for a multi-bar slope (SlopeLookback bars back)
int slopeBars = (SlopeLookback < 1) ? 1 : SlopeLookback;
int emaFastCopy = MathMax(3, slopeBars + 1);
if(CopyBuffer(emaFastHandle, 0, signalIndex, emaFastCopy, bufEMA_Fast) < emaFastCopy) return 0;
if(CopyBuffer(emaSlowHandle, 0, signalIndex, 3, bufEMA_Slow) < 3) return 0;
if(CopyBuffer(rsiHandle, 0, signalIndex, 3, bufRSI) < 3) return 0;
if(CopyBuffer(atrSignalHandle, 0, signalIndex, needed, bufATR) < needed) return 0;
// Fetch Price Data
MqlRates rates[];
ArraySetAsSeries(rates, true);
if(CopyRates(_Symbol, _Period, signalIndex, needed, rates) < needed) return 0;
// 1. TREND SCORE (Max 3)
double emaFast = bufEMA_Fast[0];
double emaSlow = bufEMA_Slow[0];
// Multi-bar slope: compare current Fast EMA against its value SlopeLookback bars ago
// (less whipsaw than a single-bar slope on noisy M1 data)
double emaFastPrev = bufEMA_Fast[slopeBars];
double trendScore = 0;
bool trendAligned = false;
if (isBuy) trendAligned = (emaFast > emaSlow);
else trendAligned = (emaFast < emaSlow);
if (trendAligned) trendScore += TrendWeight;
bool slopeAligned = false;
if (isBuy) slopeAligned = (emaFast > emaFastPrev);
else slopeAligned = (emaFast < emaFastPrev);
if (slopeAligned) trendScore += SlopeWeight;
if(trendScore > 3.0) trendScore = 3.0;
// 2. MOMENTUM SCORE (Max 3) + IMPULSE
double currentBody = MathAbs(rates[0].close - rates[0].open);
double sumBody = 0;
int validCandles = 0;
for(int i=1; i<=DirectionalBodyLookback && i<needed; i++)
{
sumBody += MathAbs(rates[i].close - rates[i].open);
validCandles++;
}
double avgRecentBody = (validCandles > 0) ? sumBody / validCandles : currentBody;
double rsi = bufRSI[0];
double baseMomentum = 0;
if (isBuy)
{
if (rsi > 50 && rsi < RSIOverbought) baseMomentum += MomentumBaseWeight;
if (rsi > RSIMomentumBuy) baseMomentum += MomentumTriggerWeight;
if (currentBody > avgRecentBody) baseMomentum += BodyMomentumWeight;
}
else
{
if (rsi < 50 && rsi > RSIOversold) baseMomentum += MomentumBaseWeight;
if (rsi < RSIMomentumSell) baseMomentum += MomentumTriggerWeight;
if (currentBody > avgRecentBody) baseMomentum += BodyMomentumWeight;
}
double momentumScore = baseMomentum;
// IMPULSE DETECTION
double bodyAccel = 0;
if (avgRecentBody > 0) bodyAccel = currentBody / avgRecentBody;
if (bodyAccel > 3.0) bodyAccel = 3.0;
double currentRange = rates[0].high - rates[0].low;
double sumRange = 0;
for(int i=1; i<=DirectionalBodyLookback && i<needed; i++)
{
sumRange += (rates[i].high - rates[i].low);
}
double avgRecentRange = (validCandles > 0) ? sumRange / validCandles : currentRange;
double rangeAccel = 0;
if (avgRecentRange > 0) rangeAccel = currentRange / avgRecentRange;
if (rangeAccel > 3.0) rangeAccel = 3.0;
int sameDirCount = 0;
for(int i=0; i<ImpulseLookback && i<needed; i++)
{
bool candleBullish = (rates[i].close > rates[i].open);
bool candleBearish = (rates[i].close < rates[i].open);
if (isBuy && candleBullish) sameDirCount++;
else if (isSell && candleBearish) sameDirCount++;
else break;
}
double continuityScore = (double)sameDirCount / ImpulseLookback;
if(continuityScore > 1.0) continuityScore = 1.0;
double rawImpulse = (0.5 * bodyAccel + 0.3 * rangeAccel + 0.2 * continuityScore) / 2.0;
if (rawImpulse > 1.0) rawImpulse = 1.0;
if (rawImpulse < 0.0) rawImpulse = 0.0;
momentumScore = momentumScore * (1.0 + ImpulseBoostWeight * rawImpulse);
if (momentumScore > 3.0) momentumScore = 3.0;
// 3. CHOP SCORE (Max 2)
double currentATR = bufATR[0];
double avgATR = 0;
if (needed >= ATRAvgLookback) {
double sumATR = 0;
for(int i=0; i<ATRAvgLookback && i<needed; i++) sumATR += bufATR[i];
avgATR = sumATR / ATRAvgLookback;
} else {
avgATR = currentATR;
}
double volRatio = 0;
if(avgATR > 0) volRatio = currentATR / avgATR;
// DEAD-MARKET FILTER: when ATR has collapsed relative to its average the
// market is too quiet to scalp profitably (costs dominate). Block the signal.
// Guard volRatio > 0 so we don't block when ATR data is unavailable.
if(MinVolRatioToTrade > 0 && volRatio > 0 && volRatio < MinVolRatioToTrade)
return 0;
double chopScore = 0;
if (volRatio > 1.0) chopScore = ChopScoreHigh;
else if (volRatio > 0.8) chopScore = ChopScoreMed;
else chopScore = ChopScoreLow;
if (chopScore > 2.0) chopScore = 2.0;
// 4. PEAK & VOLATILITY SCORES (Max 1 each)
double volatilityScore = (volRatio > 1.2) ? VolatilityScoreHigh : VolatilityScoreLow;
bool breakout = false;
double localExtreme = isBuy ? rates[1].high : rates[1].low;
for(int i=2; i<=5; i++)
{
if(isBuy) localExtreme = MathMax(localExtreme, rates[i].high);
else localExtreme = MathMin(localExtreme, rates[i].low);
}
double peakScore = 0;
if(isBuy && rates[0].close > localExtreme) breakout = true;
if(isSell && rates[0].close < localExtreme) breakout = true;
if(breakout) peakScore = PeakScoreWeight;
// 5. WICK / REJECTION PENALTY
double maxOpenClose = MathMax(rates[0].open, rates[0].close);
double minOpenClose = MathMin(rates[0].open, rates[0].close);
double upperWick = rates[0].high - maxOpenClose;
double lowerWick = minOpenClose - rates[0].low;
double safeBody = MathMax(currentBody, avgRecentBody * MinBodyRatio);
double penaltyWick = 0;
double rejection = 0;
if (safeBody > 0)
{
if (isBuy) rejection = upperWick / safeBody;
else rejection = lowerWick / safeBody;
penaltyWick = rejection * WickRejectionWeight;
}
// FINAL SCORE AGGREGATION
double rawScore = trendScore + momentumScore + chopScore + peakScore + volatilityScore;
rawScore -= penaltyWick;
if (rawScore < 0) rawScore = 0;
if (rawScore > 10.0) rawScore = 10.0;
// Fill component details for dashboard reporting
if(fillComponents)
{
components.trendScore = trendScore;
components.momentumScore = momentumScore;
components.chopScore = chopScore;
components.peakScore = peakScore;
components.volatilityScore = volatilityScore;
components.impulseStrength = rawImpulse;
components.avgBody = avgRecentBody;
components.bodySignal = currentBody;
components.upperWick = upperWick;
components.lowerWick = lowerWick;
components.rejection = rejection;
components.penaltyWick = penaltyWick;
}
return rawScore;
}
// +------------------------------------------------------------------+
// | Signal Strength Analysis - Blended Weighted Average |
// | Combines weighted avg of N closed candles + dampened current |
// | candle for smooth yet responsive signal scoring |
// +------------------------------------------------------------------+
SignalStrength GetSignalStrength(ENUM_ORDER_TYPE orderType)
{
// Return cached result if already computed this tick
if(orderType == ORDER_TYPE_BUY && _buyStrengthValid)
return _cachedBuyStrength;
if(orderType == ORDER_TYPE_SELL && _sellStrengthValid)
return _cachedSellStrength;
SignalStrength strength;
strength.finalScore = 0;
strength.trendScore = 0;
strength.momentumScore = 0;
strength.chopScore = 0;
strength.peakScore = 0;
strength.volatilityScore = 0;
strength.impulseStrength = 0;
strength.velocity = 0;
strength.normalizedVelocity = 0;
strength.avgBody = 0;
strength.bodySignal = 0;
strength.ratio = 0;
strength.upperWick = 0;
strength.lowerWick = 0;
strength.rejection = 0;
strength.penaltyBody = 0;
strength.penaltyWick = 0;
strength.reasoning = "";
bool isBuy = (orderType == ORDER_TYPE_BUY);
// Clamp smoothing parameters to safe ranges
int N = SignalSmoothingCandles;
if(N < 1) N = 1;
if(N > 10) N = 10;
double blend = CurrentCandleBlend;
if(blend < 0.0) blend = 0.0;
if(blend > 1.0) blend = 1.0;
// Step 1: Weighted average of last N closed candles (the "base")
// Weights: candle[1] = N, candle[2] = N-1, ..., candle[N] = 1
double weightedSum = 0;
double weightTotal = 0;
for(int i = 1; i <= N; i++)
{
// Fill component details on candle[1] for dashboard reporting
double score_i = (i == 1)
? ComputeRawScore(orderType, i, strength, true)
: ComputeRawScore(orderType, i);
double weight = (double)(N - i + 1); // Linear decay
weightedSum += score_i * weight;
weightTotal += weight;
}
double baseScore = (weightTotal > 0) ? weightedSum / weightTotal : 0;
// Step 2: Compute current candle score (dampened contribution)
double currentScore = ComputeRawScore(orderType, 0);
// Step 3: Blend
double finalScore = baseScore * (1.0 - blend) + currentScore * blend;
// Clamp
if(finalScore < 0) finalScore = 0;
if(finalScore > 10.0) finalScore = 10.0;
strength.finalScore = finalScore;
// VELOCITY TRACKING
// Use smoothed scores for velocity (inherently smoother)
double prevScore = 0;
if (isBuy)
{
prevScore = lastBuySignalScorePrev;
}
else
{
prevScore = lastSellSignalScorePrev;
}
double velocity = strength.finalScore - prevScore;
strength.velocity = velocity;
// Normalized Velocity
strength.normalizedVelocity = (velocity + VelocityWindow) / (2.0 * VelocityWindow);
if(strength.normalizedVelocity < 0) strength.normalizedVelocity = 0;
if(strength.normalizedVelocity > 1.0) strength.normalizedVelocity = 1.0;
// Update Globals for Position Sizing (Latest Call Wins)
if(isBuy) {
lastBuyVelocity = strength.velocity;
lastBuyNormalizedVelocity = strength.normalizedVelocity;
} else {
lastSellVelocity = strength.velocity;
lastSellNormalizedVelocity = strength.normalizedVelocity;
}
// Debug Construction
strength.reasoning = StringFormat("T:%.1f M:%.1f(Imp:%.2f) C:%.1f P:%.1f V:%.1f | Vel:%.2f [Smooth:%d Blend:%.0f%%]",
strength.trendScore, strength.momentumScore, strength.impulseStrength,
strength.chopScore, strength.peakScore, strength.volatilityScore, strength.normalizedVelocity,
N, blend * 100);
// Cache result for this tick
if(orderType == ORDER_TYPE_BUY) { _cachedBuyStrength = strength; _buyStrengthValid = true; }
else { _cachedSellStrength = strength; _sellStrengthValid = true; }
return strength;
}
// +------------------------------------------------------------------+
// | Evaluate Position Health - Measurement-Based Revalidation |
// | Checks if the trade thesis is still valid |
// | Uses smoothed inputs + graduated trend with slope awareness |
// +------------------------------------------------------------------+
PositionHealth EvaluatePositionHealth(
ENUM_POSITION_TYPE posType,
double entryPrice,
datetime posOpenTime,
double emaFast,
double emaSlow,
double emaFastPrev,
double rsi,
double currentATR,
const MqlRates &rates[],
int ratesCount)
{
PositionHealth health;
health.healthScore = 0;
health.trendValid = false;
health.momentumValid = false;
health.adverseATR = 0;
health.swingValid = true;
health.inGracePeriod = false;
health.reason = "";
bool isBuy = (posType == POSITION_TYPE_BUY);
// GRACE PERIOD CHECK
// Skip health evaluation for newly opened positions
if(HealthGraceBars > 0 && posOpenTime > 0)
{
int barsElapsed = iBarShift(_Symbol, _Period, posOpenTime, false);
if(barsElapsed < HealthGraceBars)
{
health.healthScore = 1.0;
health.trendValid = true;
health.momentumValid = true;
health.swingValid = true;
health.inGracePeriod = true;
health.reason = StringFormat("Grace period (%d/%d bars). ", barsElapsed, HealthGraceBars);
return health;
}
}
// 1. TREND ALIGNMENT (Graduated: separation + slope awareness)
// Factors:
// a. EMA crossed correctly (base requirement)
// b. EMA separation relative to ATR (how strongly crossed)
// c. EMA slope direction (is fast EMA still moving favorably?)
double trendScore = 0;
if(isBuy)
health.trendValid = (emaFast > emaSlow);
else
health.trendValid = (emaFast < emaSlow);
if(health.trendValid)
{
// a. EMA separation: how far apart the EMAs are relative to ATR
// Full score at 0.5 ATR separation, scales linearly below that
double emaSeparation = MathAbs(emaFast - emaSlow);
double separationScore = 1.0;
if(currentATR > 0)
{
separationScore = MathMin(1.0, emaSeparation / (currentATR * 0.5));
}
// b. EMA slope: is the fast EMA still moving in the favorable direction?
// Full score if slope is favorable, 0.7 penalty if slope is flattening/reversing
double slopeFactor = 1.0;
if(isBuy)
{
if(emaFast <= emaFastPrev) slopeFactor = 0.7; // Slope flattening or reversing
}
else
{
if(emaFast >= emaFastPrev) slopeFactor = 0.7; // Slope flattening or reversing
}
trendScore = separationScore * slopeFactor;
if(slopeFactor < 1.0)
health.reason += StringFormat("EMA slope weakening (sep=%.1f%% ATR). ",
currentATR > 0 ? emaSeparation / currentATR * 100 : 0);
}
else
{
trendScore = 0;
health.reason += "Trend crossed against position. ";
}
// 2. RSI ZONE (Graduated: linear ramp from 0 to 1)
// Uses configurable thresholds instead of hardcoded 45/55
double rsiScore = 0;
if(isBuy)
{
// Buy: RSI should be above HealthRSIBuyMin
// Score ramps from 0 at HealthRSIBuyMin-15 to 1.0 at HealthRSIBuyMin
double rsiFloor = HealthRSIBuyMin - 15.0;
if(rsi >= HealthRSIBuyMin)
{
rsiScore = 1.0;
health.momentumValid = true;
}
else if(rsi > rsiFloor)
{
rsiScore = (rsi - rsiFloor) / (HealthRSIBuyMin - rsiFloor);
health.momentumValid = false;
health.reason += StringFormat("RSI weakening (RSI=%.1f, min=%.1f). ", rsi, HealthRSIBuyMin);
}
else
{
rsiScore = 0;
health.momentumValid = false;
health.reason += StringFormat("RSI regime shift (RSI=%.1f, min=%.1f). ", rsi, HealthRSIBuyMin);
}
}
else
{
// Sell: RSI should be below HealthRSISellMax
// Score ramps from 0 at HealthRSISellMax+15 to 1.0 at HealthRSISellMax
double rsiCeiling = HealthRSISellMax + 15.0;
if(rsi <= HealthRSISellMax)
{
rsiScore = 1.0;
health.momentumValid = true;
}
else if(rsi < rsiCeiling)
{
rsiScore = (rsiCeiling - rsi) / (rsiCeiling - HealthRSISellMax);
health.momentumValid = false;
health.reason += StringFormat("RSI weakening (RSI=%.1f, max=%.1f). ", rsi, HealthRSISellMax);
}
else
{
rsiScore = 0;
health.momentumValid = false;
health.reason += StringFormat("RSI regime shift (RSI=%.1f, max=%.1f). ", rsi, HealthRSISellMax);
}
}
// 3. ADVERSE EXCURSION / ATR (Graduated: smooth falloff based on distance)
double currentPrice = isBuy ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double adverseMove = 0;
if(isBuy)
adverseMove = entryPrice - currentPrice; // Positive = losing
else
adverseMove = currentPrice - entryPrice; // Positive = losing
double atrScore = 1.0; // Default: fully healthy (not adverse)
if(currentATR > 0 && adverseMove > 0)
{
health.adverseATR = adverseMove / currentATR;
// Graduated: score drops linearly from 1.0 at 0 ATR to 0.0 at MaxAdverseATR
atrScore = MathMax(0.0, 1.0 - (health.adverseATR / MaxAdverseATR));
if(health.adverseATR > MaxAdverseATR)
health.reason += StringFormat("Adverse excursion %.1f ATR > Max %.1f ATR. ", health.adverseATR, MaxAdverseATR);
else if(atrScore < 0.5)
health.reason += StringFormat("Adverse excursion %.1f ATR (score=%.2f). ", health.adverseATR, atrScore);
}
// 4. SWING LEVEL (Graduated: binary — structure IS or ISN'T broken)
// Uses configurable lookback, excludes 2 most recent bars to avoid noise
double swingScore = 1.0;
int swingLookback = MathMax(5, HealthSwingLookback); // Minimum 5 bars
if(ratesCount >= swingLookback)
{
// Start from bar index 2 (skip 2 most recent to avoid noise)
int startBar = MathMin(2, ratesCount - 1);
if(isBuy)
{
// Find recent swing low — if price broke below it, structure is broken
double swingLow = rates[startBar].low;
for(int j = startBar + 1; j < swingLookback && j < ratesCount; j++)
swingLow = MathMin(swingLow, rates[j].low);
if(currentPrice < swingLow)
{
swingScore = 0;
health.swingValid = false;
health.reason += StringFormat("Price %.5f broke swing low %.5f (%d bars). ", currentPrice, swingLow, swingLookback);
}
}
else
{
// Find recent swing high — if price broke above it, structure is broken
double swingHigh = rates[startBar].high;
for(int j = startBar + 1; j < swingLookback && j < ratesCount; j++)
swingHigh = MathMax(swingHigh, rates[j].high);
if(currentPrice > swingHigh)
{
swingScore = 0;
health.swingValid = false;
health.reason += StringFormat("Price %.5f broke swing high %.5f (%d bars). ", currentPrice, swingHigh, swingLookback);
}
}
}
// AGGREGATE HEALTH SCORE (Graduated, using normalized weights)
health.healthScore = (trendScore * normHealthTrendWeight)
+ (rsiScore * normHealthRSIWeight)
+ (atrScore * normHealthATRWeight)
+ (swingScore * normHealthSwingWeight);
if(health.reason == "") health.reason = "All health checks passed.";
return health;
}
// +------------------------------------------------------------------+
// | Manage Losing Positions |
// | Scaled Partial Close, Dynamic SL Tightening, Break-Even Lock, |
// | Virtual SL + Re-entry |
// +------------------------------------------------------------------+
void ManageLosingPositions()
{
if(!EnableLossManagement) return;
// Cache indicator data once before the position loop
double bufEMA_Fast[], bufEMA_Slow[], bufRSI[], bufATR[];
ArraySetAsSeries(bufEMA_Fast, true);
ArraySetAsSeries(bufEMA_Slow, true);
ArraySetAsSeries(bufRSI, true);
ArraySetAsSeries(bufATR, true);
// Fetch 3 values: [0]=current, [1]=closed, [2]=prev closed (for slope)
if(CopyBuffer(emaFastHandle, 0, 0, 3, bufEMA_Fast) < 3) return;
if(CopyBuffer(emaSlowHandle, 0, 0, 3, bufEMA_Slow) < 3) return;
if(CopyBuffer(rsiHandle, 0, 0, 3, bufRSI) < 3) return;
if(CopyBuffer(atrSignalHandle, 0, 0, 3, bufATR) < 3) return;
// Blend closed candle + current candle indicators (consistent with signal smoothing)
// ATR stays on closed candle for stable volatility baseline
double blend = CurrentCandleBlend;
if(blend < 0.0) blend = 0.0;
if(blend > 1.0) blend = 1.0;
double emaFast = bufEMA_Fast[1] * (1.0 - blend) + bufEMA_Fast[0] * blend;
double emaSlow = bufEMA_Slow[1] * (1.0 - blend) + bufEMA_Slow[0] * blend;
double emaFastPrev = bufEMA_Fast[2]; // Previous closed candle (for slope detection)
double rsi = bufRSI[1] * (1.0 - blend) + bufRSI[0] * blend;
double currentATR = bufATR[1]; // ATR on closed candle (stable baseline)
// Cache rates for swing level check
int swingBars = MathMax(5, HealthSwingLookback);
MqlRates rates[];
ArraySetAsSeries(rates, true);
int ratesCopied = CopyRates(_Symbol, _Period, 1, swingBars, rates);
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(!PositionSelectByTicket(ticket)) continue;
if(PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
datetime posOpenTime = (datetime)PositionGetInteger(POSITION_TIME);
double volume = PositionGetDouble(POSITION_VOLUME);
double profit = PositionGetDouble(POSITION_PROFIT);
double currentSL = PositionGetDouble(POSITION_SL);
double currentTP = PositionGetDouble(POSITION_TP);
// Get managed position data
int posIndex = GetManagedPositionIndex(ticket);
if(posIndex == -1)
{
double posEntryPrice = PositionGetDouble(POSITION_PRICE_OPEN);
RegisterManagedPosition(ticket, posType, 0, posEntryPrice);
continue;
}
// HEDGE CHAIN: chain logic exclusively manages legs of an active chain.
// Skip the standard loss management (health close, partial, SL tighten, re-entry).
if(EnableHedgeChain && managedPositions[posIndex].chainId != 0)
continue;
double entryPrice = managedPositions[posIndex].entryPrice;
double initialScore = managedPositions[posIndex].signalScore;
// Evaluate position health
PositionHealth health = EvaluatePositionHealth(posType, entryPrice, posOpenTime,
emaFast, emaSlow, emaFastPrev, rsi, currentATR,
rates, ratesCopied);
// Skip all management during grace period
if(health.inGracePeriod) continue;
// 1. BREAK-EVEN LOCK (when profit exceeds spread cost)
if(EnableBreakEvenOnSpread && !managedPositions[posIndex].breakEvenLocked)
{
double spreadPoints = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * _Point;
double spreadCost = spreadPoints * volume * SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE);
double breakEvenTrigger = spreadCost * BreakEvenSpreadMultiplier;
if(profit > breakEvenTrigger)
{
// Calculate break-even SL at entry price
double newBESL = NormalizeDouble(entryPrice, _Digits);
// Validate: SL must be on the correct side
long stopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
double minDist = stopLevel * _Point;
bool canLockBE = false;
if(posType == POSITION_TYPE_BUY)
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
canLockBE = (newBESL < bid - minDist) && (currentSL == 0 || newBESL > currentSL);
}
else
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
canLockBE = (newBESL > ask + minDist) && (currentSL == 0 || newBESL < currentSL);
}
if(canLockBE)
{
if(ModifyPosition(ticket, newBESL, currentTP))
{
managedPositions[posIndex].breakEvenLocked = true;
LogPrint("+-----------------------------------------+");
LogPrint("[BREAK-EVEN LOCKED] Ticket: ", ticket);
LogPrint("Profit: $", DoubleToString(profit, 2), " > Trigger: $", DoubleToString(breakEvenTrigger, 2));
LogPrint("SL moved to entry: ", newBESL);
LogPrint("+-----------------------------------------+");
}
}
}
}
// 2. SCALED PARTIAL CLOSE (signal decay based)
if(EnablePartialClose && initialScore > 0 && managedPositions[posIndex].partialCloseLevel < 3)
{
// Get current signal strength for position's direction
ENUM_ORDER_TYPE orderType = (posType == POSITION_TYPE_BUY) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
SignalStrength currentStrength = GetSignalStrength(orderType);
double currentScore = currentStrength.finalScore;
double signalRatio = currentScore / initialScore;
// Re-read position volume (may have changed from previous partial close)
if(!PositionSelectByTicket(ticket)) continue;
volume = PositionGetDouble(POSITION_VOLUME);
double minVol = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double stepVol = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
// Level 1: Signal drops to 75% -> Close 25%
if(managedPositions[posIndex].partialCloseLevel == 0 && signalRatio <= 0.75)
{
double closeVol = NormalizeVolume(volume * PartialClose75Pct);
double remaining = volume - closeVol;
if(closeVol >= minVol && remaining >= minVol)
{
if(PartialClosePosition(ticket, closeVol))
{
managedPositions[posIndex].partialCloseLevel = 1;
LogPrint("+-----------------------------------------+");
LogPrint("[PARTIAL CLOSE L1] Ticket: ", ticket);
LogPrint("Signal: ", DoubleToString(currentScore, 1), " / ", DoubleToString(initialScore, 1), " (", DoubleToString(signalRatio * 100, 0), "%)");
LogPrint("Closed: ", closeVol, " lots | Remaining: ", remaining, " lots");
LogPrint("+-----------------------------------------+");
}
}
}
// Level 2: Signal drops to 50% -> Close 50%
else if(managedPositions[posIndex].partialCloseLevel == 1 && signalRatio <= 0.50)
{
// Re-read volume after potential L1 close
if(!PositionSelectByTicket(ticket)) continue;
volume = PositionGetDouble(POSITION_VOLUME);
double closeVol = NormalizeVolume(volume * PartialClose50Pct);
double remaining = volume - closeVol;
if(closeVol >= minVol && remaining >= minVol)
{
if(PartialClosePosition(ticket, closeVol))
{
managedPositions[posIndex].partialCloseLevel = 2;
LogPrint("+-----------------------------------------+");
LogPrint("[PARTIAL CLOSE L2] Ticket: ", ticket);
LogPrint("Signal: ", DoubleToString(currentScore, 1), " / ",
DoubleToString(initialScore, 1), " (", DoubleToString(signalRatio * 100, 0), "%)");
LogPrint("Closed: ", closeVol, " lots | Remaining: ", remaining, " lots");
LogPrint("+-----------------------------------------+");
}
}
}
// Level 3: Signal drops to 25% -> Close remaining
else if(managedPositions[posIndex].partialCloseLevel == 2 && signalRatio <= 0.25)
{
LogPrint("+-----------------------------------------+");
LogPrint("[PARTIAL CLOSE L3 - FULL EXIT] Ticket: ", ticket);
LogPrint("Signal: ", DoubleToString(currentScore, 1), " / ",
DoubleToString(initialScore, 1), " (", DoubleToString(signalRatio * 100, 0), "%)");
LogPrint("+-----------------------------------------+");
managedPositions[posIndex].partialCloseLevel = 3;
ClosePosition(ticket);
// Virtual SL Re-entry after L3 full close
if(EnableVirtualSLReentry)
{
TryVirtualSLReentry(posType, initialScore);
}
continue; // Position is fully closed
}
}
// 3. DYNAMIC SL TIGHTENING (health-based)
if(EnableHealthSLTightening && health.healthScore < SLTightenMinHealthPct && currentATR > 0)
{
// Calculate tightened SL: distance shrinks proportionally with health
// healthRatio = health / startThreshold (1.0 at threshold, 0.0 at dead)
double healthRatio = health.healthScore / SLTightenMinHealthPct;
if(healthRatio < 0.1) healthRatio = 0.1; // Prevent SL at entry (would be break-even)
double slDistance = currentATR * SLTightenATRMultiplier * healthRatio;
double newTightenedSL = 0;
// Re-read position to ensure consistency
if(!PositionSelectByTicket(ticket)) continue;
currentSL = PositionGetDouble(POSITION_SL);
currentTP = PositionGetDouble(POSITION_TP);
long stopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
double minDist = stopLevel * _Point;
if(posType == POSITION_TYPE_BUY)
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
newTightenedSL = NormalizeDouble(bid - slDistance, _Digits);
// Respect break-even lock
if(managedPositions[posIndex].breakEvenLocked && newTightenedSL < entryPrice)
newTightenedSL = NormalizeDouble(entryPrice, _Digits);
// Only move SL UP (more protective)
if(currentSL > 0 && newTightenedSL <= currentSL) continue;
// Respect minimum stop distance
if(newTightenedSL >= bid - minDist) continue;
}
else
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
newTightenedSL = NormalizeDouble(ask + slDistance, _Digits);
// Respect break-even lock
if(managedPositions[posIndex].breakEvenLocked && newTightenedSL > entryPrice)
newTightenedSL = NormalizeDouble(entryPrice, _Digits);
// Only move SL DOWN (more protective)
if(currentSL > 0 && newTightenedSL >= currentSL) continue;
// Respect minimum stop distance
if(newTightenedSL <= ask + minDist) continue;
}
if(IsSLValid(posType, newTightenedSL))
{
if(ModifyPosition(ticket, newTightenedSL, currentTP))
{
LogPrint("+-----------------------------------------+");
LogPrint("[SL TIGHTENED] Ticket: ", ticket);
LogPrint("Health: ", DoubleToString(health.healthScore, 2),
" (ratio: ", DoubleToString(healthRatio, 2), ")");
LogPrint("SL: ", currentSL, " -> ", newTightenedSL,
" (ATR dist: ", DoubleToString(slDistance / _Point, 0), " pts)");
LogPrint("+-----------------------------------------+");
}
}
}
// 5. PROFIT OFFSET SL TIGHTENING (consecutive wins offset)
// When consecutive winning trades close while this losing position is open,
// reduce the max loss exposure by tightening SL proportionally
if(EnableProfitOffsetSL && profit < 0
&& managedPositions[posIndex].profitOffsetConsecWins >= ConsecutiveWinsRequired
&& managedPositions[posIndex].profitOffsetAccumulated >= MinOffsetProfit)
{
// Calculate original risk from SL
double origSL = managedPositions[posIndex].profitOffsetOriginalSL;
// Need valid original SL to calculate offset
if(origSL > 0 && entryPrice > 0)
{
// Calculate value per point for this position's lot size
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(tickValue > 0 && tickSize > 0 && point > 0 && volume > 0)
{
double normalizedTickValue = tickValue * volume;
double pointsPerTick = tickSize / point;
double valuePerPoint = normalizedTickValue / pointsPerTick;
// Calculate original SL distance in USD
double origSLDistPoints = MathAbs(entryPrice - origSL) / _Point;
double origRiskUSD = origSLDistPoints * valuePerPoint;
// Calculate new target risk after offset
double accumulatedProfit = managedPositions[posIndex].profitOffsetAccumulated;
double newTargetRiskUSD = origRiskUSD - accumulatedProfit;
// Only proceed if there's meaningful reduction
if(newTargetRiskUSD < origRiskUSD && newTargetRiskUSD > 0)
{
// Convert new target risk back to points
double newSLDistPoints = newTargetRiskUSD / valuePerPoint;
double newSLDistPrice = newSLDistPoints * _Point;
double newOffsetSL = 0;
// Re-read position to ensure consistency
if(!PositionSelectByTicket(ticket)) continue;
currentSL = PositionGetDouble(POSITION_SL);
currentTP = PositionGetDouble(POSITION_TP);
long offsetStopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
double offsetMinDist = offsetStopLevel * _Point;
if(posType == POSITION_TYPE_BUY)
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
newOffsetSL = NormalizeDouble(entryPrice - newSLDistPrice, _Digits);
// Respect break-even lock
if(managedPositions[posIndex].breakEvenLocked && newOffsetSL < entryPrice)
newOffsetSL = NormalizeDouble(entryPrice, _Digits);
// Only tighten (move SL UP), never widen
if(currentSL > 0 && newOffsetSL <= currentSL)
{
// SL already tighter, skip
}
else if(newOffsetSL >= bid - offsetMinDist)
{
// Too close to price, skip
}
else if(IsSLValid(posType, newOffsetSL))
{
if(ModifyPosition(ticket, newOffsetSL, currentTP))
{
LogPrint("+-----------------------------------------+");
LogPrint("[PROFIT OFFSET SL] Ticket: ", ticket);
LogPrint("Consecutive Wins: ", managedPositions[posIndex].profitOffsetConsecWins,
" | Accumulated: $", DoubleToString(accumulatedProfit, 2));
LogPrint("Original Risk: $", DoubleToString(origRiskUSD, 2),
" -> New Max Risk: $", DoubleToString(newTargetRiskUSD, 2));
LogPrint("SL: ", currentSL, " -> ", newOffsetSL);
LogPrint("+-----------------------------------------+");
}
}
}
else // SELL
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
newOffsetSL = NormalizeDouble(entryPrice + newSLDistPrice, _Digits);
// Respect break-even lock
if(managedPositions[posIndex].breakEvenLocked && newOffsetSL > entryPrice)
newOffsetSL = NormalizeDouble(entryPrice, _Digits);
// Only tighten (move SL DOWN), never widen
if(currentSL > 0 && newOffsetSL >= currentSL)
{
// SL already tighter, skip
}
else if(newOffsetSL <= ask + offsetMinDist)
{
// Too close to price, skip
}
else if(IsSLValid(posType, newOffsetSL))
{
if(ModifyPosition(ticket, newOffsetSL, currentTP))
{
LogPrint("+-----------------------------------------+");
LogPrint("[PROFIT OFFSET SL] Ticket: ", ticket);
LogPrint("Consecutive Wins: ", managedPositions[posIndex].profitOffsetConsecWins,
" | Accumulated: $", DoubleToString(accumulatedProfit, 2));
LogPrint("Original Risk: $", DoubleToString(origRiskUSD, 2),
" -> New Max Risk: $", DoubleToString(newTargetRiskUSD, 2));
LogPrint("SL: ", currentSL, " -> ", newOffsetSL);
LogPrint("+-----------------------------------------+");
}
}
}
}
// If newTargetRiskUSD <= 0, the accumulated profit exceeds original risk
// In this case, try to move SL to break-even (entry price)
else if(newTargetRiskUSD <= 0)
{
if(!PositionSelectByTicket(ticket)) continue;
currentSL = PositionGetDouble(POSITION_SL);
currentTP = PositionGetDouble(POSITION_TP);
double beSL = NormalizeDouble(entryPrice, _Digits);
long beStopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
double beMinDist = beStopLevel * _Point;
bool canApplyBE = false;
if(posType == POSITION_TYPE_BUY)
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
canApplyBE = (beSL < bid - beMinDist) && (currentSL == 0 || beSL > currentSL);
}
else
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
canApplyBE = (beSL > ask + beMinDist) && (currentSL == 0 || beSL < currentSL);
}
if(canApplyBE && IsSLValid(posType, beSL))
{
if(ModifyPosition(ticket, beSL, currentTP))
{
managedPositions[posIndex].breakEvenLocked = true;
LogPrint("+-----------------------------------------+");
LogPrint("[PROFIT OFFSET SL -> BE] Ticket: ", ticket);
LogPrint("Accumulated profit ($", DoubleToString(accumulatedProfit, 2),
") >= Original risk ($", DoubleToString(origRiskUSD, 2), ")");
LogPrint("SL moved to break-even: ", beSL);
LogPrint("+-----------------------------------------+");
}
}
}
}
}
}
// 4. FULL CLOSE + VIRTUAL SL RE-ENTRY (at health threshold)
if(health.healthScore < MinHealthScore)
{
LogPrint("+-----------------------------------------+");
LogPrint("POSITION EXIT TRIGGERED (Health Decay)");
LogPrint("Ticket: ", ticket, " | Profit: $", DoubleToString(PositionGetDouble(POSITION_PROFIT), 2));
LogPrint("Health: ", DoubleToString(health.healthScore, 2), " / ", DoubleToString(MinHealthScore, 2));
LogPrint("Trend: ", health.trendValid ? "OK" : "FAIL",
" | RSI: ", health.momentumValid ? "OK" : "FAIL",
" | ATR: ", DoubleToString(health.adverseATR, 1), "x",
" | Swing: ", health.swingValid ? "OK" : "FAIL");
LogPrint("Reason: ", health.reason);
LogPrint("+-----------------------------------------+");
ClosePosition(ticket);
// Virtual SL + Re-entry: try to re-enter at better price if signal supports it
if(EnableVirtualSLReentry)
{
TryVirtualSLReentry(posType, initialScore);
}
}
}
}
// +------------------------------------------------------------------+
// | Partial Close Position - Close a portion of position volume |
// +------------------------------------------------------------------+
bool PartialClosePosition(ulong ticket, double closeVolume)
{
if(!PositionSelectByTicket(ticket))
{
LogPrint("PartialClose: Position ", ticket, " not found");
return false;
}
LockOrderSend(true);
MqlTradeRequest request = {};
MqlTradeResult result = {};
ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
request.action = TRADE_ACTION_DEAL;
request.position = ticket;
request.symbol = PositionGetString(POSITION_SYMBOL);
request.volume = NormalizeVolume(closeVolume);
request.deviation = 10;
request.magic = PositionGetInteger(POSITION_MAGIC);
request.type_filling = GetFillingMode();
request.type = (type == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
request.price = (type == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if(!OrderSend(request, result))
{
LogPrint("PartialClose failed for ", ticket, " Error: ", GetLastError());
LockOrderSend(false);
return false;
}
LogPrint("Partial close ", ticket, " | Vol: ", closeVolume, " | Retcode: ", result.retcode);
LockOrderSend(false);
return (result.retcode == TRADE_RETCODE_DONE);
}
// +------------------------------------------------------------------+
// | Virtual SL Re-entry - Re-evaluate and re-enter after exit |
// +------------------------------------------------------------------+
void TryVirtualSLReentry(ENUM_POSITION_TYPE posType, double initialScore)
{
if(initialScore <= 0) return;
// NEW-BAR ENTRY GATE (optional for re-entries)
// By default re-entries fire intrabar (immediately at the better price). When
// ReentryRespectsNewBarGate is enabled alongside EnableNewBarEntryOnly, a re-entry
// is only allowed once per closed bar — keeping backtests free of intrabar entries.
if(EnableNewBarEntryOnly && ReentryRespectsNewBarGate)
{
datetime reentryBarTime = iTime(_Symbol, _Period, 0);
if(lastEntryBarTime == reentryBarTime) return;
}
// Check if trading is allowed (respects all guards except duplicate signal filter)
if(targetEquityReached || minimumEquityReached || minEquityTriggersExceeded) return;
if(isPaused || isOutsideTradingHours || isLeverageDiffFromInitial) return;
if(isNearMarketClose) return;
if(isOrderSendLocked) return;
if(CountLosingPositions() >= MaxHoldingLossPositions) return;
if(CountOpenOrders() >= MaxOpenOrders) return;
// Get current signal strength for the same direction
ENUM_ORDER_TYPE orderType = (posType == POSITION_TYPE_BUY) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
SignalStrength strength = GetSignalStrength(orderType);
// Check minimum re-entry threshold
double minReentryScore = initialScore * ReentryMinSignalPct;
if(strength.finalScore >= minReentryScore)
{
// Check direction-specific order enable
if(posType == POSITION_TYPE_BUY && !EnableBuyOrders) return;
if(posType == POSITION_TYPE_SELL && !EnableSellOrders) return;
LogPrint("+-----------------------------------------+");
LogPrint("[VIRTUAL SL RE-ENTRY] Re-entering ", posType == POSITION_TYPE_BUY ? "BUY" : "SELL");
LogPrint("New Signal: ", DoubleToString(strength.finalScore, 1),
" >= Min: ", DoubleToString(minReentryScore, 1),
" (", DoubleToString(ReentryMinSignalPct * 100, 0), "% of ",
DoubleToString(initialScore, 1), ")");
LogPrint("+-----------------------------------------+");
// Open new position at current (better) price
OpenPosition(orderType, strength.finalScore);
// Mark this bar as consumed so the gate (and a normal entry this bar) won't double-enter
if(EnableNewBarEntryOnly && ReentryRespectsNewBarGate)
lastEntryBarTime = iTime(_Symbol, _Period, 0);
}
else
{
LogPrint("[VIRTUAL SL] No re-entry. Signal: ", DoubleToString(strength.finalScore, 1), " < Required: ", DoubleToString(minReentryScore, 1));
}
}
int CountLosingPositions()
{
int count = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(!PositionSelectByTicket(ticket)) continue;
if(PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
double profit = PositionGetDouble(POSITION_PROFIT);
if(profit < 0)
{
count++;
}
}
return count;
}
// +------------------------------------------------------------------+
// +------------------------------------------------------------------+
// | Manage Trailing TP & SL |
// | Adjusts TP/SL based on signal strength and trails price |
// +------------------------------------------------------------------+
void ManageTrailingTPSL(ulong ticket)
{
if (!EnableTrailing) return;
if(!PositionSelectByTicket(ticket)) return;
// Get Position Details
ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
// HEDGE CHAIN: skip any leg that belongs to an active chain. The chain logic
// (ManageHedgeChains) exclusively manages these legs (covered / roll / stop).
if(EnableHedgeChain)
{
int hpi = GetManagedPositionIndex(ticket);
if(hpi != -1 && managedPositions[hpi].chainId != 0)
return;
}
double currentSL = PositionGetDouble(POSITION_SL);
double currentTP = PositionGetDouble(POSITION_TP);
double entryPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double currentPrice = (posType == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double profit = PositionGetDouble(POSITION_PROFIT);
double volume = PositionGetDouble(POSITION_VOLUME);
// Get Signal Strength (smoothed score for management)
ENUM_ORDER_TYPE orderType = (posType == POSITION_TYPE_BUY) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
SignalStrength currentStrength = GetSignalStrength(orderType);
double currentScore = currentStrength.finalScore;
double initialScore = 0;
int posIndex = GetManagedPositionIndex(ticket);
if(posIndex != -1)
{
initialScore = managedPositions[posIndex].signalScore;
}
// ADAPTIVE LOGIC (Delta Based)
double tpAdjustment = 0;
double slAdjustment = 0;
string adaptiveReason = "Normal";
// Calculate score delta (Current - Initial)
// Positive delta = Signal Strengthened
// Negative delta = Signal Weakened
double scoreDelta = currentScore - initialScore;
if(initialScore > 0)
{
if(EnableAdaptiveTP)
{
tpAdjustment = scoreDelta * TrailingValueMultiplier;
}
if(EnableAdaptiveSL)
{
slAdjustment = scoreDelta * TrailingValueMultiplier;
}
if(MathAbs(scoreDelta) > 0)
{
adaptiveReason = "Adaptive (Delta: " + DoubleToString(scoreDelta, 1) + ")";
}
}
// TAKE PROFIT MANAGEMENT (Adaptive)
// Independent R:R owns the TP target: keep it fixed at the entry-set
// R:R level and skip adaptive recomputation so it isn't overwritten.
double newTP = currentTP;
if(EnableTakeProfit && !EnableRiskReward)
{
double effectiveTP = TPValue + tpAdjustment;
// Ensure effective TP doesn't go negative or too small
if(effectiveTP < (TrailingValueMultiplier * 0.1)) effectiveTP = TrailingValueMultiplier * 0.1;
double tpPoints = ConvertToPoints(TPInputType, effectiveTP, volume);
double targetTP = 0;
if(posType == POSITION_TYPE_BUY) targetTP = NormalizeDouble(entryPrice + tpPoints * _Point, _Digits);
else targetTP = NormalizeDouble(entryPrice - tpPoints * _Point, _Digits);
// Only modify if significant difference (> 1 point)
if(MathAbs(targetTP - currentTP) > _Point)
{
newTP = targetTP;
}
}
// TRAILING STOP MANAGEMENT
double newSL = currentSL; // Default to current
bool shouldModifySL = false;
// Filter by profit threshold if enabled (only trail if profit > threshold)
double profitThreshold = MinBreakEvenProfit * ProfitThresholdMultiplier;
bool canTrail = (MinBreakEvenProfit <= 0 || !TrailingSLOnProfitableOnly || profit >= profitThreshold);
if(canTrail)
{
// Calculate effective Trailing Distance
double effectiveDist = TrailingDistanceValue + slAdjustment; // Adaptive TS
// Ensure distance is safe (not negative)
if(effectiveDist < (TrailingValueMultiplier * 0.1)) effectiveDist = TrailingValueMultiplier * 0.1;
// Graduated hedge: trail at HedgeTrailATR x ATR (lot-independent). A large hedge
// lot turns a small dollar-based distance into a near-zero price gap, so the stop
// lands at market and closes instantly; an ATR distance gives it real room to run.
double finalTrailingPoints;
double trailingDistancePrice;
bool useHedgeTrail = (posIndex != -1 && managedPositions[posIndex].hedgeGraduated && HedgeTrailATR > 0);
double hedgeAtr = 0;
if(useHedgeTrail)
{
double _bufATR[];
ArraySetAsSeries(_bufATR, true);
if(CopyBuffer(atrSignalHandle, 0, 0, 2, _bufATR) >= 2) hedgeAtr = _bufATR[1];
}
if(useHedgeTrail && hedgeAtr > 0)
{
trailingDistancePrice = HedgeTrailATR * hedgeAtr;
finalTrailingPoints = trailingDistancePrice / _Point;
}
else
{
finalTrailingPoints = ConvertToPoints(TSInputType, effectiveDist, volume);
trailingDistancePrice = finalTrailingPoints * _Point;
}
long stopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
long freezeLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
double minStopDistance = stopLevel * _Point;
double minFreezeDistance = freezeLevel * _Point;
double minDistance = MathMax(minStopDistance, minFreezeDistance);
double breakEvenPrice = CalculateBreakEvenPrice(ticket, posType, entryPrice, volume);
double calculatedSL = 0;
// Buy position trailing logic
if(posType == POSITION_TYPE_BUY)
{
double profitPoints = (currentPrice - entryPrice) / _Point;
if(profitPoints >= finalTrailingPoints) // Use finalTrailingPoints check logic from original
{
calculatedSL = currentPrice - trailingDistancePrice;
double maxAllowedSL = SymbolInfoDouble(_Symbol, SYMBOL_BID) - minDistance;
if(calculatedSL > maxAllowedSL) calculatedSL = maxAllowedSL;
// Break-even lock
if(TrailingEnableBreakEvenLock && calculatedSL < breakEvenPrice) calculatedSL = breakEvenPrice;
// Only modify if moving UP
if(currentSL == 0 || calculatedSL > currentSL)
{
if(calculatedSL < SymbolInfoDouble(_Symbol, SYMBOL_BID)) // Safety
{
newSL = calculatedSL;
shouldModifySL = true;
}
}
}
}
// Sell position trailing logic
else
{
double profitPoints = (entryPrice - currentPrice) / _Point;
if(profitPoints >= finalTrailingPoints)
{
calculatedSL = currentPrice + trailingDistancePrice;
double minAllowedSL = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + minDistance;
if(calculatedSL < minAllowedSL) calculatedSL = minAllowedSL;
// Break-even lock
if(TrailingEnableBreakEvenLock && calculatedSL > breakEvenPrice) calculatedSL = breakEvenPrice;
// Only modify if moving DOWN
if(currentSL == 0 || calculatedSL < currentSL)
{
if(calculatedSL > SymbolInfoDouble(_Symbol, SYMBOL_ASK)) // Safety
{
newSL = calculatedSL;
shouldModifySL = true;
}
}
}
}
}
// HEDGE RECOVERY LOCK: a graduated hedge must never give back below the recovery level
// (profit = HedgeRecoveryPct% of the older leg's locked loss). Floor the SL at that
// profit, independent of the trailing gate; trailing still rides the SL above it.
if(posIndex != -1 && managedPositions[posIndex].hedgeLockProfit > 0)
{
double lockProfit = managedPositions[posIndex].hedgeLockProfit;
double tv = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double ts = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
if(tv > 0 && ts > 0 && volume > 0)
{
double lockDist = (lockProfit / volume) * (ts / tv); // dollars -> price distance
double bidNow = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double askNow = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double baseSL = shouldModifySL ? newSL : currentSL;
if(posType == POSITION_TYPE_BUY)
{
double lockPrice = NormalizeDouble(entryPrice + lockDist, _Digits);
// Raise the SL up to the lock (but keep an already-better trailed SL)
if(lockPrice > baseSL && lockPrice < bidNow)
{
newSL = lockPrice;
shouldModifySL = true;
}
}
else
{
double lockPrice = NormalizeDouble(entryPrice - lockDist, _Digits);
// Cap the SL down to the lock (but keep an already-better trailed SL)
if((baseSL == 0 || lockPrice < baseSL) && lockPrice > askNow)
{
newSL = lockPrice;
shouldModifySL = true;
}
}
}
}
// Skip if nothing changed
if(!shouldModifySL && MathAbs(newTP - currentTP) < _Point) return;
// Normalize
newSL = NormalizeDouble(newSL, _Digits);
newTP = NormalizeDouble(newTP, _Digits);
// Skip if SL is visually same (if modifier flag was triggered but value didn't change enough - redundant check)
if(shouldModifySL && MathAbs(newSL - currentSL) < _Point && MathAbs(newTP - currentTP) < _Point) return;
// Validate new SL
if(shouldModifySL && !IsSLValid(posType, newSL))
{
LogPrint("SL invalid, skipping. Ticket: ", ticket);
return;
}
LogPrint("+-----------------------------------------+");
LogPrint("POSITION UPDATE (", adaptiveReason, ")");
LogPrint("Ticket: ", ticket, " | Profit: $", profit);
LogPrint("Signal: Init=", initialScore, " -> Current=", currentScore, " (Delta: ", scoreDelta, ")");
if(shouldModifySL) LogPrint("SL: ", currentSL, " -> ", newSL, " (Dist: ", (TrailingDistanceValue + slAdjustment), ")");
if(MathAbs(newTP - currentTP) > _Point) LogPrint("TP: ", currentTP, " -> ", newTP, " (Base+Adj: ", (TPValue + tpAdjustment), ")");
LogPrint("+-----------------------------------------+");
// Try to modify
if(!ModifyPosition(ticket, newSL, newTP))
{
LogPrint("Modify failed. Ticket: ", ticket);
// EMERGENCY CLOSE MECHANISM
// Trigger if modification failed AND profit is substantial
// Prevents losing substantial profit due to inability to trail
// Define substantial as 3x minimum target profit
double minSubstantialProfit = MinBreakEvenProfit * 3.0;
if(MinBreakEvenProfit > 0 && profit >= minSubstantialProfit)
{
LogPrint("!! EMERGENCY CLOSE TRIGGERED !!");
ClosePosition(ticket);
}
}
}
// +------------------------------------------------------------------+
// | Positions Management |
// +------------------------------------------------------------------+
// Register managed position with initial score and entry price
void RegisterManagedPosition(ulong ticket, ENUM_POSITION_TYPE type, double signalScore, double entryPrice = 0, ulong chainId = 0, int hedgeLevel = 0, double chainAnchorLoss = 0, int cycleNum = 0)
{
// Resize array
ArrayResize(managedPositions, managedPositionCount + 1);
// Fill position data
managedPositions[managedPositionCount].ticket = ticket;
managedPositions[managedPositionCount].type = type;
managedPositions[managedPositionCount].signalScore = signalScore;
managedPositions[managedPositionCount].entryPrice = entryPrice;
managedPositions[managedPositionCount].partialCloseLevel = 0;
managedPositions[managedPositionCount].breakEvenLocked = false;
// Initialize profit offset SL tracking
managedPositions[managedPositionCount].profitOffsetConsecWins = 0;
managedPositions[managedPositionCount].profitOffsetAccumulated = 0;
// Initialize hedge chain linkage
managedPositions[managedPositionCount].chainId = chainId;
managedPositions[managedPositionCount].hedgeLevel = hedgeLevel;
managedPositions[managedPositionCount].chainAnchorLoss = chainAnchorLoss;
managedPositions[managedPositionCount].cycleNum = cycleNum;
managedPositions[managedPositionCount].noRehedge = false;
managedPositions[managedPositionCount].hedgeGraduated = false;
managedPositions[managedPositionCount].hedgeLockProfit = 0;
// Capture original SL from broker if position exists
double origSL = 0;
if(PositionSelectByTicket(ticket)) origSL = PositionGetDouble(POSITION_SL);
managedPositions[managedPositionCount].profitOffsetOriginalSL = origSL;
managedPositionCount++;
LogPrint("Registered position. Ticket: ", ticket,
" | Type: ", EnumToString(type),
" | Score: ", signalScore,
" | Entry: ", entryPrice,
" | Managed Positions: ", managedPositionCount);
}
// Remove Position from Managed Array
void RemoveManagedPosition(ulong ticket)
{
for(int i = 0; i < managedPositionCount; i++)
{
if(managedPositions[i].ticket == ticket)
{
// Shift array elements left
for(int j = i; j < managedPositionCount - 1; j++)
{
managedPositions[j] = managedPositions[j + 1];
}
managedPositionCount--;
ArrayResize(managedPositions, managedPositionCount);
LogPrint("Removed position: ", ticket,
" | Remaining Managed Positions: ", managedPositionCount);
break;
}
}
}
// Sync Managed Positions with Broker (reconciliation fallback)
// OnTradeTransaction is the PRIMARY, event-driven close handler. This per-tick pass
// only catches closes that a transaction event might have missed (e.g. an event lost
// across a restart). Both paths funnel through ProcessClosedPosition, which is
// idempotent, so a single close is never accounted for twice.
void SyncManagedPositions()
{
for(int i = managedPositionCount - 1; i >= 0; i--)
{
if(!PositionSelectByTicket(managedPositions[i].ticket))
{
ulong closedTicket = managedPositions[i].ticket;
// Query deal history to find the closing profit of this position
double closedProfit = 0;
bool foundDeal = false;
// Select history for recent period (last 24 hours should be sufficient)
datetime fromTime = TimeCurrent() - 86400;
datetime toTime = TimeCurrent();
if(HistorySelect(fromTime, toTime))
{
int totalDeals = HistoryDealsTotal();
for(int d = totalDeals - 1; d >= 0; d--)
{
ulong dealTicket = HistoryDealGetTicket(d);
if(dealTicket == 0) continue;
// Match deal to our position
ulong dealPosition = HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID);
long dealEntry = HistoryDealGetInteger(dealTicket, DEAL_ENTRY);
long dealMagic = HistoryDealGetInteger(dealTicket, DEAL_MAGIC);
if(dealPosition == closedTicket && dealEntry == DEAL_ENTRY_OUT && dealMagic == MagicNumber)
{
closedProfit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT)
+ HistoryDealGetDouble(dealTicket, DEAL_SWAP)
+ HistoryDealGetDouble(dealTicket, DEAL_COMMISSION);
foundDeal = true;
break;
}
}
}
if(foundDeal)
ProcessClosedPosition(closedTicket, closedProfit);
else
RemoveManagedPosition(closedTicket); // no closing deal found — drop the stale entry
}
}
}
// +------------------------------------------------------------------+
// | Process a Fully-Closed Managed Position (idempotent) |
// | Updates the consecutive-loss cooldown and profit-offset tracking |
// | for remaining open positions, then removes the closed position |
// | from the managed array. Safe to call from both OnTradeTransaction |
// | (primary) and SyncManagedPositions (fallback): the index guard |
// | ensures each close is accounted for exactly once. |
// +------------------------------------------------------------------+
void ProcessClosedPosition(ulong closedTicket, double closedProfit)
{
// Idempotency guard: if it is no longer tracked, this close was already handled
if(GetManagedPositionIndex(closedTicket) == -1) return;
// SIGNAL DAMPENING: Track consecutive losses for cooldown
if(EnableSignalDampening)
{
if(closedProfit < 0)
{
consecutiveLossCount++;
LogPrint("[LOSS TRACKER] Position ", closedTicket, " closed at loss: $",
DoubleToString(closedProfit, 2),
". Consecutive losses: ", consecutiveLossCount);
// Activate cooldown after the configured number of consecutive losses
if(ConsecutiveLossesBeforeCooldown > 0 && consecutiveLossCount >= ConsecutiveLossesBeforeCooldown)
{
datetime currBar = iTime(_Symbol, _Period, 0);
cooldownUntilBarTime = currBar + ConsecutiveLossCooldownBars * PeriodSeconds(_Period);
LogPrint("[COOLDOWN ACTIVATED] ", consecutiveLossCount,
" consecutive losses. No new entries until bar: ",
TimeToString(cooldownUntilBarTime));
}
}
else
{
if(consecutiveLossCount > 0)
{
LogPrint("[LOSS TRACKER] Win streak started. Reset from ",
consecutiveLossCount, " consecutive losses.");
}
consecutiveLossCount = 0; // Reset on any win
}
}
// PROFIT OFFSET SL: Update tracking on all remaining open positions
if(EnableProfitOffsetSL)
{
for(int p = 0; p < managedPositionCount; p++)
{
// Skip the position being removed (closedTicket)
if(managedPositions[p].ticket == closedTicket) continue;
// Only track for positions that are currently in loss
if(!PositionSelectByTicket(managedPositions[p].ticket)) continue;
double posProfit = PositionGetDouble(POSITION_PROFIT);
if(posProfit >= 0) continue; // Only for losing positions
if(closedProfit > 0)
{
// Winning trade: accumulate
managedPositions[p].profitOffsetConsecWins++;
managedPositions[p].profitOffsetAccumulated += closedProfit;
LogPrint("[PROFIT OFFSET] Ticket ", managedPositions[p].ticket,
" | Win #", managedPositions[p].profitOffsetConsecWins,
" | +$", DoubleToString(closedProfit, 2),
" | Total: $", DoubleToString(managedPositions[p].profitOffsetAccumulated, 2));
}
else
{
// Losing trade: reset consecutive counter and accumulated profit
if(managedPositions[p].profitOffsetConsecWins > 0)
{
LogPrint("[PROFIT OFFSET] Ticket ", managedPositions[p].ticket,
" | Consecutive wins reset (closed loss: $",
DoubleToString(closedProfit, 2), ")");
}
managedPositions[p].profitOffsetConsecWins = 0;
managedPositions[p].profitOffsetAccumulated = 0;
}
}
}
RemoveManagedPosition(closedTicket);
}
// Get Managed Position by Ticket
int GetManagedPositionIndex(ulong ticket)
{
for(int i = 0; i < managedPositionCount; i++)
{
if(managedPositions[i].ticket == ticket)
{
return i;
}
}
return -1;
}
// Get Last Position Ticket by Type
// Returns the ticket of the most recently opened position
ulong GetLastPositionTicket(ENUM_POSITION_TYPE type)
{
ulong lastTicket = 0;
datetime lastTime = 0;
for(int i = 0; i < managedPositionCount; i++)
{
ulong ticket = managedPositions[i].ticket;
if(managedPositions[i].type != type) continue;
if(PositionSelectByTicket(ticket))
{
datetime posTime = (datetime)PositionGetInteger(POSITION_TIME);
if(posTime > lastTime)
{
lastTime = posTime;
lastTicket = ticket;
}
}
}
return lastTicket;
}
// Open Position
void OpenPosition(ENUM_ORDER_TYPE orderType, double signalScore = 0)
{
if (!IsAllowedToOpenPosition()) return;
LockOrderSend(true);
MqlTradeRequest request = {};
MqlTradeResult result = {};
// Calculate lot size based on equity drop recovery
// This must be done BEFORE SL/TP conversion so dollar-based values are accurate
double currentLot = CalculateDynamicLotSize(signalScore);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double price = (orderType == ORDER_TYPE_BUY) ? ask : bid;
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = currentLot;
request.type = orderType;
request.price = price;
request.deviation = 10;
request.magic = MagicNumber;
request.comment = "Open Position by Nyao Scalper";
request.type_filling = GetFillingMode();
// Resolve SL (risk) and TP distances. Independent R:R mode overrides
// both manual SL and manual TP; otherwise the manual settings apply.
double slPoints = GetSLPoints(currentLot);
double tpPoints = GetTPPoints(currentLot);
if(slPoints > 0)
{
if(orderType == ORDER_TYPE_BUY)
request.sl = NormalizeDouble(price - (slPoints * _Point), _Digits);
else
request.sl = NormalizeDouble(price + (slPoints * _Point), _Digits);
}
if(tpPoints > 0)
{
if(orderType == ORDER_TYPE_BUY)
request.tp = NormalizeDouble(price + (tpPoints * _Point), _Digits);
else
request.tp = NormalizeDouble(price - (tpPoints * _Point), _Digits);
}
bool orderResult = OrderSend(request, result);
if(orderResult)
{
if(result.retcode == TRADE_RETCODE_DONE)
{
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
double equityDropAmount = lastPeakEquity - currentEquity;
double equityDropPercentage = 0;
if(lastPeakEquity > 0)
{
equityDropPercentage = (equityDropAmount / lastPeakEquity) * 100.0;
}
LogPrint("Order opened successfully. Ticket: ", result.order,
", Type: ", orderType == ORDER_TYPE_BUY ? "BUY" : "SELL",
", Lot Size: ", currentLot,
", Signal Score: ", signalScore,
" (Peak: $", lastPeakEquity,
", Current: $", currentEquity,
", Drop: ", equityDropPercentage, "%)");
if(request.sl > 0)
{
LogPrint(" | SL: ", request.sl);
}
if(request.tp > 0)
{
LogPrint(" | TP: ", request.tp, EnableRiskReward ? StringFormat(" (R:R 1:%.2f)", RiskRewardRatio) : "");
}
// Register position to managed array
ENUM_POSITION_TYPE posType = (orderType == ORDER_TYPE_BUY) ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
RegisterManagedPosition(result.order, posType, signalScore, price);
// Update Candle Counters
datetime currBarTime = iTime(_Symbol, _Period, 0);
if(currentBarTime != currBarTime)
{
currentBarTime = currBarTime;
buysOnCurrentBar = 0;
sellsOnCurrentBar = 0;
}
if(orderType == ORDER_TYPE_BUY) buysOnCurrentBar++;
else sellsOnCurrentBar++;
// Update global last position tracking
if(orderType == ORDER_TYPE_BUY)
{
lastBuyTime = TimeCurrent();
lastBuyPrice = price;
}
else
{
lastSellTime = TimeCurrent();
lastSellPrice = price;
}
}
else
{
LogPrint("Order failed. Return code: ", result.retcode);
}
}
else
{
LogPrint("OrderSend error: ", GetLastError());
}
LockOrderSend(false);
}
// +------------------------------------------------------------------+
// | Compute the Lot Needed to Recover the Older Leg |
// | Sizes the hedge so that, after a favorable move of |
// | HedgeRecoveryATR x ATR, its profit covers HedgeRecoveryPct% of |
// | the older leg's loss - accounting for the older leg continuing to |
// | bleed over that same move. Money<->price uses the EA's standard |
// | tickValue/tickSize convention. |
// | lot = p*olderLot + p*loss / (moneyGainedPerLotOverTargetMove) |
// | Returns 0 if it cannot be computed (caller falls back). |
// +------------------------------------------------------------------+
double ComputeRecoveryLot(double olderLot, double olderLoss, double atr)
{
double p = HedgeRecoveryPct / 100.0;
if(p <= 0) p = 1.0;
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double targetPrice = HedgeRecoveryATR * atr; // favorable move (price units) to recover within
if(tickValue <= 0 || tickSize <= 0 || targetPrice <= 0) return 0;
// Money gained per 1.0 lot over the target favorable move
double moneyPerLot = (targetPrice / tickSize) * tickValue;
if(moneyPerLot <= 0) return 0;
// p*olderLot outpaces the older leg's continued bleed; the second term funds the loss.
return p * olderLot + p * olderLoss / moneyPerLot;
}
// +------------------------------------------------------------------+
// | Decide the Hedge Lot for the Next Leg |
// | Auto-recover sizing (default) or fixed multiplier, then clamped |
// | to HedgeMaxLot and broker volume limits. |
// +------------------------------------------------------------------+
double ComputeHedgeLot(double olderLot, double olderLoss, double atr)
{
double lot = 0;
if(HedgeAutoLot)
lot = ComputeRecoveryLot(olderLot, olderLoss, atr);
// Fallback to fixed multiplier if auto-size is off or could not be computed
if(lot <= 0)
lot = olderLot * HedgeLotMultiplier;
// The hedge must exceed the older leg, otherwise the opposite-direction pair has
// no net recovery power. Use one volume step above as the hard minimum.
double stepVol = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double minLot = olderLot + (stepVol > 0 ? stepVol : 0.01);
if(lot < minLot) lot = minLot;
if(HedgeMaxLot > 0 && lot > HedgeMaxLot) lot = HedgeMaxLot;
return NormalizeVolume(lot);
}
// +------------------------------------------------------------------+
// | Open One Rolling-Hedge Leg |
// | Reversed market order at the pre-computed hedgeLot. Opened NAKED |
// | (no SL/TP): chain logic closes it. Registers the leg under the |
// | shared chainId at the given level, carrying the anchor loss |
// | forward. Bypasses IsAllowedToOpenPosition / MaxOpenOrders. |
// | Returns the new ticket, or 0 on failure. |
// +------------------------------------------------------------------+
ulong OpenChainHedge(ulong chainId, ENUM_POSITION_TYPE prevType, double hedgeLot, int newLevel, double anchorLoss, int cycleNum)
{
LockOrderSend(true);
MqlTradeRequest request = {};
MqlTradeResult result = {};
// Reverse the previous leg's direction (chain alternates BUY/SELL)
ENUM_ORDER_TYPE hedgeOrderType = (prevType == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
hedgeLot = NormalizeVolume(hedgeLot);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double price = (hedgeOrderType == ORDER_TYPE_BUY) ? ask : bid;
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = hedgeLot;
request.type = hedgeOrderType;
request.price = price;
request.deviation = 10;
request.magic = MagicNumber;
request.comment = "Hedge L" + IntegerToString(newLevel) + " by Nyao Scalper";
request.type_filling = GetFillingMode();
// NAKED: no SL/TP. The chain's covered / roll / stop logic closes it.
ulong newTicket = 0;
bool orderResult = OrderSend(request, result);
if(orderResult && result.retcode == TRADE_RETCODE_DONE)
{
ENUM_POSITION_TYPE hedgePosType = (hedgeOrderType == ORDER_TYPE_BUY) ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
RegisterManagedPosition(result.order, hedgePosType, 0, price, chainId, newLevel, anchorLoss, cycleNum);
newTicket = result.order;
LogPrint("+-----------------------------------------+");
LogPrint("[HEDGE CHAIN] Opened hedge L", newLevel, " | Chain: ", chainId, " | Cycle: ", cycleNum);
LogPrint("Leg ", result.order, " (", EnumToString(hedgePosType), ")",
" | Lot: ", hedgeLot, " | Sizing: ", (HedgeAutoLot ? "Auto-Recover" : "Fixed x" + DoubleToString(HedgeLotMultiplier, 2)));
LogPrint("+-----------------------------------------+");
}
else
{
LogPrint("[HEDGE CHAIN] OrderSend failed. Retcode: ", result.retcode, " | Error: ", GetLastError());
}
LockOrderSend(false);
return newTicket;
}
// +------------------------------------------------------------------+
// | Graduate a Leg out of Its Chain |
// | Clears the chain flags so normal trailing / loss management take |
// | over (used when a hedge has covered the loss and should be |
// | trailed, or when only a single orphan leg remains). |
// +------------------------------------------------------------------+
void GraduateChainLeg(ulong ticket)
{
int idx = GetManagedPositionIndex(ticket);
if(idx == -1) return;
managedPositions[idx].chainId = 0;
managedPositions[idx].hedgeLevel = 0;
managedPositions[idx].chainAnchorLoss = 0;
managedPositions[idx].cycleNum = 0;
managedPositions[idx].hedgeGraduated = true; // trail this big-lot leg with HedgeTrailATR
managedPositions[idx].hedgeLockProfit = 0; // caller sets a recovery floor if applicable
}
// +------------------------------------------------------------------+
// | Close Every Open Leg of a Hedge Chain |
// +------------------------------------------------------------------+
void CloseChain(ulong chainId)
{
for(int z = managedPositionCount - 1; z >= 0; z--)
{
if(managedPositions[z].chainId != chainId) continue;
if(PositionSelectByTicket(managedPositions[z].ticket))
ClosePosition(managedPositions[z].ticket);
}
}
// +------------------------------------------------------------------+
// | Release an exhausted chain to adaptive loss management |
// | When a chain can no longer expand (max cycles / lot ceiling), it |
// | is NOT force-closed: every leg is handed back to normal trailing |
// | + loss management and flagged noRehedge so no new chain starts on |
// | it. The legs then resolve via health close / partial / trailing, |
// | and (being chainId 0 again) are re-covered by the basket stop. |
// +------------------------------------------------------------------+
void ReleaseChainToLossMgmt(ulong chainId)
{
int released = 0;
for(int z = 0; z < managedPositionCount; z++)
{
if(managedPositions[z].chainId != chainId) continue;
managedPositions[z].chainId = 0;
managedPositions[z].hedgeLevel = 0;
managedPositions[z].chainAnchorLoss = 0;
managedPositions[z].cycleNum = 0;
managedPositions[z].noRehedge = true; // exhausted - do not hedge these again
managedPositions[z].hedgeGraduated = true; // trail these big-lot legs with HedgeTrailATR
released++;
}
LogPrint("[HEDGE CHAIN] Released chain ", chainId, " (", released,
" legs) to adaptive loss management - no re-hedge.");
}
// +------------------------------------------------------------------+
// | Effective chain-loss stop ($): combines the fixed-$ and |
// | %-of-equity caps. Returns the tighter (smaller) of whichever are |
// | enabled, or 0 if neither is set. |
// +------------------------------------------------------------------+
double ChainLossStopThreshold()
{
double usd = (HedgeMaxChainLossUSD > 0) ? HedgeMaxChainLossUSD : 0;
double pct = (HedgeMaxChainLossPct > 0)
? AccountInfoDouble(ACCOUNT_EQUITY) * HedgeMaxChainLossPct / 100.0
: 0;
if(usd > 0 && pct > 0) return MathMin(usd, pct);
return MathMax(usd, pct);
}
// +------------------------------------------------------------------+
// | Reseed a new cycle when a roll can't proceed (cycle level limit |
// | or lot ceiling). Closes the recovered older leg, partial-closes |
// | the deepest hedge by HedgeCyclePartialPct%, makes the reduced |
// | hedge the level-0 root of a NEW cycle, and opens a fresh L1 to |
// | recover it. Returns false if the hedge can't be reduced. |
// +------------------------------------------------------------------+
bool ReseedCycle(ulong id, ulong olderTicket, ulong hedgeTicket, double hedgeLot,
ENUM_POSITION_TYPE hedgeType, int cycleNum, double atr)
{
double minL = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
if(step <= 0) step = 0.01;
double closeVol = MathFloor((hedgeLot * HedgeCyclePartialPct / 100.0) / step) * step;
double remaining = hedgeLot - closeVol;
if(remaining < minL)
{
closeVol = MathFloor((hedgeLot - minL) / step) * step;
remaining = hedgeLot - closeVol;
}
if(closeVol < minL || remaining < minL)
return false; // can't reduce meaningfully
// 1) Partial-close the deepest hedge FIRST (shrink exposure). If it fails, leave the
// chain fully INTACT (older not yet closed) and bail so the caller releases both
// legs cleanly to loss management - never a half-dismantled chain.
if(!PartialClosePosition(hedgeTicket, closeVol))
{
LogPrint("[HEDGE CHAIN RESEED] Partial close failed for ", hedgeTicket,
" - chain left intact, releasing to loss management.");
return false;
}
// 2) Close the recovered older leg (free / near breakeven)
if(olderTicket != 0) ClosePosition(olderTicket);
// 3) Re-read the reduced hedge -> becomes the new cycle's level-0 root
if(!PositionSelectByTicket(hedgeTicket)) return false;
double remLot = PositionGetDouble(POSITION_VOLUME);
double remPL = PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
double newAnchor = (remPL < 0) ? -remPL : 0.01;
int idx = GetManagedPositionIndex(hedgeTicket);
if(idx == -1) return false;
managedPositions[idx].chainId = hedgeTicket; // new cycle id = this ticket
managedPositions[idx].hedgeLevel = 0;
managedPositions[idx].chainAnchorLoss = newAnchor;
managedPositions[idx].cycleNum = cycleNum + 1;
LogPrint("+-----------------------------------------+");
LogPrint("[HEDGE CHAIN RESEED] New cycle ", cycleNum + 1, " | Chain ", id);
LogPrint("Closed older ", olderTicket, "; closed ", DoubleToString(closeVol, 2),
" of hedge ", hedgeTicket, " (remain ", DoubleToString(remLot, 2),
", anchor $", DoubleToString(newAnchor, 2), ")");
LogPrint("+-----------------------------------------+");
// 4) Open a fresh L1 hedge to recover the reduced root
double hLot = ComputeHedgeLot(remLot, newAnchor, atr);
if(hLot > remLot)
OpenChainHedge(hedgeTicket, hedgeType, hLot, 1, newAnchor, cycleNum + 1);
else
LogPrint("[HEDGE CHAIN RESEED] Reduced root still can't be hedged within lot ceiling - holding as free leg.");
return true;
}
// +------------------------------------------------------------------+
// | Manage Hedge Chains (Rolling Martingale Recovery) |
// | A "chain" keeps at most TWO open legs: the OLDER leg (being |
// | hedged) and its HEDGE (newer, larger, opposite direction). |
// | |
// | - COVERED : hedge profit >= HedgeRecoveryPct% of the older leg's |
// | current loss -> close older, trail the hedge. End. |
// | - ROLL : hedge losing AND older recovered to >= roll min -> |
// | close older (free), open a bigger reverse hedge, up |
// | to HedgeCycleLevels per cycle. |
// | - RESEED : at the cycle level limit OR lot ceiling -> close |
// | older, partial-close the deepest hedge by |
// | HedgeCyclePartialPct%, start a NEW cycle from the |
// | reduced leg (up to HedgeMaxCycles cycles). |
// | - STOP : combined chain loss >= HedgeMaxChainLoss($/%) -> close.|
// | |
// | WARNING: martingale - lots grow each roll; ranging markets can |
// | compound drawdown. Bounded by cycle caps / HedgeMaxLot / stop. |
// +------------------------------------------------------------------+
void ManageHedgeChains()
{
if(!EnableHedgeChain) return;
// Current ATR (closed-candle [1] for stability, matching ManageLosingPositions)
double bufATR[];
ArraySetAsSeries(bufATR, true);
if(CopyBuffer(atrSignalHandle, 0, 0, 2, bufATR) < 2) return;
double atr = bufATR[1];
if(atr <= 0) return;
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
// ---- Collect distinct chain ids currently in the managed array ----
ulong chains[];
int chainCount = 0;
for(int i = 0; i < managedPositionCount; i++)
{
ulong id = managedPositions[i].chainId;
if(id == 0) continue;
bool seen = false;
for(int k = 0; k < chainCount; k++) if(chains[k] == id) { seen = true; break; }
if(!seen) { ArrayResize(chains, chainCount + 1); chains[chainCount++] = id; }
}
// ---- Phase A: manage each existing chain (rolling pair) ----
for(int c = 0; c < chainCount; c++)
{
ulong id = chains[c];
// Identify the OLDER leg (lowest level) and the HEDGE (highest level).
ulong olderTicket = 0, hedgeTicket = 0;
int olderLevel = INT_MAX, hedgeLevel = -1;
double olderPL = 0, hedgePL = 0;
double hedgeLot = 0;
ENUM_POSITION_TYPE hedgeType = POSITION_TYPE_BUY;
double anchorLoss = 0;
int openLegs = 0;
int cycleNum = 0;
double totalPL = 0;
for(int i = 0; i < managedPositionCount; i++)
{
if(managedPositions[i].chainId != id) continue;
ulong t = managedPositions[i].ticket;
if(!PositionSelectByTicket(t)) continue; // leg already gone
openLegs++;
double pl = PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
totalPL += pl;
int lvl = managedPositions[i].hedgeLevel;
cycleNum = managedPositions[i].cycleNum;
if(managedPositions[i].chainAnchorLoss > 0) anchorLoss = managedPositions[i].chainAnchorLoss;
if(lvl < olderLevel) { olderLevel = lvl; olderTicket = t; olderPL = pl; }
if(lvl > hedgeLevel)
{
hedgeLevel = lvl;
hedgeTicket = t;
hedgePL = pl;
hedgeLot = PositionGetDouble(POSITION_VOLUME);
hedgeType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
}
}
// No legs left, or only one (orphan / transient): graduate the survivor
// back to normal management and let the chain dissolve.
if(openLegs == 0) continue;
if(openLegs == 1)
{
if(hedgeTicket != 0) GraduateChainLeg(hedgeTicket);
continue;
}
// COVERED: hedge profit covers the older leg's current loss -> close older, trail hedge.
if(olderPL < 0)
{
double olderLoss = -olderPL;
double coverNeeded = (HedgeRecoveryPct / 100.0) * olderLoss;
if(hedgePL >= coverNeeded)
{
LogPrint("+-----------------------------------------+");
LogPrint("[HEDGE CHAIN COVERED] Chain ", id);
LogPrint("Hedge ", hedgeTicket, " profit $", DoubleToString(hedgePL, 2),
" >= ", DoubleToString(HedgeRecoveryPct, 0), "% of older ", olderTicket,
" loss $", DoubleToString(olderLoss, 2));
LogPrint("Closing older leg; hedge graduates and trails (SL floored at recovery).");
LogPrint("+-----------------------------------------+");
ClosePosition(olderTicket);
GraduateChainLeg(hedgeTicket);
// Recovery floor: keep at least coverNeeded profit locked on the hedge so the
// pair never gives back below the HedgeRecoveryPct net. Trailing rides above it.
int hgi = GetManagedPositionIndex(hedgeTicket);
if(hgi != -1) managedPositions[hgi].hedgeLockProfit = coverNeeded;
continue;
}
}
// ROLL: hedge losing AND older recovered -> close older (free), open next hedge.
if(hedgePL < 0 && olderPL >= HedgeRollMinProfit)
{
// A normal roll needs BOTH: room in the cycle (level cap) AND a strictly
// larger hedge (lot ceiling). If either fails, reseed a new cycle instead.
bool levelOk = (hedgeLevel < HedgeCycleLevels);
double newLot = levelOk ? ComputeHedgeLot(hedgeLot, -hedgePL, atr) : 0;
bool lotOk = (newLot > hedgeLot);
if(levelOk && lotOk)
{
LogPrint("+-----------------------------------------+");
LogPrint("[HEDGE CHAIN ROLL] Chain ", id, " | Cycle ", cycleNum);
LogPrint("Older ", olderTicket, " recovered to $", DoubleToString(olderPL, 2),
"; hedge ", hedgeTicket, " losing $", DoubleToString(hedgePL, 2));
LogPrint("Closing older; opening hedge L", hedgeLevel + 1, " (lot ",
DoubleToString(newLot, 2), " > ", DoubleToString(hedgeLot, 2), ").");
LogPrint("+-----------------------------------------+");
ClosePosition(olderTicket);
OpenChainHedge(id, hedgeType, newLot, hedgeLevel + 1, anchorLoss, cycleNum);
continue;
}
// Cannot roll within this cycle (level cap or lot ceiling) -> reseed or stop.
string why = (!levelOk) ? "cycle level limit" : "lot ceiling";
bool cyclesLeft = (HedgeMaxCycles <= 0 || cycleNum + 1 < HedgeMaxCycles);
if(EnableHedgeCycleReset && cyclesLeft)
{
LogPrint("[HEDGE CHAIN] Chain ", id, " cyc ", cycleNum, ": ", why,
" reached -> partial-close & reseed new cycle.");
if(!ReseedCycle(id, olderTicket, hedgeTicket, hedgeLot, hedgeType, cycleNum, atr))
{
LogPrint("[HEDGE CHAIN] Reseed failed (cannot reduce hedge) -> release to loss mgmt.");
ReleaseChainToLossMgmt(id);
}
continue;
}
else
{
// Chain exhausted (max cycles / lot ceiling with reseed off). Do NOT close:
// hand the legs to adaptive loss management and stop hedging them.
LogPrint("[HEDGE CHAIN EXHAUSTED] Chain ", id, " cyc ", cycleNum, ": ", why, ", ",
(!EnableHedgeCycleReset ? "reseed disabled" : "max cycles reached"),
" -> release to adaptive loss management (no re-hedge).");
ReleaseChainToLossMgmt(id);
continue;
}
}
// STOP: total open loss across the chain exceeds the backstop ($ and/or % equity;
// the tighter threshold wins) -> close every leg.
double stopThr = ChainLossStopThreshold();
if(stopThr > 0 && totalPL <= -stopThr)
{
LogPrint("+-----------------------------------------+");
LogPrint("[HEDGE CHAIN STOPPED] Chain ", id, " | Open legs: ", openLegs);
LogPrint("Total loss $", DoubleToString(totalPL, 2), " <= stop $", DoubleToString(-stopThr, 2));
LogPrint("Closing all chain legs (loss backstop).");
LogPrint("+-----------------------------------------+");
CloseChain(id);
continue;
}
// Otherwise hold and wait for price to resolve the pair.
}
// ---- Phase B: start a new chain for a qualifying standalone losing position ----
for(int i = 0; i < managedPositionCount; i++)
{
if(managedPositions[i].chainId != 0) continue; // already in a chain
if(managedPositions[i].noRehedge) continue; // exhausted chain leg - left to loss mgmt
ulong ticket = managedPositions[i].ticket;
if(!PositionSelectByTicket(ticket)) continue;
if(PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
double pl = PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
if(pl >= 0) continue; // not losing
ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
double entryPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double volume = PositionGetDouble(POSITION_VOLUME);
double curTP = PositionGetDouble(POSITION_TP);
double curSL = PositionGetDouble(POSITION_SL);
double adverse = (posType == POSITION_TYPE_BUY) ? (entryPrice - bid) : (ask - entryPrice);
if(adverse <= 0) continue;
if((adverse / atr) < HedgeTriggerATR) continue;
// ANTI-SPIKE: only hedge if the REVERSE direction's signal score confirms the move.
// A wick/spike that crosses the ATR trigger intrabar but isn't a real reversal will
// not have a strong opposite-direction score, so no doubled hedge is opened. If the
// reversal is genuine the score builds up and the hedge fires on a later tick; if it
// was a spike the position recovers and no hedge is needed.
if(HedgeRequireSignal)
{
ENUM_ORDER_TYPE hedgeDir = (posType == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
double hedgeScore = GetSignalStrength(hedgeDir).finalScore;
if(hedgeScore < HedgeMinSignalScore)
{
LogPrint("[HEDGE CHAIN] Skip start for ", ticket, ": reverse signal ",
DoubleToString(hedgeScore, 2), " < ", DoubleToString(HedgeMinSignalScore, 2),
" (likely spike) - waiting for confirmation.");
continue;
}
}
double anchorLoss = -pl; // positive loss magnitude at chain start
// Size the first hedge to recover the original's loss. Only start the chain if
// that hedge can be strictly larger than the original - otherwise the pair would
// freeze (equal opposite lots never recover). If the original is already at/above
// HedgeMaxLot, leave it to normal loss management instead of starting a doomed chain.
double hedgeLot = ComputeHedgeLot(volume, anchorLoss, atr);
if(hedgeLot <= volume)
{
LogPrint("[HEDGE CHAIN] Skip start for ", ticket, ": hedge lot ", DoubleToString(hedgeLot, 2),
" not > position lot ", DoubleToString(volume, 2), " (HedgeMaxLot ",
DoubleToString(HedgeMaxLot, 2), "). Left to normal management.");
continue;
}
// Promote this position to the first leg (level 0, cycle 0) of a new chain.
managedPositions[i].chainId = ticket;
managedPositions[i].hedgeLevel = 0;
managedPositions[i].chainAnchorLoss = anchorLoss;
managedPositions[i].cycleNum = 0;
managedPositions[i].hedgeGraduated = false; // active chain leg again, not a graduated trailer
managedPositions[i].hedgeLockProfit = 0;
// Clear the first position's SL so the chain logic alone governs it (optional).
if(HedgeClearRootSL && curSL != 0) ModifyPosition(ticket, 0, curTP);
LogPrint("+-----------------------------------------+");
LogPrint("[HEDGE CHAIN STARTED] First leg ", ticket, " (", EnumToString(posType), ")");
LogPrint("Start loss: $", DoubleToString(anchorLoss, 2),
" | Adverse: ", DoubleToString(adverse / atr, 2), " ATR >= ", DoubleToString(HedgeTriggerATR, 2));
LogPrint("+-----------------------------------------+");
// Open the first hedge (level 1) against this losing position.
OpenChainHedge(ticket, posType, hedgeLot, 1, anchorLoss, 0);
}
}
// +------------------------------------------------------------------+
// | Compute Pullback Limit Entry Price for a Direction |
// | Honors LimitEntryAnchor: |
// | FIXED_ATR : flat depth = LimitEntryATRFraction * ATR |
// | EMA : anchor at the fast EMA |
// | SWING : anchor at the recent swing low/high (structure) |
// | SMART : nearer-to-price of swing/EMA |
// | The structural modes are capped no deeper than the ATR fraction |
// | and always clamped to the broker stop level. Falls back to the |
// | fixed depth when no valid level sits on the pullback side. |
// | Returns 0 on data error. |
// +------------------------------------------------------------------+
double ComputeLimitEntryPrice(ENUM_ORDER_TYPE dir, double atr)
{
bool isBuy = (dir == ORDER_TYPE_BUY);
double ref = isBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
long stopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
double minStopDist = stopLevel * _Point;
double maxDist = atr * LimitEntryATRFraction; // ATR cap / fixed depth
if(maxDist <= 0) return 0;
double fixedDepthPrice = isBuy ? (ref - maxDist) : (ref + maxDist); // deepest allowed
double minDistPrice = isBuy ? (ref - minStopDist) : (ref + minStopDist); // shallowest allowed
// FIXED_ATR: flat depth, no structural anchor (clamp to broker stop level)
if(LimitEntryAnchor == LIMIT_ANCHOR_FIXED_ATR)
{
double pf = isBuy ? MathMin(fixedDepthPrice, minDistPrice)
: MathMax(fixedDepthPrice, minDistPrice);
return NormalizeDouble(pf, _Digits);
}
// Gather structural anchors on the pullback side of price
// ("nearer to price" = max for buy, min for sell)
double anchor = isBuy ? -DBL_MAX : DBL_MAX;
bool haveAnchor = false;
// Fast EMA (current value)
if(LimitEntryAnchor == LIMIT_ANCHOR_EMA || LimitEntryAnchor == LIMIT_ANCHOR_SMART)
{
double bufEMA[];
ArraySetAsSeries(bufEMA, true);
if(CopyBuffer(emaFastHandle, 0, 0, 1, bufEMA) >= 1)
{
double ema = bufEMA[0];
if(isBuy ? (ema < ref) : (ema > ref))
{
anchor = isBuy ? MathMax(anchor, ema) : MathMin(anchor, ema);
haveAnchor = true;
}
}
}
// Swing level over the health swing lookback
if(LimitEntryAnchor == LIMIT_ANCHOR_SWING || LimitEntryAnchor == LIMIT_ANCHOR_SMART)
{
int look = MathMax(5, HealthSwingLookback);
MqlRates rates[];
ArraySetAsSeries(rates, true);
int copied = CopyRates(_Symbol, _Period, 1, look, rates);
if(copied > 0)
{
double sw = isBuy ? rates[0].low : rates[0].high;
for(int j = 1; j < copied; j++)
sw = isBuy ? MathMin(sw, rates[j].low) : MathMax(sw, rates[j].high);
if(isBuy ? (sw < ref) : (sw > ref))
{
anchor = isBuy ? MathMax(anchor, sw) : MathMin(anchor, sw);
haveAnchor = true;
}
}
}
// No valid anchor on the pullback side -> fall back to fixed depth
double price = haveAnchor ? anchor : fixedDepthPrice;
// Cap: never deeper than the ATR fraction...
price = isBuy ? MathMax(price, fixedDepthPrice) : MathMin(price, fixedDepthPrice);
// ...and always respect the broker stop level (this bound wins)
price = isBuy ? MathMin(price, minDistPrice) : MathMax(price, minDistPrice);
return NormalizeDouble(price, _Digits);
}
// +------------------------------------------------------------------+
// | Place Pending Limit Entry (pullback) - fresh entries only |
// | Used when EnableLimitEntry is on. The resulting position is |
// | registered at FILL time via OnTradeTransaction (DEAL_ENTRY_IN); |
// | the entry-thesis score is stashed in the order comment so it |
// | survives until the fill. Virtual-SL re-entries never come here. |
// +------------------------------------------------------------------+
void PlaceLimitEntry(ENUM_ORDER_TYPE dir, double signalScore)
{
if(!IsAllowedToOpenPosition()) return;
// One working pending at a time (one entry decision per signal)
if(CountWorkingLimitOrders() > 0) return;
// Current ATR drives both the fixed depth and the cap for structural anchors
double bufATR[];
ArraySetAsSeries(bufATR, true);
if(CopyBuffer(atrSignalHandle, 0, 0, 1, bufATR) < 1) return;
double atr = bufATR[0];
if(atr <= 0) return;
double entry = ComputeLimitEntryPrice(dir, atr);
if(entry <= 0) return;
double currentLot = CalculateDynamicLotSize(signalScore);
double ref = (dir == ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK)
: SymbolInfoDouble(_Symbol, SYMBOL_BID);
LockOrderSend(true);
MqlTradeRequest request = {};
MqlTradeResult result = {};
request.action = TRADE_ACTION_PENDING;
request.symbol = _Symbol;
request.volume = currentLot;
request.deviation = 10;
request.magic = MagicNumber;
request.type_time = ORDER_TIME_GTC; // expiry handled by ManagePendingOrders (broker-agnostic)
request.comment = "NyaoLE|" + DoubleToString(signalScore, 2); // stash entry-thesis score
request.price = entry;
double slPts = GetSLPoints(currentLot);
double tpPts = GetTPPoints(currentLot);
if(dir == ORDER_TYPE_BUY)
{
request.type = ORDER_TYPE_BUY_LIMIT;
if(slPts > 0)
request.sl = NormalizeDouble(entry - slPts * _Point, _Digits);
if(tpPts > 0)
request.tp = NormalizeDouble(entry + tpPts * _Point, _Digits);
}
else
{
request.type = ORDER_TYPE_SELL_LIMIT;
if(slPts > 0)
request.sl = NormalizeDouble(entry + slPts * _Point, _Digits);
if(tpPts > 0)
request.tp = NormalizeDouble(entry - tpPts * _Point, _Digits);
}
if(OrderSend(request, result) && result.retcode == TRADE_RETCODE_DONE)
{
double depthPts = MathAbs(ref - entry) / _Point;
LogPrint("+-----------------------------------------+");
LogPrint("[LIMIT ENTRY PLACED] ", dir == ORDER_TYPE_BUY ? "BUY LIMIT" : "SELL LIMIT",
" | Anchor: ", EnumToString(LimitEntryAnchor));
LogPrint("Price: ", entry, " | Depth: ", DoubleToString(depthPts, 0),
" pts (cap ", DoubleToString(LimitEntryATRFraction, 2), " ATR)");
LogPrint("Lot: ", currentLot, " | Signal: ", DoubleToString(signalScore, 1));
LogPrint("+-----------------------------------------+");
}
else
{
LogPrint("[LIMIT ENTRY] OrderSend failed. Retcode: ", result.retcode, " Error: ", GetLastError());
}
LockOrderSend(false);
}
// Count our working (pending) limit orders on this symbol
int CountWorkingLimitOrders()
{
int count = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i);
if(ticket == 0) continue;
if(!OrderSelect(ticket)) continue;
if(OrderGetInteger(ORDER_MAGIC) != MagicNumber) continue;
if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue;
ENUM_ORDER_TYPE ot = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
if(ot == ORDER_TYPE_BUY_LIMIT || ot == ORDER_TYPE_SELL_LIMIT) count++;
}
return count;
}
// Cancel a pending order
bool DeletePendingOrder(ulong ticket)
{
LockOrderSend(true);
MqlTradeRequest request = {};
MqlTradeResult result = {};
request.action = TRADE_ACTION_REMOVE;
request.order = ticket;
bool ok = OrderSend(request, result);
if(!ok || result.retcode != TRADE_RETCODE_DONE)
LogPrint("[LIMIT ENTRY] Cancel failed for ", ticket, " Retcode: ", result.retcode, " Error: ", GetLastError());
LockOrderSend(false);
return (ok && result.retcode == TRADE_RETCODE_DONE);
}
// Recover the stashed entry-thesis score from a limit-order comment (-1 if absent)
double ParseLimitEntryScore(string comment)
{
int p = StringFind(comment, "NyaoLE|");
if(p < 0) return -1;
return StringToDouble(StringSubstr(comment, p + 7));
}
// +------------------------------------------------------------------+
// | Manage Pending Limit Entries |
// | Cancels unfilled pendings on expiry (bar age) or when the |
// | directional signal no longer clears its threshold. Runs in every |
// | state (called from ManagePositions) so stale pendings can't fill. |
// +------------------------------------------------------------------+
void ManagePendingOrders()
{
if(!EnableLimitEntry) return;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i);
if(ticket == 0) continue;
if(!OrderSelect(ticket)) continue;
if(OrderGetInteger(ORDER_MAGIC) != MagicNumber) continue;
if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue;
ENUM_ORDER_TYPE ot = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
if(ot != ORDER_TYPE_BUY_LIMIT && ot != ORDER_TYPE_SELL_LIMIT) continue;
// 1. Expiry by bar age (broker-agnostic; placed GTC and aged out here)
if(LimitEntryExpiryBars > 0)
{
datetime setup = (datetime)OrderGetInteger(ORDER_TIME_SETUP);
int barsElapsed = iBarShift(_Symbol, _Period, setup, false);
if(barsElapsed >= LimitEntryExpiryBars)
{
LogPrint("[LIMIT ENTRY] Expired after ", barsElapsed, " bar(s). Cancelling ticket ", ticket);
DeletePendingOrder(ticket);
continue;
}
}
// 2. Cancel when the directional signal no longer clears its threshold
if(LimitEntryCancelOnFlip)
{
bool buy = (ot == ORDER_TYPE_BUY_LIMIT);
SignalStrength s = GetSignalStrength(buy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);
double thr = buy ? MinBuySignalScore : MinSellSignalScore;
if(s.finalScore < thr)
{
LogPrint("[LIMIT ENTRY] Signal faded (", DoubleToString(s.finalScore, 1),
" < ", DoubleToString(thr, 1), "). Cancelling ticket ", ticket);
DeletePendingOrder(ticket);
}
}
}
}
// Close Position
bool ClosePosition(ulong ticket)
{
if(!PositionSelectByTicket(ticket))
{
LogPrint("Position ", ticket, " not found");
return false;
}
LockOrderSend(true);
MqlTradeRequest request = {};
MqlTradeResult result = {};
request.action = TRADE_ACTION_DEAL;
request.position = ticket;
request.symbol = PositionGetString(POSITION_SYMBOL);
request.volume = PositionGetDouble(POSITION_VOLUME);
request.deviation = 10;
request.magic = PositionGetInteger(POSITION_MAGIC);
request.type_filling = GetFillingMode();
ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
request.type = (type == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
request.price = (type == POSITION_TYPE_BUY) ?
SymbolInfoDouble(_Symbol, SYMBOL_BID) :
SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if(!OrderSend(request, result))
{
LogPrint("Failed to close position ", ticket, " Error: ", GetLastError());
LockOrderSend(false);
return false;
}
LogPrint("Position ", ticket, " closed successfully");
LockOrderSend(false);
return true;
}
// Close all positions regardless of profit/loss
void CloseAllPositions(bool unProfitableOnly = false, bool skipChainLegs = false)
{
int closedCount = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(PositionSelectByTicket(ticket))
{
if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
PositionGetInteger(POSITION_MAGIC) == MagicNumber)
{
double profit = PositionGetDouble(POSITION_PROFIT);
if (unProfitableOnly && profit >= 0) continue;
// Leave active hedge-chain legs alone (basket stop only sweeps normal trades)
if(skipChainLegs && EnableHedgeChain)
{
int idx = GetManagedPositionIndex(ticket);
if(idx != -1 && managedPositions[idx].chainId != 0) continue;
}
LogPrint("Closing position. Ticket: ", ticket, ", Profit/Loss: $", profit);
if(ClosePosition(ticket))
{
closedCount++;
LogPrint("Position closed successfully: ", ticket);
}
else
{
LogPrint("ERROR: Failed to close position: ", ticket, ". Error: ", GetLastError());
}
}
}
}
if(closedCount > 0)
{
LogPrint("Total positions closed: ", closedCount);
}
}
// Modify position SL/TP
bool ModifyPosition(ulong ticket, double newSL, double newTP)
{
// Select the position
if(!PositionSelectByTicket(ticket))
{
LogPrint("Error: Failed to select position #", ticket);
return false;
}
// Get position information
string symbol = PositionGetString(POSITION_SYMBOL);
double currentSL = PositionGetDouble(POSITION_SL);
double currentTP = PositionGetDouble(POSITION_TP);
// Prepare request
MqlTradeRequest request = {};
MqlTradeResult result = {};
request.action = TRADE_ACTION_SLTP;
request.position = ticket;
request.symbol = symbol;
request.sl = NormalizeDouble(newSL, _Digits);
request.tp = NormalizeDouble(newTP, _Digits);
// Prevent unnecessary modifications
if(NormalizeDouble(newSL, _Digits) == NormalizeDouble(currentSL, _Digits) &&
NormalizeDouble(newTP, _Digits) == NormalizeDouble(currentTP, _Digits))
{
return true;
}
// Send modification request
if(!OrderSend(request, result))
{
LogPrint("PositionModify failed for position #", ticket, " Error: ", GetLastError());
LogPrint("Retcode: ", result.retcode, " - ", result.comment);
return false;
}
LogPrint("Position #", ticket, " modified successfully");
LogPrint("Old SL: ", currentSL, " -> New SL: ", newSL);
LogPrint("Old TP: ", currentTP, " -> New TP: ", newTP);
return true;
}
// Helper function to check is allowed to open position
bool IsAllowedToOpenPosition()
{
if (targetEquityReached || minimumEquityReached || minEquityTriggersExceeded)
{
LogPrint("+-----------------------------------------+");
LogPrint("OPEN ORDER BLOCKED!");
LogPrint("Trading Stopped! Opening new order are not allowed!");
LogPrint("+-----------------------------------------+");
return false;
}
if (isPaused || isOutsideTradingHours || isLeverageDiffFromInitial)
{
LogPrint("+-----------------------------------------+");
LogPrint("OPEN ORDER BLOCKED!");
LogPrint("Trading Paused! Opening new order are not allowed during pause period!");
LogPrint("+-----------------------------------------+");
return false;
}
if(isNearMarketClose)
{
LogPrint("+-----------------------------------------+");
LogPrint("OPEN ORDER BLOCKED!");
LogPrint("Market closing soon! No opening new positions.");
LogPrint("+-----------------------------------------+");
return false;
}
if (CountLosingPositions() >= MaxHoldingLossPositions)
{
LogPrint("+-----------------------------------------+");
LogPrint("OPEN ORDER BLOCKED!");
LogPrint("Maximum holding loss positions reached!");
LogPrint("+-----------------------------------------+");
return false;
}
if (CountOpenOrders() >= MaxOpenOrders)
{
LogPrint("+-----------------------------------------+");
LogPrint("OPEN ORDER BLOCKED!");
LogPrint("Maximum consecutive open order reached!");
LogPrint("+-----------------------------------------+");
return false;
}
if (isOrderSendLocked) {
LogPrint("+-----------------------------------------+");
LogPrint("OPEN ORDER BLOCKED!");
LogPrint("An order is still being processed!");
LogPrint("+-----------------------------------------+");
return false;
}
if (IsSpreadTooWide())
{
LogPrint("+-----------------------------------------+");
LogPrint("OPEN ORDER BLOCKED!");
LogPrint("Spread too wide for entry.");
LogPrint("+-----------------------------------------+");
return false;
}
return true;
}
// +------------------------------------------------------------------+
// | Spread Filter - Block entries when spread is too wide |
// | When MaxSpreadPoints > 0 uses a fixed cap, otherwise derives a |
// | cap from current ATR (MaxSpreadATRRatio fraction of ATR points) |
// +------------------------------------------------------------------+
bool IsSpreadTooWide()
{
if(!EnableMaxSpreadFilter) return false;
double spreadPoints = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
double cap = MaxSpreadPoints;
if(cap <= 0)
{
// Auto mode: cap = fraction of current ATR expressed in points
double bufATR[];
ArraySetAsSeries(bufATR, true);
if(CopyBuffer(atrSignalHandle, 0, 0, 1, bufATR) < 1) return false; // can't judge, don't block
double atrPoints = (_Point > 0) ? bufATR[0] / _Point : 0;
cap = atrPoints * MaxSpreadATRRatio;
if(cap <= 0) return false; // no usable ATR, don't block
}
if(spreadPoints > cap)
{
LogPrint("[SPREAD] Blocked: spread ", DoubleToString(spreadPoints, 0),
" pts > cap ", DoubleToString(cap, 0), " pts");
return true;
}
return false;
}
// Helper function to lock/unlock OrderSend execution
void LockOrderSend(bool isLocked)
{
isOrderSendLocked = isLocked;
}
// Helper function to get the supported filling mode for the current symbol
ENUM_ORDER_TYPE_FILLING GetFillingMode()
{
uint filling = (uint)SymbolInfoInteger(_Symbol, SYMBOL_FILLING_MODE);
if((filling & SYMBOL_FILLING_FOK) != 0) return ORDER_FILLING_FOK;
if((filling & SYMBOL_FILLING_IOC) != 0) return ORDER_FILLING_IOC;
return ORDER_FILLING_RETURN;
}
// Helper function to validate SL price
bool IsSLValid(ENUM_POSITION_TYPE posType, double sl)
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
long stopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
long freezeLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
double minDistance = MathMax(stopLevel, freezeLevel) * _Point;
if(posType == POSITION_TYPE_BUY)
{
if(sl >= bid - minDistance) return false;
}
else
{
if(sl <= ask + minDistance) return false;
}
return true;
}
// Helper for normalize volume
double NormalizeVolume(double volume)
{
double minVol = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxVol = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double stepVol = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
volume = MathMax(volume, minVol);
volume = MathMin(volume, maxVol);
volume = MathRound(volume / stepVol) * stepVol;
return volume;
}
// Helper to count open orders
int CountOpenOrders()
{
int count = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(PositionSelectByTicket(ticket))
{
if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
PositionGetInteger(POSITION_MAGIC) == MagicNumber)
{
count++;
}
}
}
return count;
}
int CountOpenOrdersByType(ENUM_POSITION_TYPE posType)
{
int count = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(PositionSelectByTicket(ticket))
{
if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
PositionGetInteger(POSITION_MAGIC) == MagicNumber &&
PositionGetInteger(POSITION_TYPE) == posType)
{
count++;
}
}
}
return count;
}
// +------------------------------------------------------------------+
//+-------------------------------------------------------------------+
//| Calculate Dynamic Lot Size - Equity Drop Recovery Based |
//| Lot increases based on equity drop from peak to recover losses |
//| Only applies when signal score meets MinSignalStrengthForLot |
//+-------------------------------------------------------------------+
double CalculateDynamicLotSize(double signalScore = 0)
{
if(!EnableDynamicLots) return BaseLotSize;
double currentLot = BaseLotSize;
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
// Calculate equity drop from peak as percentage
double equityDropPercent = 0;
if(peakEquity > 0)
{
equityDropPercent = ((peakEquity - currentEquity) / peakEquity) * 100.0;
}
// Each EquityDropPercent step adds LotStepSize
// Only increase lot if signal score validates the entry
// GUARDRAILS: never scale up while already bleeding (cooldown after losses
// or an active basket loss), and cap the number of drawdown-based steps.
bool inCooldown = (cooldownUntilBarTime > 0 && iTime(_Symbol, _Period, 0) < cooldownUntilBarTime);
bool basketBleeding = (EnableBasketStop && GetTotalFloatingPL() < 0);
int equitySteps = 0;
if(equityDropPercent > 0 && EquityDropPercent > 0 && signalScore >= MinSignalStrengthForLot
&& !inCooldown && !basketBleeding)
{
equitySteps = (int)(equityDropPercent / EquityDropPercent);
// Cap cumulative drawdown lot steps (0 = unlimited)
if(MaxEquityDropLotSteps > 0 && equitySteps > MaxEquityDropLotSteps)
equitySteps = MaxEquityDropLotSteps;
}
double equityLotIncrease = equitySteps * LotStepSize;
currentLot += equityLotIncrease;
// APPLY LIMITS
// Apply user-defined limits
if(currentLot < BaseLotSize) currentLot = BaseLotSize;
if(currentLot > MaxLotSize) currentLot = MaxLotSize;
// Round to 2 decimal places (standard lot step)
currentLot = NormalizeDouble(currentLot, 2);
// Apply broker limits
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
if(currentLot < minLot) currentLot = minLot;
if(currentLot > maxLot) currentLot = maxLot;
// Round to valid lot step
currentLot = MathFloor(currentLot / lotStep) * lotStep;
// MARGIN CHECK
double marginNeeded = 0;
if(!OrderCalcMargin(ORDER_TYPE_BUY, _Symbol, currentLot, SymbolInfoDouble(_Symbol, SYMBOL_ASK), marginNeeded))
{
LogPrint("ERROR: Failed to calculate margin: ", GetLastError());
return minLot;
}
double availableMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
if(marginNeeded > availableMargin)
{
double symbolLotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double maxAffordableLot = minLot;
double testMargin = 0;
if (symbolLotStep == 0) symbolLotStep = 0.01;
double testLot = minLot;
while(testLot <= currentLot)
{
if(OrderCalcMargin(ORDER_TYPE_BUY, _Symbol, testLot, SymbolInfoDouble(_Symbol, SYMBOL_ASK), testMargin))
{
if(testMargin <= availableMargin)
{
maxAffordableLot = testLot;
testLot += symbolLotStep;
}
else
{
break;
}
}
else
{
break;
}
}
currentLot = maxAffordableLot;
if(currentLot < minLot)
{
LogPrint("WARNING: Insufficient margin. Required: $", marginNeeded,
", Available: $", availableMargin);
return minLot;
}
LogPrint("WARNING: Reduced lot from calculated to affordable: ", currentLot,
" (Required margin: $", marginNeeded, ", Available: $", availableMargin, ")");
}
LogPrint("Dynamic Lot Calculation: Base=", BaseLotSize,
" | Equity Drop Steps=", equitySteps, " (+", equityLotIncrease, ")",
" | Signal Steps=", (signalScore >= MinSignalStrengthForLot ? (signalScore - MinSignalStrengthForLot) / 2 : 0),
" | Final Lot=", currentLot);
return currentLot;
}
// +------------------------------------------------------------------+
// | Calculate True Break-even Price |
// +------------------------------------------------------------------+
double CalculateBreakEvenPrice(ulong ticket, ENUM_POSITION_TYPE posType, double entryPrice, double volume)
{
// Get current spread
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double spread = ask - bid;
// Get commission using deals
double commission = GetPositionRoundTripCommission(ticket);
// Get swap
double swap = 0;
if(PositionSelectByTicket(ticket))
{
swap = PositionGetDouble(POSITION_SWAP);
}
// For total cost, only count swap if it's negative (a cost)
double swapCost = (swap < 0) ? MathAbs(swap) : 0;
// Calculate total cost in account currency
double totalCost = commission + swapCost;
// Convert cost to price distance
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double costInPrice = 0;
double minProfitInPrice = 0;
if(tickValue != 0 && volume != 0)
{
// Convert cost to price
costInPrice = (totalCost / volume) * (tickSize / tickValue);
// Convert MinBreakEvenProfit ($) to price (0 = disabled, no offset)
if(MinBreakEvenProfit > 0)
minProfitInPrice = (MinBreakEvenProfit / volume) * (tickSize / tickValue);
}
// Calculate break-even price
double breakEvenPrice;
if(posType == POSITION_TYPE_BUY)
{
// BUY: Entry + spread + costs
breakEvenPrice = entryPrice + spread + costInPrice + minProfitInPrice;
}
else
{
// SELL: Entry - spread - costs
breakEvenPrice = entryPrice - spread - costInPrice - minProfitInPrice;
}
return NormalizeDouble(breakEvenPrice, _Digits);
}
// Get Total Commission for a position (entry + exit estimate)
double GetPositionRoundTripCommission(ulong positionTicket)
{
double entryCommission = 0.0;
if(!HistorySelectByPosition(positionTicket)) return 0.0;
// Get entry commission
for(int i = 0; i < HistoryDealsTotal(); i++)
{
ulong dealTicket = HistoryDealGetTicket(i);
if(dealTicket > 0)
{
ENUM_DEAL_ENTRY dealEntry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(dealTicket, DEAL_ENTRY);
if(dealEntry == DEAL_ENTRY_IN)
{
entryCommission += HistoryDealGetDouble(dealTicket, DEAL_COMMISSION);
break; // Found entry, no need to continue
}
}
}
// Double it to estimate round-trip (entry + exit)
// This is an approximation since exit commission hasn't happened yet
return MathAbs(entryCommission) * 2.0;
}
// +------------------------------------------------------------------+
// +------------------------------------------------------------------+
// | Convert Input Value to Points Based on Input Type |
// +------------------------------------------------------------------+
double ConvertToPoints(ENUM_INPUT_TYPE inputType, double value, double lotSize)
{
double points = 0;
switch(inputType)
{
case INPUT_POINTS:
points = value;
break;
case INPUT_DOLLAR:
{
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(tickValue > 0 && tickSize > 0 && lotSize > 0 && point > 0)
{
// Normalize tick value to the lot size we're using
double normalizedTickValue = tickValue * lotSize;
// Calculate how many points in one tick
double pointsPerTick = tickSize / point;
if(pointsPerTick <= 0)
{
LogPrint("Error: Invalid pointsPerTick (", pointsPerTick, ")");
return 0;
}
// Value per point = (value per tick) / (points per tick)
double valuePerPoint = normalizedTickValue / pointsPerTick;
// Convert dollars to points
points = value / valuePerPoint;
}
else
{
LogPrint("Error: Invalid tick value (", tickValue, "), tick size (", tickSize, "), point (", point, "), or lot size (", lotSize, ")");
}
}
break;
case INPUT_PERCENT:
{
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
double dollarAmount = equity * (value / 100.0);
// Reuse the dollar conversion logic
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(tickValue > 0 && tickSize > 0 && lotSize > 0 && point > 0)
{
// Normalize tick value to the lot size we're using
double normalizedTickValue = tickValue * lotSize;
// Calculate how many points in one tick
double pointsPerTick = tickSize / point;
if(pointsPerTick <= 0)
{
LogPrint("Error: Invalid pointsPerTick in percent conversion (", pointsPerTick, ")");
return 0;
}
// Value per point = (value per tick) / (points per tick)
double valuePerPoint = normalizedTickValue / pointsPerTick;
// Convert dollars to points
points = dollarAmount / valuePerPoint;
}
else
{
LogPrint("Error: Invalid parameters for percent conversion");
}
}
break;
}
return points;
}
// +------------------------------------------------------------------+
// | Current ATR value in price terms (last closed bar, 0 on failure). |
// +------------------------------------------------------------------+
double GetCurrentATR()
{
double buf[];
ArraySetAsSeries(buf, true);
if(CopyBuffer(atrSignalHandle, 0, 1, 1, buf) < 1) return 0;
return buf[0];
}
// +------------------------------------------------------------------+
// | Risk:Reward risk (SL) leg in points - independent of manual SL. |
// | Manual mode: own input (points/dollar/percent). |
// | ATR mode: SL = ATR × multiplier, auto-calculated on entry. |
// +------------------------------------------------------------------+
double GetRRRiskPoints(double lotSize)
{
if(RRRiskMode == RR_RISK_ATR)
{
double atr = GetCurrentATR();
if(atr <= 0 || _Point <= 0) return 0;
double atrPoints = atr / _Point;
return atrPoints * RRAtrMultiplier;
}
// Manual distance
return ConvertToPoints(RRRiskInputType, RRRiskValue, lotSize);
}
// +------------------------------------------------------------------+
// | Resolve SL distance in points for a given lot (0 = no SL). |
// | Independent R:R mode overrides the manual Stop Loss entirely. |
// +------------------------------------------------------------------+
double GetSLPoints(double lotSize)
{
if(EnableRiskReward)
return GetRRRiskPoints(lotSize);
if(EnableStopLoss)
return ConvertToPoints(SLInputType, SLValue, lotSize);
return 0;
}
// +------------------------------------------------------------------+
// | Resolve TP distance in points for a given lot (0 = no TP). |
// | Independent R:R mode sets TP = risk distance × ratio, overriding |
// | the manual Take Profit entirely. |
// +------------------------------------------------------------------+
double GetTPPoints(double lotSize)
{
if(EnableRiskReward)
{
double slPts = GetRRRiskPoints(lotSize);
if(slPts > 0 && RiskRewardRatio > 0)
return slPts * RiskRewardRatio;
return 0;
}
if(EnableTakeProfit)
return ConvertToPoints(TPInputType, TPValue, lotSize);
return 0;
}
// +------------------------------------------------------------------+
// | Monitor High-Impact News Events & Return Event Details |
// +------------------------------------------------------------------+
string IsHighImpactNewsTime(int minutesBefore, int minutesAfter, ulong &eventID)
{
MqlCalendarValue values[];
datetime serverTime = TimeTradeServer();
// Use the max of both windows to cover all events in their active pause window
// Add 120s buffer to avoid boundary exclusion issues in CalendarValueHistory
int lookRange = (int)MathMax(minutesBefore, minutesAfter);
datetime start = serverTime - lookRange * 60;
datetime end = serverTime + lookRange * 60 + 120;
if(CalendarValueHistory(values, start, end))
{
for(int i = 0; i < ArraySize(values); i++)
{
MqlCalendarEvent event;
if(CalendarEventById(values[i].event_id, event))
{
if(event.importance == CALENDAR_IMPORTANCE_HIGH)
{
// Get country info
MqlCalendarCountry country;
CalendarCountryById(event.country_id, country);
// Check if event currency matches symbol currencies
if(country.currency != symbolBaseCurrency &&
country.currency != symbolQuoteCurrency)
{
continue;
}
// Check if we're within the event window (before OR after)
datetime eventTime = values[i].time;
datetime pauseStart = eventTime - minutesBefore * 60;
datetime pauseEnd = eventTime + minutesAfter * 60;
if(serverTime < pauseStart || serverTime > pauseEnd)
{
continue;
}
eventID = values[i].event_id;
int secondsUntil = (int)(eventTime - serverTime);
int minutesUntil = secondsUntil / 60;
string eventDetails = "";
eventDetails += "**Event Name:** " + event.name + "\n";
eventDetails += "**Country:** " + country.name + " (" + country.code + ")\n";
eventDetails += "**Currency:** " + country.currency + "\n";
eventDetails += "**Event Time:** " + TimeToString(eventTime, TIME_DATE|TIME_SECONDS) + "\n";
eventDetails += "**Time Until:** " + IntegerToString(minutesUntil) + " minutes\n";
if(values[i].HasActualValue())
eventDetails += "**Actual:** " + DoubleToString(values[i].GetActualValue(), 2) + "\n";
if(values[i].HasForecastValue())
eventDetails += "**Forecast:** " + DoubleToString(values[i].GetForecastValue(), 2) + "\n";
if(values[i].HasPreviousValue())
eventDetails += "**Previous:** " + DoubleToString(values[i].GetPreviousValue(), 2) + "\n";
eventDetails += "**Importance:** " + EnumToString(event.importance) + "\n";
eventDetails += "**Pause Window:** " + TimeToString(pauseStart, TIME_SECONDS) +
" to " + TimeToString(pauseEnd, TIME_SECONDS);
LogPrint("High impact event for ", country.currency, ": ", event.name);
return eventDetails;
}
}
}
}
eventID = 0;
return "";
}
// +------------------------------------------------------------------+
// | Check If Current Time is Within Allowed Trading Hours |
// +------------------------------------------------------------------+
bool IsWithinTradingHours()
{
// Always allow trading if feature is disabled
if(!EnableTradingHours) return true;
// Get current server time
datetime currentTime = TimeTradeServer();
MqlDateTime timeStruct;
TimeToStruct(currentTime, timeStruct);
// Current time in minutes from midnight
int currentMinutes = timeStruct.hour * 60 + timeStruct.min;
// Parse start time
string startParts[];
int startCount = StringSplit(TradingStartTime, ':', startParts);
if(startCount != 2)
{
LogPrint("ERROR: Invalid TradingStartTime format. Use HH:MM");
return false;
}
int startHour = (int)StringToInteger(startParts[0]);
int startMin = (int)StringToInteger(startParts[1]);
int startMinutes = startHour * 60 + startMin;
// Parse end time
string endParts[];
int endCount = StringSplit(TradingEndTime, ':', endParts);
if(endCount != 2)
{
LogPrint("ERROR: Invalid TradingEndTime format. Use HH:MM");
return false;
}
int endHour = (int)StringToInteger(endParts[0]);
int endMin = (int)StringToInteger(endParts[1]);
int endMinutes = endHour * 60 + endMin;
// Handle overnight trading sessions (e.g., 22:00 to 02:00)
if(startMinutes > endMinutes)
{
// Trading period crosses midnight
return (currentMinutes >= startMinutes || currentMinutes <= endMinutes);
}
else
{
// Normal trading period within same day
return (currentMinutes >= startMinutes && currentMinutes <= endMinutes);
}
}
// +------------------------------------------------------------------+
// | Check and Update Peak Equity |
// +------------------------------------------------------------------+
void CheckPeakEquity()
{
// Get current equity
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
// Update peak equity if current is higher
if(currentEquity > peakEquity)
{
peakEquity = currentEquity;
lastPeakEquity = currentEquity;
// Reset min equity triggers on new peak
if (ResetOnNewPeak) minEquityTriggerCount = 0;
LogPrint("New Peak Equity reached: $", peakEquity);
// Reset pause if equity recovered above peak
if(isPaused)
{
isPaused = false;
LogPrint("Trading RESUMED - Equity recovered above peak!");
}
}
}
// +------------------------------------------------------------------+
// | Check Target Equity |
// +------------------------------------------------------------------+
void CheckTargetEquity()
{
// Get current equity
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
if(TargetEquity > 0 && !targetEquityReached && currentEquity >= TargetEquity)
{
targetEquityReached = true;
LogPrint("+-----------------------------------------+");
LogPrint("TARGET EQUITY REACHED!");
LogPrint("Current Equity: $", currentEquity);
LogPrint("Target Equity: $", TargetEquity);
LogPrint("Closing ALL positions and stopping trading...");
LogPrint("+-----------------------------------------+");
// Send Discord alert for target equity reached
if(EnableDiscordAlerts)
{
string alertMsg = "**Instrument:** " + _Symbol + "\n";
alertMsg += "**Timeframe:** " + EnumToString(_Period) + "\n";
alertMsg += "**Server Time:** " + TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS) + "\n";
alertMsg += "**Current Equity:** $" + DoubleToString(currentEquity, 2) + "\n";
alertMsg += "**Target Equity:** $" + DoubleToString(TargetEquity, 2) + "\n";
alertMsg += "**Peak Equity:** $" + DoubleToString(peakEquity, 2) + "\n";
alertMsg += "**Profit:** $" + DoubleToString(TargetEquity - initialBalance, 2) + "\n";
alertMsg += "**Action:** All Positions Closed, Trading Stopped!";
SendDiscordAlert("🎯 TARGET EQUITY REACHED!", alertMsg, 5763719); // Green color
}
Alert("TARGET EQUITY REACHED! Closing all positions and stopping trading.");
}
}
// +------------------------------------------------------------------+
// | Check minimum Tradeable Equity |
// +------------------------------------------------------------------+
void CheckMinTradeableEquity()
{
// Get current equity
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
if(MinimumEquity > 0 && !minimumEquityReached && currentEquity <= MinimumEquity)
{
minimumEquityReached = true;
LogPrint("+-----------------------------------------+");
LogPrint("MINIMUM TRADEABLE EQUITY REACHED!");
LogPrint("Current Equity: $", currentEquity);
LogPrint("Minimum Equity: $", MinimumEquity);
LogPrint("Closing ALL positions and stopping trading...");
LogPrint("+-----------------------------------------+");
// Send Discord alert for minimum equity reached
if(EnableDiscordAlerts)
{
string alertMsg = "**Instrument:** " + _Symbol + "\n";
alertMsg += "**Timeframe:** " + EnumToString(_Period) + "\n";
alertMsg += "**Server Time:** " + TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS) + "\n";
alertMsg += "**Current Equity:** $" + DoubleToString(currentEquity, 2) + "\n";
alertMsg += "**Minimum Equity:** $" + DoubleToString(MinimumEquity, 2) + "\n";
alertMsg += "**Peak Equity:** $" + DoubleToString(peakEquity, 2) + "\n";
alertMsg += "**Loss:** $" + DoubleToString(initialBalance - currentEquity, 2) + "\n";
alertMsg += "**Action:** All Positions Closed, Trading Stopped!";
SendDiscordAlert("🔴 MINIMUM TRADEABLE EQUITY REACHED", alertMsg, 15158332); // Red color
}
Alert("MINIMUM TRADEABLE EQUITY REACHED! Closing all positions and stopping trading.");
}
}
// +------------------------------------------------------------------+
// | Check Equity Drawdawn |
// +------------------------------------------------------------------+
void CheckEquityDrawdawn()
{
// Get current equity
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
// Calculate allowed drawdown
double drawdownFromPercent = lastPeakEquity * ((100.0 - MinEquityPercent) / 100.0);
// If MaxDrawdownFromPeak is 0 or negative, don't cap it
double allowedDrawdown = (MaxDrawdownFromPeak > 0) ?
MathMin(drawdownFromPercent, MaxDrawdownFromPeak)
: drawdownFromPercent;
double minAllowedEquity = lastPeakEquity - allowedDrawdown;
// Check equity condition and handle pause
if(currentEquity < minAllowedEquity)
{
if(!isPaused)
{
// Increment trigger counter
minEquityTriggerCount++;
// Check if max triggers exceeded
if(MaxMinEquityTriggers > 0 && minEquityTriggerCount > MaxMinEquityTriggers)
{
minEquityTriggersExceeded = true;
LogPrint("+-----------------------------------------+" );
LogPrint("MAX MIN EQUITY TRIGGERS EXCEEDED!");
LogPrint("Triggers Used: ", minEquityTriggerCount, " / ", MaxMinEquityTriggers);
LogPrint("Closing ALL positions and STOPPING TRADING...");
LogPrint("+-----------------------------------------+" );
if(EnableDiscordAlerts)
{
string alertMsg = "**Instrument:** " + _Symbol + "\n";
alertMsg += "**Timeframe:** " + EnumToString(_Period) + "\n";
alertMsg += "**Server Time:** " + TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS) + "\n";
alertMsg += "**Current Equity:** $" + DoubleToString(currentEquity, 2) + "\n";
alertMsg += "**Peak Equity:** $" + DoubleToString(lastPeakEquity, 2) + "\n";
alertMsg += "**Triggers Used:** " + IntegerToString(minEquityTriggerCount) + " / " + IntegerToString(MaxMinEquityTriggers) + "\n";
alertMsg += "**Action:** All Positions Closed, Trading Stopped!";
SendDiscordAlert("🔴 MAX MIN EQUITY TRIGGERS EXCEEDED", alertMsg, 15158332); // Red color
}
Alert("MAX MIN EQUITY TRIGGERS EXCEEDED! Closing all positions and stopping trading.");
return;
}
// First time hitting minimum equity
isPaused = true;
pauseStartTime = TimeTradeServer();
// Use the trigger count to calculate exponential pause duration
double calculatedDuration = PauseMinutes * MathPow(PauseMinutesMultiplier, minEquityTriggerCount - 1);
if(calculatedDuration > INT_MAX) calculatedDuration = INT_MAX;
currentPauseDuration = (int)MathMin(calculatedDuration, MaxPauseMinutes > 0 ? MaxPauseMinutes : INT_MAX);
// Update Pause Stats
totalPauseCount++;
totalPauseDurationMinutes += currentPauseDuration;
// Calculate drop peek equity
double equityDrop = lastPeakEquity - currentEquity;
double equityDropPercent = (equityDrop / lastPeakEquity) * 100.0;
// Store old peak for Discord alert
double oldPeakEquity = lastPeakEquity;
// Update peak equity to current balance
lastPeakEquity = AccountInfoDouble(ACCOUNT_BALANCE);
LogPrint("+-----------------------------------------+");
LogPrint("EQUITY PROTECTION TRIGGERED!");
LogPrint("Current Equity: $", currentEquity);
LogPrint("Peak Equity: $", peakEquity);
LogPrint("Old Peak Equity: $", oldPeakEquity);
LogPrint("New Peak Equity (Balance): $", lastPeakEquity);
LogPrint("Min Allowed (", MinEquityPercent, "%): $", minAllowedEquity);
LogPrint("Trading PAUSED for ", currentPauseDuration, " minutes");
LogPrint("Resume Time: ", TimeToString(pauseStartTime + currentPauseDuration * 60));
LogPrint("+-----------------------------------------+");
// Send Discord alert for minimum equity reached
if(EnableDiscordAlerts)
{
string alertMsg = "**Instrument:** " + _Symbol + "\n";
alertMsg += "**Timeframe:** " + EnumToString(_Period) + "\n";
alertMsg += "**Server Time:** " + TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS) + "\n";
alertMsg += "**Current Equity:** $" + DoubleToString(currentEquity, 2) + "\n";
alertMsg += "**Peak Equity:** $" + DoubleToString(peakEquity, 2) + "\n";
alertMsg += "**Previous Peak:** $" + DoubleToString(oldPeakEquity, 2) + "\n";
alertMsg += "**New Peak (Balance):** $" + DoubleToString(lastPeakEquity, 2) + "\n";
alertMsg += "**Equity Drop:** $" + DoubleToString(equityDrop, 2) + " (" + DoubleToString(equityDropPercent, 2) + "%)\n";
alertMsg += "**Min Allowed (" + DoubleToString(MinEquityPercent, 0) + "%):** $" + DoubleToString(minAllowedEquity, 2) + "\n";
alertMsg += "**Trading Paused:** " + IntegerToString(currentPauseDuration) + " minutes\n";
alertMsg += "**Resume Time:** " + TimeToString(pauseStartTime + currentPauseDuration * 60) + "\n";
alertMsg += "**Action:** Trading Paused";
SendDiscordAlert("⚠️ MINIMUM EQUITY PROTECTION TRIGGERED", alertMsg, 16705372); // Yellow color
}
}
}
}
// +------------------------------------------------------------------+
// | Check High Impact News Event |
// +------------------------------------------------------------------+
void CheckHighImpactNews()
{
if(!EnableNewsFilter) return;
ulong newsEventID = 0;
string newsDetails = IsHighImpactNewsTime(NewsMinutesBefore, NewsMinutesAfter, newsEventID);
if(!isPaused && newsDetails != "" && lastProcessedNewsEventID != newsEventID)
{
// Update last processed news event ID
lastProcessedNewsEventID = newsEventID;
// Trigger the pause mechanism
isPaused = true;
pauseStartTime = TimeTradeServer();
// Calculate remaining pause time until event ends
datetime eventTime = 0;
datetime currentServerTime = TimeTradeServer();
MqlCalendarValue values[];
if(CalendarValueHistory(values, currentServerTime - NewsMinutesBefore * 60, currentServerTime + NewsMinutesAfter * 60 + 120))
{
for(int i = 0; i < ArraySize(values); i++)
{
if(values[i].event_id == newsEventID)
{
eventTime = values[i].time;
break;
}
}
}
if(eventTime > 0)
{
int secondsUntilEventEnd = (int)((eventTime + NewsMinutesAfter * 60) - currentServerTime);
currentPauseDuration = (secondsUntilEventEnd / 60) + 1; // +1 for safety margin
}
else
{
currentPauseDuration = NewsMinutesAfter; // Fallback
}
// Update Pause Stats
totalPauseCount++;
totalPauseDurationMinutes += currentPauseDuration;
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
LogPrint("+-----------------------------------------+");
LogPrint("HIGH-IMPACT NEWS EVENT DETECTED!");
LogPrint("Server Time: ", TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS));
LogPrint("Current Equity: $", currentEquity);
LogPrint("Trading PAUSED for ", currentPauseDuration, " minutes");
LogPrint("Resume Time: ", TimeToString(pauseStartTime + currentPauseDuration * 60));
LogPrint("+-----------------------------------------+");
// Send Discord alert with full event details
if(EnableDiscordAlerts)
{
// Add event details
string alertMsg = newsDetails + "\n\n";
// Add trading info
alertMsg += "**Instrument:** " + _Symbol + "\n";
alertMsg += "**Timeframe:** " + EnumToString(_Period) + "\n";
alertMsg += "**Server Time:** " + TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS) + "\n";
alertMsg += "**Current Equity:** $" + DoubleToString(currentEquity, 2) + "\n";
alertMsg += "**Peak Equity:** $" + DoubleToString(peakEquity, 2) + "\n";
alertMsg += "**Trading Paused:** " + IntegerToString(currentPauseDuration) + " minutes\n";
alertMsg += "**Resume Time:** " + TimeToString(pauseStartTime + currentPauseDuration * 60) + "\n";
alertMsg += "**Action:** Trading Paused";
SendDiscordAlert("⚠️ HIGH-IMPACT NEWS DETECTED!", alertMsg, 16705372); // Yellow color
}
}
}
// +------------------------------------------------------------------+
// | Check Trading Hours |
// +------------------------------------------------------------------+
void CheckTradingHours()
{
if(!EnableTradingHours) return;
bool currentlyWithinHours = IsWithinTradingHours();
// Check for transition from outside to inside trading hours (Trading Started)
if(isOutsideTradingHours && currentlyWithinHours)
{
isOutsideTradingHours = false;
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
LogPrint("+-----------------------------------------+");
LogPrint("TRADING HOURS STARTED");
LogPrint("Server Time: ", TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS));
LogPrint("Trading Period: ", TradingStartTime, " - ", TradingEndTime);
LogPrint("Current Equity: $", currentEquity);
LogPrint("+-----------------------------------------+");
// Send Discord alert for trading started
if(EnableDiscordAlerts)
{
string alertMsg = "**Instrument:** " + _Symbol + "\n";
alertMsg += "**Timeframe:** " + EnumToString(_Period) + "\n";
alertMsg += "**Server Time:** " + TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS) + "\n";
alertMsg += "**Trading Period:** " + TradingStartTime + " - " + TradingEndTime + "\n";
alertMsg += "**Current Equity:** $" + DoubleToString(currentEquity, 2) + "\n";
alertMsg += "**Peak Equity:** $" + DoubleToString(peakEquity, 2) + "\n";
alertMsg += "**Action:** Trading Started";
SendDiscordAlert("🟢 TRADING HOURS STARTED!", alertMsg, 5763719); // Green color
}
}
// Check for transition from inside to outside trading hours (Trading Paused)
else if(!isOutsideTradingHours && !currentlyWithinHours)
{
isOutsideTradingHours = true;
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
LogPrint("+-----------------------------------------+");
LogPrint("TRADING HOURS ENDED");
LogPrint("Server Time: ", TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS));
LogPrint("Trading Period: ", TradingStartTime, " - ", TradingEndTime);
LogPrint("Current Equity: $", currentEquity);
LogPrint("+-----------------------------------------+");
// Send Discord alert for trading paused
if(EnableDiscordAlerts)
{
string alertMsg = "**Instrument:** " + _Symbol + "\n";
alertMsg += "**Timeframe:** " + EnumToString(_Period) + "\n";
alertMsg += "**Server Time:** " + TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS) + "\n";
alertMsg += "**Trading Period:** " + TradingStartTime + " - " + TradingEndTime + "\n";
alertMsg += "**Current Equity:** $" + DoubleToString(currentEquity, 2) + "\n";
alertMsg += "**Peak Equity:** $" + DoubleToString(peakEquity, 2) + "\n";
alertMsg += "**Action:** Trading Stopped";
SendDiscordAlert("🔴 TRADING HOURS ENDED!", alertMsg, 15158332); // Red color
// Send Daily Report
SendTradeReport();
}
}
}
// +------------------------------------------------------------------+
// | Check Market Close Time |
// +------------------------------------------------------------------+
void CheckMarketClose()
{
if(!EnableMarketCloseFilter || MinutesBeforeClose <= 0) return;
MqlDateTime dt;
TimeCurrent(dt);
ENUM_DAY_OF_WEEK dayOfWeek = (ENUM_DAY_OF_WEEK)dt.day_of_week;
datetime from, to;
datetime currentTime = TimeCurrent();
if(SymbolInfoSessionQuote(_Symbol, dayOfWeek, 0, from, to))
{
int secondsUntilClose = (int)(to - currentTime);
int minutesUntilClose = secondsUntilClose / 60;
if(minutesUntilClose > 0 && minutesUntilClose <= MinutesBeforeClose)
{
// Send alert once per session
if(!marketCloseAlertSent && EnableDiscordAlerts)
{
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
string alertMsg = "**Instrument:** " + _Symbol + "\n";
alertMsg += "**Timeframe:** " + EnumToString(_Period) + "\n";
alertMsg += "**Server Time:** " + TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS) + "\n";
alertMsg += "**Market Closes In:** " + IntegerToString(minutesUntilClose) + " minutes\n";
alertMsg += "**Market Close Time:** " + TimeToString(to, TIME_DATE|TIME_MINUTES) + "\n";
alertMsg += "**Current Equity:** $" + DoubleToString(currentEquity, 2) + "\n";
alertMsg += "**Peak Equity:** $" + DoubleToString(peakEquity, 2) + "\n";
alertMsg += "**Action:** Stopped Opening New Positions";
SendDiscordAlert("⏰ MARKET CLOSING SOON", alertMsg, 16776960); // Yellow
marketCloseAlertSent = true;
}
LogPrint("Market closes in ", minutesUntilClose, " minutes. Not opening new positions.");
isNearMarketClose = true;
return; // Already in warning window, no need to check further
}
// Reset when outside warning period
if(minutesUntilClose > MinutesBeforeClose)
{
isNearMarketClose = false;
marketCloseAlertSent = false; // Also reset alert flag for next session
return;
}
// Current time is past session 0 close — check session 1
if(currentTime >= to)
{
datetime from2, to2;
if(SymbolInfoSessionQuote(_Symbol, dayOfWeek, 1, from2, to2))
{
secondsUntilClose = (int)(to2 - currentTime);
minutesUntilClose = secondsUntilClose / 60;
if(minutesUntilClose > 0 && minutesUntilClose <= MinutesBeforeClose)
{
LogPrint("Market closes in ", minutesUntilClose, " minutes. Not opening new positions.");
isNearMarketClose = true;
return;
}
if(minutesUntilClose > MinutesBeforeClose)
{
isNearMarketClose = false;
return;
}
}
}
}
// No valid session found or market is closed
isNearMarketClose = false;
}
// +------------------------------------------------------------------+
// +------------------------------------------------------------------+
// | Check for Leverage Changes |
// +------------------------------------------------------------------+
void CheckLeverageChange()
{
// Skip if feature is disabled
if(!EnableLeveragePause) return;
long currentLeverage = AccountInfoInteger(ACCOUNT_LEVERAGE);
// Leverage changed from initial
if(currentLeverage != initialLeverage && !isLeverageDiffFromInitial)
{
isLeverageDiffFromInitial = true;
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
LogPrint("+-----------------------------------------+");
LogPrint("LEVERAGE CHANGE DETECTED - TRADING PAUSED");
LogPrint("Initial Leverage: 1:", (int)initialLeverage);
LogPrint("Current Leverage: 1:", (int)currentLeverage);
LogPrint("Current Equity: $", currentEquity);
LogPrint("Trading will resume when leverage returns to 1:", (int)initialLeverage);
LogPrint("+-----------------------------------------+");
// Send Discord alert
if(EnableDiscordAlerts)
{
string alertMsg = "**Instrument:** " + _Symbol + "\n";
alertMsg += "**Timeframe:** " + EnumToString(_Period) + "\n";
alertMsg += "**Server Time:** " + TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS) + "\n";
alertMsg += "**Initial Leverage:** 1:" + IntegerToString((int)initialLeverage) + "\n";
alertMsg += "**Current Leverage:** 1:" + IntegerToString((int)currentLeverage) + "\n";
alertMsg += "**Current Equity:** $" + DoubleToString(currentEquity, 2) + "\n";
alertMsg += "**Peak Equity:** $" + DoubleToString(peakEquity, 2) + "\n";
alertMsg += "**Action:** Trading Paused";
SendDiscordAlert("⚠️ LEVERAGE CHANGE - TRADING PAUSED", alertMsg, 16705372); // Orange color
}
CloseAllPositions();
}
// Leverage returned to initial - check if we're paused due to leverage (currentPauseDuration == 0)
else if(currentLeverage == initialLeverage && isLeverageDiffFromInitial && currentPauseDuration == 0)
{
isLeverageDiffFromInitial = false;
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
LogPrint("+-----------------------------------------+");
LogPrint("LEVERAGE RESTORED - TRADING RESUMED");
LogPrint("Leverage: 1:", (int)currentLeverage);
LogPrint("Current Equity: $", currentEquity);
LogPrint("+-----------------------------------------+");
// Send Discord alert
if(EnableDiscordAlerts)
{
string alertMsg = "**Instrument:** " + _Symbol + "\n";
alertMsg += "**Timeframe:** " + EnumToString(_Period) + "\n";
alertMsg += "**Server Time:** " + TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS) + "\n";
alertMsg += "**Leverage:** 1:" + IntegerToString((int)currentLeverage) + "\n";
alertMsg += "**Current Equity:** $" + DoubleToString(currentEquity, 2) + "\n";
alertMsg += "**Peak Equity:** $" + DoubleToString(peakEquity, 2) + "\n";
alertMsg += "**Action:** Trading Resumed";
SendDiscordAlert("▶️ LEVERAGE RESTORED - TRADING RESUMED", alertMsg, 3066993); // Blue color
}
}
}
// +------------------------------------------------------------------+
// | Check and Send Trade Report |
// +------------------------------------------------------------------+
void CheckTradeReport()
{
if (!EnableReports) return;
datetime serverTime = TimeTradeServer();
MqlDateTime dt;
TimeToStruct(serverTime, dt);
bool sendReport = false;
// Check for hourly report
if (SendReportEveryHour > 0)
{
if (lastDailyReportTime == 0)
{
lastDailyReportTime = serverTime;
}
else if (serverTime - lastDailyReportTime >= SendReportEveryHour * 3600)
{
sendReport = true;
}
}
// Check for End of Day (23:59) Report
if(!EnableTradingHours && dt.hour == 23 && dt.min == 59)
{
// Check if report already sent today (to avoid spamming in the last minute)
// lastDailyReportTime checks full timestamp
MqlDateTime lastReportDt;
TimeToStruct(lastDailyReportTime, lastReportDt);
if(lastReportDt.day != dt.day)
{
sendReport = true;
}
}
if (sendReport)
{
SendTradeReport();
}
}
// Get Trade Statistics
void GetTradeStats(TradeStats& daily, TradeStats& allTime)
{
// Initialize
daily.count = 0; daily.won = 0; daily.lost = 0;
daily.profit = 0; daily.loss = 0;
daily.maxProfit = 0; daily.minProfit = DBL_MAX;
daily.maxLoss = 0; daily.minLoss = -DBL_MAX;
allTime.count = 0; allTime.won = 0; allTime.lost = 0;
allTime.profit = 0; allTime.loss = 0;
allTime.maxProfit = 0; allTime.minProfit = DBL_MAX;
allTime.maxLoss = 0; allTime.minLoss = -DBL_MAX;
datetime now = TimeCurrent();
// Trade Stats Session Start Time
// Start from last report generated, or from start of bot started if no last report
datetime sessionStartTime = (lastDailyReportTime > 0) ? lastDailyReportTime : startTime;
if(HistorySelect(0, now)) {
int deals = HistoryDealsTotal();
for(int i = 0; i < deals; i++) {
ulong ticket = HistoryDealGetTicket(i);
long entryType = HistoryDealGetInteger(ticket, DEAL_ENTRY);
if(HistoryDealGetString(ticket, DEAL_SYMBOL) != _Symbol ||
HistoryDealGetInteger(ticket, DEAL_MAGIC) != MagicNumber) continue;
double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT) +
HistoryDealGetDouble(ticket, DEAL_SWAP) +
HistoryDealGetDouble(ticket, DEAL_COMMISSION);
if (entryType == DEAL_ENTRY_OUT || entryType == DEAL_ENTRY_INOUT) {
// ALL TIME STATS
allTime.count++;
if(profit >= 0) {
allTime.won++;
allTime.profit += profit;
if(profit > allTime.maxProfit) allTime.maxProfit = profit;
if(profit < allTime.minProfit) allTime.minProfit = profit;
} else {
allTime.lost++;
allTime.loss += profit;
if(profit < allTime.maxLoss) allTime.maxLoss = profit;
if(profit > allTime.minLoss) allTime.minLoss = profit;
}
// SESSION STATS (Since Last Report or Start)
datetime dealTime = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
if(dealTime >= sessionStartTime) {
daily.count++;
if(profit >= 0) {
daily.won++;
daily.profit += profit;
if(profit > daily.maxProfit) daily.maxProfit = profit;
if(profit < daily.minProfit) daily.minProfit = profit;
} else {
daily.lost++;
daily.loss += profit;
if(profit < daily.maxLoss) daily.maxLoss = profit;
if(profit > daily.minLoss) daily.minLoss = profit;
}
}
}
}
}
// Calculate Averages and fix Min/Max initialization if no trades
// All Time
if(allTime.won > 0) allTime.avgProfit = allTime.profit / allTime.won; else { allTime.avgProfit = 0; allTime.minProfit = 0; }
if(allTime.lost > 0) allTime.avgLoss = allTime.loss / allTime.lost; else { allTime.avgLoss = 0; allTime.minLoss = 0; allTime.maxLoss = 0; }
// Daily
if(daily.won > 0) daily.avgProfit = daily.profit / daily.won; else { daily.avgProfit = 0; daily.minProfit = 0; }
if(daily.lost > 0) daily.avgLoss = daily.loss / daily.lost; else { daily.avgLoss = 0; daily.minLoss = 0; daily.maxLoss = 0; }
}
// Send Daily Report
void SendTradeReport()
{
if(!EnableDiscordAlerts) return;
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double deposit = initialBalance;
TradeStats dailyStats;
TradeStats allTimeStats;
GetTradeStats(dailyStats, allTimeStats);
// Session net profit
double sessionNetProfit = dailyStats.profit + dailyStats.loss; // loss is already negative
double sessionNetPercent = (balance > 0) ? (sessionNetProfit / balance) * 100.0 : 0.0;
// All time net profit
double allTimeNetProfit = allTimeStats.profit + allTimeStats.loss;
double allTimeNetPercent = (deposit > 0) ? (allTimeNetProfit / deposit) * 100.0 : 0.0;
// All time profit/loss percentages (kept for existing lines)
double profitPercent = (balance > 0) ? (allTimeStats.profit / balance) * 100.0 : 0.0;
double lossPercent = (balance > 0) ? (allTimeStats.loss / balance) * 100.0 : 0.0;
// Duration
long durationSeconds = TimeCurrent() - startTime;
int days = (int)(durationSeconds / 86400);
int hours = (int)((durationSeconds % 86400) / 3600);
int minutes = (int)((durationSeconds % 3600) / 60);
string durationStr = "";
if(days > 0) durationStr += IntegerToString(days) + "d ";
if(hours > 0) durationStr += IntegerToString(hours) + "h ";
durationStr += IntegerToString(minutes) + "m";
// Report Interval Duration
long reportInterval = (lastDailyReportTime > 0) ? (TimeCurrent() - lastDailyReportTime) : durationSeconds;
int rHours = (int)(reportInterval / 3600);
int rMinutes = (int)((reportInterval % 3600) / 60);
string reportDurationStr = "";
if(rHours > 0) reportDurationStr += IntegerToString(rHours) + "h ";
reportDurationStr += IntegerToString(rMinutes) + "m";
string alertMsg = "**Instrument:** " + _Symbol + "\n";
alertMsg += "**Timeframe:** " + EnumToString(_Period) + "\n";
alertMsg += "**Server Time:** " + TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS) + "\n";
alertMsg += "**Peak Equity:** $" + DoubleToString(peakEquity, 2) + "\n";
alertMsg += "**Previous Report Equity:** $" + DoubleToString(lastReportEquity, 2) + "\n";
alertMsg += "**Current Equity:** $" + DoubleToString(currentEquity, 2) + "\n";
alertMsg += "**Initial Balance:** $" + DoubleToString(deposit, 2) + "\n";
alertMsg += "**Current Balance:** $" + DoubleToString(balance, 2) + "\n\n";
alertMsg += "**Trades:** " + IntegerToString(dailyStats.count) + "\n";
alertMsg += "**Won:** " + IntegerToString(dailyStats.won) + "\n";
alertMsg += "**Lost:** " + IntegerToString(dailyStats.lost) + "\n";
alertMsg += "**Profit:** $" + DoubleToString(dailyStats.profit, 2) + "\n";
alertMsg += "**Loss:** $" + DoubleToString(dailyStats.loss, 2) + "\n";
alertMsg += "**Net Profit:** $" + DoubleToString(sessionNetProfit, 2) + " (" + DoubleToString(sessionNetPercent, 2) + "%)\n\n";
alertMsg += "**All Time Trades:** " + IntegerToString(allTimeStats.count) + "\n";
alertMsg += "**All Time Won:** " + IntegerToString(allTimeStats.won) + "\n";
alertMsg += "**All Time Lost:** " + IntegerToString(allTimeStats.lost) + "\n";
alertMsg += "**All Time Profit:** $" + DoubleToString(allTimeStats.profit, 2) + " (" + DoubleToString(profitPercent, 2) + "%)\n";
alertMsg += "**All Time Loss:** $" + DoubleToString(allTimeStats.loss, 2) + " (" + DoubleToString(lossPercent, 2) + "%)\n";
alertMsg += "**All Time Net Profit:** $" + DoubleToString(allTimeNetProfit, 2) + " (" + DoubleToString(allTimeNetPercent, 2) + "%)\n\n";
alertMsg += "**Average Profit:** $" + DoubleToString(allTimeStats.avgProfit, 2) + "\n";
alertMsg += "**Largest Profit:** $" + DoubleToString(allTimeStats.maxProfit, 2) + "\n";
alertMsg += "**Smallest Profit:** $" + DoubleToString(allTimeStats.minProfit, 2) + "\n";
alertMsg += "**Average Loss:** $" + DoubleToString(allTimeStats.avgLoss, 2) + "\n";
alertMsg += "**Largest Loss:** $" + DoubleToString(allTimeStats.maxLoss, 2) + "\n";
alertMsg += "**Smallest Loss:** $" + DoubleToString(allTimeStats.minLoss, 2) + "\n\n";
alertMsg += "**Pauses Triggered:** " + IntegerToString(totalPauseCount) + "\n";
alertMsg += "**Total Paused Duration:** " + DoubleToString(totalPauseDurationMinutes, 0) + " minutes" + "\n";
alertMsg += "**Report Generated For:** " + reportDurationStr + "\n";
alertMsg += "**Run Duration:** " + durationStr + "\n";
SendDiscordAlert("📊 TRADE REPORT", alertMsg, 16776960); // Yellow/Gold color
lastDailyReportTime = TimeCurrent();
lastReportEquity = currentEquity;
}
// +------------------------------------------------------------------+
// +------------------------------------------------------------------+
// | Algo Trading MT5 |
// +------------------------------------------------------------------+
void CheckAlgoTradingStatus()
{
bool currentStatus = TerminalInfoInteger(TERMINAL_TRADE_ALLOWED);
// Detect status change
if(currentStatus != algoTradingStatus)
{
if(currentStatus)
{
LogPrint("Algo Trading has been ENABLED");
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
string alertMsg = "**Instrument:** " + _Symbol + "\n";
alertMsg += "**Timeframe:** " + EnumToString(_Period) + "\n";
alertMsg += "**Server Time:** " + TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS) + "\n";
alertMsg += "**Trading Hours:** " + (EnableTradingHours ? TradingStartTime + " - " + TradingEndTime + "\n" : "DISABLED\n");
alertMsg += "**Current Equity:** $" + DoubleToString(currentEquity, 2) + "\n";
alertMsg += "**Peak Equity:** $" + DoubleToString(peakEquity, 2) + "\n";
alertMsg += "**Current Balance:** $" + DoubleToString(balance, 2) + "\n";
alertMsg += "**Initial Balance:** $" + DoubleToString(initialBalance, 2) + "\n";
alertMsg += "**Action:** Trading Started (Algo Trading Enabled)";
SendDiscordAlert("🟢 AUTOMATED TRADING STARTED", alertMsg, 5763719); // Green color
}
else
{
LogPrint("Algo Trading has been DISABLED");
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
string alertMsg = "**Instrument:** " + _Symbol + "\n";
alertMsg += "**Timeframe:** " + EnumToString(_Period) + "\n";
alertMsg += "**Server Time:** " + TimeToString(TimeTradeServer(), TIME_DATE|TIME_SECONDS) + "\n";
alertMsg += "**Trading Hours:** " + (EnableTradingHours ? TradingStartTime + " - " + TradingEndTime + "\n" : "DISABLED\n");
alertMsg += "**Current Equity:** $" + DoubleToString(currentEquity, 2) + "\n";
alertMsg += "**Peak Equity:** $" + DoubleToString(peakEquity, 2) + "\n";
alertMsg += "**Current Balance:** $" + DoubleToString(balance, 2) + "\n";
alertMsg += "**Initial Balance:** $" + DoubleToString(initialBalance, 2) + "\n";
alertMsg += "**Action:** Trading Stopped (Algo Trading Disabled)";
SendDiscordAlert("🔴 AUTOMATED TRADING STOPPED", alertMsg, 15158332); // Green color
}
// Update status
algoTradingStatus = currentStatus;
}
}
// Toggle disable algo trading in MT5
void DisableAlgoTrading()
{
bool Status = (bool)TerminalInfoInteger(TERMINAL_TRADE_ALLOWED);
if(Status)
{
HANDLE hChart = (HANDLE)ChartGetInteger(ChartID(), CHART_WINDOW_HANDLE);
PostMessageW(GetAncestor(hChart, GA_ROOT), WM_COMMAND, MT_WMCMD_EXPERTS, 0);
}
}
// +------------------------------------------------------------------+
// | Send Discord alert via webhook |
// +------------------------------------------------------------------+
bool SendDiscordAlert(string title, string message, int embedColor = 3447003)
{
if(!EnableDiscordAlerts || DiscordWebhookURL == "") return false;
// Escape special characters in message
StringReplace(message, "\\", "\\\\");
StringReplace(message, "\"", "\\\"");
StringReplace(message, "\n", "\\n");
// Build JSON payload
string json = "";
json += "{\"embeds\":[{";
json += "\"title\":\"" + title + "\",";
json += "\"description\":\"" + message + "\",";
json += "\"color\":" + IntegerToString(embedColor) + ",";
json += "\"footer\":{\"text\":\"Nyao Scalper v43.0\"}";
json += "}]}";
// Prepare HTTP request
char post[];
char result[];
string headers = "Content-Type: application/json\r\n";
string resultHeaders = "";
int timeout = 5000;
// Convert JSON to char array
StringToCharArray(json, post, 0, WHOLE_ARRAY, CP_UTF8);
ArrayResize(post, ArraySize(post) - 1); // Remove null terminator
// Send webhook
int res = WebRequest("POST", DiscordWebhookURL, headers, timeout, post, result, resultHeaders);
if(res == 200 || res == 204)
{
LogPrint("Discord alert sent: ", title);
return true;
}
else
{
LogPrint("Discord ERROR: ", res);
LogPrint("Payload: ", json);
LogPrint("Response: ", CharArrayToString(result));
LogPrint("MT5 Error: ", GetLastError());
return false;
}
}
// +------------------------------------------------------------------+
// | Check and Test Discord Alert |
// +------------------------------------------------------------------+
void CheckDiscordAlert()
{
if(DiscordWebhookURL == "")
{
Print("WARNING: Discord alerts enabled but webhook URL is empty!");
}
else if(StringFind(DiscordWebhookURL, "https://discord.com/api/webhooks/") != 0 &&
StringFind(DiscordWebhookURL, "https://discordapp.com/api/webhooks/") != 0)
{
Print("WARNING: Discord webhook URL format may be incorrect!");
}
else
{
CheckAlgoTradingStatus();
}
}
// +------------------------------------------------------------------+
// | Update On-Chart Dashboard |
// +------------------------------------------------------------------+
void DrawDashboardLabel(string name, string text, int x, int y, int fontSize, color clr, bool bold = false)
{
if(ObjectFind(0, name) < 0)
{
ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
ObjectSetInteger(0, name, OBJPROP_BACK, false);
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, name, OBJPROP_SELECTED, false);
ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
}
ObjectSetString(0, name, OBJPROP_TEXT, text);
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize);
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
ObjectSetString(0, name, OBJPROP_FONT, bold ? "Arial Bold" : "Arial");
}
void UpdateDashboard()
{
// Clear old comment based dashboard
Comment("");
// Layout Constants
int startX = 20;
int startY = 20;
int lineHeight = 18;
int headersize = 10;
int textsize = 9;
int detailsSize = 8;
color colorHeader = clrGold;
color colorText = clrWhite;
color colorBuy = clrLime;
color colorSell = clrRed;
color colorNeutral = clrGray;
color colorBg = C'30,30,30';
color colorBorder = clrGold;
int currentY = startY;
// Header
DrawDashboardLabel("NyaoDash_Title", "Nyao Scalper v43.0", startX, currentY, 11, colorHeader, true);
currentY += lineHeight + 5;
// Status logic
string status = "Active";
color statusColor = clrLime;
if(isPaused) { status = "PAUSED (" + IntegerToString(currentPauseDuration) + "m)"; statusColor = clrOrange; }
else if(isOutsideTradingHours) { status = "Closed (Time)"; statusColor = clrGray; }
else if(targetEquityReached) { status = "STOPPED (Target)"; statusColor = clrRed; }
else if(minimumEquityReached) { status = "STOPPED (Min Equity)"; statusColor = clrRed; }
DrawDashboardLabel("NyaoDash_Status", "Status: " + status, startX, currentY, textsize, statusColor, true);
currentY += lineHeight;
// Account Info
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
double equityDrop = (peakEquity > 0) ? ((peakEquity - equity) / peakEquity) * 100.0 : 0.0;
DrawDashboardLabel("NyaoDash_Bal", StringFormat("Balance: $%.2f", balance), startX, currentY, textsize, colorText);
currentY += lineHeight;
DrawDashboardLabel("NyaoDash_Eq", StringFormat("Equity: $%.2f", equity), startX, currentY, textsize, colorText);
currentY += lineHeight;
DrawDashboardLabel("NyaoDash_Peak", StringFormat("Peak: $%.2f (Drop: %.1f%%)", peakEquity, equityDrop), startX, currentY, textsize, colorText);
currentY += lineHeight + 5;
// Hedge Chain status (only when feature enabled)
if(EnableHedgeChain)
{
// Count distinct active chains, total chain legs, and deepest cycle in progress
ulong dashIds[];
int dashChains = 0;
int dashLegs = 0;
int dashMaxCycle = 0;
for(int h = 0; h < managedPositionCount; h++)
{
ulong r = managedPositions[h].chainId;
if(r == 0) continue;
dashLegs++;
if(managedPositions[h].cycleNum > dashMaxCycle) dashMaxCycle = managedPositions[h].cycleNum;
bool seen = false;
for(int k = 0; k < dashChains; k++) if(dashIds[k] == r) { seen = true; break; }
if(!seen) { ArrayResize(dashIds, dashChains + 1); dashIds[dashChains++] = r; }
}
DrawDashboardLabel("NyaoDash_Hedge",
StringFormat("Hedge Chains: %d (legs %d, cycle %d/%d)", dashChains, dashLegs, dashMaxCycle,
(HedgeMaxCycles > 0 ? HedgeMaxCycles : 0)),
startX, currentY, textsize, dashChains > 0 ? clrOrange : colorText);
currentY += lineHeight + 5;
}
else
{
// Hide stale label when feature is toggled off
ObjectDelete(0, "NyaoDash_Hedge");
}
// Signal Strength (Smoothed - Unified)
SignalStrength buyStrength = GetSignalStrength(ORDER_TYPE_BUY);
SignalStrength sellStrength = GetSignalStrength(ORDER_TYPE_SELL);
// Raw closed-candle scores for reference
double rawBuyScore = ComputeRawScore(ORDER_TYPE_BUY, 1);
double rawSellScore = ComputeRawScore(ORDER_TYPE_SELL, 1);
DrawDashboardLabel("NyaoDash_SigHead", "SIGNAL STRENGTH:", startX, currentY, headersize, colorHeader, true);
currentY += lineHeight;
string reqBuyText = StringFormat("Min Buy: %.2f", MinBuySignalScore);
DrawDashboardLabel("NyaoDash_ReqBuy", reqBuyText, startX, currentY, detailsSize, colorText);
currentY += lineHeight;
string reqSellText = StringFormat("Min Sell: %.2f", MinSellSignalScore);
DrawDashboardLabel("NyaoDash_ReqSell", reqSellText, startX, currentY, detailsSize, colorText);
currentY += lineHeight;
// Buy Row
string buyText = StringFormat("BUY SCORE: %.2f", buyStrength.finalScore);
DrawDashboardLabel("NyaoDash_Buy", buyText, startX, currentY, textsize, buyStrength.finalScore > sellStrength.finalScore ? colorBuy : colorText, true);
currentY += lineHeight;
string rawBuyText = StringFormat("Raw (Closed): %.2f", rawBuyScore);
DrawDashboardLabel("NyaoDash_CurrentBuy", rawBuyText, startX, currentY, detailsSize, colorText);
currentY += lineHeight;
string buyDet = StringFormat("%s", buyStrength.reasoning);
DrawDashboardLabel("NyaoDash_BuyDet", buyDet, startX, currentY, detailsSize, colorText);
currentY += lineHeight + 2;
// Sell Row
string sellText = StringFormat("SELL SCORE: %.2f", sellStrength.finalScore);
DrawDashboardLabel("NyaoDash_Sell", sellText, startX, currentY, textsize, sellStrength.finalScore > buyStrength.finalScore ? colorSell : colorText, true);
currentY += lineHeight;
string rawSellText = StringFormat("Raw (Closed): %.2f", rawSellScore);
DrawDashboardLabel("NyaoDash_CurrentSell", rawSellText, startX, currentY, detailsSize, colorText);
currentY += lineHeight;
string sellDet = StringFormat("%s",sellStrength.reasoning);
DrawDashboardLabel("NyaoDash_SellDet", sellDet, startX, currentY, detailsSize, colorText);
currentY += lineHeight + 10;
// Statistics
TradeStats daily, allTime;
GetTradeStats(daily, allTime);
double allTimeNetProfit = allTime.profit + allTime.loss;
DrawDashboardLabel("NyaoDash_StatHead", "STATISTICS:", startX, currentY, headersize, colorHeader, true);
currentY += lineHeight;
DrawDashboardLabel("NyaoDash_Trades", StringFormat("Trades: %d (W:%d / L:%d)", allTime.count, allTime.won, allTime.lost), startX, currentY, textsize, colorText);
currentY += lineHeight;
DrawDashboardLabel("NyaoDash_PL", StringFormat("Profit: $%.2f | Loss: $%.2f", allTime.profit, allTime.loss), startX, currentY, textsize, colorText);
currentY += lineHeight;
color profitColor = allTimeNetProfit >= 0 ? colorBuy : colorSell;
DrawDashboardLabel("NyaoDash_Net", StringFormat("NET PROFIT: $%.2f", allTimeNetProfit), startX, currentY, textsize, profitColor, true);
}
// +------------------------------------------------------------------+