// +------------------------------------------------------------------+ // | 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 // Dialog Controls for Password Input #include #include #include #include // 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 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 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 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 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); } // +------------------------------------------------------------------+