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