13212 lines
503 KiB
MQL5
13212 lines
503 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
string BuildTelegramEntryMessage(const string symbol, const string side, double entry, double sl, double tp, int score, const string regime)
|
|
{
|
|
int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
|
|
if(digits <= 0)
|
|
digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
|
|
if(digits <= 0)
|
|
digits = Digits();
|
|
|
|
score = (int)MathMax(0, MathMin(10, score));
|
|
if(entry <= 0.0)
|
|
entry = (StringCompare(side, "BUY") == 0) ? SymbolInfoDouble(symbol, SYMBOL_ASK) : SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
entry = NormalizeDouble(entry, digits);
|
|
sl = NormalizeDouble(sl, digits);
|
|
tp = NormalizeDouble(tp, digits);
|
|
|
|
string sideTag = (StringCompare(side, "BUY") == 0) ? "🟢 BUY " : "🔴 SELL ";
|
|
string msg = "📢 KING GOLD SIGNAL\n";
|
|
msg += "✨ Hi, this is the King of Gold\n";
|
|
msg += sideTag + symbol + "\n";
|
|
msg += "⏰ Time: " + TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS) + "\n";
|
|
msg += "🎯 Entry: " + DoubleToString(entry, digits) + "\n";
|
|
msg += "🛑 SL: " + DoubleToString(sl, digits) + "\n";
|
|
msg += "🎯 TP: " + DoubleToString(tp, digits) + "\n";
|
|
msg += "📊 Score: " + IntegerToString(score) + "/10 | Regime: " + regime;
|
|
|
|
return msg;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// STATE PERSISTENCE HELPERS
|
|
//+------------------------------------------------------------------+
|
|
string StateFileName()
|
|
{
|
|
return "kog_v31_" + _Symbol + "_state.csv";
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
string SanitizeField(const string value)
|
|
{
|
|
string v = value;
|
|
StringReplace(v, ";", " ");
|
|
StringReplace(v, "\n", " ");
|
|
StringReplace(v, "\r", " ");
|
|
return v;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double ClampDouble(double v, double minVal, double maxVal)
|
|
{
|
|
if(v < minVal)
|
|
return minVal;
|
|
if(v > maxVal)
|
|
return maxVal;
|
|
return v;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double SafeNumber(const string s, double minVal, double maxVal, double fallback = 0.0)
|
|
{
|
|
double v = StringToDouble(s);
|
|
// Guard against NaN/inf and absurd magnitudes that corrupt state files
|
|
if(!MathIsValidNumber(v) || MathAbs(v) > (maxVal * 10.0))
|
|
return fallback;
|
|
return ClampDouble(v, minVal, maxVal);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| King of Gold Scalper |
|
|
//| MQL5 Expert Advisor |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "King Gold Scalper (c) 2026, Tudor F. | support: FTUDOR505@GMAIL.COM | with love for traders | All rights reserved"
|
|
#property link "FTUDOR505@GMAIL.COM +40765963611"
|
|
#property version "9.0"
|
|
#property strict
|
|
|
|
|
|
|
|
#include <Trade\Trade.mqh>
|
|
#include <Trade\PositionInfo.mqh>
|
|
#include "KogDecisionCore.mqh
|
|
long sqlite_openW(string dbPathW);
|
|
int sqlite_exec(long h, string sqlUtf8);
|
|
int sqlite_last_error(long h, uchar &out[], int outSize);
|
|
void sqlite_close(long h);
|
|
int sqlite_pop_next(long h, string lockedBy, string symbolFilter, long magicFilter,
|
|
long &outId, uchar &outCmd[], int outCmdSize,
|
|
uchar &outPayload[], int outPayloadSize,
|
|
uchar &outSymbol[], int outSymbolSize, long &outMagic);
|
|
int sqlite_mark_done(long h, long id, string result);
|
|
int sqlite_mark_failed(long h, long id, string error);
|
|
int sqlite_reset_stuck(long h, int timeoutSec);
|
|
#import
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
// INPUT PARAMETERS - CUSTOMIZE HERE
|
|
//+------------------------------------------------------------------+
|
|
|
|
// MAGIC NUMBER
|
|
input string _hdr00 = "=== MAGIC NUMBER ===";
|
|
input long MagicNumber = 5052025;
|
|
|
|
// TRADING TIME SETTINGS
|
|
input string _hdr01 = "=== TRADING TIME SETTINGS ===";
|
|
input int StartHour = 0; // Start trading hour (0-23)
|
|
input int EndHour = 23; // End trading hour (0-23)
|
|
input bool TradeMonday = true; // Trade on Monday
|
|
input bool TradeTuesday = true; // Trade on Tuesday
|
|
input bool TradeWednesday = true; // Trade on Wednesday
|
|
input bool TradeThursday = true; // Trade on Thursday
|
|
input bool TradeFriday = true; // Trade on Friday
|
|
input bool TradeSaturday = true; // Trade on Saturday
|
|
input bool TradeSunday = true; // Trade on Sunday
|
|
|
|
// RISK MANAGEMENT SETTINGS
|
|
input string _hdr02 = "=== RISK MANAGEMENT SETTINGS ===";
|
|
input double RiskPercent = 1.0; // Risk % per trade (0-10)
|
|
input double DefaultRiskPercent = 1.0; // Default risk % if RiskPercent is set to 0
|
|
input double TakeProfit = 2000; // Take Profit distance (pips if InputsArePips=true, otherwise points)
|
|
input double StopLoss = 1000; // Stop Loss distance (pips if InputsArePips=true, otherwise points)
|
|
input bool InputsArePips = false; // When true distances (TP/SL/trailing/breakeven/spread) are in pips; when false they are raw broker points
|
|
input int MaxOpenTrades = 9; // Maximum open positions (adjust for multiple orders)
|
|
input int MaxTradesPerSymbolDirection = 5; // Max simultaneous trades per symbol and direction (BUY/SELL)
|
|
|
|
// TRADING STRATEGY SETTINGS
|
|
input string _hdr03 = "=== MaxSpread ===";
|
|
input int MaxSpread = 3500; // Maximum allowed spread (pips if InputsArePips=true, otherwise points)
|
|
|
|
// PAIR & LOT SETTINGS
|
|
input string _hdr04 = "=== PAIR & LOT SETTINGS ===";
|
|
input double LotSize = 0; // Fixed lot size (if 0, uses risk %)
|
|
input int Slippage = 20; // Maximum slippage (pips if InputsArePips=true, otherwise points)
|
|
|
|
// TIMEZONE SETTINGS
|
|
input string _hdr05 = "=== TIMEZONE SETTINGS ===";
|
|
input bool UseBrokerTime = true; // Use broker server time (recommended). If false, uses local PC time
|
|
input int TimezoneOffset = 0; // Timezone offset in hours (only if UseBrokerTime=false). 0 = use broker time directly
|
|
|
|
// COOLDOWN SETTINGS (Revenge Trading Prevention)
|
|
input string _hdr06 = "=== COOLDOWN SETTINGS ===";
|
|
input bool UseCooldown = false; // Enable cooldown after losing trade
|
|
input int CooldownBars = 5; // Number of bars to wait after a loss
|
|
|
|
// TRAILING STOP LOSS SETTINGS
|
|
input string _hdr07 = "=== TRAILING STOP LOSS SETTINGS ===";
|
|
input bool UseTrailingStop = false; // Enable trailing stop loss
|
|
input int TrailingStartPips = 50; // Activate trailing stop after X profit (same units as TP/SL)
|
|
input int TrailingStopPips = 50; // Trail stop by X from highest/lowest price (same units as TP/SL)
|
|
|
|
// BREAKEVEN STOP SETTINGS
|
|
input string _hdr08 = "=== BREAKEVEN STOP SETTINGS ===";
|
|
input bool UseBreakevenStop = false; // Enable breakeven stop
|
|
input int BreakevenTrigger = 20; // Move SL to breakeven after X profit (same units as TP/SL)
|
|
|
|
// DYNAMIC SL/TP ADJUSTMENT SETTINGS
|
|
input string _hdr09 = "=== DYNAMIC SL/TP ADJUSTMENT SETTINGS ===";
|
|
input bool UseDynamicSLTP = false; // Enable dynamic SL/TP adjustment based on market analysis
|
|
input double MaxTPMultiplier = 1.5; // Maximum TP multiplier (2.0 = 2x original TP)
|
|
input int MinSLProfitPips = 60; // Minimum SL in profit (points above/below entry)
|
|
input double StrongMomentumThreshold = 1.3; // Volume ratio threshold for strong momentum
|
|
input double StrongImbalanceThreshold = 1.0; // DOM imbalance threshold for strong signal
|
|
input int SLTrailDistance = 30; // Distance to trail SL from current price when conditions are very favorable (points)
|
|
|
|
// AUTO-CLOSE SETTINGS (Close all trades on target profit)
|
|
input string _hdr10 = "=== AUTO-CLOSE SETTINGS(Close all trades on target profit) ===";
|
|
input bool UseAutoClose = true; // Enable auto-close when target profit reached
|
|
input double AutoCloseProfit = 10; // Close all trades when profit reaches X USD
|
|
input bool UseAutoCloseTrailing = true; // Enable trailing close - continue profiting after target
|
|
input double AutoCloseTrailingStart = 10; // Activate trailing close after X USD profit
|
|
input double AutoCloseTrailingStop = 1; // Trail close by X USD from highest profit
|
|
input bool AutoCloseResumeTrading = true; // Continue trading after auto-close triggers (same session)
|
|
|
|
// RECOVERY AUTO-CLOSE (DD-based fast positive close)
|
|
input string _hdr11 = "=== RECOVERY AUTO-CLOSE (DD-based fast positive close) ===";
|
|
input bool UseRecoveryAutoClose = true; // Enable DD-based recovery mode
|
|
input double RecoveryDDStart = 9.0; // Enter recovery mode when DD >= this %
|
|
input double RecoveryDDEnd = 4.0; // Exit recovery mode when DD <= this %
|
|
input double RecoveryMinProfitUsd = 8.0; // In recovery: close all when total profit >= this small positive value
|
|
input bool RecoveryUseAutoCloseTrailing = true; // In recovery: use trailing close instead of direct close
|
|
input double RecoveryAutoCloseTrailingStart = 5.0; // Recovery trailing start (USD)
|
|
input double RecoveryAutoCloseTrailingStop = 1.0; // Recovery trailing stop (USD)
|
|
input double RecoveryRiskMultiplier = 1.5; // Risk multiplier applied in recovery mode
|
|
|
|
// SAFE MODE SETTINGS (auto-activate under low balance)
|
|
input string _hdr12 = "=== SAFE MODE SETTINGS ===";
|
|
input bool UseSafeBalanceMode = false; // Enable safe mode when balance is low
|
|
input double SafeBalanceThreshold = 100; // Balance threshold to enter safe mode
|
|
input double SafeRiskFactor = 0.5; // Multiply risk by this in safe mode (e.g., 0.5 = half risk)
|
|
input int SafeMaxOpenTrades = 3; // Limit max open trades in safe mode
|
|
input double SafeVolumeEntryMultiplier = 1.0; // Volume ratio multiplier for entries in safe mode
|
|
|
|
// CONSECUTIVE BARS SETTINGS
|
|
input string _hdr13 = "=== CONSECUTIVE BARS SETTINGS(TickBarSize activate for signals on tick) ===";
|
|
input bool UseConsecutiveBars = true; // Enable consecutive bar detection
|
|
input bool AllowConsecutiveSell = true; // Allow SELL when consecutive-bars signal matches current mode
|
|
input bool AllowConsecutiveBuy = true; // Allow BUY when consecutive-bars signal matches current mode
|
|
input int BullishBarsForSell = 5; // Bullish streak threshold (reversal SELL / trend BUY)
|
|
input int BearishBarsForBuy = 3; // Bearish streak threshold (reversal BUY / trend SELL)
|
|
input bool UseMomentumExhaustionSignals = true; // true=reversal (bullish->SELL, bearish->BUY), false=trend-following
|
|
input bool UseTickBarSignals = false; // If true, build synthetic tick-bars for signals instead of timeframe bars
|
|
input int TickBarSize = 50; // Number of ticks per synthetic bar (for tick-bar signals)
|
|
|
|
// VOLUME FILTER SETTINGS
|
|
input string _hdr14 = "=== VOLUME FILTER SETTINGS ===";
|
|
input bool UseVolumeFilter = true; // Enable volume filter - blocks trades if volume too low
|
|
input bool UseTickVolume = true; // Use tick volume (works in tester) instead of real volume
|
|
input double MinVolumeRatio = 0.9; // Minimum volume ratio vs average (0.8 = 80% of average volume)
|
|
|
|
// ANTI-EXTREME ENTRY FILTER
|
|
input string _hdr14b = "=== ANTI-EXTREME FILTER SETTINGS ===";
|
|
input bool UseAntiExtremeFilter = false; // Block BUY near recent highs and SELL near recent lows
|
|
input int AntiExtremeLookbackBars = 60; // Lookback bars used to detect recent highs/lows
|
|
input double AntiExtremeMinDistance = 600; // Minimum distance from extreme (same units as InputsArePips)
|
|
|
|
// VOLUME-BASED SIGNAL STRENGTH
|
|
input string _hdr15 = "=== VOLUME SIGNAL SETTINGS(Use tick volume to validate bullish/bearish bars) ===";
|
|
input bool UseVolumeInSignals = true; // Use tick volume to validate bullish/bearish bars
|
|
input int VolumeSignalLookback = 1000; // Number of bars used to calculate average tick volume
|
|
input double VolumeSignalFactor = 0.5; // Current volume >= Factor * average volume => "strong" bar
|
|
|
|
// ORDER BOOK (DOM) SETTINGS
|
|
input string _hdr16 = "=== DOM (ORDER BOOK) SETTINGS(If true: DOM provides positive signal ,doesn't block trades) ===";
|
|
input bool UseDOMFilter = true; // Enable Order Book (DOM) filter
|
|
input bool ForceDisableDOM = true; // Hard bypass DOM even if UseDOMFilter=true
|
|
input bool DOMAsSignal = true; // If true: DOM provides positive signal (doesn't block trades). If false: DOM blocks trades that don't meet criteria
|
|
input bool AutoDetectECN = true; // Auto-detect ECN broker (IC Markets, etc.) - DOM only works on ECN brokers
|
|
input double MinImbalanceRatio = 1.0; // Minimum imbalance ratio (BID/ASK or ASK/BID) to confirm direction
|
|
input double MinLiquidity = 1.0; // Minimum total liquidity (lots) required to trade
|
|
input int DOMLevels = 10; // Number of DOM levels to analyze (each side)
|
|
|
|
// SMART ENTRY & LIQUIDITY ZONES SETTINGS
|
|
input string _hdr17 = "=== SMART ENTRY & LIQUIDITY ZONES(adjust entry price towards liquidity zones) ===";
|
|
input bool UseSmartEntry = true; // Enable smart entry - adjust entry price towards liquidity zones (Volume Profile works in tester; DOM does not)
|
|
input bool UseVolumeProfile = true; // Enable volume profile - identify price zones with high volume (WORKS IN TESTER)
|
|
input int VolumeProfileBars = 1000; // Number of bars to analyze for volume profile
|
|
input double LiquidityZoneDistance = 2000; // Max distance to adjust entry towards liquidity zone (pips if InputsArePips=true, otherwise points)
|
|
input bool UseLimitOrders = true; // Use limit orders when smart entry is enabled (if false, uses market orders at adjusted price)
|
|
input bool UseLimitOrderRenewal = true; // Renew pending limit orders when volume momentum weakens
|
|
input int LimitOrderMaxAgeMinutes = 60; // Close limit orders older than this (minutes) when renewal triggers
|
|
input int LimitOrderRenewalCooldownSeconds = 30; // Minimum seconds between renewal checks
|
|
|
|
// SENTIMENT & ADAPTIVE SETTINGS
|
|
input string _hdr18 = "=== SENTIMENT & ADAPTIVE SETTINGS(algo brain,increase MinWinRatePercent to be more conservative) ===";
|
|
input bool UsesentimentTracking = true; // Enable sentiment/emoji feedback on wins/losses
|
|
input bool ShowBalanceChanges = true; // Show balance changes after each trade
|
|
input bool UseAdaptiveTrading = true; // Enable adaptive trading based on sentiment/performance
|
|
input double AggressiveMultiplier = 1.5; // Increase lot size by X% on winning streak
|
|
input double ConservativeMultiplier = 1.0; // Reduce lot size by X% on losing streak
|
|
input double MinWinRatePercent = 65.0; // Minimum win rate % to be considered "winning" mode
|
|
input bool UseAdaptiveStopLoss = false; // Adjust SL/TP based on performance
|
|
input bool UseSignalStrengthGreed = true; // Enable GREEDY/MODERATE/CONSERVATIVE based on signal strength
|
|
input bool ForceConservativeGreedOnSmallAccount = false; // If true, force CONSERVATIVE greed on small-account protection
|
|
|
|
// HEARTBEAT SETTINGS
|
|
input string _hdr19 = "=== HEARTBEAT SETTINGS ===";
|
|
input bool UseHeartbeat = true; // Periodic alive/status log
|
|
input int HeartbeatIntervalSeconds = 60; // Seconds between heartbeat logs
|
|
input bool HeartbeatToJournal = true; // Print heartbeat in Journal
|
|
input int SignalReportIntervalSeconds = 60; // Signal analysis report interval (seconds)
|
|
input bool UseLearningReport = true; // Periodic learning/reward telemetry in Journal
|
|
input int LearningReportIntervalSeconds = 60; // Learning telemetry interval (seconds)
|
|
|
|
// MEMORY DECISION ENGINE SETTINGS
|
|
input string _hdr20 = "=== MEMORY DECISION ENGINE ===";
|
|
input int DecisionScoreThresholdNormal = 5; // Normal mode entry threshold (1..10)
|
|
input int DecisionScoreThresholdStress = 6; // Stress mode entry threshold (1..10)
|
|
input bool UseControlledExplore = true; // Allow controlled exploration after losing streak
|
|
input bool AllowExploreOnSmallAccounts = true; // If true, allow explore even when small-account protection is active
|
|
input int ExploreLossStreakTrigger = 2; // Start exploration after this many consecutive losses
|
|
input int ExploreOneInN = 5; // Allow 1 in N below-threshold signals
|
|
input int ExploreScoreBuffer = 4; // Allow explore for scores in [threshold-buffer, threshold-1]
|
|
input double ExploreLotMultiplier = 1.2; // Lot multiplier when explore entry is allowed (kept <= 1.0 at runtime)
|
|
input int MarketStateUpdateSeconds = 1; // Market-state refresh interval (seconds) for faster reaction
|
|
input double RewardStep = 0.06; // Reward increment for good decisions
|
|
input double PenaltyStep = 0.08; // Penalty decrement for bad decisions
|
|
input double SetupRewardClampMin = -0.5; // Minimum setup reward score
|
|
input double SetupRewardClampMax = 0.5; // Maximum setup reward score
|
|
input bool UseSmartLimitRewardBoost = true; // Extra reward shaping for limit fills (applied only at close outcome)
|
|
input double SmartLimitRewardWinBonus = 0.20; // Added to reward hint when SMART_LIMIT closes positive
|
|
input double SmartLimitRewardLossPenalty = 0.10; // Subtracted from reward hint when SMART_LIMIT closes negative
|
|
input bool LogDecisionLearningDetails = true; // Emit detailed score->fill->learn linkage logs
|
|
input bool UseEntryProfileTP_SL = true; // Adapt initial TP/SL by entry profile
|
|
input int LongTermScoreThreshold = 7; // Score threshold for LONG_TERM profile
|
|
input int ShortTermScoreThreshold = 5; // Score threshold for SHORT_TERM profile
|
|
input double ShortTermTPMultiplier = 1.8; // Initial TP multiplier for SHORT_TERM
|
|
input double ShortTermSLMultiplier = 1.8; // Initial SL multiplier for SHORT_TERM
|
|
input double LongTermTPMultiplier = 1.5; // Initial TP multiplier for LONG_TERM
|
|
input double LongTermSLMultiplier = 1.5; // Initial SL multiplier for LONG_TERM
|
|
|
|
// DECISION DLL SETTINGS (Contextual Bandit)
|
|
input string _hdr21 = "=== DECISION DLL (Bandit) ===";
|
|
// INFO: external decision core integration
|
|
input bool UseDecisionCoreDll = true; // Enable external decision engine DLL
|
|
input bool PreferConsecutiveBarsForDll = true; // If true, feed DLL trend_strength from consecutive-bars signal, not price trend slope
|
|
input string DecisionCoreDbPath = "C:\\sqlite_cmdq\\kog_decision.db"; // Runtime storage path (future-proof)
|
|
input int DecisionCoreSeed = 5052026; // Seed for deterministic runs
|
|
input int DecisionDllSnapshotIntervalSeconds = 60; // Telemetry snapshot interval (seconds)
|
|
input double DllExploreProbThreshold = 0.30; // Min DLL p_allow to trigger explore on BLOCK
|
|
|
|
// WARMUP SETTINGS
|
|
input string _hdr22 = "=== WARMUP SETTINGS ===";
|
|
input bool UseWarmupOnInit = true; // Build initial context from history on attach
|
|
input int WarmupBars = 10000; // Closed bars to scan on init (now extended for weekly context)
|
|
input int WarmupHardCap = 20000; // Safety upper cap for warmup scanning
|
|
input int PatternMemoryBars = 10000; // Bars retained in in-memory buffers per timeframe
|
|
input int PatternBars = 200; // Bars used for candle pattern extraction
|
|
input int VolWindowShort = 100; // Short volatility window (approx. multi-day on intraday TF)
|
|
input int VolWindowLong = 200; // Long volatility window (approx. 1+ week on intraday TF)
|
|
input int TrendWindowBars = 1000; // Trend window (bars) for price slope baseline
|
|
input bool WarmupAllMarketWatchSymbols = true; // Also warm up all Market Watch symbols for multi-symbol scanner
|
|
|
|
// SCANNER SETTINGS
|
|
input string _hdr23 = "=== SCANNER SETTINGS ===";
|
|
input int MaxScannedOpportunities = 20; // Number of top symbols used by scanner/trade loop (1..20)
|
|
input bool ScanForex = true; // Scan and trade Forex/Metals symbols
|
|
input bool ScanStocks = true; // Scan and trade Stocks symbols
|
|
input bool ScanIndices = false; // Scan and trade Indices symbols
|
|
input bool ScanCrypto = false; // Scan and trade Crypto symbols
|
|
|
|
// FAST SPIKE REACTION SETTINGS
|
|
input string _hdr24 = "=== FAST SPIKE REACTION ===";
|
|
input bool UseFastSpikeReaction = true; // Autonomous spike reaction (ms-based, memory-governed)
|
|
input int MaxNewsSpreadPoints = 1200; // Max spread (broker points) allowed for news-spike entries
|
|
input int FastSpikeAccelerationMs = 150; // Optional max ms from anchor open to spike detect (0=disabled)
|
|
input int NewsConfirmMs = 60; // Pending confirm window for news spike (ms)
|
|
input int FastSpikeConfirmMinMs = 120; // Minimum ms before confirming pending spike
|
|
input int FastSpikeHoldTicks = 0; // Consecutive hold ticks required beyond break level
|
|
input double FastSpikeConfirmThresholdFactor = 0.4; // Break level factor vs spike threshold (0.6..1.5)
|
|
|
|
// ALERTS (SOUND/NEWS/FUTURE EVENTS)
|
|
input string _hdr25 = "=== ALERTS (SOUND + NEWS + FUTURE EVENTS) ===";
|
|
input bool EnableNewsEventAlerts = true; // Enable economic calendar alerts
|
|
input int NewsScanIntervalSeconds = 60; // Calendar scan interval
|
|
input int NewsLookaheadMinutes = 30; // Alert for events within next X minutes
|
|
input int NewsAlertCooldownSeconds = 60; // Cooldown between news alerts
|
|
input bool NewsAlertAllCurrencies = true; // If false: only symbol currencies
|
|
input bool EnableFutureSessionAlerts = true; // Alert before session start/end
|
|
input int FutureEventLeadMinutes = 5; // Minutes before start/end to alert
|
|
input bool EnableTelegramAlerts = true; // Enable Telegram notifications via Bot API
|
|
input string TelegramBotToken = ""; // Telegram bot token from BotFather
|
|
input string TelegramChatId = ""; // Telegram chat_id (user/group/channel)
|
|
input int TelegramCooldownSeconds = 15; // Cooldown between same Telegram event messages
|
|
input bool TelegramSendEntries = true; // Send BUY/SELL execution alerts to Telegram
|
|
input bool TelegramSendCloses = true; // Send close-profit/loss alerts to Telegram
|
|
input bool TelegramSendRisk = true; // Send risk/stress alerts to Telegram
|
|
input bool TelegramSendNews = true; // Send economic news alerts to Telegram
|
|
input bool TelegramSendFutureEvents = true; // Send future session event alerts to Telegram
|
|
input bool TelegramSendBlocked = true; // Send blocked-order alerts to Telegram
|
|
input bool TelegramSendSpikes = true; // Send fast-spike detection alerts to Telegram
|
|
// Voice prompts (Zira TTS - no .wav needed)
|
|
input bool EnableVoicePrompts = true; // Use Zira voice prompts
|
|
input int VoiceCooldownSeconds = 15; // Min seconds between same voice event
|
|
input bool VoiceSendSpikes = true; // Voice alert when a fast spike is detected
|
|
|
|
// BEST SESSIONS ENGINE (London/New York analytics)
|
|
input string _hdr26 = "=== BEST SESSIONS ENGINE ===";
|
|
input bool UseBestSessionsEngine = true; // Learn and report session quality (London/NY/Overlap)
|
|
input bool UseBestSessionFilter = false; // Block new entries when current session is weak
|
|
input int BestSessionMinTrades = 5; // Minimum trades before a session is considered reliable
|
|
input double BestSessionMinWinRate = 55.0; // Session must keep at least this win rate to be optimal
|
|
input int LondonSessionStartHour = 11; // London session start hour (broker time)
|
|
input int LondonSessionEndHour = 16; // London session end hour (broker time)
|
|
input int NewYorkSessionStartHour = 16; // New York session start hour (broker time)
|
|
input int NewYorkSessionEndHour = 22; // New York session end hour (broker time)
|
|
input int BestSessionReportIntervalSeconds = 600; // Periodic best-session report interval
|
|
|
|
// STATE PERSISTENCE (file-based)
|
|
input string _hdr27 = "=== STATE PERSISTENCE (file-based) ===";
|
|
input bool UseFilePersistence = true; // Save/load EA memory to file
|
|
input int StateAutosaveSeconds = 60; // Autosave interval (seconds). 0 = disable autosave
|
|
input int MaxSavedTrades = 2000; // Limit trades persisted
|
|
input int MaxSavedRegimes = 5000; // Limit regime history persisted
|
|
input bool ResetStateFile = false; // If true, deletes state file on init
|
|
|
|
//+------------------------------------------------------------------+
|
|
// CONSTANTS - Improve performance and code readability
|
|
//+------------------------------------------------------------------+
|
|
#define ACCOUNT_UPDATE_INTERVAL 5 // seconds between account data updates
|
|
#define INVALID_PRICE 9999526456.42
|
|
#define MAX_DRAWDOWN_PERCENT 20.0
|
|
#define POSITION_UPDATE_INTERVAL 3 // seconds between position data updates
|
|
#define MAX_SCANNED_OPPORTUNITIES 20
|
|
|
|
//+------------------------------------------------------------------+
|
|
// DATA CACHE STRUCTURES - Reduce API calls and improve performance
|
|
//+------------------------------------------------------------------+
|
|
struct AccountCache
|
|
{
|
|
datetime lastUpdate;
|
|
double balance;
|
|
double equity;
|
|
double margin;
|
|
double freeMargin;
|
|
double marginLevel;
|
|
|
|
void update()
|
|
{
|
|
lastUpdate = TimeCurrent();
|
|
balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
margin = AccountInfoDouble(ACCOUNT_MARGIN);
|
|
freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
|
|
marginLevel = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);
|
|
}
|
|
|
|
bool isExpired()
|
|
{
|
|
return (TimeCurrent() - lastUpdate) > ACCOUNT_UPDATE_INTERVAL;
|
|
}
|
|
|
|
bool needsUpdate()
|
|
{
|
|
return (lastUpdate == 0 || isExpired());
|
|
}
|
|
};
|
|
|
|
struct MarketDataCache
|
|
{
|
|
datetime lastUpdate;
|
|
double bid;
|
|
double ask;
|
|
double point;
|
|
double tickSize;
|
|
double tickValue;
|
|
double minLot;
|
|
double maxLot;
|
|
double lotStep;
|
|
|
|
void update()
|
|
{
|
|
lastUpdate = TimeCurrent();
|
|
bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
|
point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
|
|
tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
|
|
minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
|
|
maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
|
|
lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
|
|
}
|
|
|
|
bool isExpired()
|
|
{
|
|
return (TimeCurrent() - lastUpdate) > 1; // Update market data every second
|
|
}
|
|
|
|
bool needsUpdate()
|
|
{
|
|
return (lastUpdate == 0 || isExpired());
|
|
}
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
// MEMORY SYSTEM - Learn from past and adapt future decisions
|
|
//+------------------------------------------------------------------+
|
|
struct TradeMemory
|
|
{
|
|
string symbol;
|
|
datetime entryTime;
|
|
int direction; // 1 = BUY, -1 = SELL
|
|
double entryPrice;
|
|
double exitPrice;
|
|
double profit;
|
|
int consecutiveBars;
|
|
double volumeRatio;
|
|
double spreadAtEntry;
|
|
int hourOfDay;
|
|
int dayOfWeek;
|
|
bool wasWinner;
|
|
double maxProfit;
|
|
double maxDrawdown;
|
|
int holdDuration; // minutes
|
|
string exitReason;
|
|
};
|
|
|
|
struct PatternMemory
|
|
{
|
|
string symbol;
|
|
int patternType; // 1=bullish_exhaustion, 2=bearish_exhaustion, 3=volume_spike, etc.
|
|
int consecutiveBars;
|
|
double volumeRatio;
|
|
double avgProfit;
|
|
double totalProfit;
|
|
int totalTrades;
|
|
int winningTrades;
|
|
double winRate;
|
|
datetime lastUpdated;
|
|
double confidence;
|
|
};
|
|
|
|
struct TimeBasedMemory
|
|
{
|
|
int hourOfDay; // 0-23
|
|
int dayOfWeek; // 0-6 (Sunday=0)
|
|
int totalTrades;
|
|
int winningTrades;
|
|
double totalProfit;
|
|
double avgProfit;
|
|
double winRate;
|
|
double avgSpread;
|
|
double avgVolume;
|
|
bool isOptimalHour;
|
|
|
|
};
|
|
|
|
struct SessionBasedMemory
|
|
{
|
|
string name;
|
|
int totalTrades;
|
|
int winningTrades;
|
|
double totalProfit;
|
|
double avgProfit;
|
|
double winRate;
|
|
bool isOptimal;
|
|
};
|
|
|
|
struct MarketRegimeMemory
|
|
{
|
|
string regime; // "TRENDING", "RANGING", "VOLATILE", "QUIET"
|
|
datetime detected;
|
|
double confidence;
|
|
int duration; // minutes
|
|
double avgVolatility;
|
|
double avgSpread;
|
|
int successfulTrades;
|
|
int totalTrades;
|
|
double performance;
|
|
};
|
|
|
|
struct SetupBucketMemory
|
|
{
|
|
int direction; // 1 = BUY, -1 = SELL
|
|
int barsBucket; // 0..10
|
|
int volumeBucket; // 0..4
|
|
int entryType; // 0 = market, 1 = smart_limit
|
|
string regime; // regime at entry
|
|
int totalTrades;
|
|
int winningTrades;
|
|
double totalProfit;
|
|
double avgProfit;
|
|
double rewardScore; // learned setup score adjustment
|
|
};
|
|
|
|
// Memory arrays with dynamic sizing
|
|
TradeMemory tradeHistory[];
|
|
PatternMemory patternHistory[];
|
|
TimeBasedMemory timeMemory[24][7]; // 24 hours x 7 days
|
|
SessionBasedMemory sessionMemory[4]; // 0=ASIA/OTHER, 1=LONDON, 2=NEW_YORK, 3=OVERLAP
|
|
MarketRegimeMemory regimeHistory[];
|
|
SetupBucketMemory setupMemory[];
|
|
|
|
// Current market state tracking
|
|
struct CurrentMarketState
|
|
{
|
|
double currentVolatility;
|
|
double avgVolatility;
|
|
double volumeTrend;
|
|
double priceTrend;
|
|
double spreadTrend;
|
|
string currentRegime;
|
|
double regimeConfidence;
|
|
datetime regimeStart;
|
|
int consecutiveWins;
|
|
int consecutiveLosses;
|
|
double recentPerformance;
|
|
bool marketStress;
|
|
|
|
// KogDecisionCore equity tracking fields
|
|
double initialBalance;
|
|
double currentEquity;
|
|
double maxEquity;
|
|
double equityDrawdownPercent;
|
|
double equityToBalanceRatio;
|
|
|
|
// KogDecisionCore streak tracking fields
|
|
int currentStreakDirection; // -1, 0, 1
|
|
int currentStreakLength;
|
|
datetime lastTradeTime;
|
|
};
|
|
|
|
CurrentMarketState marketState;
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| KOGDECISIONCORE EQUITY TRACKING FUNCTIONS
|
|
//+------------------------------------------------------------------+
|
|
void UpdateEquityMetrics()
|
|
{
|
|
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
|
|
marketState.currentEquity = equity;
|
|
|
|
// Initialize maxEquity on first run
|
|
if(marketState.maxEquity == 0.0)
|
|
{
|
|
marketState.maxEquity = equity;
|
|
marketState.initialBalance = balance;
|
|
}
|
|
|
|
if(equity > marketState.maxEquity)
|
|
{
|
|
marketState.maxEquity = equity;
|
|
}
|
|
|
|
if(marketState.maxEquity > 0)
|
|
{
|
|
marketState.equityDrawdownPercent =
|
|
((marketState.maxEquity - equity) / marketState.maxEquity) * 100.0;
|
|
}
|
|
|
|
if(balance > 0)
|
|
{
|
|
marketState.equityToBalanceRatio = equity / balance;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| KOGDECISIONCORE STREAK TRACKING FUNCTIONS
|
|
//+------------------------------------------------------------------+
|
|
void UpdateStreakMetrics(int wasWin)
|
|
{
|
|
datetime now = TimeCurrent();
|
|
|
|
if(wasWin > 0) // Win
|
|
{
|
|
if(marketState.currentStreakDirection >= 0)
|
|
{
|
|
marketState.currentStreakDirection = 1;
|
|
marketState.currentStreakLength++;
|
|
}
|
|
else
|
|
{
|
|
marketState.currentStreakDirection = 1;
|
|
marketState.currentStreakLength = 1;
|
|
}
|
|
}
|
|
else
|
|
if(wasWin < 0) // Loss
|
|
{
|
|
if(marketState.currentStreakDirection <= 0)
|
|
{
|
|
marketState.currentStreakDirection = -1;
|
|
marketState.currentStreakLength++;
|
|
}
|
|
else
|
|
{
|
|
marketState.currentStreakDirection = -1;
|
|
marketState.currentStreakLength = 1;
|
|
}
|
|
}
|
|
|
|
marketState.lastTradeTime = now;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
//+------------------------------------------------------------------+
|
|
// ENHANCED MEMORY SYSTEM - Multi-Symbol + Multi-Timeframe
|
|
//+------------------------------------------------------------------+
|
|
struct TimeframeAnalysis
|
|
{
|
|
int timeframe; // M1, M5, M15, H1, H4
|
|
double trendStrength; // -10 la +10
|
|
double volatility;
|
|
double volume;
|
|
double volumeProfile[100];
|
|
double supportLevels[5];
|
|
double resistanceLevels[5];
|
|
int bullishCandles;
|
|
int bearishCandles;
|
|
double trendScore; // 0-10
|
|
datetime lastUpdate;
|
|
double liquidityZone;
|
|
};
|
|
|
|
struct EnhancedSymbolMemory
|
|
{
|
|
string symbol;
|
|
int totalTrades;
|
|
int winningTrades;
|
|
double totalProfit;
|
|
double winRate;
|
|
double avgVolatility;
|
|
double avgSpread;
|
|
int trendScore; // -10 la +10
|
|
double liquidityScore;
|
|
bool isTradable;
|
|
datetime lastAnalysis;
|
|
|
|
// Pattern tracking
|
|
int totalPatterns;
|
|
int successfulPatterns;
|
|
double patternAccuracy;
|
|
|
|
// Timeframe performance
|
|
double tfPerformance[5]; // performance per TF
|
|
int tfTrades[5]; // trades per TF
|
|
};
|
|
|
|
struct SymbolOpportunity
|
|
{
|
|
string symbol;
|
|
double score; // 0-10
|
|
string trendDescription;
|
|
string entryTimeframe;
|
|
double currentPrice;
|
|
double volatility;
|
|
double spread;
|
|
bool isValid;
|
|
};
|
|
|
|
struct CandleAnalysis
|
|
{
|
|
int timeframe;
|
|
double open;
|
|
double high;
|
|
double low;
|
|
double close;
|
|
double volume;
|
|
bool isBullish;
|
|
bool isBearish;
|
|
double bodySize; // body size in points
|
|
double wickRatio; // upper/lower wick ratio
|
|
double volumeRatio; // volume vs average
|
|
int consecutiveBullish; // consecutive bullish candles
|
|
int consecutiveBearish; // consecutive bearish candles
|
|
double strength; // signal strength 0-10
|
|
string signalType; // "STRONG_BULLISH", "BULLISH", "NEUTRAL", "BEARISH", "STRONG_BEARISH"
|
|
};
|
|
|
|
struct CandlePattern
|
|
{
|
|
string patternName; // Pattern id (seq hash or label)
|
|
int patternType; // 1=BULLISH, 2=BEARISH, 3=NEUTRAL
|
|
double reliability; // 0-100% reliability
|
|
int consecutiveBars; // bars in pattern
|
|
double volumeConfirmation; // volume factor
|
|
bool isValid;
|
|
};
|
|
|
|
struct BullishBearishSignals
|
|
{
|
|
CandleAnalysis candleData[5][50]; // 5 timeframes x 50 candles each
|
|
string currentSignal[5]; // current signal per TF
|
|
double signalStrength[5]; // signal strength per TF
|
|
int bullishConfluence; // how many TF show bullish
|
|
int bearishConfluence; // how many TF show bearish
|
|
double overallStrength; // overall signal strength
|
|
|
|
void analyzeAllTimeframes(string symbol);
|
|
void detectCandlePatterns(string symbol, int timeframe);
|
|
void calculateSignalStrength(string symbol, int timeframe);
|
|
string getOverallSignal(string symbol);
|
|
bool validateBullishEntry(string symbol);
|
|
bool validateBearishEntry(string symbol);
|
|
};
|
|
|
|
struct PatternRecognizer
|
|
{
|
|
CandlePattern detectedPatterns[40];
|
|
int patternCount;
|
|
|
|
void analyzeAllPatterns(string symbol, int timeframe);
|
|
double getPatternReliability(string patternName);
|
|
};
|
|
|
|
struct MultiSymbolScanner
|
|
{
|
|
SymbolOpportunity opportunities[];
|
|
int opportunityCount;
|
|
string topSymbols[MAX_SCANNED_OPPORTUNITIES];
|
|
double topScores[MAX_SCANNED_OPPORTUNITIES];
|
|
datetime lastScan;
|
|
|
|
void scanAllSymbols();
|
|
void calculateSymbolScores();
|
|
double calculateSymbolScore(string symbol);
|
|
void rankOpportunities();
|
|
void updateTopSymbols();
|
|
string getTopOpportunities();
|
|
};
|
|
|
|
struct EnhancedMultiTFAnalyzer
|
|
{
|
|
TimeframeAnalysis tfData[5];
|
|
BullishBearishSignals bbSignals;
|
|
PatternRecognizer patterns;
|
|
double tfLiquidityLevel[5];
|
|
double tfLiquidityDistance[5];
|
|
bool tfLiquidityHasData[5];
|
|
|
|
// Signal confluence
|
|
int bullishTimeframes; // count of TF with bullish signals
|
|
int bearishTimeframes; // count of TF with bearish signals
|
|
double overallBullishStrength;
|
|
double overallBearishStrength;
|
|
|
|
// Entry validation
|
|
bool bullishEntryConfirmed;
|
|
bool bearishEntryConfirmed;
|
|
string dominantSignal; // "BULLISH" or "BEARISH"
|
|
double signalConfidence; // 0-100%
|
|
|
|
void analyzeSymbolWithSignals(string symbol);
|
|
void validateSignalConfluence(string symbol);
|
|
void checkPatternConfirmation(string symbol);
|
|
bool shouldEnterLong(string symbol);
|
|
bool shouldEnterShort(string symbol);
|
|
string generateSignalReport(string symbol);
|
|
};
|
|
|
|
struct EnhancedSmartEntry
|
|
{
|
|
string symbol;
|
|
double optimalEntryPrice;
|
|
double dynamicTP;
|
|
double dynamicSL;
|
|
double liquidityZoneLow;
|
|
double liquidityZoneHigh;
|
|
double confidenceLevel;
|
|
double riskPercent;
|
|
string entryReason;
|
|
|
|
void analyzeMultiTimeframe(string symbol);
|
|
void calculateLiquidityZones(string symbol);
|
|
void determineOptimalEntry(string symbol);
|
|
void calculateDynamicTPSL(string symbol);
|
|
double calculateConfidence(string symbol);
|
|
void generateEntryReport(string symbol);
|
|
};
|
|
|
|
// Global arrays and instances
|
|
EnhancedSymbolMemory enhancedSymbolMemory[];
|
|
TimeframeAnalysis globalTFData[];
|
|
string trackedSymbols[];
|
|
int totalTrackedSymbols = 0;
|
|
int totalLearnedPatterns = 0;
|
|
|
|
MultiSymbolScanner scanner;
|
|
EnhancedMultiTFAnalyzer enhancedTFAnalyzer;
|
|
PatternRecognizer patternRecognizer;
|
|
BullishBearishSignals bbSignals;
|
|
EnhancedSmartEntry smartEntry;
|
|
|
|
// Timeframe constants
|
|
int TIMEFRAMES[5] = {PERIOD_M1, PERIOD_M5, PERIOD_M15, PERIOD_H1, PERIOD_H4};
|
|
string TF_NAMES[5] = {"M1", "M5", "M15", "H1", "H4"};
|
|
|
|
struct PositionEntryContext
|
|
{
|
|
ulong positionId;
|
|
int direction; // 1 = BUY, -1 = SELL
|
|
datetime entryTime;
|
|
double entryPrice;
|
|
double liquidityLevel; // price level used/nearest at entry
|
|
double liquidityDistancePts; // distance in points from entry price
|
|
bool liquidityUsed;
|
|
int consecutiveBars;
|
|
double volumeRatio;
|
|
double spreadAtEntry;
|
|
double accumulatedProfit; // Sum of close deals for this position
|
|
int closeDealCount; // Number of close deals accumulated
|
|
int decisionScoreAtEntry; // 1..10 decision score at entry
|
|
int decisionActionAtEntry; // 0=BLOCK,1=ALLOW,2=EXPLORE_SMALL
|
|
int decisionSetupIdAtEntry; // setup_id from DLL scoring context
|
|
int entryTypeAtEntry; // 0=market, 1=smart_limit fill
|
|
ulong sourceOrderTicketAtEntry; // Order ticket that generated DEAL_ENTRY_IN
|
|
int sourceOrderTypeAtEntry; // ORDER_TYPE_* for source order
|
|
int barsBucket; // setup bucket: consecutive bars
|
|
int volumeBucket; // setup bucket: volume regime
|
|
int entryTimeframe; // timeframe chosen at entry
|
|
string regimeAtEntry; // setup bucket: market regime
|
|
string patternHash; // dynamic pattern ID at entry
|
|
};
|
|
|
|
PositionEntryContext positionEntryMemory[];
|
|
|
|
// Pending decision action cache (symbol -> last ALLOW/EXPLORE action)
|
|
string pendingDecisionSymbols[];
|
|
int pendingDecisionActions[];
|
|
int pendingDecisionSetupIds[];
|
|
|
|
//+------------------------------------------------------------------+
|
|
// MEMORY MANAGEMENT FUNCTIONS - Learning and Adaptation Engine
|
|
//+------------------------------------------------------------------+
|
|
void InitializeMemorySystem()
|
|
{
|
|
// Initialize time-based memory grid
|
|
for(int hour = 0; hour < 24; hour++)
|
|
{
|
|
for(int day = 0; day < 7; day++)
|
|
{
|
|
timeMemory[hour][day].hourOfDay = hour;
|
|
timeMemory[hour][day].dayOfWeek = day;
|
|
timeMemory[hour][day].totalTrades = 0;
|
|
timeMemory[hour][day].winningTrades = 0;
|
|
timeMemory[hour][day].totalProfit = 0;
|
|
timeMemory[hour][day].avgProfit = 0;
|
|
timeMemory[hour][day].winRate = 0;
|
|
timeMemory[hour][day].isOptimalHour = false;
|
|
}
|
|
}
|
|
|
|
// Initialize session memory buckets
|
|
sessionMemory[0].name = "ASIA_OR_OTHER";
|
|
sessionMemory[1].name = "LONDON";
|
|
sessionMemory[2].name = "NEW_YORK";
|
|
sessionMemory[3].name = "OVERLAP";
|
|
for(int s = 0; s < 4; s++)
|
|
{
|
|
sessionMemory[s].totalTrades = 0;
|
|
sessionMemory[s].winningTrades = 0;
|
|
sessionMemory[s].totalProfit = 0.0;
|
|
sessionMemory[s].avgProfit = 0.0;
|
|
sessionMemory[s].winRate = 0.0;
|
|
sessionMemory[s].isOptimal = false;
|
|
}
|
|
|
|
// Initialize market state
|
|
marketState.currentRegime = "UNKNOWN";
|
|
marketState.regimeConfidence = 0.0;
|
|
marketState.regimeStart = TimeCurrent();
|
|
marketState.consecutiveWins = 0;
|
|
marketState.consecutiveLosses = 0;
|
|
marketState.recentPerformance = 0.0;
|
|
marketState.marketStress = false;
|
|
|
|
// Initialize enhanced memory system
|
|
InitializeEnhancedMemorySystem();
|
|
|
|
Print("Memory system initialized - Ready to learn and adapt!");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Enhanced Memory System Initialization
|
|
//+------------------------------------------------------------------+
|
|
void InitializeEnhancedMemorySystem()
|
|
{
|
|
// 1. Scan all available symbols
|
|
int symbolCount = SymbolsTotal(true);
|
|
|
|
ArrayResize(enhancedSymbolMemory, symbolCount);
|
|
ArrayResize(trackedSymbols, symbolCount);
|
|
|
|
for(int i = 0; i < symbolCount; i++)
|
|
{
|
|
string sym = SymbolName(i, true);
|
|
enhancedSymbolMemory[i].symbol = sym;
|
|
enhancedSymbolMemory[i].totalTrades = 0;
|
|
enhancedSymbolMemory[i].winningTrades = 0;
|
|
enhancedSymbolMemory[i].totalProfit = 0;
|
|
enhancedSymbolMemory[i].winRate = 0;
|
|
enhancedSymbolMemory[i].trendScore = 0;
|
|
enhancedSymbolMemory[i].isTradable = true;
|
|
enhancedSymbolMemory[i].totalPatterns = 0;
|
|
enhancedSymbolMemory[i].successfulPatterns = 0;
|
|
enhancedSymbolMemory[i].avgVolatility = 0;
|
|
enhancedSymbolMemory[i].avgSpread = 0;
|
|
enhancedSymbolMemory[i].liquidityScore = 0;
|
|
|
|
// Initialize timeframe performance
|
|
for(int j = 0; j < 5; j++)
|
|
{
|
|
enhancedSymbolMemory[i].tfPerformance[j] = 0;
|
|
enhancedSymbolMemory[i].tfTrades[j] = 0;
|
|
}
|
|
|
|
trackedSymbols[i] = sym;
|
|
}
|
|
|
|
totalTrackedSymbols = symbolCount;
|
|
|
|
// 2. Initialize global timeframe data
|
|
ArrayResize(globalTFData, 5);
|
|
for(int i = 0; i < 5; i++)
|
|
{
|
|
globalTFData[i].timeframe = TIMEFRAMES[i];
|
|
globalTFData[i].trendStrength = 0;
|
|
globalTFData[i].volatility = 0;
|
|
globalTFData[i].volume = 0;
|
|
globalTFData[i].trendScore = 0;
|
|
globalTFData[i].bullishCandles = 0;
|
|
globalTFData[i].bearishCandles = 0;
|
|
globalTFData[i].lastUpdate = 0;
|
|
globalTFData[i].liquidityZone = 0;
|
|
ArrayInitialize(globalTFData[i].volumeProfile, 0);
|
|
ArrayInitialize(globalTFData[i].supportLevels, 0);
|
|
ArrayInitialize(globalTFData[i].resistanceLevels, 0);
|
|
}
|
|
|
|
// 3. Initialize scanner
|
|
scanner.scanAllSymbols();
|
|
|
|
// 4. Initialize enhanced analyzer
|
|
enhancedTFAnalyzer.bbSignals = bbSignals;
|
|
enhancedTFAnalyzer.patterns = patternRecognizer;
|
|
|
|
Print("Enhanced Memory System initialized");
|
|
Print("Symbol Memory: ", totalTrackedSymbols, " symbols tracked");
|
|
Print("Timeframe Data: 5 TF per symbol");
|
|
Print("Scanner ready - analyzing opportunities...");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| KOGDECISIONCORE MTF CONFLUENCE CALCULATION
|
|
//+------------------------------------------------------------------+
|
|
double CalculateMTFConfluence(string symbol, ENUM_TIMEFRAMES currentTF)
|
|
{
|
|
double confluence = 0.0;
|
|
int validSignals = 0;
|
|
|
|
ENUM_TIMEFRAMES timeframes[] = {PERIOD_M1, PERIOD_M5, PERIOD_M15, PERIOD_H1, PERIOD_H4};
|
|
|
|
for(int i = 0; i < 5; i++)
|
|
{
|
|
ENUM_TIMEFRAMES tf = timeframes[i];
|
|
if(tf == currentTF)
|
|
continue;
|
|
|
|
double trend = calculateTrendStrength(symbol, tf);
|
|
double avgVolume = calculateAverageVolume(symbol, tf, 20);
|
|
long currentVolume = UseTickVolume ? iTickVolume(symbol, tf, 0) : iVolume(symbol, tf, 0);
|
|
double volume = (avgVolume > 0.0) ? ((double)currentVolume / avgVolume) : 1.0;
|
|
|
|
if(trend > 2.0 && volume > 1.2)
|
|
{
|
|
confluence += 1.0;
|
|
validSignals++;
|
|
}
|
|
}
|
|
|
|
return (validSignals > 0) ? (confluence / validSignals) : 0.0;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| KOGDECISIONCORE SESSION QUALITY CALCULATION
|
|
//+------------------------------------------------------------------+
|
|
double CalculateSessionQuality(int hour, int day)
|
|
{
|
|
// Use existing timeMemory array
|
|
if(hour < 0 || hour > 23 || day < 0 || day > 6)
|
|
{
|
|
return 0.5; // Default neutral score
|
|
}
|
|
|
|
TimeBasedMemory memory = timeMemory[hour][day];
|
|
|
|
if(memory.totalTrades < 5)
|
|
{
|
|
return 0.5; // Not enough data
|
|
}
|
|
|
|
// Weight win rate more heavily
|
|
double winRateWeight = 0.7;
|
|
double profitWeight = 0.3;
|
|
|
|
double normalizedWinRate = memory.winRate / 100.0;
|
|
double normalizedProfit = MathMin(MathMax(memory.avgProfit / 100.0, -1.0), 1.0);
|
|
|
|
return (normalizedWinRate * winRateWeight) + (normalizedProfit * profitWeight);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Update Enhanced Memory System
|
|
//+------------------------------------------------------------------+
|
|
void UpdateEnhancedMemorySystem()
|
|
{
|
|
totalLearnedPatterns = 0;
|
|
|
|
for(int i = 0; i < totalTrackedSymbols; i++)
|
|
{
|
|
// Update symbol performance
|
|
UpdateSymbolPerformance(trackedSymbols[i]);
|
|
|
|
// Count patterns
|
|
totalLearnedPatterns += enhancedSymbolMemory[i].totalPatterns;
|
|
}
|
|
|
|
Print("=== ENHANCED MEMORY SYSTEM ===");
|
|
Print("Symbol Memory: ", totalTrackedSymbols, " symbols tracked");
|
|
Print("Timeframe Data: 5 TF per symbol");
|
|
Print("Total Patterns: ", totalLearnedPatterns, " learned");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateSymbolPerformance(string symbol)
|
|
{
|
|
int symbolIndex = getSymbolIndex(symbol);
|
|
if(symbolIndex < 0)
|
|
return;
|
|
|
|
// Refresh per-symbol pattern stats from learned pattern memory.
|
|
int symbolPatterns = 0;
|
|
int symbolSuccessful = 0;
|
|
for(int p = 0; p < ArraySize(patternHistory); p++)
|
|
{
|
|
if(patternHistory[p].symbol != symbol)
|
|
continue;
|
|
if(patternHistory[p].totalTrades <= 0)
|
|
continue;
|
|
|
|
symbolPatterns++;
|
|
if(patternHistory[p].winRate >= 50.0 && patternHistory[p].totalProfit > 0.0)
|
|
symbolSuccessful++;
|
|
}
|
|
enhancedSymbolMemory[symbolIndex].totalPatterns = symbolPatterns;
|
|
enhancedSymbolMemory[symbolIndex].successfulPatterns = symbolSuccessful;
|
|
enhancedSymbolMemory[symbolIndex].patternAccuracy = (symbolPatterns > 0)
|
|
? ((double)symbolSuccessful / (double)symbolPatterns)
|
|
: 0.0;
|
|
|
|
// Recompute per-symbol metrics across all configured timeframes.
|
|
double totalVolatility = 0;
|
|
double totalTrend = 0;
|
|
double totalVolumeRatio = 0;
|
|
int validTf = 0;
|
|
for(int i = 0; i < 5; i++)
|
|
{
|
|
int timeframe = TIMEFRAMES[i];
|
|
ENUM_TIMEFRAMES tf = (ENUM_TIMEFRAMES)timeframe;
|
|
if(Bars(symbol, tf) < 50)
|
|
continue;
|
|
|
|
double volatility = calculateVolatility(symbol, timeframe);
|
|
double trend = calculateTrendStrength(symbol, timeframe);
|
|
double avgVolume = calculateAverageVolume(symbol, timeframe, 20);
|
|
long currentVolume = UseTickVolume ? iTickVolume(symbol, tf, 1) : iVolume(symbol, tf, 1);
|
|
if(currentVolume <= 0)
|
|
currentVolume = iTickVolume(symbol, tf, 1);
|
|
double volumeRatio = (avgVolume > 0.0) ? ((double)currentVolume / avgVolume) : 1.0;
|
|
|
|
totalVolatility += volatility;
|
|
totalTrend += trend;
|
|
totalVolumeRatio += volumeRatio;
|
|
validTf++;
|
|
}
|
|
if(validTf <= 0)
|
|
{
|
|
enhancedSymbolMemory[symbolIndex].isTradable = false;
|
|
return;
|
|
}
|
|
enhancedSymbolMemory[symbolIndex].avgVolatility = totalVolatility / (double)validTf;
|
|
|
|
// Calculate average spread
|
|
double spread = (SymbolInfoDouble(symbol, SYMBOL_ASK) - SymbolInfoDouble(symbol, SYMBOL_BID)) / SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
enhancedSymbolMemory[symbolIndex].avgSpread = spread;
|
|
|
|
// Update trend score
|
|
enhancedSymbolMemory[symbolIndex].trendScore = (int)MathRound(totalTrend / (double)validTf);
|
|
|
|
// Update liquidity score
|
|
enhancedSymbolMemory[symbolIndex].liquidityScore = totalVolumeRatio / (double)validTf;
|
|
|
|
// Update last analysis time
|
|
enhancedSymbolMemory[symbolIndex].lastAnalysis = TimeCurrent();
|
|
enhancedSymbolMemory[symbolIndex].isTradable = true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int GetConsecutiveBarsForSymbolTF(string symbol, ENUM_TIMEFRAMES tf, bool countBullish)
|
|
{
|
|
int bars = Bars(symbol, tf);
|
|
if(bars <= 2)
|
|
return 0;
|
|
|
|
int consecutive = 0;
|
|
int maxBars = MathMin(20, bars - 1);
|
|
for(int i = 1; i <= maxBars; i++)
|
|
{
|
|
double open = iOpen(symbol, tf, i);
|
|
double close = iClose(symbol, tf, i);
|
|
if(open == 0.0 && close == 0.0)
|
|
break;
|
|
|
|
bool match = countBullish ? (close > open) : (close < open);
|
|
if(match)
|
|
consecutive++;
|
|
else
|
|
break;
|
|
}
|
|
return consecutive;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int DetectEntryTimeframeForSymbol(string symbol, int direction)
|
|
{
|
|
int bestTf = (int)Period();
|
|
int bestScore = -1;
|
|
bool countBullish = (direction == -1);
|
|
|
|
for(int i = 0; i < 5; i++)
|
|
{
|
|
ENUM_TIMEFRAMES tf = (ENUM_TIMEFRAMES)TIMEFRAMES[i];
|
|
int streak = GetConsecutiveBarsForSymbolTF(symbol, tf, countBullish);
|
|
if(streak > bestScore)
|
|
{
|
|
bestScore = streak;
|
|
bestTf = TIMEFRAMES[i];
|
|
}
|
|
}
|
|
|
|
return bestTf;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Utility Functions
|
|
//+------------------------------------------------------------------+
|
|
int getTimeframeIndex(int timeframe)
|
|
{
|
|
for(int i = 0; i < 5; i++)
|
|
{
|
|
if(TIMEFRAMES[i] == timeframe)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int GetEffectiveMaxScannedOpportunities()
|
|
{
|
|
if(MaxScannedOpportunities < 1)
|
|
return 1;
|
|
if(MaxScannedOpportunities > MAX_SCANNED_OPPORTUNITIES)
|
|
return MAX_SCANNED_OPPORTUNITIES;
|
|
return MaxScannedOpportunities;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int getSymbolIndex(string symbol)
|
|
{
|
|
for(int i = 0; i < totalTrackedSymbols; i++)
|
|
{
|
|
if(enhancedSymbolMemory[i].symbol == symbol)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetMAValue(string symbol, int timeframe, int period, ENUM_MA_METHOD ma_method, int applied_price, int shift)
|
|
{
|
|
int handle = iMA(symbol, (ENUM_TIMEFRAMES)timeframe, period, 0, ma_method, applied_price);
|
|
if(handle == INVALID_HANDLE)
|
|
return 0.0;
|
|
|
|
double buf[];
|
|
ArraySetAsSeries(buf, true);
|
|
if(CopyBuffer(handle, 0, shift, 1, buf) <= 0)
|
|
{
|
|
IndicatorRelease(handle);
|
|
return 0.0;
|
|
}
|
|
double val = buf[0];
|
|
IndicatorRelease(handle);
|
|
return val;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetATRValue(string symbol, int timeframe, int period, int shift)
|
|
{
|
|
int handle = iATR(symbol, (ENUM_TIMEFRAMES)timeframe, period);
|
|
if(handle == INVALID_HANDLE)
|
|
return 0.0;
|
|
|
|
double buf[];
|
|
ArraySetAsSeries(buf, true);
|
|
if(CopyBuffer(handle, 0, shift, 1, buf) <= 0)
|
|
{
|
|
IndicatorRelease(handle);
|
|
return 0.0;
|
|
}
|
|
double val = buf[0];
|
|
IndicatorRelease(handle);
|
|
return val;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double calculateAverageVolume(string symbol, int timeframe, int period)
|
|
{
|
|
if(period <= 0)
|
|
return 0.0;
|
|
|
|
double totalVolume = 0;
|
|
for(int i = 0; i < period; i++)
|
|
{
|
|
totalVolume += (double)iVolume(symbol, (ENUM_TIMEFRAMES)timeframe, i);
|
|
}
|
|
return totalVolume / period;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double calculateTrendStrength(string symbol, int timeframe)
|
|
{
|
|
double ema20 = GetMAValue(symbol, timeframe, 20, MODE_EMA, PRICE_CLOSE, 0);
|
|
double ema50 = GetMAValue(symbol, timeframe, 50, MODE_EMA, PRICE_CLOSE, 0);
|
|
double current = iClose(symbol, (ENUM_TIMEFRAMES)timeframe, 0);
|
|
|
|
if(MathAbs(ema20) <= DBL_EPSILON || MathAbs(ema50) <= DBL_EPSILON)
|
|
return 0.0;
|
|
|
|
double trend = (ema20 - ema50) / ema50 * 100;
|
|
double pricePosition = (current - ema20) / ema20 * 100;
|
|
|
|
return MathMax(-10, MathMin(10, (trend + pricePosition) / 2));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double calculateVolatility(string symbol, int timeframe)
|
|
{
|
|
double atr = GetATRValue(symbol, timeframe, 14, 0);
|
|
double current = iClose(symbol, (ENUM_TIMEFRAMES)timeframe, 0);
|
|
return (current > 0) ? (atr / current * 100) : 0;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool AreAnyScanCategoriesEnabled()
|
|
{
|
|
return (ScanForex || ScanStocks || ScanIndices || ScanCrypto);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsLikelyForexByName(string lowerName)
|
|
{
|
|
if(StringFind(lowerName, "xau") == 0 || StringFind(lowerName, "xag") == 0 ||
|
|
StringFind(lowerName, "xpt") == 0 || StringFind(lowerName, "xpd") == 0)
|
|
return true;
|
|
|
|
string letters = "";
|
|
int nameLen = StringLen(lowerName);
|
|
for(int i = 0; i < nameLen; i++)
|
|
{
|
|
ushort ch = StringGetCharacter(lowerName, i);
|
|
bool isLetter = ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'));
|
|
if(isLetter)
|
|
letters += CharToString((uchar)ch);
|
|
else
|
|
break;
|
|
}
|
|
|
|
return (StringLen(letters) == 6);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsSymbolAllowedByScanCategories(string symbol)
|
|
{
|
|
if(!AreAnyScanCategoriesEnabled())
|
|
return false;
|
|
|
|
string path = "";
|
|
SymbolInfoString(symbol, SYMBOL_PATH, path);
|
|
string p = path;
|
|
string n = symbol;
|
|
StringToLower(p);
|
|
StringToLower(n);
|
|
|
|
bool isCrypto = (StringFind(p, "crypto") >= 0 ||
|
|
StringFind(n, "btc") >= 0 || StringFind(n, "eth") >= 0 ||
|
|
StringFind(n, "ltc") >= 0 || StringFind(n, "xrp") >= 0 ||
|
|
StringFind(n, "sol") >= 0 || StringFind(n, "ada") >= 0 ||
|
|
StringFind(n, "doge") >= 0);
|
|
|
|
bool isIndices = (StringFind(p, "index") >= 0 || StringFind(p, "indices") >= 0 ||
|
|
StringFind(n, "us30") >= 0 || StringFind(n, "dj30") >= 0 ||
|
|
StringFind(n, "nas100") >= 0 || StringFind(n, "us100") >= 0 ||
|
|
StringFind(n, "spx500") >= 0 || StringFind(n, "us500") >= 0 ||
|
|
StringFind(n, "de40") >= 0 || StringFind(n, "ger40") >= 0 ||
|
|
StringFind(n, "uk100") >= 0 || StringFind(n, "jp225") >= 0 ||
|
|
StringFind(n, "jpn225") >= 0 || StringFind(n, "fr40") >= 0 ||
|
|
StringFind(n, "es35") >= 0 || StringFind(n, "hk50") >= 0 ||
|
|
StringFind(n, "au200") >= 0 || StringFind(n, "stoxx50") >= 0);
|
|
|
|
bool isStocks = (StringFind(p, "stock") >= 0 || StringFind(p, "equit") >= 0 ||
|
|
StringFind(p, "share") >= 0 || StringFind(p, "nyse") >= 0 ||
|
|
StringFind(p, "nasdaq") >= 0);
|
|
|
|
bool isForex = (!isCrypto && !isIndices && !isStocks &&
|
|
(StringFind(p, "forex") >= 0 || StringFind(p, "fx") >= 0 ||
|
|
StringFind(p, "currenc") >= 0 || StringFind(p, "metal") >= 0 ||
|
|
IsLikelyForexByName(n)));
|
|
|
|
// Fail-open for unknown symbols to avoid hard blocking broker-specific naming.
|
|
if(!isForex && !isStocks && !isIndices && !isCrypto)
|
|
return true;
|
|
|
|
if(isForex && ScanForex)
|
|
return true;
|
|
if(isStocks && ScanStocks)
|
|
return true;
|
|
if(isIndices && ScanIndices)
|
|
return true;
|
|
if(isCrypto && ScanCrypto)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Multi-Symbol Scanner Implementation
|
|
//+------------------------------------------------------------------+
|
|
void MultiSymbolScanner::scanAllSymbols()
|
|
{
|
|
opportunityCount = 0;
|
|
ArrayResize(opportunities, totalTrackedSymbols);
|
|
|
|
if(!AreAnyScanCategoriesEnabled())
|
|
{
|
|
updateTopSymbols();
|
|
return;
|
|
}
|
|
|
|
for(int i = 0; i < totalTrackedSymbols; i++)
|
|
{
|
|
string symbol = trackedSymbols[i];
|
|
|
|
// Skip if symbol not tradable
|
|
if(!enhancedSymbolMemory[i].isTradable)
|
|
continue;
|
|
|
|
// Skip symbols filtered out by category selection
|
|
if(!IsSymbolAllowedByScanCategories(symbol))
|
|
continue;
|
|
|
|
// Analyze symbol
|
|
enhancedTFAnalyzer.analyzeSymbolWithSignals(symbol);
|
|
|
|
// Calculate opportunity score
|
|
double score = calculateSymbolScore(symbol);
|
|
|
|
if(score > 5.0) // Only consider scores > 5
|
|
{
|
|
opportunities[opportunityCount].symbol = symbol;
|
|
opportunities[opportunityCount].score = score;
|
|
opportunities[opportunityCount].trendDescription = enhancedTFAnalyzer.generateSignalReport(symbol);
|
|
opportunities[opportunityCount].currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
opportunities[opportunityCount].volatility = enhancedSymbolMemory[i].avgVolatility;
|
|
opportunities[opportunityCount].spread = (SymbolInfoDouble(symbol, SYMBOL_ASK) - SymbolInfoDouble(symbol, SYMBOL_BID)) / SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
opportunities[opportunityCount].isValid = true;
|
|
opportunityCount++;
|
|
}
|
|
}
|
|
|
|
// Rank opportunities
|
|
rankOpportunities();
|
|
updateTopSymbols();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double MultiSymbolScanner::calculateSymbolScore(string symbol)
|
|
{
|
|
double score = 0;
|
|
int symbolIndex = getSymbolIndex(symbol);
|
|
|
|
// 1. Trend confluence (40% weight)
|
|
double confluence = enhancedTFAnalyzer.signalConfidence / 10.0; // Convert percentage to 0-10 scale
|
|
score += confluence * 4.0;
|
|
|
|
// 2. Volatility (20% weight)
|
|
double volatility = enhancedSymbolMemory[symbolIndex].avgVolatility;
|
|
if(volatility > 0.5 && volatility < 3.0)
|
|
score += 2.0;
|
|
else
|
|
if(volatility > 0.3 && volatility < 5.0)
|
|
score += 1.5;
|
|
else
|
|
if(volatility > 0.1)
|
|
score += 1.0;
|
|
|
|
// 3. Volume (20% weight)
|
|
double avgVolume = 0;
|
|
for(int i = 0; i < 5; i++)
|
|
{
|
|
avgVolume += enhancedTFAnalyzer.tfData[i].volume;
|
|
}
|
|
avgVolume /= 5.0;
|
|
if(avgVolume > 1.2)
|
|
score += 2.0;
|
|
else
|
|
if(avgVolume > 1.0)
|
|
score += 1.5;
|
|
else
|
|
if(avgVolume > 0.8)
|
|
score += 1.0;
|
|
|
|
// 4. Memory performance (20% weight)
|
|
double winRate = enhancedSymbolMemory[symbolIndex].winRate;
|
|
score += winRate * 2.0;
|
|
|
|
return MathMin(score, 10.0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void MultiSymbolScanner::rankOpportunities()
|
|
{
|
|
// Simple bubble sort for ranking
|
|
for(int i = 0; i < opportunityCount - 1; i++)
|
|
{
|
|
for(int j = 0; j < opportunityCount - i - 1; j++)
|
|
{
|
|
if(opportunities[j].score < opportunities[j + 1].score)
|
|
{
|
|
SymbolOpportunity temp = opportunities[j];
|
|
opportunities[j] = opportunities[j + 1];
|
|
opportunities[j + 1] = temp;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void MultiSymbolScanner::updateTopSymbols()
|
|
{
|
|
int maxTop = GetEffectiveMaxScannedOpportunities();
|
|
for(int i = 0; i < MAX_SCANNED_OPPORTUNITIES; i++)
|
|
{
|
|
if(i < maxTop && i < opportunityCount)
|
|
{
|
|
topSymbols[i] = opportunities[i].symbol;
|
|
topScores[i] = opportunities[i].score;
|
|
}
|
|
else
|
|
{
|
|
topSymbols[i] = "";
|
|
topScores[i] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
string MultiSymbolScanner::getTopOpportunities()
|
|
{
|
|
int maxTop = GetEffectiveMaxScannedOpportunities();
|
|
string report = "Top " + IntegerToString(maxTop) + " Opportunities:\n";
|
|
|
|
for(int i = 0; i < MathMin(maxTop, opportunityCount); i++)
|
|
{
|
|
report += IntegerToString(i + 1) + ". " + opportunities[i].symbol +
|
|
" (ScannerRank: " + DoubleToString(opportunities[i].score, 1) + ") - " +
|
|
opportunities[i].trendDescription + "\n";
|
|
}
|
|
|
|
return report;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Dynamic Pattern Learning Engine - Generic Pattern Recognition |
|
|
//+------------------------------------------------------------------+
|
|
struct CandleFingerprint
|
|
{
|
|
// Geometric signature (0.0 - 1.0)
|
|
double bodyPosition; // 0=bottom, 0.5=middle, 1=top
|
|
double bodyRatio; // body / total range
|
|
double upperWickRatio; // upper wick / total range
|
|
double lowerWickRatio; // lower wick / total range
|
|
|
|
// Relative dimensions
|
|
double bodyToRange; // body vs total range
|
|
double wickToBody; // total wicks vs body
|
|
|
|
// Classification
|
|
int direction; // 1=bullish, -1=bearish, 0=doji
|
|
|
|
// Compact hash for lookup
|
|
string patternHash; // e.g., "B_35_20_45_10"
|
|
};
|
|
|
|
struct DynamicPattern
|
|
{
|
|
string patternHash; // Unique ID
|
|
string description; // Auto-generated description
|
|
int patternType; // 1=bullish, 2=bearish, 0=neutral
|
|
int occurrenceCount; // Times seen
|
|
int winCount; // Winning trades
|
|
int lossCount; // Losing trades
|
|
double reliability; // 0-100%
|
|
double avgProfit; // Average profit
|
|
double totalProfit; // Cumulative profit
|
|
datetime firstSeen; // First occurrence
|
|
datetime lastSeen; // Last occurrence
|
|
int timeframe; // Most common timeframe
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int FindDynamicPatternIndex(string patternHash);
|
|
int QuantizeRatioTo10(double value);
|
|
int InferSequencePatternType(string symbol, int timeframe, int startShift, int barsCount);
|
|
string BuildSequenceDescription(string symbol, int timeframe, int startShift, int barsCount);
|
|
string GeneratePatternSequenceHash(string symbol, int timeframe, int startShift, int barsCount);
|
|
int FindOrCreateDynamicPatternMeta(string patternHash, int patternType, int timeframe, string description);
|
|
double ConvertInputDistanceToPriceForSymbol(string symbol, double inputDistance);
|
|
void AdjustEntryDistancesSmart(string symbol, int direction, int decisionScore, double bid, double ask,
|
|
double baseTPDistance, double baseSLDistance,
|
|
double &tpDistance, double &slDistance);
|
|
void EmitOrderExecutedSound(int direction);
|
|
void EmitOrderBlockedSound();
|
|
void EmitRiskWarningSound();
|
|
void EmitCloseResultSound(double profit);
|
|
void EmitSpikeDetectedAlert(int direction, double movePoints, double anchorPrice, int thresholdPoints);
|
|
void CheckUpcomingEconomicNewsAlerts();
|
|
void CheckFutureSessionEventAlerts();
|
|
void GetAutoFastSpikeParameters(int &thresholdPoints, uint &windowMs, uint &cooldownMs, bool &preferMomentum);
|
|
double GetInstantVolumeBurst(string symbol, ENUM_TIMEFRAMES tf, int lookbackBars);
|
|
double GetAverageTickVolumeForSymbol(string symbol, ENUM_TIMEFRAMES tf, int lookback);
|
|
bool DetectFastSpike(int &spikeDirection, double &movePoints, double &anchorPrice, uint &elapsedMs, int &thresholdPointsOut);
|
|
void ResetFastSpikePendingState();
|
|
bool SaveStateToFile();
|
|
bool LoadStateFromFile();
|
|
void AutosaveStateIfDue();
|
|
string StateFileName();
|
|
string SanitizeField(const string value);
|
|
double NormalizeToTickForSymbol(string symbol, double price);
|
|
double GetMinStopDistanceForSymbol(string symbol);
|
|
double GetFreezeDistanceForSymbol(string symbol);
|
|
double EnforceMinStopDistanceForSymbol(string symbol, double entry, double target, bool isBuy, bool isSL);
|
|
bool CheckStopLossTakeProfitForSymbol(string symbol, ENUM_ORDER_TYPE type, double sl, double tp);
|
|
bool IsSmartPendingOrderType(long orderType);
|
|
string UrlEncodeUtf8(const string value);
|
|
bool SendTelegramMessage(const string message, datetime &lastSendTime, int cooldownSeconds);
|
|
string BuildTelegramEntryMessage(const string symbol, const string side, double entry, double sl, double tp, int score, const string regime);
|
|
void UpdateSessionBasedMemory(TradeMemory &tradeRecord);
|
|
double GetCurrentSessionConfidence();
|
|
bool IsCurrentSessionTradable();
|
|
void PrintBestSessionReport();
|
|
double GetAccountDrawdownPercentAuto();
|
|
double GetRecentWinRateAuto(int lookbackTrades);
|
|
bool IsSmallAccountForAutoProtection();
|
|
bool IsLargeAccountForAutoExplore();
|
|
int GetEffectiveDecisionThresholdAuto(int baseThreshold);
|
|
double GetAutoRiskBalanceFactor(double accountBalance);
|
|
double GetConfiguredBaseRiskPercent();
|
|
double GetLiquidityZoneDistancePointsForSymbol(string symbol);
|
|
bool ShouldEmitFilterStatusLog(const string symbol, const string direction, bool passed, const string scope);
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Bullish/Bearish Signals Implementation
|
|
//+------------------------------------------------------------------+
|
|
void BullishBearishSignals::detectCandlePatterns(string symbol, int timeframe)
|
|
{
|
|
int tfIndex = getTimeframeIndex(timeframe);
|
|
if(tfIndex < 0 || tfIndex >= 5)
|
|
return;
|
|
|
|
// Keep previous candle context for consecutive counting
|
|
candleData[tfIndex][1] = candleData[tfIndex][0];
|
|
|
|
// Get last N candles for pattern analysis
|
|
int barsForPatterns = MathMax(3, PatternBars);
|
|
double close[], open[], high[], low[], volume[];
|
|
ArrayResize(close, barsForPatterns);
|
|
ArrayResize(open, barsForPatterns);
|
|
ArrayResize(high, barsForPatterns);
|
|
ArrayResize(low, barsForPatterns);
|
|
ArrayResize(volume, barsForPatterns);
|
|
|
|
for(int i = 0; i < barsForPatterns; i++)
|
|
{
|
|
close[i] = iClose(symbol, (ENUM_TIMEFRAMES)timeframe, i);
|
|
open[i] = iOpen(symbol, (ENUM_TIMEFRAMES)timeframe, i);
|
|
high[i] = iHigh(symbol, (ENUM_TIMEFRAMES)timeframe, i);
|
|
low[i] = iLow(symbol, (ENUM_TIMEFRAMES)timeframe, i);
|
|
volume[i] = (double)iVolume(symbol, (ENUM_TIMEFRAMES)timeframe, i);
|
|
}
|
|
|
|
// Current candle analysis
|
|
CandleAnalysis current;
|
|
current.timeframe = timeframe;
|
|
current.open = open[0];
|
|
current.high = high[0];
|
|
current.low = low[0];
|
|
current.close = close[0];
|
|
current.volume = volume[0];
|
|
|
|
// Determine candle type
|
|
current.isBullish = (close[0] > open[0]);
|
|
current.isBearish = (close[0] < open[0]);
|
|
current.bodySize = MathAbs(close[0] - open[0]) / SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
|
|
// Calculate wick ratio
|
|
double upperWick = high[0] - MathMax(open[0], close[0]);
|
|
double lowerWick = MathMin(open[0], close[0]) - low[0];
|
|
current.wickRatio = (upperWick + lowerWick > 0) ? upperWick / (upperWick + lowerWick) : 0.5;
|
|
|
|
// Volume analysis
|
|
double avgVolume = calculateAverageVolume(symbol, timeframe, 20);
|
|
current.volumeRatio = (avgVolume > 0) ? volume[0] / avgVolume : 1.0;
|
|
|
|
// Consecutive candles
|
|
current.consecutiveBullish = 0;
|
|
current.consecutiveBearish = 0;
|
|
if(current.isBullish)
|
|
{
|
|
current.consecutiveBullish = (candleData[tfIndex][1].isBullish) ?
|
|
candleData[tfIndex][1].consecutiveBullish + 1 : 1;
|
|
current.consecutiveBearish = 0;
|
|
}
|
|
else
|
|
if(current.isBearish)
|
|
{
|
|
current.consecutiveBearish = (candleData[tfIndex][1].isBearish) ?
|
|
candleData[tfIndex][1].consecutiveBearish + 1 : 1;
|
|
current.consecutiveBullish = 0;
|
|
}
|
|
|
|
// Store first, then compute signal from the current candle
|
|
candleData[tfIndex][0] = current;
|
|
calculateSignalStrength(symbol, timeframe);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void BullishBearishSignals::calculateSignalStrength(string symbol, int timeframe)
|
|
{
|
|
int tfIndex = getTimeframeIndex(timeframe);
|
|
if(tfIndex < 0 || tfIndex >= 5)
|
|
return;
|
|
|
|
CandleAnalysis current = candleData[tfIndex][0];
|
|
|
|
double strength = 0;
|
|
|
|
// 1. Body size contribution (30%)
|
|
if(current.bodySize > 20)
|
|
strength += 3;
|
|
else
|
|
if(current.bodySize > 10)
|
|
strength += 2;
|
|
else
|
|
if(current.bodySize > 5)
|
|
strength += 1;
|
|
|
|
// 2. Volume contribution (25%)
|
|
if(current.volumeRatio > 2.0)
|
|
strength += 2.5;
|
|
else
|
|
if(current.volumeRatio > 1.5)
|
|
strength += 2;
|
|
else
|
|
if(current.volumeRatio > 1.2)
|
|
strength += 1.5;
|
|
else
|
|
if(current.volumeRatio > 1.0)
|
|
strength += 1;
|
|
|
|
// 3. Consecutive candles contribution (25%)
|
|
if(current.consecutiveBullish >= 3)
|
|
strength += 2.5;
|
|
else
|
|
if(current.consecutiveBullish >= 2)
|
|
strength += 2;
|
|
else
|
|
if(current.consecutiveBullish >= 1)
|
|
strength += 1;
|
|
else
|
|
if(current.consecutiveBearish >= 3)
|
|
strength -= 2.5;
|
|
else
|
|
if(current.consecutiveBearish >= 2)
|
|
strength -= 2;
|
|
else
|
|
if(current.consecutiveBearish >= 1)
|
|
strength -= 1;
|
|
|
|
// 4. Wick pattern contribution (20%)
|
|
if(current.isBullish && current.wickRatio < 0.3)
|
|
strength += 2; // Strong bullish close
|
|
else
|
|
if(current.isBearish && current.wickRatio > 0.7)
|
|
strength -= 2; // Strong bearish close
|
|
|
|
// 5. Dynamic Pattern Learning contribution
|
|
// Use last closed candle for stable pattern reliability lookup.
|
|
CandleFingerprint fp = GenerateCandleFingerprint(symbol, timeframe, 1);
|
|
string seqHash = GeneratePatternSequenceHash(symbol, timeframe, 1, 3);
|
|
double dynamicReliability = GetPatternReliability(seqHash);
|
|
if(FindDynamicPatternIndex(seqHash) < 0)
|
|
{
|
|
dynamicReliability = GetPatternReliability(fp.patternHash);
|
|
}
|
|
if(current.isBullish || current.isBearish)
|
|
{
|
|
strength += (dynamicReliability - 50.0) / 25.0; // approx -2 .. +2
|
|
}
|
|
|
|
current.strength = MathMax(0, MathMin(10, strength + 5)); // Normalize 0-10
|
|
|
|
// Determine signal type
|
|
if(current.strength >= 8)
|
|
{
|
|
current.signalType = (current.isBullish) ? "STRONG_BULLISH" : "STRONG_BEARISH";
|
|
}
|
|
else
|
|
if(current.strength >= 6)
|
|
{
|
|
current.signalType = (current.isBullish) ? "BULLISH" : "BEARISH";
|
|
}
|
|
else
|
|
if(current.strength >= 4)
|
|
{
|
|
current.signalType = "NEUTRAL";
|
|
}
|
|
else
|
|
{
|
|
current.signalType = (current.isBullish) ? "WEAK_BULLISH" : "WEAK_BEARISH";
|
|
}
|
|
|
|
candleData[tfIndex][0] = current;
|
|
currentSignal[tfIndex] = current.signalType;
|
|
signalStrength[tfIndex] = current.strength;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Pattern Recognition Implementation
|
|
//+------------------------------------------------------------------+
|
|
double NormalizePatternReliability(double baseReliability, double dynamicReliability, double dynWeight)
|
|
{
|
|
return MathMax(0.0, MathMin(100.0, (baseReliability * (1.0 - dynWeight)) + (dynamicReliability * dynWeight)));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetDynamicPatternReliability(string symbol, int timeframe, int barsCount, CandleFingerprint &fp)
|
|
{
|
|
string seqHash = GeneratePatternSequenceHash(symbol, timeframe, 1, barsCount);
|
|
double dynamicReliability = GetPatternReliability(seqHash);
|
|
if(FindDynamicPatternIndex(seqHash) < 0)
|
|
{
|
|
dynamicReliability = GetPatternReliability(fp.patternHash);
|
|
}
|
|
return dynamicReliability;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void AddDetectedPattern(PatternRecognizer &recognizer,
|
|
string patternName,
|
|
int patternType,
|
|
int barsCount,
|
|
double baseReliability,
|
|
double dynamicReliability,
|
|
double dynWeight,
|
|
double volumeConfirmation)
|
|
{
|
|
if(recognizer.patternCount >= ArraySize(recognizer.detectedPatterns))
|
|
return;
|
|
|
|
CandlePattern pattern;
|
|
pattern.patternName = patternName;
|
|
pattern.patternType = patternType;
|
|
pattern.reliability = NormalizePatternReliability(baseReliability, dynamicReliability, dynWeight);
|
|
pattern.consecutiveBars = barsCount;
|
|
pattern.volumeConfirmation = volumeConfirmation;
|
|
pattern.isValid = true;
|
|
|
|
recognizer.detectedPatterns[recognizer.patternCount] = pattern;
|
|
recognizer.patternCount++;
|
|
|
|
for(int i = recognizer.patternCount - 1; i > 0; i--)
|
|
{
|
|
if(recognizer.detectedPatterns[i].reliability > recognizer.detectedPatterns[i - 1].reliability)
|
|
{
|
|
CandlePattern tmp = recognizer.detectedPatterns[i - 1];
|
|
recognizer.detectedPatterns[i - 1] = recognizer.detectedPatterns[i];
|
|
recognizer.detectedPatterns[i] = tmp;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void PatternRecognizer::analyzeAllPatterns(string symbol, int timeframe)
|
|
{
|
|
// AUTO pattern detection based on sequence hash (no fixed templates)
|
|
int barsCount = MathMax(3, MathMin(PatternBars, 50));
|
|
string seqHash = GeneratePatternSequenceHash(symbol, timeframe, 1, barsCount);
|
|
int direction = InferSequencePatternType(symbol, timeframe, 1, barsCount); // 1 bullish, 2 bearish, 0 neutral
|
|
|
|
double avgVolume = calculateAverageVolume(symbol, timeframe, MathMin(50, barsCount));
|
|
double volumeConfirmation = (avgVolume > 0.0) ? ((double)iVolume(symbol, (ENUM_TIMEFRAMES)timeframe, 1) / avgVolume) : 1.0;
|
|
|
|
CandleFingerprint fp = GenerateCandleFingerprint(symbol, timeframe, 1);
|
|
double dynamicReliability = GetPatternReliability(seqHash);
|
|
double dynWeight = 0.30; // generic weight for auto-learned pattern
|
|
|
|
int patternType = (direction == 0) ? (fp.direction == 1 ? 1 : (fp.direction == -1 ? 2 : 3)) : direction;
|
|
AddDetectedPattern(*this, seqHash, patternType, barsCount, 70.0, dynamicReliability, dynWeight, volumeConfirmation);
|
|
}
|
|
|
|
// Dynamic pattern storage
|
|
DynamicPattern dynamicPatterns[];
|
|
int dynamicPatternCount = 0;
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int FindDynamicPatternIndex(string patternHash)
|
|
{
|
|
for(int i = 0; i < dynamicPatternCount; i++)
|
|
{
|
|
if(dynamicPatterns[i].patternHash == patternHash)
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Generate Candle Fingerprint from price data |
|
|
//+------------------------------------------------------------------+
|
|
CandleFingerprint GenerateCandleFingerprint(string symbol, int timeframe, int shift)
|
|
{
|
|
CandleFingerprint fp;
|
|
|
|
double open = iOpen(symbol, (ENUM_TIMEFRAMES)timeframe, shift);
|
|
double high = iHigh(symbol, (ENUM_TIMEFRAMES)timeframe, shift);
|
|
double low = iLow(symbol, (ENUM_TIMEFRAMES)timeframe, shift);
|
|
double close = iClose(symbol, (ENUM_TIMEFRAMES)timeframe, shift);
|
|
|
|
double range = high - low;
|
|
double body = MathAbs(close - open);
|
|
double upperWick = high - MathMax(open, close);
|
|
double lowerWick = MathMin(open, close) - low;
|
|
|
|
if(range <= 0)
|
|
range = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
|
|
// Calculate geometric signature
|
|
fp.bodyPosition = (close > open) ?
|
|
(close - low) / range :
|
|
(open - low) / range;
|
|
fp.bodyRatio = body / range;
|
|
fp.upperWickRatio = upperWick / range;
|
|
fp.lowerWickRatio = lowerWick / range;
|
|
fp.bodyToRange = body / range;
|
|
fp.wickToBody = (upperWick + lowerWick) / MathMax(body, SymbolInfoDouble(symbol, SYMBOL_POINT));
|
|
|
|
// Direction
|
|
if(close > open)
|
|
fp.direction = 1;
|
|
else
|
|
if(close < open)
|
|
fp.direction = -1;
|
|
else
|
|
fp.direction = 0;
|
|
|
|
// Generate hash: B/B/N_body_upper_lower (rounded to 10s)
|
|
string prefix = (fp.direction == 1) ? "B" : (fp.direction == -1) ? "S" : "N";
|
|
int b = (int)(fp.bodyRatio * 100);
|
|
int u = (int)(fp.upperWickRatio * 100);
|
|
int l = (int)(fp.lowerWickRatio * 100);
|
|
int p = (int)(fp.bodyPosition * 100);
|
|
fp.patternHash = prefix + "_" + IntegerToString(b) + "_" + IntegerToString(u) + "_" + IntegerToString(l) + "_" + IntegerToString(p);
|
|
|
|
return fp;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int QuantizeRatioTo10(double value)
|
|
{
|
|
double clamped = MathMax(0.0, MathMin(1.0, value));
|
|
return (int)MathRound(clamped * 10.0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int InferSequencePatternType(string symbol, int timeframe, int startShift, int barsCount)
|
|
{
|
|
int bullish = 0;
|
|
int bearish = 0;
|
|
|
|
for(int i = 0; i < barsCount; i++)
|
|
{
|
|
CandleFingerprint fp = GenerateCandleFingerprint(symbol, timeframe, startShift + i);
|
|
if(fp.direction > 0)
|
|
bullish++;
|
|
else
|
|
if(fp.direction < 0)
|
|
bearish++;
|
|
}
|
|
|
|
if(bullish > bearish)
|
|
return 1;
|
|
if(bearish > bullish)
|
|
return 2;
|
|
return 0;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
string BuildSequenceDescription(string symbol, int timeframe, int startShift, int barsCount)
|
|
{
|
|
int bullish = 0;
|
|
int bearish = 0;
|
|
double avgBody = 0.0;
|
|
double avgUpper = 0.0;
|
|
double avgLower = 0.0;
|
|
|
|
for(int i = 0; i < barsCount; i++)
|
|
{
|
|
CandleFingerprint fp = GenerateCandleFingerprint(symbol, timeframe, startShift + i);
|
|
if(fp.direction > 0)
|
|
bullish++;
|
|
else
|
|
if(fp.direction < 0)
|
|
bearish++;
|
|
avgBody += fp.bodyRatio;
|
|
avgUpper += fp.upperWickRatio;
|
|
avgLower += fp.lowerWickRatio;
|
|
}
|
|
|
|
if(barsCount > 0)
|
|
{
|
|
avgBody /= barsCount;
|
|
avgUpper /= barsCount;
|
|
avgLower /= barsCount;
|
|
}
|
|
|
|
string desc = "SEQ" + IntegerToString(barsCount) + "_";
|
|
if(bullish > bearish)
|
|
desc += "BullishBias_";
|
|
else
|
|
if(bearish > bullish)
|
|
desc += "BearishBias_";
|
|
else
|
|
desc += "NeutralBias_";
|
|
|
|
if(avgBody > 0.55)
|
|
desc += "LargeBody_";
|
|
else
|
|
if(avgBody < 0.20)
|
|
desc += "SmallBody_";
|
|
else
|
|
desc += "MediumBody_";
|
|
|
|
if(avgUpper > 0.35)
|
|
desc += "UpperWicks_";
|
|
if(avgLower > 0.35)
|
|
desc += "LowerWicks_";
|
|
|
|
return desc;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
string GeneratePatternSequenceHash(string symbol, int timeframe, int startShift, int barsCount)
|
|
{
|
|
int maxSeqBars = MathMax(3, MathMin(PatternBars, 50)); // cap hash length for performance
|
|
int safeBars = MathMax(1, MathMin(barsCount, maxSeqBars));
|
|
string hash = "SEQ" + IntegerToString(safeBars);
|
|
|
|
for(int i = 0; i < safeBars; i++)
|
|
{
|
|
CandleFingerprint fp = GenerateCandleFingerprint(symbol, timeframe, startShift + i);
|
|
string d = (fp.direction == 1) ? "B" : ((fp.direction == -1) ? "S" : "N");
|
|
int b = QuantizeRatioTo10(fp.bodyRatio);
|
|
int u = QuantizeRatioTo10(fp.upperWickRatio);
|
|
int l = QuantizeRatioTo10(fp.lowerWickRatio);
|
|
int p = QuantizeRatioTo10(fp.bodyPosition);
|
|
hash += "_" + d + IntegerToString(b) + IntegerToString(u) + IntegerToString(l) + IntegerToString(p);
|
|
}
|
|
|
|
return hash;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int FindOrCreateDynamicPatternMeta(string patternHash, int patternType, int timeframe, string description)
|
|
{
|
|
int existing = FindDynamicPatternIndex(patternHash);
|
|
if(existing >= 0)
|
|
{
|
|
return existing;
|
|
}
|
|
|
|
int idx = dynamicPatternCount;
|
|
ArrayResize(dynamicPatterns, dynamicPatternCount + 1);
|
|
dynamicPatternCount++;
|
|
|
|
dynamicPatterns[idx].patternHash = patternHash;
|
|
dynamicPatterns[idx].occurrenceCount = 0;
|
|
dynamicPatterns[idx].winCount = 0;
|
|
dynamicPatterns[idx].lossCount = 0;
|
|
dynamicPatterns[idx].reliability = 50.0;
|
|
dynamicPatterns[idx].avgProfit = 0.0;
|
|
dynamicPatterns[idx].totalProfit = 0.0;
|
|
dynamicPatterns[idx].firstSeen = TimeCurrent();
|
|
dynamicPatterns[idx].lastSeen = TimeCurrent();
|
|
dynamicPatterns[idx].timeframe = timeframe;
|
|
dynamicPatterns[idx].description = description;
|
|
dynamicPatterns[idx].patternType = patternType;
|
|
|
|
Print("DYNAMIC PATTERN DISCOVERED: ", patternHash, " | ", description);
|
|
return idx;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Find or Create Dynamic Pattern in memory |
|
|
//+------------------------------------------------------------------+
|
|
int FindOrCreateDynamicPattern(string patternHash, CandleFingerprint &fp, int timeframe)
|
|
{
|
|
string desc = "";
|
|
if(fp.bodyRatio < 0.1)
|
|
desc += "SmallBody_";
|
|
else
|
|
if(fp.bodyRatio > 0.6)
|
|
desc += "LargeBody_";
|
|
else
|
|
desc += "MediumBody_";
|
|
|
|
if(fp.upperWickRatio > 0.3)
|
|
desc += "LongUpperWick_";
|
|
if(fp.lowerWickRatio > 0.3)
|
|
desc += "LongLowerWick_";
|
|
|
|
if(fp.bodyPosition > 0.7)
|
|
desc += "TopPosition";
|
|
else
|
|
if(fp.bodyPosition < 0.3)
|
|
desc += "BottomPosition";
|
|
else
|
|
desc += "MidPosition";
|
|
|
|
int patternType = (fp.direction == 1) ? 1 : (fp.direction == -1) ? 2 : 0;
|
|
return FindOrCreateDynamicPatternMeta(patternHash, patternType, timeframe, desc);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Update Dynamic Pattern with Trade Outcome |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateDynamicPattern(string patternHash, double profit, int timeframe)
|
|
{
|
|
// Find pattern
|
|
int idx = -1;
|
|
for(int i = 0; i < dynamicPatternCount; i++)
|
|
{
|
|
if(dynamicPatterns[i].patternHash == patternHash)
|
|
{
|
|
idx = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(idx < 0)
|
|
return; // Pattern not found
|
|
|
|
// Update statistics
|
|
dynamicPatterns[idx].occurrenceCount++;
|
|
dynamicPatterns[idx].lastSeen = TimeCurrent();
|
|
dynamicPatterns[idx].totalProfit += profit;
|
|
|
|
if(profit > 0)
|
|
{
|
|
dynamicPatterns[idx].winCount++;
|
|
}
|
|
else
|
|
if(profit < 0)
|
|
{
|
|
dynamicPatterns[idx].lossCount++;
|
|
}
|
|
|
|
// Calculate reliability
|
|
int total = dynamicPatterns[idx].winCount + dynamicPatterns[idx].lossCount;
|
|
if(total > 0)
|
|
{
|
|
dynamicPatterns[idx].reliability = (double)dynamicPatterns[idx].winCount / total * 100.0;
|
|
}
|
|
|
|
// Update average profit
|
|
if(dynamicPatterns[idx].occurrenceCount > 0)
|
|
{
|
|
dynamicPatterns[idx].avgProfit = dynamicPatterns[idx].totalProfit / dynamicPatterns[idx].occurrenceCount;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Get Pattern Reliability for Decision Making |
|
|
//+------------------------------------------------------------------+
|
|
double GetPatternReliability(string patternHash)
|
|
{
|
|
for(int i = 0; i < dynamicPatternCount; i++)
|
|
{
|
|
if(dynamicPatterns[i].patternHash == patternHash)
|
|
{
|
|
return dynamicPatterns[i].reliability;
|
|
}
|
|
}
|
|
return 50.0; // Unknown pattern = neutral
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Get Best Patterns by Reliability |
|
|
//+------------------------------------------------------------------+
|
|
void GetBestPatterns(int direction, DynamicPattern &bestPatterns[], int maxCount)
|
|
{
|
|
ArrayResize(bestPatterns, 0);
|
|
|
|
// Collect patterns with minimum samples and matching direction
|
|
DynamicPattern candidates[];
|
|
int candidateCount = 0;
|
|
|
|
for(int i = 0; i < dynamicPatternCount; i++)
|
|
{
|
|
if(dynamicPatterns[i].patternType == direction && dynamicPatterns[i].occurrenceCount >= 3)
|
|
{
|
|
ArrayResize(candidates, candidateCount + 1);
|
|
candidates[candidateCount] = dynamicPatterns[i];
|
|
candidateCount++;
|
|
}
|
|
}
|
|
|
|
// Sort by reliability (simple bubble sort)
|
|
for(int i = 0; i < candidateCount - 1; i++)
|
|
{
|
|
for(int j = i + 1; j < candidateCount; j++)
|
|
{
|
|
if(candidates[j].reliability > candidates[i].reliability)
|
|
{
|
|
DynamicPattern temp = candidates[i];
|
|
candidates[i] = candidates[j];
|
|
candidates[j] = temp;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return top N
|
|
int count = MathMin(maxCount, candidateCount);
|
|
ArrayResize(bestPatterns, count);
|
|
for(int i = 0; i < count; i++)
|
|
{
|
|
bestPatterns[i] = candidates[i];
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Print Dynamic Pattern Report |
|
|
//+------------------------------------------------------------------+
|
|
void PrintDynamicPatternReport()
|
|
{
|
|
Print("=== DYNAMIC PATTERN LEARNING REPORT ===");
|
|
Print("Total patterns discovered: ", dynamicPatternCount);
|
|
|
|
if(dynamicPatternCount == 0)
|
|
{
|
|
Print("No patterns learned yet.");
|
|
return;
|
|
}
|
|
|
|
// Find best bullish and bearish patterns
|
|
DynamicPattern bestBullish[];
|
|
DynamicPattern bestBearish[];
|
|
|
|
GetBestPatterns(1, bestBullish, 3);
|
|
GetBestPatterns(2, bestBearish, 3);
|
|
|
|
Print("\n--- TOP BULLISH PATTERNS ---");
|
|
for(int i = 0; i < ArraySize(bestBullish); i++)
|
|
{
|
|
Print("#", i+1, ": ", bestBullish[i].patternHash,
|
|
" | Reliability: ", DoubleToString(bestBullish[i].reliability, 1), "%",
|
|
" | Samples: ", bestBullish[i].occurrenceCount,
|
|
" | AvgProfit: $", DoubleToString(bestBullish[i].avgProfit, 2),
|
|
" | ", bestBullish[i].description);
|
|
}
|
|
|
|
Print("\n--- TOP BEARISH PATTERNS ---");
|
|
for(int i = 0; i < ArraySize(bestBearish); i++)
|
|
{
|
|
Print("#", i+1, ": ", bestBearish[i].patternHash,
|
|
" | Reliability: ", DoubleToString(bestBearish[i].reliability, 1), "%",
|
|
" | Samples: ", bestBearish[i].occurrenceCount,
|
|
" | AvgProfit: $", DoubleToString(bestBearish[i].avgProfit, 2),
|
|
" | ", bestBearish[i].description);
|
|
}
|
|
|
|
Print("=========================================");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void EnhancedSmartEntry::analyzeMultiTimeframe(string sym)
|
|
{
|
|
this.symbol = sym;
|
|
|
|
enhancedTFAnalyzer.analyzeSymbolWithSignals(sym);
|
|
calculateLiquidityZones(sym);
|
|
determineOptimalEntry(sym);
|
|
calculateDynamicTPSL(sym);
|
|
confidenceLevel = calculateConfidence(sym);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void EnhancedSmartEntry::calculateLiquidityZones(string sym)
|
|
{
|
|
double totalLiquidity = 0;
|
|
int count = 0;
|
|
|
|
for(int i = 0; i < 5; i++)
|
|
{
|
|
if(enhancedTFAnalyzer.tfData[i].liquidityZone > 0)
|
|
{
|
|
totalLiquidity += enhancedTFAnalyzer.tfData[i].liquidityZone;
|
|
count++;
|
|
}
|
|
}
|
|
|
|
double point = SymbolInfoDouble(sym, SYMBOL_POINT);
|
|
if(point <= 0)
|
|
point = Point();
|
|
|
|
if(count > 0)
|
|
{
|
|
double avgLiquidity = totalLiquidity / count;
|
|
liquidityZoneLow = avgLiquidity - (point * 20);
|
|
liquidityZoneHigh = avgLiquidity + (point * 20);
|
|
}
|
|
else
|
|
{
|
|
double current = SymbolInfoDouble(sym, SYMBOL_BID);
|
|
liquidityZoneLow = current - (point * 10);
|
|
liquidityZoneHigh = current + (point * 10);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void EnhancedSmartEntry::determineOptimalEntry(string sym)
|
|
{
|
|
double currentPrice = SymbolInfoDouble(sym, SYMBOL_BID);
|
|
double bid = SymbolInfoDouble(sym, SYMBOL_BID);
|
|
double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
|
|
|
|
// Determine direction from dominant signal for proper entry logic
|
|
bool isBullish = (enhancedTFAnalyzer.dominantSignal == "BULLISH");
|
|
bool isBearish = (enhancedTFAnalyzer.dominantSignal == "BEARISH");
|
|
|
|
if(currentPrice >= liquidityZoneLow && currentPrice <= liquidityZoneHigh)
|
|
{
|
|
optimalEntryPrice = currentPrice;
|
|
entryReason = "Current price in liquidity zone";
|
|
}
|
|
else
|
|
if(isBullish)
|
|
{
|
|
// For BUY: want to enter at support (low of liquidity zone), not resistance
|
|
if(currentPrice > liquidityZoneHigh)
|
|
{
|
|
// Price is above liquidity zone - wait for dip to low
|
|
optimalEntryPrice = liquidityZoneLow;
|
|
entryReason = "Waiting for dip to support zone for BUY";
|
|
}
|
|
else
|
|
{
|
|
// Price below liquidity zone - enter at current or at zone low
|
|
optimalEntryPrice = liquidityZoneLow;
|
|
entryReason = "Entry at support zone (deep) for BUY";
|
|
}
|
|
}
|
|
else
|
|
if(isBearish)
|
|
{
|
|
// For SELL: want to enter at resistance (high of liquidity zone), not support
|
|
if(currentPrice < liquidityZoneLow)
|
|
{
|
|
// Price is below liquidity zone - wait for bounce to high
|
|
optimalEntryPrice = liquidityZoneHigh;
|
|
entryReason = "Waiting for bounce to resistance zone for SELL";
|
|
}
|
|
else
|
|
{
|
|
// Price above liquidity zone - enter at current or at zone high
|
|
optimalEntryPrice = liquidityZoneHigh;
|
|
entryReason = "Entry at resistance zone (peak) for SELL";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Neutral signal - use midpoint
|
|
optimalEntryPrice = (liquidityZoneLow + liquidityZoneHigh) / 2.0;
|
|
entryReason = "Neutral - entry at zone midpoint";
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void EnhancedSmartEntry::calculateDynamicTPSL(string sym)
|
|
{
|
|
double currentPrice = optimalEntryPrice;
|
|
double atr = GetATRValue(sym, PERIOD_H1, 14, 0);
|
|
if(atr <= 0)
|
|
atr = GetATRValue(sym, PERIOD_M15, 14, 0);
|
|
|
|
double h4Resistance = enhancedTFAnalyzer.tfData[3].resistanceLevels[0];
|
|
double h4Support = enhancedTFAnalyzer.tfData[3].supportLevels[0];
|
|
|
|
if(enhancedTFAnalyzer.dominantSignal == "BULLISH")
|
|
{
|
|
dynamicTP = (h4Resistance > currentPrice) ? h4Resistance : (currentPrice + (atr * 2));
|
|
dynamicSL = (h4Support > 0) ? MathMax(h4Support, currentPrice - (atr * 1.5)) : (currentPrice - (atr * 1.5));
|
|
}
|
|
else
|
|
if(enhancedTFAnalyzer.dominantSignal == "BEARISH")
|
|
{
|
|
dynamicTP = (h4Support > 0 && h4Support < currentPrice) ? h4Support : (currentPrice - (atr * 2));
|
|
dynamicSL = (h4Resistance > 0) ? MathMin(h4Resistance, currentPrice + (atr * 1.5)) : (currentPrice + (atr * 1.5));
|
|
}
|
|
else
|
|
{
|
|
dynamicTP = currentPrice + (atr * 1.5);
|
|
dynamicSL = currentPrice - (atr * 1.5);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double EnhancedSmartEntry::calculateConfidence(string sym)
|
|
{
|
|
double confidence = 0;
|
|
|
|
confidence += enhancedTFAnalyzer.signalConfidence * 0.4;
|
|
|
|
if(enhancedTFAnalyzer.patterns.patternCount > 0)
|
|
{
|
|
double patternReliability = enhancedTFAnalyzer.patterns.detectedPatterns[0].reliability;
|
|
confidence += patternReliability * 0.2;
|
|
}
|
|
|
|
double avgVolume = 0;
|
|
for(int i = 0; i < 5; i++)
|
|
{
|
|
avgVolume += enhancedTFAnalyzer.tfData[i].volume;
|
|
}
|
|
avgVolume /= 5.0;
|
|
if(avgVolume > 1.2)
|
|
confidence += 20;
|
|
else
|
|
if(avgVolume > 1.0)
|
|
confidence += 15;
|
|
else
|
|
if(avgVolume > 0.8)
|
|
confidence += 10;
|
|
|
|
int symbolIndex = getSymbolIndex(sym);
|
|
if(symbolIndex >= 0)
|
|
{
|
|
confidence += enhancedSymbolMemory[symbolIndex].winRate * 20;
|
|
}
|
|
|
|
return MathMin(100, confidence);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void EnhancedSmartEntry::generateEntryReport(string sym)
|
|
{
|
|
string report = "Symbol: " + sym + "\n";
|
|
report += "Entry: " + DoubleToString(optimalEntryPrice, 5) + " (" + entryReason + ")\n";
|
|
report += "TP: " + DoubleToString(dynamicTP, 5) + "\n";
|
|
report += "SL: " + DoubleToString(dynamicSL, 5) + "\n";
|
|
report += "Confidence: " + DoubleToString(confidenceLevel, 1) + "%\n";
|
|
|
|
int openTrades = PositionsTotal();
|
|
double baseRisk = 0.5;
|
|
if(openTrades > 5)
|
|
baseRisk = 0.3;
|
|
else
|
|
if(openTrades > 3)
|
|
baseRisk = 0.4;
|
|
riskPercent = baseRisk;
|
|
|
|
Print("=== ENHANCED SMART ENTRY ===");
|
|
Print(report);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Enhanced Multi-TF Analyzer Implementation
|
|
//+------------------------------------------------------------------+
|
|
void EnhancedMultiTFAnalyzer::analyzeSymbolWithSignals(string symbol)
|
|
{
|
|
// Reset once per symbol, then accumulate patterns across all timeframes
|
|
patterns.patternCount = 0;
|
|
for(int i = 0; i < 5; i++)
|
|
{
|
|
tfLiquidityHasData[i] = false;
|
|
tfLiquidityLevel[i] = 0.0;
|
|
tfLiquidityDistance[i] = 0.0;
|
|
}
|
|
|
|
// Analyze all timeframes for this symbol
|
|
for(int i = 0; i < 5; i++)
|
|
{
|
|
int timeframe = TIMEFRAMES[i];
|
|
|
|
// Update timeframe data
|
|
tfData[i].timeframe = timeframe;
|
|
tfData[i].trendStrength = calculateTrendStrength(symbol, timeframe);
|
|
tfData[i].volatility = calculateVolatility(symbol, timeframe);
|
|
double avgTfVolume = calculateAverageVolume(symbol, timeframe, 20);
|
|
tfData[i].volume = (avgTfVolume > 0.0) ? ((double)iVolume(symbol, (ENUM_TIMEFRAMES)timeframe, 0) / avgTfVolume) : 1.0;
|
|
tfData[i].trendScore = MathAbs(tfData[i].trendStrength);
|
|
tfData[i].lastUpdate = TimeCurrent();
|
|
|
|
// Nearest liquidity zone per TF (volume profile)
|
|
double zones[];
|
|
CalculateVolumeProfileForSymbol(symbol, (ENUM_TIMEFRAMES)timeframe, zones);
|
|
double midPrice = (SymbolInfoDouble(symbol, SYMBOL_ASK) + SymbolInfoDouble(symbol, SYMBOL_BID)) / 2.0;
|
|
double pointVal = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
double bestZone = 0.0;
|
|
double bestDist = DBL_MAX;
|
|
for(int z = 0; z < ArraySize(zones); z++)
|
|
{
|
|
double d = MathAbs(zones[z] - midPrice);
|
|
if(d < bestDist)
|
|
{
|
|
bestDist = d;
|
|
bestZone = zones[z];
|
|
}
|
|
}
|
|
if(bestZone > 0.0 && bestDist < DBL_MAX)
|
|
{
|
|
tfLiquidityHasData[i] = true;
|
|
tfLiquidityLevel[i] = bestZone;
|
|
tfLiquidityDistance[i] = (pointVal > 0.0) ? (bestDist / pointVal) : 0.0;
|
|
tfData[i].liquidityZone = bestZone;
|
|
}
|
|
else
|
|
{
|
|
tfData[i].liquidityZone = 0.0;
|
|
}
|
|
|
|
// Detect candle patterns
|
|
bbSignals.detectCandlePatterns(symbol, timeframe);
|
|
patterns.analyzeAllPatterns(symbol, timeframe);
|
|
|
|
// Count candles
|
|
int bullishCount = 0, bearishCount = 0;
|
|
for(int j = 0; j < 20; j++)
|
|
{
|
|
double close = iClose(symbol, (ENUM_TIMEFRAMES)timeframe, j);
|
|
double open = iOpen(symbol, (ENUM_TIMEFRAMES)timeframe, j);
|
|
if(close > open)
|
|
bullishCount++;
|
|
else
|
|
if(close < open)
|
|
bearishCount++;
|
|
}
|
|
tfData[i].bullishCandles = bullishCount;
|
|
tfData[i].bearishCandles = bearishCount;
|
|
|
|
// Calculate support/resistance levels
|
|
calculateSupportResistance(symbol, timeframe, i);
|
|
}
|
|
|
|
// Validate signal confluence
|
|
validateSignalConfluence(symbol);
|
|
|
|
// Check pattern confirmation
|
|
checkPatternConfirmation(symbol);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void EnhancedMultiTFAnalyzer::validateSignalConfluence(string symbol)
|
|
{
|
|
bullishTimeframes = 0;
|
|
bearishTimeframes = 0;
|
|
overallBullishStrength = 0;
|
|
overallBearishStrength = 0;
|
|
|
|
int consideredTF = 0;
|
|
double currentVolRatio = GetCurrentVolumeRatioForSymbol(symbol, (ENUM_TIMEFRAMES)Period());
|
|
|
|
// Count bullish/bearish signals across selected timeframes.
|
|
// Auto-protection must not disable analysis; it should only tune risk sizing.
|
|
for(int i = 0; i < 5; i++)
|
|
{
|
|
string signal = bbSignals.currentSignal[i];
|
|
double strength = bbSignals.signalStrength[i];
|
|
|
|
consideredTF++;
|
|
if(StringFind(signal, "BULLISH") >= 0)
|
|
{
|
|
bullishTimeframes++;
|
|
overallBullishStrength += strength;
|
|
}
|
|
else
|
|
if(StringFind(signal, "BEARISH") >= 0)
|
|
{
|
|
bearishTimeframes++;
|
|
overallBearishStrength += strength;
|
|
}
|
|
}
|
|
|
|
// Calculate average strengths
|
|
if(bullishTimeframes > 0)
|
|
overallBullishStrength /= bullishTimeframes;
|
|
if(bearishTimeframes > 0)
|
|
overallBearishStrength /= bearishTimeframes;
|
|
|
|
if(consideredTF <= 0)
|
|
{
|
|
dominantSignal = "NEUTRAL";
|
|
bullishEntryConfirmed = false;
|
|
bearishEntryConfirmed = false;
|
|
signalConfidence = 0;
|
|
return;
|
|
}
|
|
|
|
int minTfConfirm = 3;
|
|
|
|
// Determine dominant signal
|
|
if(bullishTimeframes >= minTfConfirm && overallBullishStrength > overallBearishStrength)
|
|
{
|
|
dominantSignal = "BULLISH";
|
|
bullishEntryConfirmed = true;
|
|
bearishEntryConfirmed = false;
|
|
signalConfidence = (bullishTimeframes / (double)consideredTF) * (overallBullishStrength / 10.0) * 100;
|
|
}
|
|
else
|
|
if(bearishTimeframes >= minTfConfirm && overallBearishStrength > overallBullishStrength)
|
|
{
|
|
dominantSignal = "BEARISH";
|
|
bearishEntryConfirmed = true;
|
|
bullishEntryConfirmed = false;
|
|
signalConfidence = (bearishTimeframes / (double)consideredTF) * (overallBearishStrength / 10.0) * 100;
|
|
}
|
|
else
|
|
{
|
|
dominantSignal = "NEUTRAL";
|
|
bullishEntryConfirmed = false;
|
|
bearishEntryConfirmed = false;
|
|
signalConfidence = 0;
|
|
}
|
|
|
|
int direction = (dominantSignal == "BULLISH") ? 1 : ((dominantSignal == "BEARISH") ? -1 : 0);
|
|
if(direction != 0)
|
|
{
|
|
int bars = (direction == 1) ? GetConsecutiveBarsMultiTFForSymbol(symbol, false)
|
|
: GetConsecutiveBarsMultiTFForSymbol(symbol, true);
|
|
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
double memConf = GetMemoryBasedConfidenceForSymbol(symbol, direction, bars, currentVolRatio);
|
|
signalConfidence *= ClampValue(memConf + 0.20, 0.40, 1.10);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void EnhancedMultiTFAnalyzer::checkPatternConfirmation(string symbol)
|
|
{
|
|
// 1) Static pattern confirmation (rule-based)
|
|
if(patterns.patternCount > 0)
|
|
{
|
|
for(int j = 0; j < patterns.patternCount; j++)
|
|
{
|
|
CandlePattern pattern = patterns.detectedPatterns[j];
|
|
|
|
if(dominantSignal == "BULLISH" && pattern.patternType == 1)
|
|
{
|
|
signalConfidence += pattern.reliability * 0.1;
|
|
}
|
|
else
|
|
if(dominantSignal == "BEARISH" && pattern.patternType == 2)
|
|
{
|
|
signalConfidence += pattern.reliability * 0.1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2) Dynamic Pattern Learning confirmation (adaptive)
|
|
double dynamicReliabilitySum = 0.0;
|
|
int dynamicSampleCount = 0;
|
|
|
|
for(int i = 0; i < 5; i++)
|
|
{
|
|
int timeframe = TIMEFRAMES[i];
|
|
CandleFingerprint fp = GenerateCandleFingerprint(symbol, timeframe, 1); // last closed candle
|
|
|
|
bool matchesDominantDirection = (dominantSignal == "BULLISH" && fp.direction == 1) ||
|
|
(dominantSignal == "BEARISH" && fp.direction == -1);
|
|
if(!matchesDominantDirection)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string seqHash = GeneratePatternSequenceHash(symbol, timeframe, 1, 3);
|
|
double reliability = GetPatternReliability(seqHash);
|
|
if(FindDynamicPatternIndex(seqHash) < 0)
|
|
{
|
|
reliability = GetPatternReliability(fp.patternHash);
|
|
}
|
|
dynamicReliabilitySum += reliability;
|
|
dynamicSampleCount++;
|
|
}
|
|
|
|
if(dynamicSampleCount > 0)
|
|
{
|
|
double avgDynamicReliability = dynamicReliabilitySum / dynamicSampleCount;
|
|
|
|
// Neutral is 50; above adds confidence, below reduces it.
|
|
signalConfidence += (avgDynamicReliability - 50.0) * 0.20;
|
|
|
|
// If dynamic reliability is weak, cancel confirmation.
|
|
if(avgDynamicReliability < 45.0)
|
|
{
|
|
if(dominantSignal == "BULLISH")
|
|
bullishEntryConfirmed = false;
|
|
else
|
|
if(dominantSignal == "BEARISH")
|
|
bearishEntryConfirmed = false;
|
|
}
|
|
}
|
|
|
|
// Keep confidence bounded
|
|
signalConfidence = MathMax(0.0, MathMin(100.0, signalConfidence));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool EnhancedMultiTFAnalyzer::shouldEnterLong(string symbol)
|
|
{
|
|
double minConfidence = 70.0;
|
|
double minStrength = 6.0;
|
|
int minTf = 3;
|
|
|
|
return bullishEntryConfirmed &&
|
|
signalConfidence > minConfidence &&
|
|
overallBullishStrength > minStrength &&
|
|
bullishTimeframes >= minTf;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool EnhancedMultiTFAnalyzer::shouldEnterShort(string symbol)
|
|
{
|
|
double minConfidence = 70.0;
|
|
double minStrength = 6.0;
|
|
int minTf = 3;
|
|
|
|
return bearishEntryConfirmed &&
|
|
signalConfidence > minConfidence &&
|
|
overallBearishStrength > minStrength &&
|
|
bearishTimeframes >= minTf;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
string EnhancedMultiTFAnalyzer::generateSignalReport(string symbol)
|
|
{
|
|
string report = "";
|
|
|
|
// Generate trend description
|
|
if(tfData[4].trendStrength > 7)
|
|
report += "Strong Uptrend H4";
|
|
else
|
|
if(tfData[4].trendStrength > 3)
|
|
report += "Uptrend H4";
|
|
else
|
|
if(tfData[4].trendStrength < -7)
|
|
report += "Strong Downtrend H4";
|
|
else
|
|
if(tfData[4].trendStrength < -3)
|
|
report += "Downtrend H4";
|
|
else
|
|
report += "Range H4";
|
|
|
|
if(tfData[3].trendStrength > 5)
|
|
report += "+H1";
|
|
if(tfData[2].trendStrength > 5)
|
|
report += "+M15";
|
|
if(tfData[1].trendStrength > 5)
|
|
report += "+M5";
|
|
|
|
// Add volume info
|
|
if(tfData[3].volume > 1.5)
|
|
report += " - Volume: High";
|
|
else
|
|
if(tfData[3].volume > 1.2)
|
|
report += " - Volume: Above Avg";
|
|
|
|
// Add pattern info
|
|
if(patterns.patternCount > 0)
|
|
{
|
|
report += " - Pattern: " + patterns.detectedPatterns[0].patternName;
|
|
}
|
|
|
|
return report;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void calculateSupportResistance(string symbol, int timeframe, int tfIndex)
|
|
{
|
|
int highCount = 0, lowCount = 0;
|
|
|
|
if(tfIndex < 0 || tfIndex >= 5)
|
|
return;
|
|
|
|
// Reset levels
|
|
ArrayInitialize(globalTFData[tfIndex].supportLevels, 0);
|
|
ArrayInitialize(globalTFData[tfIndex].resistanceLevels, 0);
|
|
|
|
ENUM_TIMEFRAMES tf = (ENUM_TIMEFRAMES)timeframe;
|
|
|
|
for(int i = 1; i < 20; i++)
|
|
{
|
|
double high = iHigh(symbol, tf, i);
|
|
double low = iLow(symbol, tf, i);
|
|
|
|
// Find swing highs
|
|
if(i+1 < 20 && high > iHigh(symbol, tf, i-1) && high > iHigh(symbol, tf, i+1))
|
|
{
|
|
if(highCount < 5)
|
|
{
|
|
globalTFData[tfIndex].resistanceLevels[highCount] = high;
|
|
highCount++;
|
|
}
|
|
}
|
|
|
|
// Find swing lows
|
|
if(i+1 < 20 && low < iLow(symbol, tf, i-1) && low < iLow(symbol, tf, i+1))
|
|
{
|
|
if(lowCount < 5)
|
|
{
|
|
globalTFData[tfIndex].supportLevels[lowCount] = low;
|
|
lowCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//| Enhanced Trade Execution Functions
|
|
//+------------------------------------------------------------------+
|
|
void ExecuteBullishEntry(string symbol)
|
|
{
|
|
if(IsSymbolAutoCloseBlocked(symbol, GetCurrentTimeWithOffset()))
|
|
{
|
|
Print("AUTO CLOSE GATE [", symbol, "] - BUY blocked until next session window.");
|
|
return;
|
|
}
|
|
|
|
// Get enhanced entry data
|
|
smartEntry.analyzeMultiTimeframe(symbol);
|
|
|
|
// Validate with bullish signals AND strict multi-timeframe confluence
|
|
if(!enhancedTFAnalyzer.bullishEntryConfirmed)
|
|
return;
|
|
|
|
// Enforce strict confluence: must meet minimum requirements
|
|
if(!enhancedTFAnalyzer.shouldEnterLong(symbol))
|
|
{
|
|
Print("BULLISH ENTRY BLOCKED [", symbol, "] | Failed strict confluence validation");
|
|
Print(" Confidence: ", DoubleToString(enhancedTFAnalyzer.signalConfidence, 1), "% (min 70%)");
|
|
Print(" Strength: ", DoubleToString(enhancedTFAnalyzer.overallBullishStrength, 1), " (min 6.0)");
|
|
Print(" Timeframes: ", enhancedTFAnalyzer.bullishTimeframes, " (min 3)");
|
|
return;
|
|
}
|
|
|
|
// Calculate entry parameters
|
|
double entryPrice = smartEntry.optimalEntryPrice;
|
|
double tp = smartEntry.dynamicTP;
|
|
double sl = smartEntry.dynamicSL;
|
|
double tpMultiplier = 1.0;
|
|
double slMultiplier = 1.0;
|
|
string entryProfile = "NORMAL";
|
|
int entryScore = 5;
|
|
double symbolBid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
double symbolAsk = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
SelectEntryProfileForSymbol(symbol, 1, symbolBid, symbolAsk, tpMultiplier, slMultiplier, entryProfile, entryScore);
|
|
|
|
if(UseEntryProfileTP_SL)
|
|
{
|
|
double baseTPDistance = MathAbs(tp - entryPrice) * tpMultiplier;
|
|
double baseSLDistance = MathAbs(entryPrice - sl) * slMultiplier;
|
|
if(baseTPDistance <= 0.0)
|
|
baseTPDistance = ConvertInputDistanceToPriceForSymbol(symbol, TakeProfit) * tpMultiplier;
|
|
if(baseSLDistance <= 0.0)
|
|
baseSLDistance = ConvertInputDistanceToPriceForSymbol(symbol, StopLoss) * slMultiplier;
|
|
|
|
double tpDistance = baseTPDistance;
|
|
double slDistance = baseSLDistance;
|
|
AdjustEntryDistancesSmart(symbol, 1, entryScore, symbolBid, symbolAsk, baseTPDistance, baseSLDistance, tpDistance, slDistance);
|
|
|
|
tp = entryPrice + tpDistance;
|
|
sl = entryPrice - slDistance;
|
|
}
|
|
|
|
// For market execution, validate stops against current live quote on the traded symbol.
|
|
double tpDistanceFinal = MathAbs(tp - entryPrice);
|
|
double slDistanceFinal = MathAbs(entryPrice - sl);
|
|
if(tpDistanceFinal <= 0.0)
|
|
tpDistanceFinal = ConvertInputDistanceToPriceForSymbol(symbol, TakeProfit);
|
|
if(slDistanceFinal <= 0.0)
|
|
slDistanceFinal = ConvertInputDistanceToPriceForSymbol(symbol, StopLoss);
|
|
|
|
symbolBid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
symbolAsk = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
tp = EnforceMinStopDistanceForSymbol(symbol, symbolBid, symbolBid + tpDistanceFinal, true, false);
|
|
sl = EnforceMinStopDistanceForSymbol(symbol, symbolBid, symbolBid - slDistanceFinal, true, true);
|
|
if(!CheckStopLossTakeProfitForSymbol(symbol, ORDER_TYPE_BUY, sl, tp))
|
|
{
|
|
Print("BULLISH ENTRY BLOCKED [", symbol, "] | Invalid stops after broker validation | Bid=", DoubleToString(symbolBid, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)),
|
|
" SL=", DoubleToString(sl, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)),
|
|
" TP=", DoubleToString(tp, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)));
|
|
EmitOrderBlockedSound();
|
|
return;
|
|
}
|
|
|
|
// Market orders should use price=0.0; broker fills at current market.
|
|
entryPrice = 0.0;
|
|
double lotSize = CalculateLotSizeForSymbol(symbol);
|
|
if(!CheckMoneyForTrade(symbol, lotSize, ORDER_TYPE_BUY))
|
|
{
|
|
EmitOrderBlockedSound();
|
|
return;
|
|
}
|
|
string volumeDesc = "";
|
|
if(!CheckVolumeValueForSymbol(symbol, lotSize, volumeDesc))
|
|
{
|
|
Print("BULLISH ENTRY BLOCKED [", symbol, "] | Volume check failed: ", volumeDesc);
|
|
EmitOrderBlockedSound();
|
|
return;
|
|
}
|
|
double allowedVolume = NewOrderAllowedVolume(symbol);
|
|
if(allowedVolume <= 0.0 || lotSize > allowedVolume)
|
|
{
|
|
Print("BULLISH ENTRY BLOCKED [", symbol, "] | Volume limit exceeded: lots=", lotSize, " allowed=", allowedVolume);
|
|
EmitOrderBlockedSound();
|
|
return;
|
|
}
|
|
|
|
// Execute BUY trade
|
|
if(EnableVoicePrompts)
|
|
SpeakZira("Buy signal on " + symbol + ". Sending order.");
|
|
bool executed = trade.Buy(lotSize, symbol, entryPrice, sl, tp, "Bullish Entry - Multi-TF Confluence");
|
|
double executedEntryPrice = trade.ResultPrice();
|
|
if(executedEntryPrice <= 0.0)
|
|
executedEntryPrice = symbolAsk;
|
|
if(executed)
|
|
{
|
|
EmitOrderExecutedSound(1);
|
|
if(TelegramSendEntries)
|
|
{
|
|
string msg = BuildTelegramEntryMessage(symbol, "BUY", executedEntryPrice, sl, tp, entryScore, marketState.currentRegime);
|
|
SendTelegramMessage(msg, lastTelegramEntryTime, TelegramCooldownSeconds);
|
|
}
|
|
Print("BULLISH ENTRY EXECUTED: ", symbol);
|
|
Print("Entry: ", DoubleToString(executedEntryPrice, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)));
|
|
Print("TP: ", DoubleToString(tp, 5));
|
|
Print("SL: ", DoubleToString(sl, 5));
|
|
Print("Profile: ", entryProfile, " | Score: ", entryScore, "/10 | TPx: ", DoubleToString(tpMultiplier, 2), " | SLx: ", DoubleToString(slMultiplier, 2));
|
|
Print("Confidence: ", DoubleToString(enhancedTFAnalyzer.signalConfidence, 1), "%");
|
|
}
|
|
else
|
|
{
|
|
Print("BULLISH ENTRY FAILED [", symbol, "] | retcode=", trade.ResultRetcode(), " | ", trade.ResultRetcodeDescription());
|
|
EmitOrderBlockedSound();
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void ExecuteBearishEntry(string symbol)
|
|
{
|
|
if(IsSymbolAutoCloseBlocked(symbol, GetCurrentTimeWithOffset()))
|
|
{
|
|
Print("AUTO CLOSE GATE [", symbol, "] - SELL blocked until next session window.");
|
|
return;
|
|
}
|
|
|
|
// Get enhanced entry data
|
|
smartEntry.analyzeMultiTimeframe(symbol);
|
|
|
|
// Validate with bearish signals AND strict multi-timeframe confluence
|
|
if(!enhancedTFAnalyzer.bearishEntryConfirmed)
|
|
return;
|
|
|
|
// Enforce strict confluence: must meet minimum requirements
|
|
if(!enhancedTFAnalyzer.shouldEnterShort(symbol))
|
|
{
|
|
Print("BEARISH ENTRY BLOCKED [", symbol, "] | Failed strict confluence validation");
|
|
Print(" Confidence: ", DoubleToString(enhancedTFAnalyzer.signalConfidence, 1), "% (min 70%)");
|
|
Print(" Strength: ", DoubleToString(enhancedTFAnalyzer.overallBearishStrength, 1), " (min 6.0)");
|
|
Print(" Timeframes: ", enhancedTFAnalyzer.bearishTimeframes, " (min 3)");
|
|
return;
|
|
}
|
|
|
|
// Calculate entry parameters
|
|
double entryPrice = smartEntry.optimalEntryPrice;
|
|
double tp = smartEntry.dynamicTP;
|
|
double sl = smartEntry.dynamicSL;
|
|
double tpMultiplier = 1.0;
|
|
double slMultiplier = 1.0;
|
|
string entryProfile = "NORMAL";
|
|
int entryScore = 5;
|
|
double symbolBid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
double symbolAsk = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
SelectEntryProfileForSymbol(symbol, -1, symbolBid, symbolAsk, tpMultiplier, slMultiplier, entryProfile, entryScore);
|
|
|
|
if(UseEntryProfileTP_SL)
|
|
{
|
|
double baseTPDistance = MathAbs(entryPrice - tp) * tpMultiplier;
|
|
double baseSLDistance = MathAbs(sl - entryPrice) * slMultiplier;
|
|
if(baseTPDistance <= 0.0)
|
|
baseTPDistance = ConvertInputDistanceToPriceForSymbol(symbol, TakeProfit) * tpMultiplier;
|
|
if(baseSLDistance <= 0.0)
|
|
baseSLDistance = ConvertInputDistanceToPriceForSymbol(symbol, StopLoss) * slMultiplier;
|
|
|
|
double tpDistance = baseTPDistance;
|
|
double slDistance = baseSLDistance;
|
|
AdjustEntryDistancesSmart(symbol, -1, entryScore, symbolBid, symbolAsk, baseTPDistance, baseSLDistance, tpDistance, slDistance);
|
|
|
|
tp = entryPrice - tpDistance;
|
|
sl = entryPrice + slDistance;
|
|
}
|
|
|
|
// For market execution, validate stops against current live quote on the traded symbol.
|
|
double tpDistanceFinal = MathAbs(entryPrice - tp);
|
|
double slDistanceFinal = MathAbs(sl - entryPrice);
|
|
if(tpDistanceFinal <= 0.0)
|
|
tpDistanceFinal = ConvertInputDistanceToPriceForSymbol(symbol, TakeProfit);
|
|
if(slDistanceFinal <= 0.0)
|
|
slDistanceFinal = ConvertInputDistanceToPriceForSymbol(symbol, StopLoss);
|
|
|
|
symbolBid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
symbolAsk = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
tp = EnforceMinStopDistanceForSymbol(symbol, symbolAsk, symbolAsk - tpDistanceFinal, false, false);
|
|
sl = EnforceMinStopDistanceForSymbol(symbol, symbolAsk, symbolAsk + slDistanceFinal, false, true);
|
|
if(!CheckStopLossTakeProfitForSymbol(symbol, ORDER_TYPE_SELL, sl, tp))
|
|
{
|
|
Print("BEARISH ENTRY BLOCKED [", symbol, "] | Invalid stops after broker validation | Ask=", DoubleToString(symbolAsk, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)),
|
|
" SL=", DoubleToString(sl, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)),
|
|
" TP=", DoubleToString(tp, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)));
|
|
EmitOrderBlockedSound();
|
|
return;
|
|
}
|
|
|
|
// Market orders should use price=0.0; broker fills at current market.
|
|
entryPrice = 0.0;
|
|
double lotSize = CalculateLotSizeForSymbol(symbol);
|
|
if(!CheckMoneyForTrade(symbol, lotSize, ORDER_TYPE_SELL))
|
|
{
|
|
EmitOrderBlockedSound();
|
|
return;
|
|
}
|
|
string volumeDesc = "";
|
|
if(!CheckVolumeValueForSymbol(symbol, lotSize, volumeDesc))
|
|
{
|
|
Print("BEARISH ENTRY BLOCKED [", symbol, "] | Volume check failed: ", volumeDesc);
|
|
EmitOrderBlockedSound();
|
|
return;
|
|
}
|
|
double allowedVolume = NewOrderAllowedVolume(symbol);
|
|
if(allowedVolume <= 0.0 || lotSize > allowedVolume)
|
|
{
|
|
Print("BEARISH ENTRY BLOCKED [", symbol, "] | Volume limit exceeded: lots=", lotSize, " allowed=", allowedVolume);
|
|
EmitOrderBlockedSound();
|
|
return;
|
|
}
|
|
|
|
// Execute SELL trade
|
|
if(EnableVoicePrompts)
|
|
SpeakZira("Sell signal on " + symbol + ". Sending order.");
|
|
bool executed = trade.Sell(lotSize, symbol, entryPrice, sl, tp, "Bearish Entry - Multi-TF Confluence");
|
|
double executedEntryPrice = trade.ResultPrice();
|
|
if(executedEntryPrice <= 0.0)
|
|
executedEntryPrice = symbolBid;
|
|
if(executed)
|
|
{
|
|
EmitOrderExecutedSound(-1);
|
|
if(TelegramSendEntries)
|
|
{
|
|
string msg = BuildTelegramEntryMessage(symbol, "SELL", executedEntryPrice, sl, tp, entryScore, marketState.currentRegime);
|
|
SendTelegramMessage(msg, lastTelegramEntryTime, TelegramCooldownSeconds);
|
|
}
|
|
Print("BEARISH ENTRY EXECUTED: ", symbol);
|
|
Print("Entry: ", DoubleToString(executedEntryPrice, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)));
|
|
Print("TP: ", DoubleToString(tp, 5));
|
|
Print("SL: ", DoubleToString(sl, 5));
|
|
Print("Profile: ", entryProfile, " | Score: ", entryScore, "/10 | TPx: ", DoubleToString(tpMultiplier, 2), " | SLx: ", DoubleToString(slMultiplier, 2));
|
|
Print("Confidence: ", DoubleToString(enhancedTFAnalyzer.signalConfidence, 1), "%");
|
|
}
|
|
else
|
|
{
|
|
Print("BEARISH ENTRY FAILED [", symbol, "] | retcode=", trade.ResultRetcode(), " | ", trade.ResultRetcodeDescription());
|
|
EmitOrderBlockedSound();
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int FindPositionEntryContextIndex(ulong positionId)
|
|
{
|
|
for(int i = 0; i < ArraySize(positionEntryMemory); i++)
|
|
{
|
|
if(positionEntryMemory[i].positionId == positionId)
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void SavePositionEntryContext(ulong positionId, int direction, double entryPrice, int consecutiveBars, double volumeRatio, double spreadAtEntry, datetime entryTime,
|
|
int decisionScore, int decisionAction, int decisionSetupId, int entryTypeAtEntry, ulong sourceOrderTicketAtEntry, int sourceOrderTypeAtEntry,
|
|
int barsBucket, int volumeBucket, int entryTimeframe, string regimeAtEntry, string patternHash,
|
|
double liquidityLevel, double liquidityDistancePts, bool liquidityUsed)
|
|
{
|
|
if(positionId == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int index = FindPositionEntryContextIndex(positionId);
|
|
bool createdNewContext = false;
|
|
if(index == -1)
|
|
{
|
|
index = ArraySize(positionEntryMemory);
|
|
ArrayResize(positionEntryMemory, index + 1);
|
|
createdNewContext = true;
|
|
}
|
|
|
|
positionEntryMemory[index].positionId = positionId;
|
|
positionEntryMemory[index].direction = direction;
|
|
positionEntryMemory[index].entryTime = entryTime;
|
|
positionEntryMemory[index].entryPrice = entryPrice;
|
|
positionEntryMemory[index].consecutiveBars = consecutiveBars;
|
|
positionEntryMemory[index].volumeRatio = volumeRatio;
|
|
positionEntryMemory[index].spreadAtEntry = spreadAtEntry;
|
|
positionEntryMemory[index].decisionScoreAtEntry = decisionScore;
|
|
positionEntryMemory[index].decisionActionAtEntry = (int)ClampValue((double)decisionAction, 0.0, 2.0);
|
|
positionEntryMemory[index].decisionSetupIdAtEntry = (int)ClampValue((double)decisionSetupId, 0.0, 2147483647.0);
|
|
positionEntryMemory[index].entryTypeAtEntry = (entryTypeAtEntry != 0) ? 1 : 0;
|
|
positionEntryMemory[index].sourceOrderTicketAtEntry = sourceOrderTicketAtEntry;
|
|
positionEntryMemory[index].sourceOrderTypeAtEntry = sourceOrderTypeAtEntry;
|
|
positionEntryMemory[index].barsBucket = barsBucket;
|
|
positionEntryMemory[index].volumeBucket = volumeBucket;
|
|
positionEntryMemory[index].entryTimeframe = entryTimeframe;
|
|
positionEntryMemory[index].regimeAtEntry = regimeAtEntry;
|
|
positionEntryMemory[index].patternHash = patternHash;
|
|
positionEntryMemory[index].liquidityLevel = liquidityLevel;
|
|
positionEntryMemory[index].liquidityDistancePts = liquidityDistancePts;
|
|
positionEntryMemory[index].liquidityUsed = liquidityUsed;
|
|
|
|
if(createdNewContext)
|
|
{
|
|
// Reset accumulators for new slots; ArrayResize can reuse stale struct memory.
|
|
positionEntryMemory[index].accumulatedProfit = 0.0;
|
|
positionEntryMemory[index].closeDealCount = 0;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool GetPositionEntryContext(ulong positionId, PositionEntryContext &context)
|
|
{
|
|
int index = FindPositionEntryContextIndex(positionId);
|
|
if(index == -1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
context = positionEntryMemory[index];
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsPositionIdentifierStillOpen(ulong positionId)
|
|
{
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if(!PositionSelectByTicket(ticket))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if(PositionGetString(POSITION_SYMBOL) != Symbol())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if((ulong)PositionGetInteger(POSITION_MAGIC) != (ulong)MagicNumber)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if((ulong)PositionGetInteger(POSITION_IDENTIFIER) == positionId)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void RemovePositionEntryContext(ulong positionId)
|
|
{
|
|
int index = FindPositionEntryContextIndex(positionId);
|
|
if(index == -1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int last = ArraySize(positionEntryMemory) - 1;
|
|
if(index != last)
|
|
{
|
|
positionEntryMemory[index] = positionEntryMemory[last];
|
|
}
|
|
ArrayResize(positionEntryMemory, last);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
string DealReasonToText(long reason)
|
|
{
|
|
switch((ENUM_DEAL_REASON)reason)
|
|
{
|
|
case DEAL_REASON_SL:
|
|
return "STOP_LOSS";
|
|
case DEAL_REASON_TP:
|
|
return "TAKE_PROFIT";
|
|
case DEAL_REASON_SO:
|
|
return "STOP_OUT";
|
|
case DEAL_REASON_CLIENT:
|
|
return "CLIENT";
|
|
case DEAL_REASON_EXPERT:
|
|
return "EXPERT";
|
|
case DEAL_REASON_MOBILE:
|
|
return "MOBILE";
|
|
case DEAL_REASON_WEB:
|
|
return "WEB";
|
|
case DEAL_REASON_ROLLOVER:
|
|
return "ROLLOVER";
|
|
case DEAL_REASON_VMARGIN:
|
|
return "VMARGIN";
|
|
case DEAL_REASON_SPLIT:
|
|
return "SPLIT";
|
|
default:
|
|
return "UNKNOWN";
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void RecordTradeToMemory(int direction, double entryPrice, double exitPrice,
|
|
double profit, int consecutiveBars, double volumeRatio,
|
|
double spread, datetime entryTime, datetime exitTime, string exitReason,
|
|
string tradeSymbol = "")
|
|
{
|
|
// Clamp incoming metrics to keep persistence sane
|
|
double rawProfit = profit;
|
|
profit = ClampDouble(profit, -1000000.0, 1000000.0);
|
|
volumeRatio = ClampDouble(volumeRatio, 0.0, 1000.0);
|
|
spread = ClampDouble(spread, 0.0, 100000.0);
|
|
|
|
if(rawProfit != profit)
|
|
{
|
|
Print("MEMORY ANOMALY | Skipping out-of-range trade profit: ", DoubleToString(rawProfit, 2));
|
|
return;
|
|
}
|
|
|
|
TradeMemory newTrade;
|
|
if(StringLen(tradeSymbol) <= 0)
|
|
tradeSymbol = _Symbol;
|
|
newTrade.symbol = tradeSymbol;
|
|
newTrade.entryTime = entryTime;
|
|
newTrade.direction = direction;
|
|
newTrade.entryPrice = entryPrice;
|
|
newTrade.exitPrice = exitPrice;
|
|
newTrade.profit = profit;
|
|
newTrade.consecutiveBars = consecutiveBars;
|
|
newTrade.volumeRatio = volumeRatio;
|
|
newTrade.spreadAtEntry = spread;
|
|
// Initialize P/L extrema to avoid garbage values in state file
|
|
newTrade.maxProfit = profit;
|
|
newTrade.maxDrawdown = 0.0;
|
|
newTrade.wasWinner = (profit > 0);
|
|
newTrade.exitReason = exitReason;
|
|
|
|
MqlDateTime entryDt, exitDt;
|
|
TimeToStruct(entryTime, entryDt);
|
|
TimeToStruct(exitTime, exitDt);
|
|
newTrade.hourOfDay = entryDt.hour;
|
|
newTrade.dayOfWeek = entryDt.day_of_week;
|
|
newTrade.holdDuration = (int)((exitTime - entryTime) / 60); // minutes
|
|
|
|
// Add to trade history
|
|
int size = ArraySize(tradeHistory);
|
|
ArrayResize(tradeHistory, size + 1);
|
|
tradeHistory[size] = newTrade;
|
|
|
|
// Update time-based memory
|
|
UpdateTimeBasedMemory(newTrade);
|
|
UpdateSessionBasedMemory(newTrade);
|
|
|
|
// Update pattern memory
|
|
UpdatePatternMemory(newTrade);
|
|
|
|
// Update market regime
|
|
UpdateMarketRegimeMemory(newTrade);
|
|
|
|
// Keep only last 1000 trades in memory
|
|
if(ArraySize(tradeHistory) > 1000)
|
|
{
|
|
int total = ArraySize(tradeHistory);
|
|
for(int i = 1; i < total; i++)
|
|
{
|
|
tradeHistory[i - 1] = tradeHistory[i];
|
|
}
|
|
ArrayResize(tradeHistory, 1000);
|
|
}
|
|
|
|
Print("Trade recorded to memory: ", direction == 1 ? "BUY" : "SELL",
|
|
" | Profit: $", DoubleToString(profit, 2),
|
|
" | Duration: ", newTrade.holdDuration, " min");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateTimeBasedMemory(TradeMemory &tradeRecord)
|
|
{
|
|
int hour = tradeRecord.hourOfDay;
|
|
int day = tradeRecord.dayOfWeek;
|
|
|
|
timeMemory[hour][day].totalTrades++;
|
|
timeMemory[hour][day].totalProfit += tradeRecord.profit;
|
|
|
|
if(tradeRecord.wasWinner)
|
|
{
|
|
timeMemory[hour][day].winningTrades++;
|
|
}
|
|
|
|
// Calculate updated statistics
|
|
timeMemory[hour][day].avgProfit = timeMemory[hour][day].totalProfit / timeMemory[hour][day].totalTrades;
|
|
timeMemory[hour][day].winRate = (timeMemory[hour][day].winningTrades * 100.0) / timeMemory[hour][day].totalTrades;
|
|
|
|
// Mark optimal hours (win rate > 60% and avg profit > 0)
|
|
timeMemory[hour][day].isOptimalHour = (timeMemory[hour][day].winRate > 60.0 && timeMemory[hour][day].avgProfit > 0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void UpdatePatternMemory(TradeMemory &tradeRecord)
|
|
{
|
|
int patternType = (tradeRecord.direction == 1) ? 1 : 2; // 1=bullish_exhaustion, 2=bearish_exhaustion
|
|
string tradeSymbol = tradeRecord.symbol;
|
|
if(StringLen(tradeSymbol) <= 0)
|
|
tradeSymbol = _Symbol;
|
|
|
|
// Find existing pattern or create new one
|
|
int patternIndex = -1;
|
|
for(int i = 0; i < ArraySize(patternHistory); i++)
|
|
{
|
|
if(patternHistory[i].symbol == tradeSymbol &&
|
|
patternHistory[i].patternType == patternType &&
|
|
MathAbs(patternHistory[i].consecutiveBars - tradeRecord.consecutiveBars) <= 1)
|
|
{
|
|
patternIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(patternIndex == -1)
|
|
{
|
|
// Create new pattern entry
|
|
int size = ArraySize(patternHistory);
|
|
ArrayResize(patternHistory, size + 1);
|
|
patternIndex = size;
|
|
patternHistory[patternIndex].symbol = tradeSymbol;
|
|
patternHistory[patternIndex].patternType = patternType;
|
|
patternHistory[patternIndex].consecutiveBars = tradeRecord.consecutiveBars;
|
|
patternHistory[patternIndex].totalTrades = 0;
|
|
patternHistory[patternIndex].winningTrades = 0;
|
|
patternHistory[patternIndex].totalProfit = 0;
|
|
}
|
|
|
|
// Update pattern statistics
|
|
patternHistory[patternIndex].totalTrades++;
|
|
patternHistory[patternIndex].totalProfit += tradeRecord.profit;
|
|
if(tradeRecord.wasWinner)
|
|
{
|
|
patternHistory[patternIndex].winningTrades++;
|
|
}
|
|
|
|
patternHistory[patternIndex].avgProfit = patternHistory[patternIndex].totalProfit / patternHistory[patternIndex].totalTrades;
|
|
patternHistory[patternIndex].winRate = (patternHistory[patternIndex].winningTrades * 100.0) / patternHistory[patternIndex].totalTrades;
|
|
patternHistory[patternIndex].lastUpdated = TimeCurrent();
|
|
|
|
// Calculate confidence based on sample size and performance
|
|
double sampleSizeFactor = MathMin(patternHistory[patternIndex].totalTrades / 10.0, 1.0);
|
|
double performanceFactor = patternHistory[patternIndex].winRate / 100.0;
|
|
patternHistory[patternIndex].confidence = sampleSizeFactor * performanceFactor;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateMarketRegimeMemory(TradeMemory &tradeRecord)
|
|
{
|
|
// Simple regime detection based on recent volatility and performance
|
|
UpdateMarketState();
|
|
|
|
// Check if we should record regime transition
|
|
if(marketState.currentRegime != "UNKNOWN")
|
|
{
|
|
int regimeIndex = FindOrCreateRegime(marketState.currentRegime);
|
|
regimeHistory[regimeIndex].totalTrades++;
|
|
if(tradeRecord.wasWinner)
|
|
{
|
|
regimeHistory[regimeIndex].successfulTrades++;
|
|
}
|
|
regimeHistory[regimeIndex].performance = (regimeHistory[regimeIndex].successfulTrades * 100.0) / regimeHistory[regimeIndex].totalTrades;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int FindOrCreateRegime(string regimeName)
|
|
{
|
|
for(int i = 0; i < ArraySize(regimeHistory); i++)
|
|
{
|
|
if(regimeHistory[i].regime == regimeName)
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
|
|
// Create new regime entry
|
|
int size = ArraySize(regimeHistory);
|
|
ArrayResize(regimeHistory, size + 1);
|
|
regimeHistory[size].regime = "";
|
|
regimeHistory[size].detected = 0;
|
|
regimeHistory[size].confidence = 0.0;
|
|
regimeHistory[size].duration = 0;
|
|
regimeHistory[size].avgVolatility = 0.0;
|
|
regimeHistory[size].avgSpread = 0.0;
|
|
regimeHistory[size].totalTrades = 0;
|
|
regimeHistory[size].successfulTrades = 0;
|
|
regimeHistory[size].performance = 0.0;
|
|
regimeHistory[size].regime = regimeName;
|
|
regimeHistory[size].detected = TimeCurrent();
|
|
return size;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateMarketState()
|
|
{
|
|
static datetime lastUpdate = 0;
|
|
datetime now = TimeCurrent();
|
|
|
|
int refreshSeconds = MathMax(5, MarketStateUpdateSeconds);
|
|
if(now - lastUpdate < refreshSeconds)
|
|
return;
|
|
|
|
lastUpdate = now;
|
|
|
|
// Calculate volatility from closed bars (returns-based, stable across ticks).
|
|
int volWindow = 20;
|
|
int barsAvailable = Bars(_Symbol, Period());
|
|
if(barsAvailable < volWindow + 2)
|
|
{
|
|
marketState.currentRegime = "UNKNOWN";
|
|
marketState.regimeConfidence = 0.0;
|
|
return;
|
|
}
|
|
|
|
double sumRet = 0.0;
|
|
double sumSqRet = 0.0;
|
|
int samples = 0;
|
|
for(int i = 1; i <= volWindow; i++)
|
|
{
|
|
double newerClose = iClose(_Symbol, Period(), i - 1);
|
|
double olderClose = iClose(_Symbol, Period(), i);
|
|
if(newerClose <= 0.0 || olderClose <= 0.0)
|
|
continue;
|
|
|
|
double retPct = ((newerClose - olderClose) / olderClose) * 100.0;
|
|
sumRet += retPct;
|
|
sumSqRet += retPct * retPct;
|
|
samples++;
|
|
}
|
|
|
|
if(samples < 5)
|
|
{
|
|
marketState.currentRegime = "UNKNOWN";
|
|
marketState.regimeConfidence = 0.0;
|
|
return;
|
|
}
|
|
|
|
double meanRet = sumRet / (double)samples;
|
|
double variance = (sumSqRet / (double)samples) - (meanRet * meanRet);
|
|
if(variance < 0.0)
|
|
variance = 0.0;
|
|
marketState.currentVolatility = MathSqrt(variance);
|
|
|
|
// Seed average volatility to avoid early bias toward VOLATILE
|
|
if(marketState.avgVolatility <= 0.0)
|
|
{
|
|
marketState.avgVolatility = marketState.currentVolatility;
|
|
}
|
|
else
|
|
{
|
|
marketState.avgVolatility = (marketState.avgVolatility * 0.95) + (marketState.currentVolatility * 0.05);
|
|
}
|
|
|
|
double pointFloorPct = (SymbolInfoDouble(_Symbol, SYMBOL_POINT) > 0.0 && SymbolInfoDouble(_Symbol, SYMBOL_BID) > 0.0)
|
|
? ((SymbolInfoDouble(_Symbol, SYMBOL_POINT) / SymbolInfoDouble(_Symbol, SYMBOL_BID)) * 100.0)
|
|
: 0.0001;
|
|
double baselineVolatility = MathMax(marketState.avgVolatility, pointFloorPct);
|
|
if(marketState.currentVolatility > baselineVolatility * 1.5)
|
|
{
|
|
marketState.currentRegime = "VOLATILE";
|
|
marketState.regimeConfidence = 0.8;
|
|
}
|
|
else
|
|
if(marketState.currentVolatility < baselineVolatility * 0.5)
|
|
{
|
|
marketState.currentRegime = "QUIET";
|
|
marketState.regimeConfidence = 0.8;
|
|
}
|
|
else
|
|
{
|
|
marketState.currentRegime = "RANGING";
|
|
marketState.regimeConfidence = 0.6;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double ClampValue(double value, double minValue, double maxValue)
|
|
{
|
|
if(value < minValue)
|
|
{
|
|
return minValue;
|
|
}
|
|
if(value > maxValue)
|
|
{
|
|
return maxValue;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsHourInWindow(int hour, int startHour, int endHour)
|
|
{
|
|
int h = (hour % 24 + 24) % 24;
|
|
int start = (startHour % 24 + 24) % 24;
|
|
int end = (endHour % 24 + 24) % 24;
|
|
|
|
if(start == end)
|
|
return true;
|
|
if(start < end)
|
|
return (h >= start && h < end);
|
|
return (h >= start || h < end);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int GetSessionBucketByHour(int hour)
|
|
{
|
|
bool inLondon = IsHourInWindow(hour, LondonSessionStartHour, LondonSessionEndHour);
|
|
bool inNewYork = IsHourInWindow(hour, NewYorkSessionStartHour, NewYorkSessionEndHour);
|
|
|
|
if(inLondon && inNewYork)
|
|
return 3; // overlap
|
|
if(inLondon)
|
|
return 1;
|
|
if(inNewYork)
|
|
return 2;
|
|
return 0; // asia/other
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateSessionBasedMemory(TradeMemory &tradeRecord)
|
|
{
|
|
if(!UseBestSessionsEngine)
|
|
return;
|
|
|
|
int bucket = GetSessionBucketByHour(tradeRecord.hourOfDay);
|
|
if(bucket < 0 || bucket > 3)
|
|
return;
|
|
|
|
sessionMemory[bucket].totalTrades++;
|
|
sessionMemory[bucket].totalProfit += tradeRecord.profit;
|
|
if(tradeRecord.wasWinner)
|
|
sessionMemory[bucket].winningTrades++;
|
|
|
|
if(sessionMemory[bucket].totalTrades > 0)
|
|
{
|
|
sessionMemory[bucket].avgProfit = sessionMemory[bucket].totalProfit / (double)sessionMemory[bucket].totalTrades;
|
|
sessionMemory[bucket].winRate = ((double)sessionMemory[bucket].winningTrades * 100.0) / (double)sessionMemory[bucket].totalTrades;
|
|
}
|
|
|
|
int minTrades = MathMax(1, BestSessionMinTrades);
|
|
sessionMemory[bucket].isOptimal = (sessionMemory[bucket].totalTrades >= minTrades &&
|
|
sessionMemory[bucket].winRate >= BestSessionMinWinRate &&
|
|
sessionMemory[bucket].avgProfit > 0.0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetCurrentSessionConfidence()
|
|
{
|
|
if(!UseBestSessionsEngine)
|
|
return 0.5;
|
|
|
|
MqlDateTime dt;
|
|
TimeToStruct(GetCurrentTimeWithOffset(), dt);
|
|
int bucket = GetSessionBucketByHour(dt.hour);
|
|
int minTrades = MathMax(1, BestSessionMinTrades);
|
|
if(bucket < 0 || bucket > 3 || sessionMemory[bucket].totalTrades < minTrades)
|
|
return 0.5;
|
|
|
|
if(sessionMemory[bucket].isOptimal)
|
|
return 0.8;
|
|
|
|
return ClampValue(sessionMemory[bucket].winRate / 100.0, 0.2, 0.8);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsCurrentSessionTradable()
|
|
{
|
|
if(!UseBestSessionsEngine || !UseBestSessionFilter)
|
|
return true;
|
|
|
|
MqlDateTime dt;
|
|
TimeToStruct(GetCurrentTimeWithOffset(), dt);
|
|
int bucket = GetSessionBucketByHour(dt.hour);
|
|
int minTrades = MathMax(1, BestSessionMinTrades);
|
|
if(bucket < 0 || bucket > 3 || sessionMemory[bucket].totalTrades < minTrades)
|
|
return true; // not enough evidence yet
|
|
|
|
return sessionMemory[bucket].isOptimal;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void PrintBestSessionReport()
|
|
{
|
|
if(!UseBestSessionsEngine)
|
|
return;
|
|
|
|
MqlDateTime dt;
|
|
TimeToStruct(GetCurrentTimeWithOffset(), dt);
|
|
int currentBucket = GetSessionBucketByHour(dt.hour);
|
|
|
|
Print("=== BEST SESSIONS REPORT ===");
|
|
for(int i = 0; i < 4; i++)
|
|
{
|
|
Print("SESSION ", sessionMemory[i].name,
|
|
" | Trades=", sessionMemory[i].totalTrades,
|
|
" | WinRate=", DoubleToString(sessionMemory[i].winRate, 1), "%",
|
|
" | AvgProfit=$", DoubleToString(sessionMemory[i].avgProfit, 2),
|
|
" | Optimal=", (sessionMemory[i].isOptimal ? "YES" : "NO"));
|
|
}
|
|
Print("CURRENT SESSION: ", sessionMemory[currentBucket].name,
|
|
" | Tradable=", (IsCurrentSessionTradable() ? "YES" : "NO"));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetAccountDrawdownPercentAuto()
|
|
{
|
|
UpdateAccountCache();
|
|
if(accountCache.balance <= 0.0)
|
|
return 0.0;
|
|
|
|
double dd = ((accountCache.balance - accountCache.equity) / accountCache.balance) * 100.0;
|
|
if(dd < 0.0)
|
|
dd = 0.0;
|
|
return dd;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetRecentWinRateAuto(int lookbackTrades)
|
|
{
|
|
int total = ArraySize(tradeHistory);
|
|
if(total <= 0)
|
|
return -1.0;
|
|
|
|
int lookback = MathMax(5, lookbackTrades);
|
|
int start = MathMax(0, total - lookback);
|
|
int counted = 0;
|
|
int wins = 0;
|
|
|
|
for(int i = start; i < total; i++)
|
|
{
|
|
counted++;
|
|
if(tradeHistory[i].wasWinner)
|
|
wins++;
|
|
}
|
|
|
|
if(counted <= 0)
|
|
return -1.0;
|
|
|
|
return ((double)wins * 100.0) / (double)counted;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsSmallAccountForAutoProtection()
|
|
{
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
if(balance <= 500.0)
|
|
return true;
|
|
|
|
double dd = GetAccountDrawdownPercentAuto();
|
|
return (balance <= 1000.0 && dd >= 6.0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsLargeAccountForAutoExplore()
|
|
{
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
if(balance < 10000.0)
|
|
return false;
|
|
|
|
double dd = GetAccountDrawdownPercentAuto();
|
|
double winRate = GetRecentWinRateAuto(40);
|
|
|
|
if(marketState.marketStress)
|
|
return false;
|
|
if(dd > 5.0)
|
|
return false;
|
|
if(winRate > 0.0 && winRate < 50.0)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int GetEffectiveDecisionThresholdAuto(int baseThreshold)
|
|
{
|
|
int threshold = (int)ClampValue((double)baseThreshold, 1.0, 10.0);
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
double dd = GetAccountDrawdownPercentAuto();
|
|
double winRate = GetRecentWinRateAuto(30);
|
|
|
|
if(balance <= 100.0)
|
|
threshold += 3;
|
|
else
|
|
if(balance <= 500.0)
|
|
threshold += 2;
|
|
else
|
|
if(balance <= 1000.0)
|
|
threshold += 1;
|
|
|
|
if(dd >= 10.0)
|
|
threshold += 2;
|
|
else
|
|
if(dd >= 6.0)
|
|
threshold += 1;
|
|
|
|
if(winRate > 0.0 && winRate < 45.0)
|
|
threshold += 1;
|
|
|
|
if(balance >= 10000.0 && dd < 3.0 && winRate >= 60.0)
|
|
threshold -= 1;
|
|
|
|
return (int)ClampValue((double)threshold, 1.0, 10.0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetLiquidityZoneDistancePointsForSymbol(string symbol)
|
|
{
|
|
double symbolPoint = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
if(symbolPoint <= 0.0)
|
|
symbolPoint = Point();
|
|
if(symbolPoint <= 0.0)
|
|
return 0.0;
|
|
|
|
double maxDistancePrice = ConvertInputDistanceToPriceForSymbol(symbol, LiquidityZoneDistance);
|
|
if(maxDistancePrice <= 0.0)
|
|
return 0.0;
|
|
|
|
return maxDistancePrice / symbolPoint;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetAutoRiskBalanceFactor(double accountBalance)
|
|
{
|
|
double factor = 1.0;
|
|
double dd = GetAccountDrawdownPercentAuto();
|
|
double winRate = GetRecentWinRateAuto(30);
|
|
|
|
if(accountBalance <= 50.0)
|
|
factor *= 0.20;
|
|
else
|
|
if(accountBalance <= 100.0)
|
|
factor *= 0.25;
|
|
else
|
|
if(accountBalance <= 250.0)
|
|
factor *= 0.35;
|
|
else
|
|
if(accountBalance <= 500.0)
|
|
factor *= 0.50;
|
|
else
|
|
if(accountBalance <= 1000.0)
|
|
factor *= 0.70;
|
|
else
|
|
if(accountBalance >= 100000.0)
|
|
factor *= 0.65;
|
|
else
|
|
if(accountBalance >= 50000.0)
|
|
factor *= 0.75;
|
|
else
|
|
if(accountBalance >= 10000.0)
|
|
factor *= 0.85;
|
|
|
|
if(dd >= 15.0)
|
|
factor *= 0.55;
|
|
else
|
|
if(dd >= 10.0)
|
|
factor *= 0.70;
|
|
else
|
|
if(dd >= 6.0)
|
|
factor *= 0.85;
|
|
|
|
if(winRate > 0.0 && winRate < 45.0)
|
|
factor *= 0.85;
|
|
else
|
|
if(winRate >= 65.0 && accountBalance >= 10000.0 && dd < 3.0)
|
|
factor *= 1.05;
|
|
|
|
return ClampValue(factor, 0.15, 1.10);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetConfiguredBaseRiskPercent()
|
|
{
|
|
double baseRisk = RiskPercent;
|
|
if(baseRisk <= 0.0)
|
|
baseRisk = DefaultRiskPercent;
|
|
|
|
return ClampValue(baseRisk, 0.01, 10.0);
|
|
}
|
|
|
|
// Build safe paths for the MicCmd grammar file (common Files area)
|
|
string g_voiceDirCache = "";
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
string BuildVoiceGrammarDir()
|
|
{
|
|
if(g_voiceDirCache == "")
|
|
g_voiceDirCache = TerminalInfoString(TERMINAL_COMMONDATA_PATH) + "\\Files";
|
|
return g_voiceDirCache;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
string BuildVoiceGrammarPath()
|
|
{
|
|
return BuildVoiceGrammarDir() + "\\kog_voice_grammar.xml";
|
|
}
|
|
|
|
// Ensure the grammar file exists where MicCmd can read it; recreate if missing.
|
|
// Tries terminal sandbox first; falls back to legacy C:\\sqlite_cmdq if needed.
|
|
bool EnsureVoiceGrammarFile()
|
|
{
|
|
g_voiceGrammarPath = BuildVoiceGrammarPath();
|
|
|
|
// Write into common Files without creating folders manually
|
|
int h = FileOpen("kog_voice_grammar.xml", FILE_WRITE | FILE_TXT | FILE_ANSI | FILE_COMMON | FILE_SHARE_READ);
|
|
if(h == INVALID_HANDLE)
|
|
{
|
|
int err = GetLastError();
|
|
Print("VOICE CMD | Cannot create grammar file (common) err=", err);
|
|
return false;
|
|
}
|
|
|
|
FileWriteString(h, g_voiceGrammarContent);
|
|
FileClose(h);
|
|
Print("VOICE CMD | Default grammar created at ", g_voiceGrammarPath);
|
|
return true;
|
|
}
|
|
|
|
// Legacy fallback: place grammar near existing sqlite_cmdq assets (keeps older setups working)
|
|
bool EnsureVoiceGrammarFileLegacy()
|
|
{
|
|
// Legacy path no longer attempted (permission issues); keep for compatibility
|
|
Print("VOICE CMD | Legacy grammar path disabled (permissions).");
|
|
return false;
|
|
}
|
|
|
|
// Normalize Telegram inputs (trim spaces/newlines); keep originals untouched
|
|
void NormalizeTelegramInputs()
|
|
{
|
|
// Trim whitespace safely (MQL5 has only left/right trim)
|
|
string bot = TelegramBotToken;
|
|
string chat = TelegramChatId;
|
|
StringTrimLeft(bot);
|
|
StringTrimRight(bot);
|
|
StringTrimLeft(chat);
|
|
StringTrimRight(chat);
|
|
g_telegramBotToken = bot;
|
|
g_telegramChatId = chat;
|
|
}
|
|
|
|
// Prepare voice command log path under the common data folder (shared by all terminals)
|
|
void EnsureVoiceLogPath()
|
|
{
|
|
if(g_voiceLogPath != "")
|
|
return;
|
|
|
|
string baseDir = TerminalInfoString(TERMINAL_COMMONDATA_PATH) + "\\KingOfGold";
|
|
if(!FolderCreate(baseDir))
|
|
{
|
|
Print("VOICE CMD | Failed to create log directory: ", baseDir);
|
|
return;
|
|
}
|
|
|
|
g_voiceLogPath = baseDir + "\\voice_commands.log";
|
|
}
|
|
|
|
// Append a single voice command entry with timestamp
|
|
void LogVoiceCommand(const string cmd)
|
|
{
|
|
EnsureVoiceLogPath();
|
|
if(g_voiceLogPath == "")
|
|
return;
|
|
|
|
int h = FileOpen(g_voiceLogPath, FILE_READ|FILE_WRITE|FILE_TXT|FILE_ANSI);
|
|
if(h == INVALID_HANDLE)
|
|
h = FileOpen(g_voiceLogPath, FILE_WRITE|FILE_TXT|FILE_ANSI);
|
|
if(h == INVALID_HANDLE)
|
|
{
|
|
Print("VOICE CMD | Failed to open log file: ", g_voiceLogPath);
|
|
return;
|
|
}
|
|
|
|
FileSeek(h, 0, SEEK_END);
|
|
string line = TimeToString(TimeLocal(), TIME_DATE|TIME_SECONDS) + "\t" + cmd;
|
|
FileWriteString(h, line + "\n");
|
|
FileClose(h);
|
|
}
|
|
|
|
// Lowercase and trim helper for voice commands
|
|
string NormalizeVoiceText(const string src)
|
|
{
|
|
string s = src;
|
|
StringTrimLeft(s);
|
|
StringTrimRight(s);
|
|
StringToLower(s);
|
|
// Replace double spaces with single
|
|
while(StringFind(s, " ") >= 0)
|
|
StringReplace(s, " ", " ");
|
|
// Remove trailing punctuation
|
|
StringReplace(s, ".", "");
|
|
StringReplace(s, ",", "");
|
|
return s;
|
|
}
|
|
|
|
// Extract first numeric value (integer/decimal) from text; returns true if found
|
|
bool ExtractFirstNumber(const string text, double &out)
|
|
{
|
|
string digits = "";
|
|
bool dotSeen = false;
|
|
bool started = false;
|
|
for(int i = 0; i < StringLen(text); i++)
|
|
{
|
|
string ch = StringSubstr(text, i, 1);
|
|
if((ch >= "0" && ch <= "9") || (ch == "." && !dotSeen))
|
|
{
|
|
started = true;
|
|
if(ch == ".")
|
|
dotSeen = true;
|
|
digits += ch;
|
|
}
|
|
else
|
|
{
|
|
if(started)
|
|
break;
|
|
}
|
|
}
|
|
if(digits == "")
|
|
return false;
|
|
out = StringToDouble(digits);
|
|
return MathIsValidNumber(out);
|
|
}
|
|
|
|
// Map simple number words (one..ten) to double; returns true if matched
|
|
bool WordToNumber(const string text, double &out)
|
|
{
|
|
if(text == "one")
|
|
{
|
|
out = 1;
|
|
return true;
|
|
}
|
|
if(text == "two")
|
|
{
|
|
out = 2;
|
|
return true;
|
|
}
|
|
if(text == "three")
|
|
{
|
|
out = 3;
|
|
return true;
|
|
}
|
|
if(text == "four")
|
|
{
|
|
out = 4;
|
|
return true;
|
|
}
|
|
if(text == "five")
|
|
{
|
|
out = 5;
|
|
return true;
|
|
}
|
|
if(text == "six")
|
|
{
|
|
out = 6;
|
|
return true;
|
|
}
|
|
if(text == "seven")
|
|
{
|
|
out = 7;
|
|
return true;
|
|
}
|
|
if(text == "eight")
|
|
{
|
|
out = 8;
|
|
return true;
|
|
}
|
|
if(text == "nine")
|
|
{
|
|
out = 9;
|
|
return true;
|
|
}
|
|
if(text == "ten")
|
|
{
|
|
out = 10;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Poll MicCmd.dll queue (SAPI-based voice) and dispatch into existing ExecuteVoiceCommand
|
|
void PollMicCmdQueue()
|
|
{
|
|
if(!g_micStarted)
|
|
return;
|
|
|
|
uchar buf[];
|
|
ArrayResize(buf, 256);
|
|
|
|
for(int i = 0; i < 3; i++)
|
|
{
|
|
int r = MicPopCommandUtf8(buf);
|
|
if(r == 1)
|
|
{
|
|
string cmd = CharArrayToString(buf);
|
|
StringTrimLeft(cmd);
|
|
StringTrimRight(cmd);
|
|
if(cmd != "")
|
|
{
|
|
LogVoiceCommand(cmd);
|
|
string vcResp="", vcErr="";
|
|
ExecuteVoiceCommand(cmd, Symbol(), vcResp, vcErr);
|
|
}
|
|
}
|
|
else
|
|
if(r < 0)
|
|
{
|
|
string err = "";
|
|
MicGetLastError(err);
|
|
if(err != "")
|
|
Print("MicCmd error: ", err);
|
|
break;
|
|
}
|
|
else
|
|
break; // no data
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// INTELLIGENCE FUNCTIONS - Memory-Based Decision Making
|
|
//+------------------------------------------------------------------+
|
|
void CalculateVolumeProfile(double &liquidityZones[], int positionType, double currentPrice);
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetMemoryBasedConfidence(int direction, int consecutiveBars, double volumeRatio, string symbol = "")
|
|
{
|
|
MqlDateTime currentDt;
|
|
TimeToStruct(TimeCurrent(), currentDt);
|
|
int hour = currentDt.hour;
|
|
int day = currentDt.day_of_week;
|
|
|
|
double timeConfidence = 0.0;
|
|
double patternConfidence = 0.0;
|
|
double regimeConfidence = 0.0;
|
|
double sessionConfidence = 0.5;
|
|
bool hasTimeData = false;
|
|
bool hasPatternData = false;
|
|
bool hasRegimeData = false;
|
|
bool hasSessionData = false;
|
|
string targetSymbol = symbol;
|
|
if(StringLen(targetSymbol) <= 0)
|
|
targetSymbol = _Symbol;
|
|
|
|
// Time-based confidence
|
|
if(timeMemory[hour][day].totalTrades >= 5)
|
|
{
|
|
hasTimeData = true;
|
|
if(timeMemory[hour][day].isOptimalHour)
|
|
{
|
|
timeConfidence = 0.8;
|
|
}
|
|
else
|
|
{
|
|
timeConfidence = timeMemory[hour][day].winRate / 100.0;
|
|
}
|
|
}
|
|
|
|
// Pattern-based confidence
|
|
int patternType = (direction == 1) ? 1 : 2;
|
|
for(int i = 0; i < ArraySize(patternHistory); i++)
|
|
{
|
|
if(patternHistory[i].symbol == targetSymbol &&
|
|
patternHistory[i].patternType == patternType &&
|
|
MathAbs(patternHistory[i].consecutiveBars - consecutiveBars) <= 1)
|
|
{
|
|
patternConfidence = patternHistory[i].confidence;
|
|
hasPatternData = (patternHistory[i].totalTrades >= 3);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Regime-based confidence
|
|
for(int i = 0; i < ArraySize(regimeHistory); i++)
|
|
{
|
|
if(regimeHistory[i].regime == marketState.currentRegime)
|
|
{
|
|
regimeConfidence = regimeHistory[i].performance / 100.0;
|
|
hasRegimeData = (regimeHistory[i].totalTrades >= 5);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Session-based confidence (London/New York engine)
|
|
if(UseBestSessionsEngine)
|
|
{
|
|
MqlDateTime nowDt;
|
|
TimeToStruct(GetCurrentTimeWithOffset(), nowDt);
|
|
int sessionBucket = GetSessionBucketByHour(nowDt.hour);
|
|
int minTrades = MathMax(1, BestSessionMinTrades);
|
|
if(sessionBucket >= 0 && sessionBucket <= 3 && sessionMemory[sessionBucket].totalTrades >= minTrades)
|
|
{
|
|
hasSessionData = true;
|
|
if(sessionMemory[sessionBucket].isOptimal)
|
|
sessionConfidence = 0.8;
|
|
else
|
|
sessionConfidence = ClampValue(sessionMemory[sessionBucket].winRate / 100.0, 0.2, 0.8);
|
|
}
|
|
}
|
|
|
|
// Warm-up: if memory has no meaningful evidence yet, stay neutral (don't block trades).
|
|
if(!hasTimeData && !hasPatternData && !hasRegimeData && !hasSessionData)
|
|
{
|
|
return 0.5;
|
|
}
|
|
|
|
// Weighted combination
|
|
double combinedConfidence = (timeConfidence * 0.35) + (patternConfidence * 0.35) + (regimeConfidence * 0.20) + (sessionConfidence * 0.10);
|
|
|
|
return ClampValue(combinedConfidence, 0.0, 1.0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetMemoryBasedConfidenceForSymbol(string symbol, int direction, int consecutiveBars, double volumeRatio)
|
|
{
|
|
double baseConfidence = GetMemoryBasedConfidence(direction, consecutiveBars, volumeRatio, symbol);
|
|
int symbolIndex = getSymbolIndex(symbol);
|
|
if(symbolIndex < 0)
|
|
return baseConfidence;
|
|
|
|
// Blend in per-symbol empirical win rate once enough trades exist.
|
|
if(enhancedSymbolMemory[symbolIndex].totalTrades >= 5)
|
|
{
|
|
double symbolConfidence = ClampValue(enhancedSymbolMemory[symbolIndex].winRate, 0.0, 1.0);
|
|
return ClampValue((baseConfidence * 0.70) + (symbolConfidence * 0.30), 0.0, 1.0);
|
|
}
|
|
|
|
return baseConfidence;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsOptimalTradingTime()
|
|
{
|
|
MqlDateTime currentDt;
|
|
TimeToStruct(TimeCurrent(), currentDt);
|
|
int hour = currentDt.hour;
|
|
int day = currentDt.day_of_week;
|
|
|
|
return timeMemory[hour][day].isOptimalHour &&
|
|
timeMemory[hour][day].totalTrades >= 3;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetAdaptiveRiskMultiplier()
|
|
{
|
|
double baseMultiplier = 1.0;
|
|
|
|
// Adjust based on time performance
|
|
MqlDateTime currentDt;
|
|
TimeToStruct(TimeCurrent(), currentDt);
|
|
int hour = currentDt.hour;
|
|
int day = currentDt.day_of_week;
|
|
|
|
if(timeMemory[hour][day].totalTrades >= 5)
|
|
{
|
|
double timeWinRate = timeMemory[hour][day].winRate;
|
|
if(timeWinRate > 70.0)
|
|
{
|
|
baseMultiplier *= 1.2; // Increase risk in optimal hours
|
|
}
|
|
else
|
|
if(timeWinRate < 40.0)
|
|
{
|
|
baseMultiplier *= 0.7; // Decrease risk in poor hours
|
|
}
|
|
}
|
|
|
|
// Adjust based on recent performance
|
|
if(marketState.consecutiveLosses >= 3)
|
|
{
|
|
baseMultiplier *= 0.6; // Conservative after losses
|
|
}
|
|
else
|
|
if(marketState.consecutiveWins >= 3)
|
|
{
|
|
baseMultiplier *= 1.3; // Aggressive after wins
|
|
}
|
|
|
|
// Adjust based on market regime
|
|
if(marketState.currentRegime == "VOLATILE")
|
|
{
|
|
baseMultiplier *= 0.8; // More conservative in volatile markets
|
|
}
|
|
else
|
|
if(marketState.currentRegime == "QUIET")
|
|
{
|
|
baseMultiplier *= 1.1; // Slightly more aggressive in quiet markets
|
|
}
|
|
|
|
// Adjust using session quality (London/New York engine)
|
|
if(UseBestSessionsEngine)
|
|
{
|
|
MqlDateTime sessionDt;
|
|
TimeToStruct(GetCurrentTimeWithOffset(), sessionDt);
|
|
int bucket = GetSessionBucketByHour(sessionDt.hour);
|
|
int minTrades = MathMax(1, BestSessionMinTrades);
|
|
if(bucket >= 0 && bucket <= 3 && sessionMemory[bucket].totalTrades >= minTrades)
|
|
{
|
|
if(sessionMemory[bucket].isOptimal)
|
|
baseMultiplier *= 1.10;
|
|
else
|
|
if(sessionMemory[bucket].winRate < (BestSessionMinWinRate - 10.0))
|
|
baseMultiplier *= 0.85;
|
|
}
|
|
}
|
|
|
|
return ClampValue(baseMultiplier, 0.3, 2.0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
string GetMarketAnalysisReport()
|
|
{
|
|
string report = "";
|
|
report += "=== MEMORY-BASED MARKET ANALYSIS ===\n";
|
|
|
|
// Current regime info
|
|
report += "Current Regime: " + marketState.currentRegime +
|
|
" (Confidence: " + DoubleToString(marketState.regimeConfidence * 100, 1) + "%)\n";
|
|
report += "Volatility: " + DoubleToString(marketState.currentVolatility, 5) +
|
|
" (Avg: " + DoubleToString(marketState.avgVolatility, 5) + ")\n";
|
|
|
|
// Recent performance
|
|
report += "Recent Streak: " + IntegerToString(marketState.consecutiveWins) + "W / " +
|
|
IntegerToString(marketState.consecutiveLosses) + "L\n";
|
|
|
|
// Best trading times
|
|
report += "Optimal Hours: ";
|
|
int optimalCount = 0;
|
|
for(int h = 0; h < 24; h++)
|
|
{
|
|
for(int d = 0; d < 7; d++)
|
|
{
|
|
if(timeMemory[h][d].isOptimalHour && timeMemory[h][d].totalTrades >= 3)
|
|
{
|
|
if(optimalCount > 0)
|
|
report += ", ";
|
|
report += IntegerToString(h) + ":00";
|
|
optimalCount++;
|
|
}
|
|
}
|
|
}
|
|
if(optimalCount == 0)
|
|
report += "None identified yet";
|
|
report += "\n";
|
|
|
|
// Pattern performance
|
|
report += "Pattern Performance:\n";
|
|
for(int i = 0; i < ArraySize(patternHistory); i++)
|
|
{
|
|
if(patternHistory[i].totalTrades >= 5)
|
|
{
|
|
string patternName = (patternHistory[i].patternType == 1) ? "Bullish Exhaustion" : "Bearish Exhaustion";
|
|
report += " " + patternName + " (" + IntegerToString(patternHistory[i].consecutiveBars) +
|
|
" bars): " + DoubleToString(patternHistory[i].winRate, 1) + "% win rate, " +
|
|
DoubleToString(patternHistory[i].confidence * 100, 1) + "% confidence\n";
|
|
}
|
|
}
|
|
|
|
if(UseBestSessionsEngine)
|
|
{
|
|
report += "Session Performance:\n";
|
|
for(int s = 0; s < 4; s++)
|
|
{
|
|
report += " " + sessionMemory[s].name +
|
|
": WR " + DoubleToString(sessionMemory[s].winRate, 1) + "% | Avg $" +
|
|
DoubleToString(sessionMemory[s].avgProfit, 2) + " | Trades " +
|
|
IntegerToString(sessionMemory[s].totalTrades) + "\n";
|
|
}
|
|
}
|
|
|
|
return report;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void PrintLearningTelemetry()
|
|
{
|
|
int trades = ArraySize(tradeHistory);
|
|
int patterns = ArraySize(patternHistory);
|
|
int setups = ArraySize(setupMemory);
|
|
int regimes = ArraySize(regimeHistory);
|
|
|
|
double allowRate = (memoryDecisionTotal > 0) ? ((double)memoryDecisionAllow * 100.0 / (double)memoryDecisionTotal) : 0.0;
|
|
double blockRate = (memoryDecisionTotal > 0) ? ((double)memoryDecisionBlock * 100.0 / (double)memoryDecisionTotal) : 0.0;
|
|
double exploreRate = (memoryDecisionTotal > 0) ? ((double)memoryDecisionExplore * 100.0 / (double)memoryDecisionTotal) : 0.0;
|
|
|
|
double bestReward = -DBL_MAX;
|
|
double worstReward = DBL_MAX;
|
|
int bestIdx = -1;
|
|
int worstIdx = -1;
|
|
double rewardSum = 0.0;
|
|
int rewardCount = 0;
|
|
|
|
for(int i = 0; i < setups; i++)
|
|
{
|
|
rewardSum += setupMemory[i].rewardScore;
|
|
rewardCount++;
|
|
|
|
if(setupMemory[i].rewardScore > bestReward)
|
|
{
|
|
bestReward = setupMemory[i].rewardScore;
|
|
bestIdx = i;
|
|
}
|
|
if(setupMemory[i].rewardScore < worstReward)
|
|
{
|
|
worstReward = setupMemory[i].rewardScore;
|
|
worstIdx = i;
|
|
}
|
|
}
|
|
|
|
double avgReward = (rewardCount > 0) ? (rewardSum / (double)rewardCount) : 0.0;
|
|
|
|
Print("LEARNING TELEMETRY | Trades=", trades,
|
|
" | PatternMem=", patterns,
|
|
" | SetupMem=", setups,
|
|
" | Regimes=", regimes,
|
|
" | DynamicPatterns=", dynamicPatternCount,
|
|
" | RewardUpdates=", setupRewardUpdates);
|
|
|
|
Print("LEARNING DECISIONS | Total=", memoryDecisionTotal,
|
|
" | Allow=", memoryDecisionAllow, " (", DoubleToString(allowRate, 1), "%)",
|
|
" | Block=", memoryDecisionBlock, " (", DoubleToString(blockRate, 1), "%)",
|
|
" | Explore=", memoryDecisionExplore, " (", DoubleToString(exploreRate, 1), "%)");
|
|
|
|
Print("REWARD STATUS | Avg=", DoubleToString(avgReward, 3),
|
|
" | Clamp=[", DoubleToString(SetupRewardClampMin, 2), ",", DoubleToString(SetupRewardClampMax, 2), "]");
|
|
|
|
if(bestIdx >= 0)
|
|
Print("REWARD BEST | Dir=", setupMemory[bestIdx].direction,
|
|
" | BarsBucket=", setupMemory[bestIdx].barsBucket,
|
|
" | VolBucket=", setupMemory[bestIdx].volumeBucket,
|
|
" | EntryType=", (setupMemory[bestIdx].entryType == 1 ? "SMART_LIMIT" : "MARKET"),
|
|
" | Regime=", setupMemory[bestIdx].regime,
|
|
" | Score=", DoubleToString(setupMemory[bestIdx].rewardScore, 3),
|
|
" | Trades=", setupMemory[bestIdx].totalTrades,
|
|
" | WinRate=", DoubleToString((setupMemory[bestIdx].totalTrades > 0) ? ((double)setupMemory[bestIdx].winningTrades * 100.0 / (double)setupMemory[bestIdx].totalTrades) : 0.0, 1), "%");
|
|
|
|
if(worstIdx >= 0)
|
|
Print("REWARD WORST | Dir=", setupMemory[worstIdx].direction,
|
|
" | BarsBucket=", setupMemory[worstIdx].barsBucket,
|
|
" | VolBucket=", setupMemory[worstIdx].volumeBucket,
|
|
" | EntryType=", (setupMemory[worstIdx].entryType == 1 ? "SMART_LIMIT" : "MARKET"),
|
|
" | Regime=", setupMemory[worstIdx].regime,
|
|
" | Score=", DoubleToString(setupMemory[worstIdx].rewardScore, 3),
|
|
" | Trades=", setupMemory[worstIdx].totalTrades,
|
|
" | WinRate=", DoubleToString((setupMemory[worstIdx].totalTrades > 0) ? ((double)setupMemory[worstIdx].winningTrades * 100.0 / (double)setupMemory[worstIdx].totalTrades) : 0.0, 1), "%");
|
|
|
|
if(UseBestSessionsEngine)
|
|
{
|
|
int minTrades = MathMax(1, BestSessionMinTrades);
|
|
for(int s = 0; s < 4; s++)
|
|
{
|
|
Print("SESSION TELEMETRY | ", sessionMemory[s].name,
|
|
" | Trades=", sessionMemory[s].totalTrades,
|
|
" | WinRate=", DoubleToString(sessionMemory[s].winRate, 1), "%",
|
|
" | AvgProfit=$", DoubleToString(sessionMemory[s].avgProfit, 2),
|
|
" | Optimal=", (sessionMemory[s].isOptimal ? "YES" : "NO"),
|
|
" | MinTrades=", minTrades);
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int GetBarsBucket(int consecutiveBars)
|
|
{
|
|
if(consecutiveBars <= 0)
|
|
return 0;
|
|
if(consecutiveBars >= 10)
|
|
return 10;
|
|
return consecutiveBars;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int GetVolumeBucket(double volumeRatio)
|
|
{
|
|
if(volumeRatio < 0.8)
|
|
return 0;
|
|
if(volumeRatio < 1.0)
|
|
return 1;
|
|
if(volumeRatio < 1.3)
|
|
return 2;
|
|
if(volumeRatio < 1.8)
|
|
return 3;
|
|
return 4;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int FindOrCreateSetupBucket(int direction, int barsBucket, int volumeBucket, string regime, int entryType = 0)
|
|
{
|
|
for(int i = 0; i < ArraySize(setupMemory); i++)
|
|
{
|
|
if(setupMemory[i].direction == direction &&
|
|
setupMemory[i].barsBucket == barsBucket &&
|
|
setupMemory[i].volumeBucket == volumeBucket &&
|
|
setupMemory[i].entryType == entryType &&
|
|
setupMemory[i].regime == regime)
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
|
|
int size = ArraySize(setupMemory);
|
|
ArrayResize(setupMemory, size + 1);
|
|
setupMemory[size].direction = direction;
|
|
setupMemory[size].barsBucket = barsBucket;
|
|
setupMemory[size].volumeBucket = volumeBucket;
|
|
setupMemory[size].entryType = (entryType != 0) ? 1 : 0;
|
|
setupMemory[size].regime = regime;
|
|
setupMemory[size].totalTrades = 0;
|
|
setupMemory[size].winningTrades = 0;
|
|
setupMemory[size].totalProfit = 0.0;
|
|
setupMemory[size].avgProfit = 0.0;
|
|
setupMemory[size].rewardScore = 0.0;
|
|
return size;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetSetupRewardScore(int direction, int barsBucket, int volumeBucket, string regime, int entryType = 0)
|
|
{
|
|
for(int i = 0; i < ArraySize(setupMemory); i++)
|
|
{
|
|
if(setupMemory[i].direction == direction &&
|
|
setupMemory[i].barsBucket == barsBucket &&
|
|
setupMemory[i].volumeBucket == volumeBucket &&
|
|
setupMemory[i].entryType == entryType &&
|
|
setupMemory[i].regime == regime)
|
|
{
|
|
return setupMemory[i].rewardScore;
|
|
}
|
|
}
|
|
return 0.0;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateSetupBucketReward(int direction, int barsBucket, int volumeBucket, string regime, double profit, int entryType = 0)
|
|
{
|
|
int idx = FindOrCreateSetupBucket(direction, barsBucket, volumeBucket, regime, entryType);
|
|
setupRewardUpdates++;
|
|
setupMemory[idx].totalTrades++;
|
|
setupMemory[idx].totalProfit += profit;
|
|
if(profit > 0.0)
|
|
{
|
|
setupMemory[idx].winningTrades++;
|
|
setupMemory[idx].rewardScore += RewardStep;
|
|
}
|
|
else
|
|
if(profit < 0.0)
|
|
{
|
|
setupMemory[idx].rewardScore -= PenaltyStep;
|
|
}
|
|
|
|
setupMemory[idx].avgProfit = setupMemory[idx].totalProfit / setupMemory[idx].totalTrades;
|
|
setupMemory[idx].rewardScore = ClampValue(setupMemory[idx].rewardScore, SetupRewardClampMin, SetupRewardClampMax);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void ApplyRewardPenaltyToPattern(string symbol, int direction, int consecutiveBars, double profit)
|
|
{
|
|
int patternType = (direction == 1) ? 1 : 2;
|
|
string targetSymbol = symbol;
|
|
if(StringLen(targetSymbol) <= 0)
|
|
targetSymbol = _Symbol;
|
|
for(int i = 0; i < ArraySize(patternHistory); i++)
|
|
{
|
|
if(patternHistory[i].symbol == targetSymbol &&
|
|
patternHistory[i].patternType == patternType &&
|
|
MathAbs(patternHistory[i].consecutiveBars - consecutiveBars) <= 1)
|
|
{
|
|
if(profit > 0.0)
|
|
{
|
|
patternHistory[i].confidence = ClampValue(patternHistory[i].confidence + RewardStep, 0.0, 1.0);
|
|
}
|
|
else
|
|
if(profit < 0.0)
|
|
{
|
|
patternHistory[i].confidence = ClampValue(patternHistory[i].confidence - PenaltyStep, 0.0, 1.0);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetRegimeQualityScore()
|
|
{
|
|
if(marketState.marketStress)
|
|
{
|
|
return 0.35;
|
|
}
|
|
if(marketState.currentRegime == "QUIET")
|
|
return 0.75;
|
|
if(marketState.currentRegime == "RANGING")
|
|
return 0.65;
|
|
if(marketState.currentRegime == "VOLATILE")
|
|
return 0.45;
|
|
return 0.55;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void CalculateVolumeProfileForSymbol(string symbol, ENUM_TIMEFRAMES tf, double &liquidityZones[])
|
|
{
|
|
ArrayResize(liquidityZones, 0);
|
|
|
|
if(!UseVolumeProfile)
|
|
return;
|
|
|
|
int totalBars = Bars(symbol, tf);
|
|
int numBars = MathMin(VolumeProfileBars, totalBars);
|
|
if(numBars < 10)
|
|
return;
|
|
|
|
double minPrice = DBL_MAX;
|
|
double maxPrice = -DBL_MAX;
|
|
|
|
for(int i = 1; i <= numBars; i++)
|
|
{
|
|
double high = iHigh(symbol, tf, i);
|
|
double low = iLow(symbol, tf, i);
|
|
if(high <= 0.0 || low <= 0.0)
|
|
continue;
|
|
if(high > maxPrice)
|
|
maxPrice = high;
|
|
if(low < minPrice)
|
|
minPrice = low;
|
|
}
|
|
|
|
if(minPrice == DBL_MAX || maxPrice == -DBL_MAX)
|
|
return;
|
|
|
|
double priceRange = maxPrice - minPrice;
|
|
if(priceRange <= 0.0)
|
|
return;
|
|
|
|
int numBuckets = 50;
|
|
double bucketSize = priceRange / numBuckets;
|
|
if(bucketSize <= 0.0)
|
|
return;
|
|
|
|
double volumeBuckets[];
|
|
double priceBuckets[];
|
|
ArrayResize(volumeBuckets, numBuckets);
|
|
ArrayResize(priceBuckets, numBuckets);
|
|
ArrayInitialize(volumeBuckets, 0.0);
|
|
|
|
for(int i = 0; i < numBuckets; i++)
|
|
priceBuckets[i] = minPrice + (i * bucketSize) + (bucketSize / 2.0);
|
|
|
|
for(int i = 1; i <= numBars; i++)
|
|
{
|
|
double high = iHigh(symbol, tf, i);
|
|
double low = iLow(symbol, tf, i);
|
|
long volume = UseTickVolume ? iTickVolume(symbol, tf, i) : iVolume(symbol, tf, i);
|
|
if(volume <= 0 || high <= 0.0 || low <= 0.0)
|
|
continue;
|
|
|
|
for(int j = 0; j < numBuckets; j++)
|
|
{
|
|
double bucketPrice = priceBuckets[j];
|
|
if(bucketPrice >= low && bucketPrice <= high)
|
|
volumeBuckets[j] += (double)volume;
|
|
}
|
|
}
|
|
|
|
double totalVolume = 0.0;
|
|
for(int i = 0; i < numBuckets; i++)
|
|
totalVolume += volumeBuckets[i];
|
|
|
|
double avgVolume = (numBuckets > 0) ? (totalVolume / numBuckets) : 0.0;
|
|
if(avgVolume <= 0.0)
|
|
return;
|
|
|
|
for(int i = 0; i < numBuckets; i++)
|
|
{
|
|
if(volumeBuckets[i] >= avgVolume * 1.5)
|
|
{
|
|
int size = ArraySize(liquidityZones);
|
|
ArrayResize(liquidityZones, size + 1);
|
|
liquidityZones[size] = priceBuckets[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetVolumeProfileProximityScoreForSymbol(string symbol, int direction, double referencePrice)
|
|
{
|
|
if(!UseSmartEntry || !UseVolumeProfile)
|
|
{
|
|
return 0.5;
|
|
}
|
|
|
|
double symbolPoint = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
double pointDistance = point_value;
|
|
if(pointDistance <= 0.0)
|
|
pointDistance = symbolPoint;
|
|
if(pointDistance <= 0.0)
|
|
return 0.5;
|
|
|
|
double zones[];
|
|
ENUM_TIMEFRAMES tf = (ENUM_TIMEFRAMES)Period();
|
|
CalculateVolumeProfileForSymbol(symbol, tf, zones);
|
|
|
|
if(ArraySize(zones) == 0)
|
|
{
|
|
return 0.5;
|
|
}
|
|
|
|
double minDistancePoints = DBL_MAX;
|
|
for(int i = 0; i < ArraySize(zones); i++)
|
|
{
|
|
bool validSide = (direction == 1) ? (zones[i] < referencePrice) : (zones[i] > referencePrice);
|
|
if(!validSide)
|
|
continue;
|
|
|
|
double distancePoints = MathAbs(zones[i] - referencePrice) / pointDistance;
|
|
if(distancePoints < minDistancePoints)
|
|
{
|
|
minDistancePoints = distancePoints;
|
|
}
|
|
}
|
|
|
|
double maxDistancePoints = GetLiquidityZoneDistancePointsForSymbol(symbol);
|
|
if(minDistancePoints == DBL_MAX || maxDistancePoints <= 0.0)
|
|
{
|
|
return 0.5;
|
|
}
|
|
if(minDistancePoints > maxDistancePoints)
|
|
{
|
|
return 0.0;
|
|
}
|
|
|
|
return ClampValue(1.0 - (minDistancePoints / maxDistancePoints), 0.0, 1.0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetVolumeProfileProximityScore(int direction, double referencePrice)
|
|
{
|
|
return GetVolumeProfileProximityScoreForSymbol(Symbol(), direction, referencePrice);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int GetDecisionScore10ForSymbol(string symbol, int direction, int consecutiveBars, double volumeRatio, double bid, double ask, string &details)
|
|
{
|
|
double memoryConfidence = GetMemoryBasedConfidenceForSymbol(symbol, direction, consecutiveBars, volumeRatio);
|
|
int barsTrigger = (direction == 1) ? MathMax(BearishBarsForBuy, 1) : MathMax(BullishBarsForSell, 1);
|
|
double barsStrength = ClampValue((double)consecutiveBars / (double)barsTrigger, 0.0, 1.0);
|
|
|
|
double volumeStrength = 0.0;
|
|
if(StrongMomentumThreshold > MinVolumeRatio)
|
|
{
|
|
volumeStrength = ClampValue((volumeRatio - MinVolumeRatio) / (StrongMomentumThreshold - MinVolumeRatio), 0.0, 1.0);
|
|
}
|
|
else
|
|
{
|
|
volumeStrength = (volumeRatio >= MinVolumeRatio) ? 1.0 : 0.0;
|
|
}
|
|
|
|
double referencePrice = (direction == 1) ? ask : bid;
|
|
double zoneStrength = GetVolumeProfileProximityScoreForSymbol(symbol, direction, referencePrice);
|
|
double regimeStrength = GetRegimeQualityScore();
|
|
int barsBucket = GetBarsBucket(consecutiveBars);
|
|
int volumeBucket = GetVolumeBucket(volumeRatio);
|
|
double rewardRaw = GetSetupRewardScore(direction, barsBucket, volumeBucket, marketState.currentRegime);
|
|
double rewardNorm = (rewardRaw - SetupRewardClampMin) / MathMax(SetupRewardClampMax - SetupRewardClampMin, 0.0001);
|
|
rewardNorm = ClampValue(rewardNorm, 0.0, 1.0);
|
|
|
|
double score01 = (memoryConfidence * 0.30) +
|
|
(barsStrength * 0.15) +
|
|
(volumeStrength * 0.20) +
|
|
(zoneStrength * 0.15) +
|
|
(regimeStrength * 0.10) +
|
|
(rewardNorm * 0.10);
|
|
|
|
if(marketState.marketStress)
|
|
{
|
|
// In stress regimes, treat valid opportunities as slightly higher quality
|
|
// so the system can actively hunt recovery trades instead of freezing.
|
|
score01 *= 1.05;
|
|
}
|
|
|
|
int score10 = (int)MathRound(ClampValue(score01, 0.0, 1.0) * 10.0);
|
|
score10 = (int)ClampValue((double)score10, 1.0, 10.0);
|
|
|
|
details = "mem=" + DoubleToString(memoryConfidence * 10.0, 1) +
|
|
" bars=" + DoubleToString(barsStrength * 10.0, 1) +
|
|
" vol=" + DoubleToString(volumeStrength * 10.0, 1) +
|
|
" zone=" + DoubleToString(zoneStrength * 10.0, 1) +
|
|
" regime=" + DoubleToString(regimeStrength * 10.0, 1) +
|
|
" reward=" + DoubleToString(rewardNorm * 10.0, 1);
|
|
return score10;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int GetDecisionScore10(int direction, int consecutiveBars, double volumeRatio, double bid, double ask, string &details)
|
|
{
|
|
return GetDecisionScore10ForSymbol(Symbol(), direction, consecutiveBars, volumeRatio, bid, ask, details);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool ShouldAllowControlledExplore(int decisionScore, int threshold)
|
|
{
|
|
if(!UseControlledExplore)
|
|
return false;
|
|
|
|
if(IsSmallAccountForAutoProtection() && !AllowExploreOnSmallAccounts)
|
|
return false;
|
|
|
|
int buffer = MathMax(1, ExploreScoreBuffer);
|
|
int minExploreScore = MathMax(1, threshold - buffer);
|
|
if(decisionScore < minExploreScore || decisionScore >= threshold)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Large, stable accounts: auto-enable controlled explore.
|
|
if(IsLargeAccountForAutoExplore())
|
|
{
|
|
int fastN = MathMax(2, ExploreOneInN - 1);
|
|
exploreSignalCounter++;
|
|
return (exploreSignalCounter % fastN) == 0;
|
|
}
|
|
|
|
if(marketState.consecutiveLosses < ExploreLossStreakTrigger)
|
|
return false;
|
|
if(ExploreOneInN <= 1)
|
|
return false;
|
|
|
|
exploreSignalCounter++;
|
|
return (exploreSignalCounter % ExploreOneInN) == 0;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int IncrementExploreCounterForSymbol(string symbol)
|
|
{
|
|
static string exploreSymbols[];
|
|
static int exploreCounters[];
|
|
|
|
for(int i = 0; i < ArraySize(exploreSymbols); i++)
|
|
{
|
|
if(exploreSymbols[i] == symbol)
|
|
{
|
|
exploreCounters[i]++;
|
|
return exploreCounters[i];
|
|
}
|
|
}
|
|
|
|
int idx = ArraySize(exploreSymbols);
|
|
ArrayResize(exploreSymbols, idx + 1);
|
|
ArrayResize(exploreCounters, idx + 1);
|
|
exploreSymbols[idx] = symbol;
|
|
exploreCounters[idx] = 1;
|
|
return exploreCounters[idx];
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int NextDecisionSetupId()
|
|
{
|
|
static int setupIdSeq = 1;
|
|
int nextId = setupIdSeq;
|
|
setupIdSeq++;
|
|
if(setupIdSeq <= 0)
|
|
setupIdSeq = 1;
|
|
return nextId;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void SetPendingDecisionActionForSymbol(string symbol, int action, int setupId = 0)
|
|
{
|
|
action = (int)ClampValue((double)action, 0.0, 2.0);
|
|
setupId = (int)ClampValue((double)setupId, 0.0, 2147483647.0);
|
|
for(int i = 0; i < ArraySize(pendingDecisionSymbols); i++)
|
|
{
|
|
if(pendingDecisionSymbols[i] == symbol)
|
|
{
|
|
pendingDecisionActions[i] = action;
|
|
pendingDecisionSetupIds[i] = setupId;
|
|
return;
|
|
}
|
|
}
|
|
|
|
int idx = ArraySize(pendingDecisionSymbols);
|
|
ArrayResize(pendingDecisionSymbols, idx + 1);
|
|
ArrayResize(pendingDecisionActions, idx + 1);
|
|
ArrayResize(pendingDecisionSetupIds, idx + 1);
|
|
pendingDecisionSymbols[idx] = symbol;
|
|
pendingDecisionActions[idx] = action;
|
|
pendingDecisionSetupIds[idx] = setupId;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int ConsumePendingDecisionActionForSymbol(string symbol, int &setupId)
|
|
{
|
|
setupId = 0;
|
|
for(int i = 0; i < ArraySize(pendingDecisionSymbols); i++)
|
|
{
|
|
if(pendingDecisionSymbols[i] == symbol)
|
|
{
|
|
int action = pendingDecisionActions[i];
|
|
setupId = (i < ArraySize(pendingDecisionSetupIds)) ? pendingDecisionSetupIds[i] : 0;
|
|
pendingDecisionActions[i] = 1; // default next action to ALLOW
|
|
if(i < ArraySize(pendingDecisionSetupIds))
|
|
pendingDecisionSetupIds[i] = 0;
|
|
return action;
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int ConsumePendingDecisionActionForSymbol(string symbol)
|
|
{
|
|
int setupId = 0;
|
|
return ConsumePendingDecisionActionForSymbol(symbol, setupId);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
string BuildBlockReasonSummary(const string symbol,
|
|
const double spreadPts,
|
|
const double volumeRatio,
|
|
const bool strongBar,
|
|
const double zoneScore,
|
|
const double liquidityDistanceNorm,
|
|
const double mtfConfluence,
|
|
const double drawdownPct,
|
|
const double recentWinRate,
|
|
const double sessionQuality,
|
|
const bool marketStress,
|
|
const string regimeText)
|
|
{
|
|
double penalties[11];
|
|
string labels[11];
|
|
ArrayInitialize(penalties, 0.0);
|
|
|
|
penalties[0] = marketStress ? 1.0 : 0.0;
|
|
labels[0] = "stress";
|
|
|
|
penalties[1] = ClampValue((spreadPts - 120.0) / 220.0, 0.0, 1.0);
|
|
labels[1] = "spread";
|
|
|
|
penalties[2] = ClampValue((drawdownPct - 4.0) / 12.0, 0.0, 1.0);
|
|
labels[2] = "drawdown";
|
|
|
|
penalties[3] = ClampValue((55.0 - recentWinRate) / 25.0, 0.0, 1.0);
|
|
labels[3] = "winrate";
|
|
|
|
penalties[4] = ClampValue((0.55 - sessionQuality) / 0.35, 0.0, 1.0);
|
|
labels[4] = "session";
|
|
|
|
penalties[5] = ClampValue((1.00 - volumeRatio) / 0.70, 0.0, 1.0);
|
|
labels[5] = "volume";
|
|
|
|
penalties[6] = (regimeText == "VOLATILE") ? 0.70 : ((regimeText == "QUIET") ? 0.20 : 0.0);
|
|
labels[6] = "regime";
|
|
|
|
penalties[7] = strongBar ? 0.0 : 0.60;
|
|
labels[7] = "strong_bar";
|
|
|
|
penalties[8] = ClampValue((0.55 - zoneScore) / 0.55, 0.0, 1.0);
|
|
labels[8] = "zone";
|
|
|
|
penalties[9] = ClampValue(liquidityDistanceNorm, 0.0, 1.0);
|
|
labels[9] = "liq_dist";
|
|
|
|
penalties[10] = ClampValue((0.60 - mtfConfluence) / 0.60, 0.0, 1.0);
|
|
labels[10] = "mtf";
|
|
|
|
int top1 = -1;
|
|
int top2 = -1;
|
|
for(int i = 0; i < 11; i++)
|
|
{
|
|
if(top1 == -1 || penalties[i] > penalties[top1])
|
|
{
|
|
top2 = top1;
|
|
top1 = i;
|
|
}
|
|
else
|
|
if(top2 == -1 || penalties[i] > penalties[top2])
|
|
{
|
|
top2 = i;
|
|
}
|
|
}
|
|
|
|
string r1 = (top1 >= 0) ? labels[top1] + "=" + DoubleToString(penalties[top1], 2) : "n/a";
|
|
string r2 = (top2 >= 0) ? labels[top2] + "=" + DoubleToString(penalties[top2], 2) : "n/a";
|
|
return "reasons: " + r1 + ", " + r2
|
|
+ " | spreadPts=" + DoubleToString(spreadPts, 1)
|
|
+ " dd=" + DoubleToString(drawdownPct, 2) + "%"
|
|
+ " wr=" + DoubleToString(recentWinRate, 1) + "%"
|
|
+ " sess=" + DoubleToString(sessionQuality, 2)
|
|
+ " vol=" + DoubleToString(volumeRatio, 2)
|
|
+ " strong=" + (strongBar ? "1" : "0")
|
|
+ " zone=" + DoubleToString(zoneScore, 2)
|
|
+ " liq=" + DoubleToString(liquidityDistanceNorm, 2)
|
|
+ " mtf=" + DoubleToString(mtfConfluence, 2);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool ShouldEmitFilterStatusLog(const string symbol, const string direction, bool passed, const string scope)
|
|
{
|
|
static string keys[];
|
|
static datetime bars[];
|
|
static int statuses[];
|
|
|
|
string key = scope + "|" + symbol + "|" + direction;
|
|
datetime barTime = iTime(symbol, (ENUM_TIMEFRAMES)Period(), 0);
|
|
if(barTime <= 0)
|
|
barTime = TimeCurrent();
|
|
int status = passed ? 1 : 0;
|
|
|
|
int idx = -1;
|
|
for(int i = 0; i < ArraySize(keys); i++)
|
|
{
|
|
if(keys[i] == key)
|
|
{
|
|
idx = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(idx < 0)
|
|
{
|
|
int newIdx = ArraySize(keys);
|
|
ArrayResize(keys, newIdx + 1);
|
|
ArrayResize(bars, newIdx + 1);
|
|
ArrayResize(statuses, newIdx + 1);
|
|
keys[newIdx] = key;
|
|
bars[newIdx] = barTime;
|
|
statuses[newIdx] = status;
|
|
return true;
|
|
}
|
|
|
|
bool barChanged = (bars[idx] != barTime);
|
|
bool statusChanged = (statuses[idx] != status);
|
|
if(barChanged || statusChanged)
|
|
{
|
|
bars[idx] = barTime;
|
|
statuses[idx] = status;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool ShouldAllowControlledExploreForSymbol(string symbol, int decisionScore, int threshold)
|
|
{
|
|
if(!UseControlledExplore)
|
|
return false;
|
|
|
|
if(IsSmallAccountForAutoProtection() && !AllowExploreOnSmallAccounts)
|
|
return false;
|
|
|
|
int buffer = MathMax(1, ExploreScoreBuffer);
|
|
int minExploreScore = MathMax(1, threshold - buffer);
|
|
if(decisionScore < minExploreScore || decisionScore >= threshold)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Large, stable accounts: auto-enable controlled explore.
|
|
if(IsLargeAccountForAutoExplore())
|
|
{
|
|
int fastN = MathMax(2, ExploreOneInN - 1);
|
|
int symbolCounterLarge = IncrementExploreCounterForSymbol(symbol);
|
|
return (symbolCounterLarge % fastN) == 0;
|
|
}
|
|
|
|
// Bootstrap phase: allow sparse exploration so DLL can gather initial outcomes faster.
|
|
if(ArraySize(tradeHistory) < 20)
|
|
{
|
|
int bootstrapN = MathMax(2, ExploreOneInN - 1);
|
|
int symbolCounterBootstrap = IncrementExploreCounterForSymbol(symbol);
|
|
return (symbolCounterBootstrap % bootstrapN) == 0;
|
|
}
|
|
|
|
if(marketState.consecutiveLosses < ExploreLossStreakTrigger)
|
|
return false;
|
|
if(ExploreOneInN <= 1)
|
|
return false;
|
|
|
|
int symbolCounter = IncrementExploreCounterForSymbol(symbol);
|
|
return (symbolCounter % ExploreOneInN) == 0;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool ShouldSkipTradeBasedOnMemoryForSymbol(string symbol, int direction, int consecutiveBars, double volumeRatio, double bid, double ask, bool &useExploreLot, int &decisionScore)
|
|
{
|
|
memoryDecisionTotal++;
|
|
|
|
useExploreLot = false;
|
|
|
|
int baseThreshold = marketState.marketStress ? DecisionScoreThresholdStress : DecisionScoreThresholdNormal;
|
|
int autoThreshold = GetEffectiveDecisionThresholdAuto(baseThreshold);
|
|
int threshold = MathMax(baseThreshold, autoThreshold);
|
|
|
|
// Keep memory gate active in warmup, but slightly softer until enough samples are collected.
|
|
if(ArraySize(tradeHistory) < 20)
|
|
threshold = MathMax(3, threshold - 1);
|
|
|
|
bool smallAccount = IsSmallAccountForAutoProtection();
|
|
|
|
// For very small accounts where we explicitly allow exploration, slightly soften the gate.
|
|
if(smallAccount && AllowExploreOnSmallAccounts)
|
|
threshold = MathMax(2, threshold - 2);
|
|
|
|
if(UseDecisionCoreDll && g_decisionDllReady)
|
|
{
|
|
DC_SetupFeatures features;
|
|
int decisionSetupId = NextDecisionSetupId();
|
|
MqlDateTime nowDt;
|
|
TimeToStruct(GetCurrentTimeWithOffset(), nowDt);
|
|
double symbolPoint = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
if(symbolPoint <= 0.0)
|
|
symbolPoint = Point();
|
|
double spreadPts = (symbolPoint > 0.0) ? (MathAbs(ask - bid) / symbolPoint) : 0.0;
|
|
double recentWr = GetRecentWinRateAuto(30);
|
|
if(recentWr < 0.0)
|
|
recentWr = 50.0;
|
|
bool strongBar = (volumeRatio >= VolumeSignalFactor);
|
|
double referencePrice = (direction == 1) ? ask : bid;
|
|
double zoneScore = GetVolumeProfileProximityScoreForSymbol(symbol, direction, referencePrice);
|
|
double liqDistPts = 0.0;
|
|
FindNearestLiquidityZone(referencePrice, (direction == 1) ? POSITION_TYPE_BUY : POSITION_TYPE_SELL, liqDistPts);
|
|
double maxLiqDistancePts = GetLiquidityZoneDistancePointsForSymbol(symbol);
|
|
double liquidityDistanceNorm = (maxLiqDistancePts > 0.0) ? ClampValue(liqDistPts / maxLiqDistancePts, 0.0, 1.0) : 1.0;
|
|
int dirStreakMtf = (direction == 1) ? GetConsecutiveBarsMultiTFForSymbol(symbol, false) : GetConsecutiveBarsMultiTFForSymbol(symbol, true);
|
|
double mtfConfluence = ClampValue((double)dirStreakMtf / 5.0, 0.0, 1.0);
|
|
double dllTrendStrength = marketState.priceTrend;
|
|
if(PreferConsecutiveBarsForDll)
|
|
{
|
|
double localStreakNorm = ClampValue((double)consecutiveBars / 5.0, 0.0, 1.0);
|
|
double combinedStreak = ClampValue(localStreakNorm * 0.7 + mtfConfluence * 0.3, 0.0, 1.0);
|
|
double dirSign = (direction >= 0) ? 1.0 : -1.0;
|
|
dllTrendStrength = dirSign * (combinedStreak * 20.0);
|
|
}
|
|
double drawdownPct = GetAccountDrawdownPercentAuto();
|
|
double sessionQuality = GetCurrentSessionConfidence();
|
|
double rewardScore = GetSetupRewardScore(direction, GetBarsBucket(consecutiveBars), GetVolumeBucket(volumeRatio), marketState.currentRegime);
|
|
double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
double equityToBalance = (accountBalance > 0.0) ? (accountEquity / accountBalance) : 1.0;
|
|
double equityDdPct = (accountBalance > 0.0) ? ClampValue(((accountBalance - accountEquity) / accountBalance) * 100.0, 0.0, 100.0) : 0.0;
|
|
double avgTickVol = GetAverageTickVolumeForSymbol(symbol, (ENUM_TIMEFRAMES)Period(), VolumeSignalLookback);
|
|
double tickVolumeRatio = (avgTickVol > 0.0) ? ((double)iTickVolume(symbol, (ENUM_TIMEFRAMES)Period(), 0) / avgTickVol) : 1.0;
|
|
int bullishStreak = GetConsecutiveBarsForSymbol(symbol, (ENUM_TIMEFRAMES)Period(), true);
|
|
int bearishStreak = GetConsecutiveBarsForSymbol(symbol, (ENUM_TIMEFRAMES)Period(), false);
|
|
int streakDir = 0;
|
|
int streakLen = 0;
|
|
if(bullishStreak > bearishStreak && bullishStreak > 0)
|
|
{
|
|
streakDir = 1;
|
|
streakLen = bullishStreak;
|
|
}
|
|
else
|
|
if(bearishStreak > bullishStreak && bearishStreak > 0)
|
|
{
|
|
streakDir = -1;
|
|
streakLen = bearishStreak;
|
|
}
|
|
DC_FillSetupFeaturesSafe(features,
|
|
symbol,
|
|
(int)Period(),
|
|
direction,
|
|
nowDt.hour,
|
|
nowDt.day_of_week,
|
|
marketState.marketStress ? 1 : 0,
|
|
spreadPts,
|
|
volumeRatio,
|
|
consecutiveBars,
|
|
marketState.currentVolatility,
|
|
dllTrendStrength,
|
|
drawdownPct,
|
|
recentWr,
|
|
sessionQuality,
|
|
rewardScore,
|
|
strongBar ? 1 : 0,
|
|
zoneScore,
|
|
liquidityDistanceNorm,
|
|
mtfConfluence,
|
|
decisionSetupId,
|
|
equityDdPct,
|
|
equityToBalance,
|
|
tickVolumeRatio,
|
|
streakDir,
|
|
streakLen);
|
|
|
|
DC_DecisionOut dOut;
|
|
int dcRc = 0;
|
|
bool scoreOk = DC_ScoreSafe(features, dOut, dcRc);
|
|
if(scoreOk)
|
|
{
|
|
decisionScore = (int)MathRound(ClampValue(dOut.decision_score_01, 0.0, 1.0) * 10.0);
|
|
decisionScore = (int)ClampValue((double)decisionScore, 1.0, 10.0);
|
|
useExploreLot = (dOut.action == 2);
|
|
memoryAdaptiveRiskMultiplier = ClampValue(dOut.risk_multiplier, 0.15, 1.25);
|
|
SetPendingDecisionActionForSymbol(symbol, dOut.action, decisionSetupId);
|
|
|
|
string regimeText = DC_BytesToString(dOut.regime);
|
|
string reasonSummary = BuildBlockReasonSummary(symbol,
|
|
spreadPts,
|
|
volumeRatio,
|
|
strongBar,
|
|
zoneScore,
|
|
liquidityDistanceNorm,
|
|
mtfConfluence,
|
|
features.drawdown_pct,
|
|
features.recent_win_rate,
|
|
features.session_quality,
|
|
(features.market_stress != 0),
|
|
regimeText);
|
|
g_lastDllDecisionAction = DC_ActionToText(dOut.action);
|
|
g_lastDllDecisionSymbol = symbol;
|
|
g_lastDllDecisionRegime = regimeText;
|
|
g_lastDllDecisionScore = decisionScore;
|
|
g_lastDllDecisionAllowProb = dOut.allow_probability;
|
|
g_lastDllDecisionRiskMul = memoryAdaptiveRiskMultiplier;
|
|
g_lastDllDecisionLatencyUs = dOut.latency_us;
|
|
bool thresholdPass = (decisionScore >= threshold);
|
|
|
|
if(!thresholdPass)
|
|
{
|
|
if(ShouldAllowControlledExploreForSymbol(symbol, decisionScore, threshold))
|
|
{
|
|
useExploreLot = true;
|
|
memoryDecisionAllow++;
|
|
memoryDecisionExplore++;
|
|
memoryAdaptiveRiskMultiplier = MathMin(memoryAdaptiveRiskMultiplier, 0.35);
|
|
SetPendingDecisionActionForSymbol(symbol, 2, decisionSetupId);
|
|
Print("DLL DECISION [", symbol, "] | ALLOW_EXPLORE | score=", decisionScore, "/10",
|
|
" | threshold=", threshold,
|
|
" | threshold_pass=0",
|
|
" | p_allow=", DoubleToString(dOut.allow_probability, 3),
|
|
" | regime=", regimeText,
|
|
" | latency_us=", DoubleToString(dOut.latency_us, 0),
|
|
" | mode=below_threshold");
|
|
return false;
|
|
}
|
|
|
|
memoryDecisionBlock++;
|
|
SetPendingDecisionActionForSymbol(symbol, 0, decisionSetupId);
|
|
Print("DLL DECISION [", symbol, "] | BLOCK | score=", decisionScore, "/10",
|
|
" | threshold=", threshold,
|
|
" | threshold_pass=0",
|
|
" | p_allow=", DoubleToString(dOut.allow_probability, 3),
|
|
" | regime=", regimeText,
|
|
" | latency_us=", DoubleToString(dOut.latency_us, 0),
|
|
" | ", reasonSummary);
|
|
return true;
|
|
}
|
|
|
|
if(dOut.action == 0)
|
|
{
|
|
int triggerBars = (direction == 1) ? BearishBarsForBuy : BullishBarsForSell;
|
|
double minBootstrapVolume = MathMax(0.2, MinVolumeRatio * 0.75);
|
|
bool bootstrapSignalReady = (ArraySize(tradeHistory) < 20 &&
|
|
consecutiveBars >= triggerBars &&
|
|
volumeRatio >= minBootstrapVolume);
|
|
if(bootstrapSignalReady)
|
|
{
|
|
int bootstrapSignalN = MathMax(2, ExploreOneInN - 1);
|
|
int bootstrapSignalCounter = IncrementExploreCounterForSymbol(symbol);
|
|
if((bootstrapSignalCounter % bootstrapSignalN) == 0)
|
|
{
|
|
useExploreLot = true;
|
|
memoryDecisionAllow++;
|
|
memoryDecisionExplore++;
|
|
memoryAdaptiveRiskMultiplier = MathMin(memoryAdaptiveRiskMultiplier, 0.35);
|
|
SetPendingDecisionActionForSymbol(symbol, 2, decisionSetupId);
|
|
Print("DLL DECISION [", symbol, "] | ALLOW_EXPLORE | score=", decisionScore, "/10",
|
|
" | threshold=", threshold,
|
|
" | p_allow=", DoubleToString(dOut.allow_probability, 3),
|
|
" | regime=", regimeText,
|
|
" | latency_us=", DoubleToString(dOut.latency_us, 0),
|
|
" | mode=bootstrap_consecutive_signal");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool canExploreFromProb = UseControlledExplore && (!smallAccount || AllowExploreOnSmallAccounts);
|
|
if(canExploreFromProb && dOut.allow_probability >= DllExploreProbThreshold)
|
|
{
|
|
useExploreLot = true;
|
|
memoryDecisionAllow++;
|
|
memoryDecisionExplore++;
|
|
memoryAdaptiveRiskMultiplier = MathMin(memoryAdaptiveRiskMultiplier, 0.35);
|
|
SetPendingDecisionActionForSymbol(symbol, 2, decisionSetupId);
|
|
Print("DLL DECISION [", symbol, "] | ALLOW_EXPLORE | score=", decisionScore, "/10",
|
|
" | threshold=", threshold,
|
|
" | p_allow=", DoubleToString(dOut.allow_probability, 3),
|
|
" | regime=", regimeText,
|
|
" | latency_us=", DoubleToString(dOut.latency_us, 0),
|
|
" | mode=p_allow_override");
|
|
return false;
|
|
}
|
|
|
|
if(ShouldAllowControlledExploreForSymbol(symbol, decisionScore, threshold))
|
|
{
|
|
useExploreLot = true;
|
|
memoryDecisionAllow++;
|
|
memoryDecisionExplore++;
|
|
memoryAdaptiveRiskMultiplier = MathMin(memoryAdaptiveRiskMultiplier, 0.35);
|
|
SetPendingDecisionActionForSymbol(symbol, 2, decisionSetupId);
|
|
Print("DLL DECISION [", symbol, "] | ALLOW_EXPLORE | score=", decisionScore, "/10",
|
|
" | threshold=", threshold,
|
|
" | p_allow=", DoubleToString(dOut.allow_probability, 3),
|
|
" | regime=", regimeText,
|
|
" | latency_us=", DoubleToString(dOut.latency_us, 0),
|
|
" | mode=bootstrap_memory");
|
|
return false;
|
|
}
|
|
|
|
memoryDecisionBlock++;
|
|
SetPendingDecisionActionForSymbol(symbol, 0, decisionSetupId);
|
|
Print("DLL DECISION [", symbol, "] | BLOCK | score=", decisionScore, "/10",
|
|
" | threshold=", threshold,
|
|
" | p_allow=", DoubleToString(dOut.allow_probability, 3),
|
|
" | regime=", regimeText,
|
|
" | latency_us=", DoubleToString(dOut.latency_us, 0),
|
|
" | ", reasonSummary);
|
|
return true;
|
|
}
|
|
|
|
memoryDecisionAllow++;
|
|
if(dOut.action == 2)
|
|
memoryDecisionExplore++;
|
|
|
|
Print("DLL DECISION [", symbol, "] | ", DC_ActionToText(dOut.action),
|
|
" | score=", decisionScore, "/10",
|
|
" | threshold=", threshold,
|
|
" | threshold_pass=", (thresholdPass ? "1" : "0"),
|
|
" | p_allow=", DoubleToString(dOut.allow_probability, 3),
|
|
" | riskMul=", DoubleToString(memoryAdaptiveRiskMultiplier, 2),
|
|
" | regime=", regimeText,
|
|
" | latency_us=", DoubleToString(dOut.latency_us, 0));
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
Print("DLL DECISION [", symbol, "] | INVALID_SCORE | rc=", dcRc,
|
|
" | fallback=memory_score");
|
|
}
|
|
}
|
|
|
|
string scoreDetails = "";
|
|
decisionScore = GetDecisionScore10ForSymbol(symbol, direction, consecutiveBars, volumeRatio, bid, ask, scoreDetails);
|
|
|
|
if(decisionScore >= threshold)
|
|
{
|
|
memoryDecisionAllow++;
|
|
SetPendingDecisionActionForSymbol(symbol, 1);
|
|
Print("MEMORY SCORE: ", decisionScore, "/10 | threshold ", threshold,
|
|
" | regime ", marketState.currentRegime, " | decision: ALLOW | ", scoreDetails);
|
|
return false;
|
|
}
|
|
|
|
if(ShouldAllowControlledExploreForSymbol(symbol, decisionScore, threshold))
|
|
{
|
|
useExploreLot = true;
|
|
memoryDecisionAllow++;
|
|
memoryDecisionExplore++;
|
|
SetPendingDecisionActionForSymbol(symbol, 2);
|
|
Print("MEMORY SCORE: ", decisionScore, "/10 | threshold ", threshold,
|
|
" | regime ", marketState.currentRegime, " | decision: ALLOW_EXPLORE (reduced lot) | ", scoreDetails);
|
|
return false;
|
|
}
|
|
|
|
memoryDecisionBlock++;
|
|
SetPendingDecisionActionForSymbol(symbol, 0);
|
|
double symbolPointFallback = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
if(symbolPointFallback <= 0.0)
|
|
symbolPointFallback = Point();
|
|
double spreadPtsFallback = (symbolPointFallback > 0.0) ? (MathAbs(ask - bid) / symbolPointFallback) : 0.0;
|
|
bool strongBarFallback = (volumeRatio >= VolumeSignalFactor);
|
|
double refPriceFallback = (direction == 1) ? ask : bid;
|
|
double zoneScoreFallback = GetVolumeProfileProximityScoreForSymbol(symbol, direction, refPriceFallback);
|
|
double liqDistPtsFallback = 0.0;
|
|
FindNearestLiquidityZone(refPriceFallback, (direction == 1) ? POSITION_TYPE_BUY : POSITION_TYPE_SELL, liqDistPtsFallback);
|
|
double maxLiqDistancePtsFallback = GetLiquidityZoneDistancePointsForSymbol(symbol);
|
|
double liqNormFallback = (maxLiqDistancePtsFallback > 0.0) ? ClampValue(liqDistPtsFallback / maxLiqDistancePtsFallback, 0.0, 1.0) : 1.0;
|
|
int mtfStreakFallback = (direction == 1) ? GetConsecutiveBarsMultiTFForSymbol(symbol, false) : GetConsecutiveBarsMultiTFForSymbol(symbol, true);
|
|
double mtfConfluenceFallback = ClampValue((double)mtfStreakFallback / 5.0, 0.0, 1.0);
|
|
string fallbackReasonSummary = BuildBlockReasonSummary(symbol,
|
|
spreadPtsFallback,
|
|
volumeRatio,
|
|
strongBarFallback,
|
|
zoneScoreFallback,
|
|
liqNormFallback,
|
|
mtfConfluenceFallback,
|
|
GetAccountDrawdownPercentAuto(),
|
|
MathMax(0.0, GetRecentWinRateAuto(30)),
|
|
GetCurrentSessionConfidence(),
|
|
marketState.marketStress,
|
|
marketState.currentRegime);
|
|
Print("MEMORY SCORE: ", decisionScore, "/10 | threshold ", threshold,
|
|
" | regime ", marketState.currentRegime, " | decision: BLOCK | ", scoreDetails,
|
|
" | ", fallbackReasonSummary);
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool ShouldSkipTradeBasedOnMemory(int direction, int consecutiveBars, double volumeRatio, double bid, double ask, bool &useExploreLot, int &decisionScore)
|
|
{
|
|
return ShouldSkipTradeBasedOnMemoryForSymbol(Symbol(), direction, consecutiveBars, volumeRatio, bid, ask, useExploreLot, decisionScore);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int GetConsecutiveBarsForSymbol(string symbol, ENUM_TIMEFRAMES tf, bool countBullish);
|
|
int GetConsecutiveBarsMultiTFForSymbol(string symbol, bool countBullish);
|
|
double GetCurrentVolumeRatioForSymbol(string symbol, ENUM_TIMEFRAMES tf);
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void SelectEntryProfileForSymbol(string symbol, int direction, double bid, double ask,
|
|
double &tpMultiplier, double &slMultiplier,
|
|
string &profileLabel, int &decisionScore)
|
|
{
|
|
tpMultiplier = 1.0;
|
|
slMultiplier = 1.0;
|
|
profileLabel = "NORMAL";
|
|
decisionScore = 5;
|
|
|
|
if(!UseEntryProfileTP_SL)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ENUM_TIMEFRAMES tf = (ENUM_TIMEFRAMES)Period();
|
|
int consecutiveBars = (direction == 1) ? GetConsecutiveBarsForSymbol(symbol, tf, false) : GetConsecutiveBarsForSymbol(symbol, tf, true);
|
|
double volumeRatio = GetCurrentVolumeRatioForSymbol(symbol, tf);
|
|
string details = "";
|
|
decisionScore = GetDecisionScore10ForSymbol(symbol, direction, consecutiveBars, volumeRatio, bid, ask, details);
|
|
|
|
bool longTerm = (decisionScore >= LongTermScoreThreshold &&
|
|
!marketState.marketStress &&
|
|
volumeRatio >= StrongMomentumThreshold);
|
|
|
|
bool shortTerm = (decisionScore <= ShortTermScoreThreshold ||
|
|
marketState.marketStress);
|
|
|
|
if(longTerm)
|
|
{
|
|
tpMultiplier = LongTermTPMultiplier;
|
|
slMultiplier = LongTermSLMultiplier;
|
|
profileLabel = "LONG_TERM";
|
|
}
|
|
else
|
|
if(shortTerm)
|
|
{
|
|
tpMultiplier = ShortTermTPMultiplier;
|
|
slMultiplier = ShortTermSLMultiplier;
|
|
profileLabel = "SHORT_TERM";
|
|
}
|
|
|
|
tpMultiplier = ClampValue(tpMultiplier, 0.6, 1.6);
|
|
slMultiplier = ClampValue(slMultiplier, 0.6, 1.6);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void SelectEntryProfile(int direction, double bid, double ask,
|
|
double &tpMultiplier, double &slMultiplier,
|
|
string &profileLabel, int &decisionScore)
|
|
{
|
|
SelectEntryProfileForSymbol(Symbol(), direction, bid, ask, tpMultiplier, slMultiplier, profileLabel, decisionScore);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// UTILITY FUNCTIONS - Memory System Support
|
|
//+------------------------------------------------------------------+
|
|
double GetCurrentVolumeRatio()
|
|
{
|
|
long currentVolume = UseTickVolume ? iTickVolume(Symbol(), Period(), 0) : iVolume(Symbol(), Period(), 0);
|
|
if(!UseTickVolume && currentVolume <= 0)
|
|
Print("Real volume unavailable (0) - broker did not provide volume for current bar.");
|
|
|
|
long sumVolume = 0;
|
|
int lookback = MathMin(VolumeSignalLookback, 200);
|
|
if(lookback <= 0)
|
|
return 1.3;
|
|
for(int i = 1; i <= lookback; i++)
|
|
{
|
|
sumVolume += UseTickVolume ? iTickVolume(Symbol(), Period(), i) : iVolume(Symbol(), Period(), i);
|
|
}
|
|
long avgVolume = sumVolume / lookback;
|
|
|
|
return (avgVolume > 0) ? ((double)currentVolume / avgVolume) : 1.3;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// Multi-symbol helpers (same validation pipeline per symbol)
|
|
//+------------------------------------------------------------------+
|
|
double GetCurrentVolumeRatioForSymbol(string symbol, ENUM_TIMEFRAMES tf)
|
|
{
|
|
long currentVolume = UseTickVolume ? iTickVolume(symbol, tf, 0) : iVolume(symbol, tf, 0);
|
|
long sumVolume = 0;
|
|
int lookback = MathMin(VolumeSignalLookback, 200);
|
|
if(lookback <= 0)
|
|
return 1.3;
|
|
|
|
for(int i = 1; i <= lookback; i++)
|
|
{
|
|
sumVolume += UseTickVolume ? iTickVolume(symbol, tf, i) : iVolume(symbol, tf, i);
|
|
}
|
|
|
|
long avgVolume = sumVolume / lookback;
|
|
return (avgVolume > 0) ? ((double)currentVolume / avgVolume) : 1.3;
|
|
}
|
|
|
|
// Returns consecutive bullish/bearish bars on closed candles (signal mapping depends on mode).
|
|
int GetConsecutiveBarsForSymbol(string symbol, ENUM_TIMEFRAMES tf, bool countBullish)
|
|
{
|
|
int streak = 0;
|
|
int maxLookback = 30;
|
|
|
|
for(int shift = 1; shift <= maxLookback; shift++)
|
|
{
|
|
double o = iOpen(symbol, tf, shift);
|
|
double c = iClose(symbol, tf, shift);
|
|
|
|
if(o == 0.0 && c == 0.0)
|
|
break;
|
|
|
|
bool isBullish = (c > o);
|
|
bool isBearish = (c < o);
|
|
bool sameDirection = countBullish ? isBullish : isBearish;
|
|
if(!sameDirection)
|
|
break;
|
|
|
|
if(UseVolumeInSignals)
|
|
{
|
|
long currVol = iTickVolume(symbol, tf, shift);
|
|
long sumVol = 0;
|
|
int counted = 0;
|
|
int lookback = MathMin(VolumeSignalLookback, 50);
|
|
for(int i = shift + 1; i <= shift + lookback; i++)
|
|
{
|
|
long v = iTickVolume(symbol, tf, i);
|
|
if(v <= 0)
|
|
continue;
|
|
sumVol += v;
|
|
counted++;
|
|
}
|
|
|
|
if(counted > 0)
|
|
{
|
|
double avgVol = (double)sumVol / (double)counted;
|
|
double volThreshold = VolumeSignalFactor * avgVol;
|
|
// For MTF streak counting, weak-volume bars are still counted.
|
|
}
|
|
}
|
|
|
|
streak++;
|
|
}
|
|
|
|
return streak;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int GetConsecutiveBarsMultiTFForSymbol(string symbol, bool countBullish)
|
|
{
|
|
int bestStreak = 0;
|
|
|
|
for(int i = 0; i < 5; i++)
|
|
{
|
|
ENUM_TIMEFRAMES tf = (ENUM_TIMEFRAMES)TIMEFRAMES[i];
|
|
int streak = GetConsecutiveBarsForSymbol(symbol, tf, countBullish);
|
|
if(streak > bestStreak)
|
|
bestStreak = streak;
|
|
}
|
|
|
|
return bestStreak;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int CountOpenTradesOfTypeForSymbol(string symbol, int pos_type)
|
|
{
|
|
int cnt = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0)
|
|
continue;
|
|
if(!PositionSelectByTicket(ticket))
|
|
continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != symbol)
|
|
continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != MagicNumber)
|
|
continue;
|
|
if(PositionGetInteger(POSITION_TYPE) == pos_type)
|
|
cnt++;
|
|
}
|
|
return cnt;
|
|
}
|
|
|
|
struct SymbolEntryGuard
|
|
{
|
|
string symbol;
|
|
int lastBuyBar;
|
|
int lastSellBar;
|
|
};
|
|
|
|
SymbolEntryGuard symbolEntryGuards[];
|
|
|
|
struct AutoCloseSymbolState
|
|
{
|
|
string symbol;
|
|
bool triggered;
|
|
datetime timestamp;
|
|
bool inTrailing;
|
|
double highestProfitInTrailing;
|
|
};
|
|
|
|
AutoCloseSymbolState autoCloseSymbolStates[];
|
|
|
|
struct AutoCloseBasketLearningContext
|
|
{
|
|
string symbol;
|
|
bool active;
|
|
bool learnSent;
|
|
datetime activatedAt;
|
|
datetime expiresAt;
|
|
double basketProfitAtTrigger;
|
|
double drawdownPctAtTrigger;
|
|
int tradeCountAtTrigger;
|
|
bool recoveryModeAtTrigger;
|
|
};
|
|
|
|
AutoCloseBasketLearningContext autoCloseLearningContexts[];
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int FindSymbolEntryGuardIndex(string symbol)
|
|
{
|
|
for(int i = 0; i < ArraySize(symbolEntryGuards); i++)
|
|
{
|
|
if(symbolEntryGuards[i].symbol == symbol)
|
|
return i;
|
|
}
|
|
|
|
int idx = ArraySize(symbolEntryGuards);
|
|
ArrayResize(symbolEntryGuards, idx + 1);
|
|
symbolEntryGuards[idx].symbol = symbol;
|
|
symbolEntryGuards[idx].lastBuyBar = -1;
|
|
symbolEntryGuards[idx].lastSellBar = -1;
|
|
return idx;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool CanEnterSymbolDirectionOnBar(string symbol, int direction, int barId)
|
|
{
|
|
int idx = FindSymbolEntryGuardIndex(symbol);
|
|
if(direction == 1)
|
|
return symbolEntryGuards[idx].lastBuyBar != barId;
|
|
return symbolEntryGuards[idx].lastSellBar != barId;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void MarkSymbolDirectionEnteredOnBar(string symbol, int direction, int barId)
|
|
{
|
|
int idx = FindSymbolEntryGuardIndex(symbol);
|
|
if(direction == 1)
|
|
symbolEntryGuards[idx].lastBuyBar = barId;
|
|
else
|
|
symbolEntryGuards[idx].lastSellBar = barId;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int FindAutoCloseStateIndex(string symbol, bool createIfMissing = false)
|
|
{
|
|
for(int i = 0; i < ArraySize(autoCloseSymbolStates); i++)
|
|
{
|
|
if(autoCloseSymbolStates[i].symbol == symbol)
|
|
return i;
|
|
}
|
|
|
|
if(!createIfMissing)
|
|
return -1;
|
|
|
|
int idx = ArraySize(autoCloseSymbolStates);
|
|
ArrayResize(autoCloseSymbolStates, idx + 1);
|
|
autoCloseSymbolStates[idx].symbol = symbol;
|
|
autoCloseSymbolStates[idx].triggered = false;
|
|
autoCloseSymbolStates[idx].timestamp = 0;
|
|
autoCloseSymbolStates[idx].inTrailing = false;
|
|
autoCloseSymbolStates[idx].highestProfitInTrailing = 0.0;
|
|
return idx;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int FindAutoCloseLearningContextIndex(string symbol, bool createIfMissing = false)
|
|
{
|
|
for(int i = 0; i < ArraySize(autoCloseLearningContexts); i++)
|
|
{
|
|
if(autoCloseLearningContexts[i].symbol == symbol)
|
|
return i;
|
|
}
|
|
|
|
if(!createIfMissing)
|
|
return -1;
|
|
|
|
int idx = ArraySize(autoCloseLearningContexts);
|
|
ArrayResize(autoCloseLearningContexts, idx + 1);
|
|
autoCloseLearningContexts[idx].symbol = symbol;
|
|
autoCloseLearningContexts[idx].active = false;
|
|
autoCloseLearningContexts[idx].learnSent = false;
|
|
autoCloseLearningContexts[idx].activatedAt = 0;
|
|
autoCloseLearningContexts[idx].expiresAt = 0;
|
|
autoCloseLearningContexts[idx].basketProfitAtTrigger = 0.0;
|
|
autoCloseLearningContexts[idx].drawdownPctAtTrigger = 0.0;
|
|
autoCloseLearningContexts[idx].tradeCountAtTrigger = 0;
|
|
autoCloseLearningContexts[idx].recoveryModeAtTrigger = false;
|
|
return idx;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void RegisterAutoCloseLearningContext(string symbol, double basketProfit, datetime nowTime, int tradeCount, bool recoveryMode)
|
|
{
|
|
int idx = FindAutoCloseLearningContextIndex(symbol, true);
|
|
autoCloseLearningContexts[idx].basketProfitAtTrigger = basketProfit;
|
|
autoCloseLearningContexts[idx].drawdownPctAtTrigger = GetAccountDrawdownPercentAuto();
|
|
autoCloseLearningContexts[idx].tradeCountAtTrigger = MathMax(1, tradeCount);
|
|
autoCloseLearningContexts[idx].recoveryModeAtTrigger = recoveryMode;
|
|
autoCloseLearningContexts[idx].activatedAt = nowTime;
|
|
autoCloseLearningContexts[idx].expiresAt = nowTime + 300; // valid 5 min for close wave
|
|
autoCloseLearningContexts[idx].active = (basketProfit > 0.0);
|
|
autoCloseLearningContexts[idx].learnSent = false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsAutoCloseLearningPositiveContext(string symbol, datetime nowTime)
|
|
{
|
|
int idx = FindAutoCloseLearningContextIndex(symbol, false);
|
|
if(idx < 0)
|
|
return false;
|
|
|
|
if(!autoCloseLearningContexts[idx].active)
|
|
return false;
|
|
|
|
if(nowTime > autoCloseLearningContexts[idx].expiresAt)
|
|
{
|
|
autoCloseLearningContexts[idx].active = false;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void ClearAutoCloseLearningContext(string symbol)
|
|
{
|
|
int idx = FindAutoCloseLearningContextIndex(symbol, false);
|
|
if(idx < 0)
|
|
return;
|
|
autoCloseLearningContexts[idx].active = false;
|
|
autoCloseLearningContexts[idx].learnSent = false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void SyncChartAutoCloseState()
|
|
{
|
|
int idx = FindAutoCloseStateIndex(Symbol(), false);
|
|
if(idx < 0)
|
|
{
|
|
autoCloseTriggered = false;
|
|
autoCloseTimestamp = 0;
|
|
isInAutoCloseTrailing = false;
|
|
highestProfitInTrailing = 0.0;
|
|
return;
|
|
}
|
|
|
|
autoCloseTriggered = autoCloseSymbolStates[idx].triggered;
|
|
autoCloseTimestamp = autoCloseSymbolStates[idx].timestamp;
|
|
isInAutoCloseTrailing = autoCloseSymbolStates[idx].inTrailing;
|
|
highestProfitInTrailing = autoCloseSymbolStates[idx].highestProfitInTrailing;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsPositionManagedByEA()
|
|
{
|
|
long posMagic = PositionGetInteger(POSITION_MAGIC);
|
|
if(posMagic == MagicNumber)
|
|
return true;
|
|
|
|
// Compatibility fallback for legacy/edge executions where magic may differ.
|
|
string comment = PositionGetString(POSITION_COMMENT);
|
|
if(StringFind(comment, "King Gold Scalper") >= 0)
|
|
return true;
|
|
if(StringFind(comment, "Multi-TF Confluence") >= 0)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int CountOpenTradesForSymbolMagic(string symbol)
|
|
{
|
|
int count = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0)
|
|
continue;
|
|
if(!PositionSelectByTicket(ticket))
|
|
continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != symbol)
|
|
continue;
|
|
if(!IsPositionManagedByEA())
|
|
continue;
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double CalculateTotalProfitForSymbol(string symbol)
|
|
{
|
|
double totalProfit = 0.0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0)
|
|
continue;
|
|
if(!PositionSelectByTicket(ticket))
|
|
continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != symbol)
|
|
continue;
|
|
if(!IsPositionManagedByEA())
|
|
continue;
|
|
totalProfit += PositionGetDouble(POSITION_PROFIT);
|
|
}
|
|
return totalProfit;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void CloseAllTradesForSymbol(string symbol)
|
|
{
|
|
int maxAttempts = 3;
|
|
for(int attempt = 1; attempt <= maxAttempts; attempt++)
|
|
{
|
|
int remainingBefore = 0;
|
|
int closedNow = 0;
|
|
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0)
|
|
continue;
|
|
if(!PositionSelectByTicket(ticket))
|
|
continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != symbol)
|
|
continue;
|
|
if(!IsPositionManagedByEA())
|
|
continue;
|
|
|
|
remainingBefore++;
|
|
if(trade.PositionClose(ticket))
|
|
closedNow++;
|
|
else
|
|
Print("AUTOCLOSE FAILED [", symbol, "] attempt=", attempt, " ticket=", ticket,
|
|
" | retcode=", trade.ResultRetcode(),
|
|
" | ", trade.ResultRetcodeDescription());
|
|
}
|
|
|
|
if(remainingBefore == 0)
|
|
return;
|
|
|
|
if(attempt < maxAttempts)
|
|
{
|
|
Sleep(120);
|
|
}
|
|
}
|
|
|
|
int remainingAfter = CountOpenTradesForSymbolMagic(symbol);
|
|
if(remainingAfter > 0)
|
|
Print("AUTOCLOSE WARNING [", symbol, "] still open after retries: ", remainingAfter);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void GetAutoCloseActiveSymbols(string &symbols[])
|
|
{
|
|
ArrayResize(symbols, 0);
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0)
|
|
continue;
|
|
if(!PositionSelectByTicket(ticket))
|
|
continue;
|
|
if(!IsPositionManagedByEA())
|
|
continue;
|
|
|
|
string symbol = PositionGetString(POSITION_SYMBOL);
|
|
bool exists = false;
|
|
for(int j = 0; j < ArraySize(symbols); j++)
|
|
{
|
|
if(symbols[j] == symbol)
|
|
{
|
|
exists = true;
|
|
break;
|
|
}
|
|
}
|
|
if(!exists)
|
|
{
|
|
int idx = ArraySize(symbols);
|
|
ArrayResize(symbols, idx + 1);
|
|
symbols[idx] = symbol;
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsSymbolAutoCloseBlocked(string symbol, datetime gateTime)
|
|
{
|
|
if(AutoCloseResumeTrading)
|
|
return false;
|
|
|
|
int idx = FindAutoCloseStateIndex(symbol, false);
|
|
if(idx < 0 || !autoCloseSymbolStates[idx].triggered)
|
|
return false;
|
|
|
|
if(autoCloseSymbolStates[idx].timestamp == 0)
|
|
autoCloseSymbolStates[idx].timestamp = gateTime;
|
|
|
|
MqlDateTime nowDt;
|
|
TimeToStruct(gateTime, nowDt);
|
|
MqlDateTime closedDt;
|
|
TimeToStruct(autoCloseSymbolStates[idx].timestamp, closedDt);
|
|
|
|
bool newDayReached = (nowDt.year > closedDt.year) || (nowDt.mon > closedDt.mon) || (nowDt.day > closedDt.day);
|
|
bool timeWindowOk = IsHourInWindow(nowDt.hour, StartHour, EndHour);
|
|
|
|
if(newDayReached && timeWindowOk)
|
|
{
|
|
Print(" NEW DAY REACHED [", symbol, "] - Re-entry enabled at ", nowDt.hour, ":00");
|
|
autoCloseSymbolStates[idx].triggered = false;
|
|
autoCloseSymbolStates[idx].timestamp = 0;
|
|
autoCloseSymbolStates[idx].inTrailing = false;
|
|
autoCloseSymbolStates[idx].highestProfitInTrailing = 0.0;
|
|
SyncChartAutoCloseState();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void MarkSymbolAutoCloseTriggered(string symbol, datetime triggerTime)
|
|
{
|
|
int idx = FindAutoCloseStateIndex(symbol, true);
|
|
autoCloseSymbolStates[idx].timestamp = triggerTime;
|
|
autoCloseSymbolStates[idx].triggered = !AutoCloseResumeTrading;
|
|
autoCloseSymbolStates[idx].inTrailing = false;
|
|
autoCloseSymbolStates[idx].highestProfitInTrailing = 0.0;
|
|
|
|
if(autoCloseSymbolStates[idx].triggered)
|
|
Print(" AUTO CLOSE TRIGGERED [", symbol, "] - Stopping new trades until next session (Time: ",
|
|
TimeToString(autoCloseSymbolStates[idx].timestamp, TIME_DATE|TIME_SECONDS), ")");
|
|
else
|
|
Print(" AUTO CLOSE TRIGGERED [", symbol, "] - Trading remains enabled this session (Time: ",
|
|
TimeToString(autoCloseSymbolStates[idx].timestamp, TIME_DATE|TIME_SECONDS), ")");
|
|
|
|
SyncChartAutoCloseState();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool CheckAllFiltersForTradeForSymbol(string symbol, int positionType, double bid, double ask, int consecutiveBars)
|
|
{
|
|
string direction = (positionType == POSITION_TYPE_BUY) ? "BUY" : "SELL";
|
|
bool allFiltersPassed = true;
|
|
|
|
// Spread check
|
|
long spreadPoints = 0;
|
|
SymbolInfoInteger(symbol, SYMBOL_SPREAD, spreadPoints);
|
|
long maxSpreadInPoints = (long)(MaxSpread * pip_value);
|
|
if(spreadPoints > maxSpreadInPoints)
|
|
allFiltersPassed = false;
|
|
|
|
// Volume check
|
|
if(UseVolumeFilter)
|
|
{
|
|
long currentVolume = UseTickVolume ? iTickVolume(symbol, Period(), 0) : iVolume(symbol, Period(), 0);
|
|
long sumVolume = 0;
|
|
for(int i = 1; i <= 200; i++)
|
|
{
|
|
sumVolume += UseTickVolume ? iTickVolume(symbol, Period(), i) : iVolume(symbol, Period(), i);
|
|
}
|
|
long avgVolume = sumVolume / 200;
|
|
double volumeRatio = (avgVolume > 0) ? ((double)currentVolume / avgVolume) : 1.3;
|
|
double requiredVolumeRatio = MinVolumeRatio;
|
|
if(safeModeActive)
|
|
requiredVolumeRatio *= SafeVolumeEntryMultiplier;
|
|
if(volumeRatio < requiredVolumeRatio)
|
|
allFiltersPassed = false;
|
|
}
|
|
|
|
// DOM check: strict only for chart symbol where DOM context exists
|
|
if(UseDOMFilter && !ForceDisableDOM && domFilterActive && domInitialized)
|
|
{
|
|
if(symbol == Symbol())
|
|
{
|
|
if(!DOMAsSignal && !CheckDOMFilter(positionType, bid, ask))
|
|
allFiltersPassed = false;
|
|
}
|
|
else
|
|
{
|
|
if(!DOMAsSignal)
|
|
allFiltersPassed = false;
|
|
}
|
|
}
|
|
|
|
// Anti-extreme check: avoid buying near recent highs / selling near recent lows
|
|
if(UseAntiExtremeFilter)
|
|
{
|
|
int lookbackBars = MathMax(10, AntiExtremeLookbackBars);
|
|
double minDistancePrice = ConvertInputDistanceToPriceForSymbol(symbol, AntiExtremeMinDistance);
|
|
if(minDistancePrice < 0.0)
|
|
minDistancePrice = 0.0;
|
|
|
|
int highShift = iHighest(symbol, (ENUM_TIMEFRAMES)Period(), MODE_HIGH, lookbackBars, 1);
|
|
int lowShift = iLowest(symbol, (ENUM_TIMEFRAMES)Period(), MODE_LOW, lookbackBars, 1);
|
|
double recentHigh = (highShift >= 0) ? iHigh(symbol, (ENUM_TIMEFRAMES)Period(), highShift) : 0.0;
|
|
double recentLow = (lowShift >= 0) ? iLow(symbol, (ENUM_TIMEFRAMES)Period(), lowShift) : 0.0;
|
|
|
|
if(positionType == POSITION_TYPE_BUY && recentHigh > 0.0 && ask >= (recentHigh - minDistancePrice))
|
|
allFiltersPassed = false;
|
|
if(positionType == POSITION_TYPE_SELL && recentLow > 0.0 && bid <= (recentLow + minDistancePrice))
|
|
allFiltersPassed = false;
|
|
}
|
|
|
|
if(ShouldEmitFilterStatusLog(symbol, direction, allFiltersPassed, "symbol_filters"))
|
|
{
|
|
if(allFiltersPassed)
|
|
Print(" ALL FILTERS PASSED [", direction, "][", symbol, "] | Consecutive=", consecutiveBars);
|
|
else
|
|
Print(" FILTERS BLOCKED [", direction, "][", symbol, "] | Consecutive=", consecutiveBars);
|
|
}
|
|
|
|
return allFiltersPassed;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateMarketStressDetection()
|
|
{
|
|
static bool wasStress = false;
|
|
|
|
// Check stress conditions once and emit logs only on state transitions.
|
|
bool highVolStress = (marketState.currentVolatility > marketState.avgVolatility * 2.0);
|
|
bool lossStress = (marketState.consecutiveLosses >= 5);
|
|
UpdateAccountCache();
|
|
double drawdown = (accountCache.balance > 0.0) ? (((accountCache.balance - accountCache.equity) / accountCache.balance) * 100.0) : 0.0;
|
|
bool drawdownStress = (drawdown > 15.0);
|
|
|
|
marketState.marketStress = (highVolStress || lossStress || drawdownStress);
|
|
|
|
if(marketState.marketStress && !wasStress)
|
|
{
|
|
if(highVolStress)
|
|
Print("STRESS: High volatility detected");
|
|
if(lossStress)
|
|
Print("STRESS: Consecutive losses detected");
|
|
if(drawdownStress)
|
|
Print("STRESS: High drawdown detected: ", DoubleToString(drawdown, 1), "%");
|
|
}
|
|
|
|
if(!marketState.marketStress && wasStress)
|
|
Print("STRESS: Conditions normalized");
|
|
|
|
wasStress = marketState.marketStress;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// GLOBAL VARIABLES
|
|
//+------------------------------------------------------------------+
|
|
double point_value;
|
|
double pip_value; // Value of 1 pip in points (for spread conversion)
|
|
int MaxSpreadRuntime; // Runtime-adjustable spread limit (tester-safe)
|
|
int lastLossBar = -999; // Bar number when last loss occurred
|
|
datetime lastLossTime = 0; // Time when last loss occurred (for cooldown)
|
|
int consecutiveBullishBars = 0; // Counter for consecutive bullish (green) bars
|
|
int consecutiveBearishBars = 0; // Counter for consecutive bearish (red) bars
|
|
|
|
// Learning telemetry counters
|
|
long memoryDecisionTotal = 0;
|
|
long memoryDecisionAllow = 0;
|
|
long memoryDecisionBlock = 0;
|
|
long memoryDecisionExplore = 0;
|
|
long setupRewardUpdates = 0;
|
|
int lastBullishSignalBar = -1; // Track last bar we triggered bullish signal on
|
|
int lastBearishSignalBar = -1; // Track last bar we triggered bearish signal on
|
|
int lastMemoryBlockSellBar = -1; // Track last bar SELL was blocked by memory gate
|
|
int lastMemoryBlockBuyBar = -1; // Track last bar BUY was blocked by memory gate
|
|
datetime lastProcessedBarTime = 0; // Track which closed bar we already processed to avoid recounting
|
|
bool autoCloseTriggered = false; // Track if AutoCloseProfit was just triggered
|
|
datetime autoCloseTimestamp = 0; // Time when auto-close was triggered (full timestamp for rollover safety)
|
|
bool isInAutoCloseTrailing = false; // Track if we're in trailing close mode
|
|
double highestProfitInTrailing = 0; // Track highest profit during trailing mode
|
|
double lastBalance = 0; // Track balance for sentiment
|
|
int winCount = 0; // Count winning trades
|
|
int lossCount = 0; // Count losing trades
|
|
double lastDealProfit = 0; // Last trade profit/loss
|
|
ulong lastProcessedDealTicket = 0; // Track last processed deal to avoid double counting
|
|
double currentRiskPercent = 1.0; // Adaptive risk percent (changes based on performance)
|
|
double memoryAdaptiveRiskMultiplier = 1.0; // Memory-driven multiplier applied in lot sizing
|
|
string performanceMode = "NORMAL"; // Current performance mode: AGGRESSIVE, NORMAL, CONSERVATIVE
|
|
string chartComment = ""; // Store text to display on chart
|
|
string greedLevel = "MODERATE"; // Signal-based greed: GREEDY, MODERATE, CONSERVATIVE
|
|
int trendDirection = 0; // 1 = up, -1 = down, 0 = neutral (for display)
|
|
bool safeModeActive = false; // Safe mode if balance below threshold
|
|
int currentMaxOpenTrades = 0; // Dynamic max trades (normal or safe)
|
|
double queenAccountHealthScore = 0.50; // 0..1 account health (display only)
|
|
double queenAccountDrawdownPct = 0.0; // Equity drawdown vs balance in %
|
|
double queenMarginLevel = 0.0; // Current margin level %
|
|
double queenExposureLots = 0.0; // Total open + pending lots (all symbols)
|
|
// Tick-bar construction state
|
|
int tickBarIndex = 0; // Synthetic tick-bar index used for gating signals
|
|
int tickBarCount = 0; // How many ticks are in the current synthetic bar
|
|
double tickBarOpen = 0; // Open price of the current synthetic bar
|
|
double tickBarHigh = 0; // High price of the current synthetic bar
|
|
double tickBarLow = 0; // Low price of the current synthetic bar
|
|
double tickBarClose = 0; // Close price of the current synthetic bar
|
|
datetime lastHistoryUpdate = 0; // Last time we refreshed deal history
|
|
datetime lastDynamicAdjust = 0; // Last time we adjusted SL/TP dynamically
|
|
int dynamicSLTPThrottleSeconds = 5; // Minimum seconds between dynamic SL/TP adjustments
|
|
ulong processedTickets[]; // Track processed deal tickets to avoid double counting
|
|
CTrade trade;
|
|
datetime lastLimitRenewal = 0; // Throttle limit order renewal checks
|
|
datetime lastHeartbeatLog = 0; // Throttle heartbeat logs
|
|
datetime lastBestSessionReport = 0; // Throttle best-session reports
|
|
string heartbeatStatus = "Heartbeat: INIT";
|
|
bool nextTradeUseExploreLot = false; // Lot reduction flag for controlled explore entries
|
|
int exploreSignalCounter = 0; // Counter used by 1-in-N controlled explore gate
|
|
bool recoveryModeActive = false; // DD-based recovery mode flag
|
|
bool recoveryHoldConfiguredTarget = false; // In recovery: true = keep normal AutoClose targets
|
|
uint lastFastSpikeSignalMs = 0; // Last tick-count when a spike-reaction order was attempted
|
|
uint fastSpikeReferenceMs = 0; // Tick-count baseline for spike measurement
|
|
double fastSpikeReferencePrice = 0.0; // Baseline price for spike measurement
|
|
datetime fastSpikeAnchorBarTime = 0; // Current anchor bar time (stable during the bar)
|
|
double fastSpikeAnchorOpen = 0.0; // Current anchor bar open price
|
|
bool fastSpikePending = false; // Pending spike waiting for break+hold confirmation
|
|
int fastSpikePendingDirection = 0; // 1=up/buy, -1=down/sell
|
|
uint fastSpikePendingStartMs = 0; // Pending start tick-count
|
|
double fastSpikePendingAnchorPrice = 0.0; // Anchor price captured at spike detect
|
|
int fastSpikePendingThresholdPoints = 0; // Threshold used for pending confirmation level
|
|
double fastSpikePendingMovePoints = 0.0; // Initial move detected at pending start
|
|
int fastSpikePendingHoldTicks = 0; // Consecutive hold ticks beyond break level
|
|
datetime lastStateSaveTime = 0; // Autosave throttle for persistence
|
|
// Voice command queue (SQLite)
|
|
long g_cmdDb = -1;
|
|
string g_cmdDbPath = "C:\\sqlite_cmdq\\cmd_queue.db";
|
|
string g_cmdInstance = "KOG-VOICE";
|
|
datetime g_lastCmdPoll = 0;
|
|
int g_cmdPollIntervalSeconds = 1;
|
|
bool tradingPaused = false;
|
|
// MicCmd.dll (SAPI) voice input
|
|
string g_voiceGrammarPath = "";
|
|
string g_voiceGrammarContent =
|
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
|
"<grammar xmlns=\"http://www.w3.org/2001/06/grammar\" version=\"1.0\" xml:lang=\"en-US\" mode=\"voice\" root=\"cmd\">\n"
|
|
" <rule id=\"cmd\" scope=\"public\">\n"
|
|
" <one-of>\n"
|
|
" <item>pause</item>\n"
|
|
" <item>pause trading</item>\n"
|
|
" <item>stop trading</item>\n"
|
|
" <item>hold on</item>\n"
|
|
" <item>resume</item>\n"
|
|
" <item>resume trading</item>\n"
|
|
" <item>continue trading</item>\n"
|
|
" <item>close all</item>\n"
|
|
" <item>close everything</item>\n"
|
|
" <item>close positions</item>\n"
|
|
" <item>panic close</item>\n"
|
|
" <item>risk one percent</item>\n"
|
|
" <item>risk two percent</item>\n"
|
|
" <item>risk three percent</item>\n"
|
|
" <item>risk four percent</item>\n"
|
|
" <item>risk five percent</item>\n"
|
|
" <item>set risk to one percent</item>\n"
|
|
" <item>set risk to two percent</item>\n"
|
|
" <item>set risk to three percent</item>\n"
|
|
" <item>set risk to four percent</item>\n"
|
|
" <item>set risk to five percent</item>\n"
|
|
" <item>hello</item>\n"
|
|
" <item>hi</item>\n"
|
|
" <item>good morning</item>\n"
|
|
" <item>good afternoon</item>\n"
|
|
" <item>good evening</item>\n"
|
|
" <item>how are you</item>\n"
|
|
" <item>status</item>\n"
|
|
" <item>status report</item>\n"
|
|
" </one-of>\n"
|
|
" </rule>\n"
|
|
"</grammar>\n";
|
|
string g_voiceLogPath = "";
|
|
bool g_micStarted = false;
|
|
// Telegram runtime-safe copies (trimmed)
|
|
string g_telegramBotToken = "";
|
|
string g_telegramChatId = "";
|
|
bool g_decisionDllReady = false;
|
|
datetime g_lastDecisionSnapshotLog = 0;
|
|
string g_lastDllDecisionAction = "N/A";
|
|
string g_lastDllDecisionSymbol = "";
|
|
string g_lastDllDecisionRegime = "UNKNOWN";
|
|
int g_lastDllDecisionScore = 5;
|
|
double g_lastDllDecisionAllowProb = 0.0;
|
|
double g_lastDllDecisionRiskMul = 1.0;
|
|
double g_lastDllDecisionLatencyUs = 0.0;
|
|
string g_lastDllSnapshot = "";
|
|
long g_lastDllSnapScoreCalls = -1;
|
|
long g_lastDllSnapLearnCalls = -1;
|
|
long g_lastDllSnapBasketLearnCalls = -1;
|
|
long g_lastDllSnapCacheHits = -1;
|
|
double g_lastDllSnapBaselineVol = -1.0;
|
|
double g_lastDllSnapBaselineSpread = -1.0;
|
|
|
|
// Alert throttling state
|
|
datetime lastBuySoundTime = 0;
|
|
datetime lastSellSoundTime = 0;
|
|
datetime lastBlockedSoundTime = 0;
|
|
datetime lastRiskSoundTime = 0;
|
|
datetime lastProfitSoundTime = 0;
|
|
datetime lastLossSoundTime = 0;
|
|
datetime lastSpikeSoundTime = 0;
|
|
datetime lastNewsSoundTime = 0;
|
|
datetime lastFutureSoundTime = 0;
|
|
datetime lastAutoCloseSoundTime = 0;
|
|
datetime lastNewsScanTime = 0;
|
|
int lastFutureStartAlertDateKey = 0;
|
|
int lastFutureEndAlertDateKey = 0;
|
|
bool newsCalendarWarnedUnavailable = false;
|
|
datetime lastTelegramEntryTime = 0;
|
|
datetime lastTelegramBlockedTime = 0;
|
|
datetime lastTelegramRiskTime = 0;
|
|
datetime lastTelegramCloseTime = 0;
|
|
datetime lastTelegramNewsTime = 0;
|
|
datetime lastTelegramFutureTime = 0;
|
|
datetime lastTelegramSpikeTime = 0;
|
|
|
|
// DOM (Order Book) Variables
|
|
MqlBookInfo bookArray[]; // Array to store Order Book data
|
|
bool domInitialized = false; // Track if DOM is initialized
|
|
bool domAvailable = false; // Track if DOM is available (ECN broker detected)
|
|
bool domFilterActive = false; // Track if DOM filter is actually active (after ECN check)
|
|
datetime lastBookUpdate = 0; // Last time Order Book was updated
|
|
|
|
// Cache instances - Global access for performance
|
|
AccountCache accountCache;
|
|
MarketDataCache marketCache;
|
|
|
|
//+------------------------------------------------------------------+
|
|
// CACHE MANAGEMENT FUNCTIONS - Reduce API calls and improve performance
|
|
//+------------------------------------------------------------------+
|
|
void UpdateAccountCache()
|
|
{
|
|
if(accountCache.needsUpdate())
|
|
{
|
|
accountCache.update();
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateRecoveryMode()
|
|
{
|
|
if(!UseRecoveryAutoClose)
|
|
{
|
|
recoveryModeActive = false;
|
|
return;
|
|
}
|
|
|
|
UpdateAccountCache();
|
|
double balance = accountCache.balance;
|
|
double equity = accountCache.equity;
|
|
if(balance <= 0.0)
|
|
return;
|
|
|
|
// Recovery mode only makes sense if we already have open trades to recover.
|
|
if(CountOpenTradesAllSymbols() <= 0)
|
|
{
|
|
recoveryModeActive = false;
|
|
return;
|
|
}
|
|
|
|
double ddPct = ((balance - equity) / balance) * 100.0;
|
|
if(ddPct < 0.0)
|
|
ddPct = 0.0;
|
|
|
|
double ddStart = MathMax(0.1, RecoveryDDStart);
|
|
double ddEnd = MathMax(0.0, MathMin(RecoveryDDEnd, ddStart - 0.1));
|
|
|
|
bool previous = recoveryModeActive;
|
|
if(!recoveryModeActive && ddPct >= ddStart)
|
|
recoveryModeActive = true;
|
|
else
|
|
if(recoveryModeActive && ddPct <= ddEnd)
|
|
recoveryModeActive = false;
|
|
|
|
if(previous != recoveryModeActive)
|
|
{
|
|
if(recoveryModeActive)
|
|
Print("RECOVERY MODE ON | DD: ", DoubleToString(ddPct, 2), "% | AutoClose at >= $", DoubleToString(MathMax(0.0, RecoveryMinProfitUsd), 2));
|
|
else
|
|
Print("RECOVERY MODE OFF | DD recovered to ", DoubleToString(ddPct, 2), "%");
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int CountOpenTradesOfType(int pos_type)
|
|
{
|
|
int cnt = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0)
|
|
continue;
|
|
if(!PositionSelectByTicket(ticket))
|
|
continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != Symbol())
|
|
continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != MagicNumber)
|
|
continue;
|
|
if(PositionGetInteger(POSITION_TYPE) == pos_type)
|
|
cnt++;
|
|
}
|
|
return cnt;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool ShouldHoldConfiguredAutoCloseInRecoveryForSymbol(string symbol)
|
|
{
|
|
// Determine dominant open basket direction for this EA/symbol
|
|
int buyCount = CountOpenTradesOfTypeForSymbol(symbol, (int)POSITION_TYPE_BUY);
|
|
int sellCount = CountOpenTradesOfTypeForSymbol(symbol, (int)POSITION_TYPE_SELL);
|
|
|
|
int direction = 0;
|
|
if(buyCount > sellCount)
|
|
direction = 1;
|
|
else
|
|
if(sellCount > buyCount)
|
|
direction = -1;
|
|
|
|
if(direction == 0)
|
|
return false;
|
|
|
|
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
double volumeRatio = GetCurrentVolumeRatioForSymbol(symbol, (ENUM_TIMEFRAMES)Period());
|
|
int consecutiveBars = (direction == 1) ? GetConsecutiveBarsMultiTFForSymbol(symbol, false)
|
|
: GetConsecutiveBarsMultiTFForSymbol(symbol, true);
|
|
string details = "";
|
|
int decisionScore = GetDecisionScore10ForSymbol(symbol, direction, consecutiveBars, volumeRatio, bid, ask, details);
|
|
|
|
bool strongMomentum = (volumeRatio >= StrongMomentumThreshold);
|
|
bool acceptableRegime = (marketState.currentRegime != "VOLATILE");
|
|
bool hold = (decisionScore >= LongTermScoreThreshold) ||
|
|
(decisionScore >= DecisionScoreThresholdNormal && strongMomentum && acceptableRegime);
|
|
|
|
return hold;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool ShouldHoldConfiguredAutoCloseInRecovery()
|
|
{
|
|
return ShouldHoldConfiguredAutoCloseInRecoveryForSymbol(Symbol());
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void LogRecoveryTargetModeForSymbol(string symbol, bool holdConfiguredTarget)
|
|
{
|
|
static string loggedSymbols[];
|
|
static int loggedModes[];
|
|
|
|
int idx = -1;
|
|
for(int i = 0; i < ArraySize(loggedSymbols); i++)
|
|
{
|
|
if(loggedSymbols[i] == symbol)
|
|
{
|
|
idx = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(idx < 0)
|
|
{
|
|
idx = ArraySize(loggedSymbols);
|
|
ArrayResize(loggedSymbols, idx + 1);
|
|
ArrayResize(loggedModes, idx + 1);
|
|
loggedSymbols[idx] = symbol;
|
|
loggedModes[idx] = -1;
|
|
}
|
|
|
|
int mode = holdConfiguredTarget ? 1 : 0;
|
|
if(loggedModes[idx] == mode)
|
|
return;
|
|
|
|
if(mode == 1)
|
|
Print("RECOVERY TARGET MODE [", symbol, "]: HOLD_CONFIGURED | Favorable analysis detected - using normal AutoClose target");
|
|
else
|
|
Print("RECOVERY TARGET MODE [", symbol, "]: QUICK_CLOSE | Analysis not favorable - using fast positive close");
|
|
|
|
loggedModes[idx] = mode;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void EmitHeartbeat()
|
|
{
|
|
if(!UseHeartbeat)
|
|
return;
|
|
|
|
datetime now = TimeCurrent();
|
|
int interval = HeartbeatIntervalSeconds;
|
|
if(interval < 5)
|
|
interval = 5;
|
|
|
|
if(lastHeartbeatLog != 0 && (now - lastHeartbeatLog) < interval)
|
|
return;
|
|
|
|
lastHeartbeatLog = now;
|
|
|
|
int openTrades = CountOpenTrades();
|
|
long spreadPoints = 0;
|
|
SymbolInfoInteger(_Symbol, SYMBOL_SPREAD, spreadPoints);
|
|
double spreadDisplay = (pip_value > 0.0) ? ((double)spreadPoints / pip_value) : (double)spreadPoints;
|
|
|
|
heartbeatStatus = "Heartbeat: " + TimeToString(now, TIME_SECONDS)
|
|
+ " | Open: " + IntegerToString(openTrades)
|
|
+ " | Regime: " + marketState.currentRegime
|
|
+ " | MemRisk: " + DoubleToString(memoryAdaptiveRiskMultiplier, 2) + "x"
|
|
+ " | Spread: " + DoubleToString(spreadDisplay, 1);
|
|
|
|
if(HeartbeatToJournal)
|
|
Print(" HEARTBEAT | Time: ", TimeToString(now, TIME_DATE | TIME_SECONDS),
|
|
" | OpenTrades: ", openTrades,
|
|
" | Mode: ", performanceMode,
|
|
" | Greed: ", greedLevel,
|
|
" | Regime: ", marketState.currentRegime,
|
|
" | MemRisk: ", DoubleToString(memoryAdaptiveRiskMultiplier, 2), "x",
|
|
" | Spread: ", DoubleToString(spreadDisplay, 1));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
string ExtractSnapshotValueByKey(const string json, const string key)
|
|
{
|
|
string token = "\"" + key + "\":";
|
|
int p = StringFind(json, token);
|
|
if(p < 0)
|
|
return "";
|
|
|
|
int n = StringLen(json);
|
|
int s = p + StringLen(token);
|
|
while(s < n)
|
|
{
|
|
int ch = StringGetCharacter(json, s);
|
|
if(ch != 32 && ch != 9 && ch != 10 && ch != 13)
|
|
break;
|
|
s++;
|
|
}
|
|
|
|
int e = s;
|
|
while(e < n)
|
|
{
|
|
int ch = StringGetCharacter(json, e);
|
|
if(ch == 44 || ch == 125) // ',' or '}'
|
|
break;
|
|
e++;
|
|
}
|
|
|
|
if(e <= s)
|
|
return "";
|
|
|
|
return StringSubstr(json, s, e - s);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void RefreshDecisionSnapshotParsedMetrics()
|
|
{
|
|
string scoreCallsText = ExtractSnapshotValueByKey(g_lastDllSnapshot, "score_calls");
|
|
string learnCallsText = ExtractSnapshotValueByKey(g_lastDllSnapshot, "learn_calls");
|
|
string basketLearnCallsText = ExtractSnapshotValueByKey(g_lastDllSnapshot, "basket_learn_calls");
|
|
string cacheHitsText = ExtractSnapshotValueByKey(g_lastDllSnapshot, "cache_hits");
|
|
string baselineVolText = ExtractSnapshotValueByKey(g_lastDllSnapshot, "baseline_vol");
|
|
string baselineSpreadText = ExtractSnapshotValueByKey(g_lastDllSnapshot, "baseline_spread");
|
|
|
|
g_lastDllSnapScoreCalls = (scoreCallsText == "") ? -1 : (long)StringToInteger(scoreCallsText);
|
|
g_lastDllSnapLearnCalls = (learnCallsText == "") ? -1 : (long)StringToInteger(learnCallsText);
|
|
g_lastDllSnapBasketLearnCalls = (basketLearnCallsText == "") ? -1 : (long)StringToInteger(basketLearnCallsText);
|
|
g_lastDllSnapCacheHits = (cacheHitsText == "") ? -1 : (long)StringToInteger(cacheHitsText);
|
|
g_lastDllSnapBaselineVol = (baselineVolText == "") ? -1.0 : StringToDouble(baselineVolText);
|
|
g_lastDllSnapBaselineSpread = (baselineSpreadText == "") ? -1.0 : StringToDouble(baselineSpreadText);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void EmitDecisionDllSnapshot()
|
|
{
|
|
if(!UseDecisionCoreDll || !g_decisionDllReady)
|
|
return;
|
|
|
|
int interval = MathMax(10, DecisionDllSnapshotIntervalSeconds);
|
|
datetime now = TimeCurrent();
|
|
if(g_lastDecisionSnapshotLog != 0 && (now - g_lastDecisionSnapshotLog) < interval)
|
|
return;
|
|
|
|
g_lastDecisionSnapshotLog = now;
|
|
uchar snapBuf[];
|
|
ArrayResize(snapBuf, 1024);
|
|
int snapLen = dc_snapshot(snapBuf, 1024);
|
|
if(snapLen > 0)
|
|
{
|
|
g_lastDllSnapshot = CharArrayToString(snapBuf, 0, snapLen);
|
|
RefreshDecisionSnapshotParsedMetrics();
|
|
Print("DECISION DLL SNAPSHOT | ", g_lastDllSnapshot);
|
|
}
|
|
else
|
|
{
|
|
g_lastDllSnapScoreCalls = -1;
|
|
g_lastDllSnapLearnCalls = -1;
|
|
g_lastDllSnapBasketLearnCalls = -1;
|
|
g_lastDllSnapCacheHits = -1;
|
|
g_lastDllSnapBaselineVol = -1.0;
|
|
g_lastDllSnapBaselineSpread = -1.0;
|
|
Print("DECISION DLL SNAPSHOT | failed rc=", snapLen);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
string UrlEncodeUtf8(const string value)
|
|
{
|
|
uchar bytes[];
|
|
int len = StringToCharArray(value, bytes, 0, WHOLE_ARRAY, CP_UTF8);
|
|
if(len <= 0)
|
|
return "";
|
|
|
|
string encoded = "";
|
|
for(int i = 0; i < len - 1; i++) // skip string terminator
|
|
{
|
|
uchar c = bytes[i];
|
|
bool isAlphaNum = ((c >= 'a' && c <= 'z') ||
|
|
(c >= 'A' && c <= 'Z') ||
|
|
(c >= '0' && c <= '9'));
|
|
bool isUnreserved = (c == '-' || c == '_' || c == '.' || c == '~');
|
|
|
|
if(isAlphaNum || isUnreserved)
|
|
encoded += CharToString(c);
|
|
else
|
|
if(c == ' ')
|
|
encoded += "%20";
|
|
else
|
|
encoded += "%" + StringFormat("%02X", (int)c);
|
|
}
|
|
return encoded;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool SendTelegramMessage(const string message, datetime &lastSendTime, int cooldownSeconds)
|
|
{
|
|
if(!EnableTelegramAlerts)
|
|
return false;
|
|
|
|
// Use normalized runtime copies to avoid accidental spaces
|
|
NormalizeTelegramInputs();
|
|
if(StringLen(g_telegramBotToken) <= 0 || StringLen(g_telegramChatId) <= 0)
|
|
return false;
|
|
|
|
datetime now = TimeCurrent();
|
|
int cooldown = MathMax(0, cooldownSeconds);
|
|
if(lastSendTime != 0 && (now - lastSendTime) < cooldown)
|
|
return false;
|
|
|
|
string url = "https://api.telegram.org/bot" + g_telegramBotToken + "/sendMessage";
|
|
string payload = "chat_id=" + UrlEncodeUtf8(g_telegramChatId) +
|
|
"&text=" + UrlEncodeUtf8(message) +
|
|
"&disable_web_page_preview=true";
|
|
char post[];
|
|
StringToCharArray(payload, post, 0, WHOLE_ARRAY, CP_UTF8);
|
|
char response[];
|
|
string responseHeaders = "";
|
|
string requestHeaders = "Content-Type: application/x-www-form-urlencoded\r\n";
|
|
|
|
ResetLastError();
|
|
int httpCode = WebRequest("POST", url, requestHeaders, 7000, post, response, responseHeaders);
|
|
if(httpCode == -1)
|
|
{
|
|
Print("TELEGRAM ALERT FAILED | WebRequest error=", GetLastError(), " | Add https://api.telegram.org to allowed URLs in MT5.");
|
|
return false;
|
|
}
|
|
if(httpCode < 200 || httpCode >= 300)
|
|
{
|
|
string body = CharArrayToString(response, 0, WHOLE_ARRAY, CP_UTF8);
|
|
Print("TELEGRAM ALERT FAILED | HTTP=", httpCode, " | Response=", body);
|
|
return false;
|
|
}
|
|
|
|
lastSendTime = now;
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// STATE PERSISTENCE (FILE_COMMON)
|
|
//+------------------------------------------------------------------+
|
|
bool SaveStateToFile()
|
|
{
|
|
if(!UseFilePersistence)
|
|
return false;
|
|
|
|
string file = StateFileName();
|
|
int handle = FileOpen(file, FILE_WRITE | FILE_COMMON | FILE_TXT | FILE_ANSI | FILE_SHARE_READ);
|
|
if(handle == INVALID_HANDLE)
|
|
{
|
|
Print("STATE SAVE FAILED | FileOpen error=", GetLastError(), " | file=", file);
|
|
return false;
|
|
}
|
|
|
|
// State file version header (allows safe schema changes)
|
|
FileWriteString(handle, "VER;6\n");
|
|
|
|
// Trades (capped)
|
|
FileWriteString(handle, "[TRADE]\n");
|
|
int tradeCount = ArraySize(tradeHistory);
|
|
int startTrade = MathMax(0, tradeCount - MaxSavedTrades);
|
|
for(int i = startTrade; i < tradeCount; i++)
|
|
{
|
|
TradeMemory t = tradeHistory[i];
|
|
string line = StringFormat("T;%I64d;%d;%.5f;%.5f;%.2f;%d;%.4f;%.5f;%d;%d;%d;%.2f;%.2f;%d;%s\n",
|
|
(long)t.entryTime, t.direction, t.entryPrice, t.exitPrice, t.profit,
|
|
t.consecutiveBars, t.volumeRatio, t.spreadAtEntry, t.hourOfDay, t.dayOfWeek,
|
|
(int)t.wasWinner, t.maxProfit, t.maxDrawdown, t.holdDuration, SanitizeField(t.exitReason));
|
|
FileWriteString(handle, line);
|
|
}
|
|
|
|
// Patterns
|
|
FileWriteString(handle, "[PATTERN]\n");
|
|
int pattCount = ArraySize(patternHistory);
|
|
for(int i = 0; i < pattCount; i++)
|
|
{
|
|
PatternMemory p = patternHistory[i];
|
|
string line = StringFormat("P;%d;%d;%.4f;%.2f;%.2f;%d;%d;%.2f;%I64d;%.2f;%s\n",
|
|
p.patternType, p.consecutiveBars, p.volumeRatio, p.avgProfit, p.totalProfit,
|
|
p.totalTrades, p.winningTrades, p.winRate, (long)p.lastUpdated, p.confidence, SanitizeField(p.symbol));
|
|
FileWriteString(handle, line);
|
|
}
|
|
|
|
// Time memory 24x7
|
|
FileWriteString(handle, "[TIME]\n");
|
|
for(int h = 0; h < 24; h++)
|
|
for(int d = 0; d < 7; d++)
|
|
{
|
|
TimeBasedMemory tm = timeMemory[h][d];
|
|
string line = StringFormat("TM;%d;%d;%d;%d;%.2f;%.2f;%.2f;%.2f;%.2f;%d\n",
|
|
h, d, tm.totalTrades, tm.winningTrades, tm.totalProfit, tm.avgProfit,
|
|
tm.winRate, tm.avgSpread, tm.avgVolume, (int)tm.isOptimalHour);
|
|
FileWriteString(handle, line);
|
|
}
|
|
|
|
// Session memory
|
|
FileWriteString(handle, "[SESSION]\n");
|
|
for(int s = 0; s < ArraySize(sessionMemory); s++)
|
|
{
|
|
SessionBasedMemory sm = sessionMemory[s];
|
|
string line = StringFormat("S;%s;%d;%d;%.2f;%.2f;%.2f;%d\n",
|
|
SanitizeField(sm.name), sm.totalTrades, sm.winningTrades,
|
|
sm.totalProfit, sm.avgProfit, sm.winRate, (int)sm.isOptimal);
|
|
FileWriteString(handle, line);
|
|
}
|
|
|
|
// Regime history (capped)
|
|
FileWriteString(handle, "[REGIME]\n");
|
|
int regCount = ArraySize(regimeHistory);
|
|
int startReg = MathMax(0, regCount - MaxSavedRegimes);
|
|
for(int i = startReg; i < regCount; i++)
|
|
{
|
|
MarketRegimeMemory r = regimeHistory[i];
|
|
string line = StringFormat("R;%s;%I64d;%.2f;%d;%.4f;%.4f;%d;%d;%.2f\n",
|
|
SanitizeField(r.regime), (long)r.detected, r.confidence, r.duration,
|
|
r.avgVolatility, r.avgSpread, r.successfulTrades, r.totalTrades, r.performance);
|
|
FileWriteString(handle, line);
|
|
}
|
|
|
|
// Setup buckets
|
|
FileWriteString(handle, "[SETUP]\n");
|
|
int setupCount = ArraySize(setupMemory);
|
|
for(int i = 0; i < setupCount; i++)
|
|
{
|
|
SetupBucketMemory sb = setupMemory[i];
|
|
string line = StringFormat("SB;%d;%d;%d;%d;%s;%d;%d;%.2f;%.2f;%.2f\n",
|
|
sb.direction, sb.barsBucket, sb.volumeBucket, sb.entryType, SanitizeField(sb.regime),
|
|
sb.totalTrades, sb.winningTrades, sb.totalProfit, sb.avgProfit, sb.rewardScore);
|
|
FileWriteString(handle, line);
|
|
}
|
|
|
|
// Dynamic patterns
|
|
FileWriteString(handle, "[DYNAMIC_PATTERN]\n");
|
|
for(int i = 0; i < dynamicPatternCount; i++)
|
|
{
|
|
DynamicPattern dp = dynamicPatterns[i];
|
|
string line = StringFormat("DP;%s;%s;%d;%d;%d;%d;%.2f;%.5f;%.5f;%I64d;%I64d;%d\n",
|
|
SanitizeField(dp.patternHash),
|
|
SanitizeField(dp.description),
|
|
dp.patternType,
|
|
dp.occurrenceCount,
|
|
dp.winCount,
|
|
dp.lossCount,
|
|
dp.reliability,
|
|
dp.avgProfit,
|
|
dp.totalProfit,
|
|
(long)dp.firstSeen,
|
|
(long)dp.lastSeen,
|
|
dp.timeframe);
|
|
FileWriteString(handle, line);
|
|
}
|
|
|
|
// Symbol timeframe memory (per-symbol, per-timeframe)
|
|
FileWriteString(handle, "[SYMBOL_TF]\n");
|
|
for(int s = 0; s < ArraySize(enhancedSymbolMemory); s++)
|
|
{
|
|
string sym = enhancedSymbolMemory[s].symbol;
|
|
if(sym == "")
|
|
continue;
|
|
for(int t = 0; t < 5; t++)
|
|
{
|
|
string line = StringFormat("STF;%s;%d;%.5f;%d\n",
|
|
SanitizeField(sym),
|
|
TIMEFRAMES[t],
|
|
enhancedSymbolMemory[s].tfPerformance[t],
|
|
enhancedSymbolMemory[s].tfTrades[t]);
|
|
FileWriteString(handle, line);
|
|
}
|
|
}
|
|
|
|
// State snapshot
|
|
FileWriteString(handle, "[STATE]\n");
|
|
string stateLine = StringFormat("ST;%s;%.2f;%I64d;%d;%d;%.2f;%d;%.2f;%.2f;%.2f;%.2f;%.2f;%s;%s;%.2f;%.2f\n",
|
|
SanitizeField(marketState.currentRegime),
|
|
marketState.regimeConfidence,
|
|
(long)marketState.regimeStart,
|
|
marketState.consecutiveWins,
|
|
marketState.consecutiveLosses,
|
|
marketState.recentPerformance,
|
|
(int)marketState.marketStress,
|
|
queenAccountDrawdownPct,
|
|
queenMarginLevel,
|
|
queenAccountHealthScore,
|
|
currentRiskPercent,
|
|
memoryAdaptiveRiskMultiplier,
|
|
SanitizeField(performanceMode),
|
|
SanitizeField(greedLevel),
|
|
lastBalance,
|
|
lastDealProfit);
|
|
FileWriteString(handle, stateLine);
|
|
|
|
FileClose(handle);
|
|
FileFlush(handle);
|
|
lastStateSaveTime = TimeCurrent();
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool LoadStateFromFile()
|
|
{
|
|
if(!UseFilePersistence)
|
|
return false;
|
|
|
|
string file = StateFileName();
|
|
|
|
if(ResetStateFile)
|
|
{
|
|
FileDelete(file, FILE_COMMON);
|
|
return false;
|
|
}
|
|
|
|
if(!FileIsExist(file, FILE_COMMON))
|
|
return false;
|
|
|
|
int handle = FileOpen(file, FILE_READ | FILE_COMMON | FILE_TXT | FILE_ANSI);
|
|
if(handle == INVALID_HANDLE)
|
|
{
|
|
Print("STATE LOAD FAILED | FileOpen error=", GetLastError(), " | file=", file);
|
|
return false;
|
|
}
|
|
|
|
// reset containers
|
|
ArrayResize(tradeHistory, 0);
|
|
ArrayResize(patternHistory, 0);
|
|
ArrayResize(regimeHistory, 0);
|
|
ArrayResize(setupMemory, 0);
|
|
ArrayResize(dynamicPatterns, 0);
|
|
dynamicPatternCount = 0;
|
|
|
|
for(int h = 0; h < 24; h++)
|
|
for(int d = 0; d < 7; d++)
|
|
{
|
|
timeMemory[h][d].hourOfDay = h;
|
|
timeMemory[h][d].dayOfWeek = d;
|
|
timeMemory[h][d].totalTrades = 0;
|
|
timeMemory[h][d].winningTrades = 0;
|
|
timeMemory[h][d].totalProfit = 0.0;
|
|
timeMemory[h][d].avgProfit = 0.0;
|
|
timeMemory[h][d].winRate = 0.0;
|
|
timeMemory[h][d].avgSpread = 0.0;
|
|
timeMemory[h][d].avgVolume = 0.0;
|
|
timeMemory[h][d].isOptimalHour = false;
|
|
}
|
|
|
|
for(int s = 0; s < ArraySize(sessionMemory); s++)
|
|
{
|
|
sessionMemory[s].name = "";
|
|
sessionMemory[s].totalTrades = 0;
|
|
sessionMemory[s].winningTrades = 0;
|
|
sessionMemory[s].totalProfit = 0.0;
|
|
sessionMemory[s].avgProfit = 0.0;
|
|
sessionMemory[s].winRate = 0.0;
|
|
sessionMemory[s].isOptimal = false;
|
|
}
|
|
|
|
string line = "";
|
|
while(!FileIsEnding(handle))
|
|
{
|
|
line = FileReadString(handle);
|
|
if(StringLen(line) <= 0)
|
|
continue;
|
|
if(StringLen(line) > 2048)
|
|
{
|
|
Print("STATE LOAD WARN | Line too long, skipped: ", StringSubstr(line, 0, 80), "...");
|
|
continue;
|
|
}
|
|
|
|
if(line == "[TRADE]" || line == "[PATTERN]" || line == "[TIME]" || line == "[SESSION]" || line == "[REGIME]" || line == "[SETUP]" || line == "[DYNAMIC_PATTERN]" || line == "[SYMBOL_TF]" || line == "[STATE]")
|
|
continue;
|
|
|
|
string parts[];
|
|
int count = StringSplit(line, ';', parts);
|
|
if(count <= 1)
|
|
continue;
|
|
|
|
string tag = parts[0];
|
|
|
|
if(tag == "VER" && count >= 2)
|
|
{
|
|
int version = (int)StringToInteger(parts[1]);
|
|
if(version < 3)
|
|
Print("STATE LOAD WARN | Unsupported version ", version, " expected >=3 | file=", file);
|
|
continue;
|
|
}
|
|
|
|
if(tag == "T" && count >= 16)
|
|
{
|
|
TradeMemory t;
|
|
t.entryTime = (datetime)StringToInteger(parts[1]);
|
|
t.direction = (int)StringToInteger(parts[2]);
|
|
t.entryPrice = SafeNumber(parts[3], -DBL_MAX, DBL_MAX, 0.0);
|
|
t.exitPrice = SafeNumber(parts[4], -DBL_MAX, DBL_MAX, 0.0);
|
|
t.profit = SafeNumber(parts[5], -1000000.0, 1000000.0, 0.0);
|
|
t.consecutiveBars = (int)StringToInteger(parts[6]);
|
|
t.volumeRatio = SafeNumber(parts[7], 0.0, 1000.0, 0.0);
|
|
t.spreadAtEntry = SafeNumber(parts[8], 0.0, 100000.0, 0.0);
|
|
t.hourOfDay = (int)StringToInteger(parts[9]);
|
|
t.dayOfWeek = (int)StringToInteger(parts[10]);
|
|
t.wasWinner = (StringToInteger(parts[11]) != 0);
|
|
t.maxProfit = SafeNumber(parts[12], -1000000.0, 1000000.0, 0.0);
|
|
t.maxDrawdown = SafeNumber(parts[13], -1000000.0, 1000000.0, 0.0);
|
|
t.holdDuration = (int)StringToInteger(parts[14]);
|
|
t.exitReason = parts[15];
|
|
int size = ArraySize(tradeHistory);
|
|
ArrayResize(tradeHistory, size + 1);
|
|
tradeHistory[size] = t;
|
|
}
|
|
else
|
|
if(tag == "P" && count >= 11)
|
|
{
|
|
PatternMemory p;
|
|
p.patternType = (int)StringToInteger(parts[1]);
|
|
p.consecutiveBars = (int)StringToInteger(parts[2]);
|
|
p.volumeRatio = StringToDouble(parts[3]);
|
|
p.avgProfit = StringToDouble(parts[4]);
|
|
p.totalProfit = StringToDouble(parts[5]);
|
|
p.totalTrades = (int)StringToInteger(parts[6]);
|
|
p.winningTrades = (int)StringToInteger(parts[7]);
|
|
p.winRate = StringToDouble(parts[8]);
|
|
p.lastUpdated = (datetime)StringToInteger(parts[9]);
|
|
p.confidence = StringToDouble(parts[10]);
|
|
p.symbol = (count >= 12) ? parts[11] : _Symbol;
|
|
if(StringLen(p.symbol) <= 0)
|
|
p.symbol = _Symbol;
|
|
int size = ArraySize(patternHistory);
|
|
ArrayResize(patternHistory, size + 1);
|
|
patternHistory[size] = p;
|
|
}
|
|
else
|
|
if(tag == "TM" && count >= 11)
|
|
{
|
|
int hIdx = (int)StringToInteger(parts[1]);
|
|
int dIdx = (int)StringToInteger(parts[2]);
|
|
if(hIdx >= 0 && hIdx < 24 && dIdx >= 0 && dIdx < 7)
|
|
{
|
|
timeMemory[hIdx][dIdx].hourOfDay = hIdx;
|
|
timeMemory[hIdx][dIdx].dayOfWeek = dIdx;
|
|
timeMemory[hIdx][dIdx].totalTrades = (int)StringToInteger(parts[3]);
|
|
timeMemory[hIdx][dIdx].winningTrades = (int)StringToInteger(parts[4]);
|
|
timeMemory[hIdx][dIdx].totalProfit = StringToDouble(parts[5]);
|
|
timeMemory[hIdx][dIdx].avgProfit = StringToDouble(parts[6]);
|
|
timeMemory[hIdx][dIdx].winRate = StringToDouble(parts[7]);
|
|
timeMemory[hIdx][dIdx].avgSpread = StringToDouble(parts[8]);
|
|
timeMemory[hIdx][dIdx].avgVolume = StringToDouble(parts[9]);
|
|
timeMemory[hIdx][dIdx].isOptimalHour = (StringToInteger(parts[10]) != 0);
|
|
}
|
|
}
|
|
else
|
|
if(tag == "S" && count >= 8)
|
|
{
|
|
string name = parts[1];
|
|
int slot = 0;
|
|
if(name == "LONDON")
|
|
slot = 1;
|
|
else
|
|
if(name == "NEW_YORK")
|
|
slot = 2;
|
|
else
|
|
if(name == "OVERLAP")
|
|
slot = 3;
|
|
else
|
|
slot = 0;
|
|
|
|
sessionMemory[slot].name = name;
|
|
sessionMemory[slot].totalTrades = (int)StringToInteger(parts[2]);
|
|
sessionMemory[slot].winningTrades = (int)StringToInteger(parts[3]);
|
|
sessionMemory[slot].totalProfit = StringToDouble(parts[4]);
|
|
sessionMemory[slot].avgProfit = StringToDouble(parts[5]);
|
|
sessionMemory[slot].winRate = StringToDouble(parts[6]);
|
|
sessionMemory[slot].isOptimal = (StringToInteger(parts[7]) != 0);
|
|
}
|
|
else
|
|
if(tag == "R" && count >= 10)
|
|
{
|
|
MarketRegimeMemory r;
|
|
r.regime = parts[1];
|
|
r.detected = (datetime)StringToInteger(parts[2]);
|
|
r.confidence = SafeNumber(parts[3], 0.0, 1.0, 0.0);
|
|
r.duration = (int)SafeNumber(parts[4], 0.0, 5256000.0, 0.0);
|
|
r.avgVolatility = SafeNumber(parts[5], 0.0, 100.0, 0.0);
|
|
r.avgSpread = SafeNumber(parts[6], 0.0, 100000.0, 0.0);
|
|
r.successfulTrades = (int)SafeNumber(parts[7], 0.0, 1000000.0, 0.0);
|
|
r.totalTrades = (int)SafeNumber(parts[8], 0.0, 1000000.0, 0.0);
|
|
r.performance = SafeNumber(parts[9], 0.0, 100.0, 0.0);
|
|
if(r.totalTrades < r.successfulTrades)
|
|
r.successfulTrades = r.totalTrades;
|
|
if(r.regime != "TRENDING" && r.regime != "RANGING" && r.regime != "VOLATILE" && r.regime != "QUIET" && r.regime != "UNKNOWN")
|
|
r.regime = "UNKNOWN";
|
|
int size = ArraySize(regimeHistory);
|
|
ArrayResize(regimeHistory, size + 1);
|
|
regimeHistory[size] = r;
|
|
}
|
|
else
|
|
if(tag == "SB" && count >= 10)
|
|
{
|
|
SetupBucketMemory sb;
|
|
sb.direction = (int)SafeNumber(parts[1], -1.0, 1.0, 1.0);
|
|
if(sb.direction == 0)
|
|
sb.direction = 1;
|
|
sb.barsBucket = (int)SafeNumber(parts[2], 0.0, 10.0, 0.0);
|
|
sb.volumeBucket = (int)SafeNumber(parts[3], 0.0, 4.0, 0.0);
|
|
int idxShift = 0;
|
|
sb.entryType = 0;
|
|
if(count >= 11)
|
|
{
|
|
sb.entryType = ((int)SafeNumber(parts[4], 0.0, 1.0, 0.0) != 0) ? 1 : 0;
|
|
idxShift = 1;
|
|
}
|
|
sb.regime = parts[4 + idxShift];
|
|
if(sb.regime != "TRENDING" && sb.regime != "RANGING" && sb.regime != "VOLATILE" && sb.regime != "QUIET" && sb.regime != "UNKNOWN")
|
|
sb.regime = "UNKNOWN";
|
|
sb.totalTrades = (int)SafeNumber(parts[5 + idxShift], 0.0, 1000000.0, 0.0);
|
|
sb.winningTrades = (int)SafeNumber(parts[6 + idxShift], 0.0, 1000000.0, 0.0);
|
|
if(sb.winningTrades > sb.totalTrades)
|
|
sb.winningTrades = sb.totalTrades;
|
|
sb.totalProfit = SafeNumber(parts[7 + idxShift], -1000000.0, 1000000.0, 0.0);
|
|
sb.avgProfit = SafeNumber(parts[8 + idxShift], -1000000.0, 1000000.0, 0.0);
|
|
sb.rewardScore = SafeNumber(parts[9 + idxShift], SetupRewardClampMin, SetupRewardClampMax, 0.0);
|
|
int size = ArraySize(setupMemory);
|
|
ArrayResize(setupMemory, size + 1);
|
|
setupMemory[size] = sb;
|
|
}
|
|
else
|
|
if(tag == "DP" && count >= 13)
|
|
{
|
|
DynamicPattern dp;
|
|
dp.patternHash = parts[1];
|
|
dp.description = parts[2];
|
|
dp.patternType = (int)SafeNumber(parts[3], 0.0, 3.0, 0.0);
|
|
dp.occurrenceCount = (int)SafeNumber(parts[4], 0.0, 1000000.0, 0.0);
|
|
dp.winCount = (int)SafeNumber(parts[5], 0.0, 1000000.0, 0.0);
|
|
dp.lossCount = (int)SafeNumber(parts[6], 0.0, 1000000.0, 0.0);
|
|
dp.reliability = SafeNumber(parts[7], 0.0, 100.0, 50.0);
|
|
dp.avgProfit = SafeNumber(parts[8], -1000000.0, 1000000.0, 0.0);
|
|
dp.totalProfit = SafeNumber(parts[9], -10000000.0, 10000000.0, 0.0);
|
|
dp.firstSeen = (datetime)StringToInteger(parts[10]);
|
|
dp.lastSeen = (datetime)StringToInteger(parts[11]);
|
|
dp.timeframe = (int)SafeNumber(parts[12], 1.0, 100000.0, (double)Period());
|
|
if(dp.patternHash != "")
|
|
{
|
|
int size = ArraySize(dynamicPatterns);
|
|
ArrayResize(dynamicPatterns, size + 1);
|
|
dynamicPatterns[size] = dp;
|
|
}
|
|
}
|
|
else
|
|
if(tag == "STF" && count >= 5)
|
|
{
|
|
string sym = parts[1];
|
|
int tf = (int)StringToInteger(parts[2]);
|
|
double perf = SafeNumber(parts[3], -1000000.0, 1000000.0, 0.0);
|
|
int trades = (int)SafeNumber(parts[4], 0.0, 1000000.0, 0.0);
|
|
int symIdx = getSymbolIndex(sym);
|
|
int tfIdx = getTimeframeIndex(tf);
|
|
if(symIdx >= 0 && tfIdx >= 0)
|
|
{
|
|
enhancedSymbolMemory[symIdx].tfPerformance[tfIdx] = perf;
|
|
enhancedSymbolMemory[symIdx].tfTrades[tfIdx] = trades;
|
|
}
|
|
}
|
|
else
|
|
if(tag == "ST" && count >= 17)
|
|
{
|
|
marketState.currentRegime = parts[1];
|
|
marketState.regimeConfidence = SafeNumber(parts[2], 0.0, 1.0, 0.0);
|
|
marketState.regimeStart = (datetime)StringToInteger(parts[3]);
|
|
marketState.consecutiveWins = (int)StringToInteger(parts[4]);
|
|
marketState.consecutiveLosses = (int)StringToInteger(parts[5]);
|
|
marketState.recentPerformance = SafeNumber(parts[6], -100.0, 100.0, 0.0);
|
|
marketState.marketStress = (StringToInteger(parts[7]) != 0);
|
|
queenAccountDrawdownPct = SafeNumber(parts[8], 0.0, 100.0, 0.0);
|
|
queenMarginLevel = SafeNumber(parts[9], 0.0, 2000.0, 0.0);
|
|
queenAccountHealthScore = SafeNumber(parts[10], 0.0, 1.0, 0.0);
|
|
currentRiskPercent = SafeNumber(parts[11], 0.0, 20.0, 0.0);
|
|
memoryAdaptiveRiskMultiplier = SafeNumber(parts[12], 0.0, 5.0, 1.0);
|
|
performanceMode = parts[13];
|
|
if(performanceMode == "AUTO_PROTECT")
|
|
performanceMode = "NORMAL";
|
|
greedLevel = parts[14];
|
|
lastBalance = SafeNumber(parts[15], 0.0, 10000000.0, 0.0);
|
|
lastDealProfit = SafeNumber(parts[16], -1000000.0, 1000000.0, 0.0);
|
|
}
|
|
}
|
|
|
|
FileClose(handle);
|
|
|
|
if(ArraySize(tradeHistory) > MaxSavedTrades)
|
|
ArrayResize(tradeHistory, MaxSavedTrades);
|
|
if(ArraySize(regimeHistory) > MaxSavedRegimes)
|
|
ArrayResize(regimeHistory, MaxSavedRegimes);
|
|
dynamicPatternCount = ArraySize(dynamicPatterns);
|
|
|
|
Print("STATE LOAD OK | Loaded trades=", ArraySize(tradeHistory), " patterns=", ArraySize(patternHistory),
|
|
" regimes=", ArraySize(regimeHistory), " setups=", ArraySize(setupMemory),
|
|
" dynamic=", dynamicPatternCount);
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void AutosaveStateIfDue()
|
|
{
|
|
if(!UseFilePersistence || StateAutosaveSeconds <= 0)
|
|
return;
|
|
|
|
datetime now = TimeCurrent();
|
|
if(lastStateSaveTime != 0 && (now - lastStateSaveTime) < StateAutosaveSeconds)
|
|
return;
|
|
|
|
SaveStateToFile();
|
|
}
|
|
|
|
// Simple Zira TTS helper (no wav needed)
|
|
void SpeakZira(const string text)
|
|
{
|
|
if(!EnableVoicePrompts)
|
|
return;
|
|
static datetime lastSpeak = 0;
|
|
datetime now = TimeCurrent();
|
|
if(now - lastSpeak < 2) // throttle
|
|
return;
|
|
lastSpeak = now;
|
|
|
|
string t = text;
|
|
StringReplace(t, "'", "''"); // escape for PS single quotes
|
|
string ps = "powershell -NoProfile -ExecutionPolicy Bypass -Command \""
|
|
"Add-Type -AssemblyName System.Speech;"
|
|
"$s=New-Object System.Speech.Synthesis.SpeechSynthesizer;"
|
|
"$s.SelectVoice('Microsoft Zira Desktop');"
|
|
"$s.Rate=-1;"
|
|
"$s.Volume=100;"
|
|
"$s.Speak('" + t + "');\"";
|
|
|
|
ShellExecuteW(0, "open", "cmd.exe", "/c " + ps, "", 0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool VoiceCooldown(datetime &lastTime, int cooldownSeconds)
|
|
{
|
|
datetime now = TimeCurrent();
|
|
int cd = MathMax(0, cooldownSeconds);
|
|
if(lastTime != 0 && (now - lastTime) < cd)
|
|
return false;
|
|
lastTime = now;
|
|
return true;
|
|
}
|
|
|
|
// Simple trim helper (MQL5 has no StringTrim)
|
|
string Trim(const string src)
|
|
{
|
|
string t = src;
|
|
StringTrimLeft(t);
|
|
StringTrimRight(t);
|
|
return t;
|
|
}
|
|
|
|
// ---------- SQLite voice command helpers ----------
|
|
string BufToString(uchar &buf[])
|
|
{
|
|
int n = 0;
|
|
while(n < ArraySize(buf) && buf[n] != 0)
|
|
n++;
|
|
return CharArrayToString(buf, 0, n, CP_UTF8);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool PopNextVoiceCommand(int &id, string &cmd, string &payload, string &symbol, long &magic)
|
|
{
|
|
if(g_cmdDb <= 0)
|
|
return false;
|
|
|
|
uchar bCmd[256];
|
|
ArrayInitialize(bCmd, 0);
|
|
uchar bPay[512];
|
|
ArrayInitialize(bPay, 0);
|
|
uchar bSym[64];
|
|
ArrayInitialize(bSym, 0);
|
|
long lid = 0, lmg = 0;
|
|
|
|
// Accept any symbol/magic (script already filters)
|
|
int ok = sqlite_pop_next(g_cmdDb, g_cmdInstance, "", 0, lid,
|
|
bCmd, 256, bPay, 512, bSym, 64, lmg);
|
|
if(ok != 1 || lid <= 0)
|
|
return false;
|
|
|
|
id = (int)lid;
|
|
magic = (long)lmg;
|
|
cmd = BufToString(bCmd);
|
|
payload = BufToString(bPay);
|
|
symbol = BufToString(bSym);
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool ExecuteVoiceCommand(const string cmd, const string payload, string &result, string &error)
|
|
{
|
|
string c = NormalizeVoiceText(cmd);
|
|
if(c == "")
|
|
{
|
|
error = "empty command";
|
|
return false;
|
|
}
|
|
|
|
// Pause / Resume
|
|
if(c == "pause" || c == "pause trading" || c == "stop trading" || c == "hold on")
|
|
{
|
|
tradingPaused = true;
|
|
result = "trading paused";
|
|
SpeakZira("Trading paused. Awaiting your next command.");
|
|
return true;
|
|
}
|
|
if(c == "resume" || c == "resume trading" || c == "continue trading")
|
|
{
|
|
tradingPaused = false;
|
|
result = "trading resumed";
|
|
SpeakZira("Yes master, I will resume trading.");
|
|
return true;
|
|
}
|
|
// Close all positions
|
|
if(c == "close_all" || c == "close all" || c == "close everything" || c == "close positions" || c == "panic close")
|
|
{
|
|
CloseAllTrades();
|
|
result = "close all sent";
|
|
SpeakZira("Closing all positions now.");
|
|
return true;
|
|
}
|
|
// Risk commands: accept formats like "risk=2", "risk two percent", "set risk to 1.5 percent"
|
|
if(StringFind(c, "risk=") == 0 || StringFind(c, "risk ") == 0 || StringFind(c, "set risk") == 0)
|
|
{
|
|
double r = 0.0;
|
|
if(StringFind(c, "risk=") == 0)
|
|
r = StringToDouble(StringSubstr(c, 5));
|
|
else
|
|
{
|
|
if(!ExtractFirstNumber(c, r))
|
|
{
|
|
// try word numbers
|
|
string words[];
|
|
int wc = StringSplit(c, ' ', words);
|
|
for(int i = 0; i < wc; i++)
|
|
{
|
|
if(WordToNumber(words[i], r))
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if(r <= 0.0 || r > 10.0)
|
|
{
|
|
error = "invalid risk";
|
|
return false;
|
|
}
|
|
currentRiskPercent = r;
|
|
result = "risk set to " + DoubleToString(r, 2);
|
|
SpeakZira("Risk set to " + DoubleToString(r, 1) + " percent.");
|
|
return true;
|
|
}
|
|
|
|
// Greetings
|
|
if(c == "hello" || c == "hi" || c == "good morning" || c == "good afternoon" || c == "good evening" || c == "how are you")
|
|
{
|
|
result = "greeting";
|
|
SpeakZira("Hello. I am monitoring the markets for you.");
|
|
return true;
|
|
}
|
|
|
|
// Status shorthand
|
|
if(c == "status" || c == "status report")
|
|
{
|
|
double eq = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
double bal = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
int closedTrades = winCount + lossCount;
|
|
double winRatePct = (closedTrades > 0) ? (((double)winCount * 100.0) / (double)closedTrades) : 0.0;
|
|
string paused = tradingPaused ? "paused" : "active";
|
|
result = "status";
|
|
SpeakZira("Status check. Trading is " + paused + ". Balance " + DoubleToString(bal, 2) +
|
|
". Equity " + DoubleToString(eq, 2) +
|
|
". Risk " + DoubleToString(currentRiskPercent, 1) +
|
|
" percent. Win rate " + DoubleToString(winRatePct, 1) + " percent.");
|
|
return true;
|
|
}
|
|
|
|
// Unknown command fallback
|
|
error = "unknown command: " + c;
|
|
SpeakZira("I did not understand. Please repeat.");
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void PollVoiceCommandQueue()
|
|
{
|
|
if(g_cmdDb <= 0)
|
|
return;
|
|
|
|
datetime now = TimeCurrent();
|
|
if(g_lastCmdPoll != 0 && (now - g_lastCmdPoll) < g_cmdPollIntervalSeconds)
|
|
return;
|
|
g_lastCmdPoll = now;
|
|
|
|
for(int i = 0; i < 3; i++) // process max 3 per poll
|
|
{
|
|
int id = 0;
|
|
long mg = 0;
|
|
string cmd = "", pay = "", sym = "";
|
|
if(!PopNextVoiceCommand(id, cmd, pay, sym, mg))
|
|
break;
|
|
|
|
string res = "", err = "";
|
|
string cmdNorm = NormalizeVoiceText(cmd);
|
|
bool ok = ExecuteVoiceCommand(cmd, pay, res, err);
|
|
if(ok)
|
|
{
|
|
sqlite_mark_done(g_cmdDb, id, res);
|
|
Print("VOICE CMD DONE: ", res, " | raw=", cmd, " | norm=", cmdNorm, " | payload=", pay);
|
|
}
|
|
else
|
|
{
|
|
sqlite_mark_failed(g_cmdDb, id, err);
|
|
Print("VOICE CMD FAILED: ", err, " | raw=", cmd, " | norm=", cmdNorm, " | payload=", pay);
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void EmitOrderExecutedSound(int direction)
|
|
{
|
|
if(!EnableVoicePrompts)
|
|
return;
|
|
if(direction == 1)
|
|
{
|
|
if(VoiceCooldown(lastBuySoundTime, VoiceCooldownSeconds))
|
|
SpeakZira("sir tudor.Buy order executed on " + _Symbol + ".");
|
|
}
|
|
else
|
|
if(direction == -1)
|
|
{
|
|
if(VoiceCooldown(lastSellSoundTime, VoiceCooldownSeconds))
|
|
SpeakZira("sir tudor.Sell order executed on " + _Symbol + ".");
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void EmitSpikeDetectedAlert(int direction, double movePoints, double anchorPrice, int thresholdPoints)
|
|
{
|
|
if(direction != 1 && direction != -1)
|
|
return;
|
|
|
|
string side = (direction == 1) ? "UP" : "DOWN";
|
|
if(TelegramSendSpikes)
|
|
{
|
|
string msg = "KING GOLD SPIKE | " + _Symbol +
|
|
" | " + side +
|
|
" | move=" + DoubleToString(movePoints, 1) +
|
|
" | threshold=" + IntegerToString(thresholdPoints) +
|
|
" | anchor=" + DoubleToString(anchorPrice, _Digits) +
|
|
" | time=" + TimeToString(TimeCurrent(), TIME_SECONDS);
|
|
SendTelegramMessage(msg, lastTelegramSpikeTime, TelegramCooldownSeconds);
|
|
}
|
|
|
|
if(EnableVoicePrompts && VoiceSendSpikes)
|
|
{
|
|
if(VoiceCooldown(lastSpikeSoundTime, VoiceCooldownSeconds))
|
|
{
|
|
string sideVoice = side;
|
|
StringToLower(sideVoice);
|
|
SpeakZira("Fast spike detected.big moves " + sideVoice + " on " + _Symbol + ".");
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void EmitOrderBlockedSound()
|
|
{
|
|
if(TelegramSendBlocked)
|
|
SendTelegramMessage("KING GOLD ALERT | Order blocked | " + _Symbol + " | Time=" + TimeToString(TimeCurrent(), TIME_SECONDS),
|
|
lastTelegramBlockedTime, TelegramCooldownSeconds);
|
|
// Zira voice
|
|
if(EnableVoicePrompts)
|
|
{
|
|
if(VoiceCooldown(lastBlockedSoundTime, VoiceCooldownSeconds))
|
|
SpeakZira("Order blocked on " + _Symbol + ".");
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void EmitRiskWarningSound()
|
|
{
|
|
if(EnableVoicePrompts)
|
|
{
|
|
if(VoiceCooldown(lastRiskSoundTime, VoiceCooldownSeconds))
|
|
SpeakZira("sir.Bandits detects big moves coming on " + _Symbol + ". I will look for entry.");
|
|
}
|
|
if(TelegramSendRisk)
|
|
SendTelegramMessage("KING GOLD RISK | Stress detected | " + _Symbol + " | Regime=" + marketState.currentRegime,
|
|
lastTelegramRiskTime, TelegramCooldownSeconds);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void EmitCloseResultSound(double profit)
|
|
{
|
|
if(EnableVoicePrompts && MathAbs(profit) > 0.0)
|
|
{
|
|
if(profit > 0.0)
|
|
{
|
|
if(VoiceCooldown(lastProfitSoundTime, VoiceCooldownSeconds))
|
|
SpeakZira("Position closed in profit " + DoubleToString(profit, 2));
|
|
}
|
|
else
|
|
{
|
|
if(VoiceCooldown(lastLossSoundTime, VoiceCooldownSeconds))
|
|
SpeakZira("Position closed in loss " + DoubleToString(profit, 2));
|
|
}
|
|
}
|
|
|
|
if(TelegramSendCloses && MathAbs(profit) > 0.0)
|
|
{
|
|
string outcome = (profit > 0.0) ? "✅ Profit" : "❌ Loss";
|
|
string msg = "📌 POSITION CLOSED\n" + outcome + " | " + _Symbol +
|
|
"\nPnL: $" + DoubleToString(profit, 2) +
|
|
"\n⏰ " + TimeToString(TimeCurrent(), TIME_SECONDS);
|
|
SendTelegramMessage(msg, lastTelegramCloseTime, TelegramCooldownSeconds);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int GetDateKey(datetime when)
|
|
{
|
|
MqlDateTime dt;
|
|
TimeToStruct(when, dt);
|
|
return (dt.year * 10000) + (dt.mon * 100) + dt.day;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void CheckFutureSessionEventAlerts()
|
|
{
|
|
if(!EnableFutureSessionAlerts)
|
|
return;
|
|
|
|
datetime now = TimeCurrent();
|
|
MqlDateTime dt;
|
|
TimeToStruct(now, dt);
|
|
int leadMinutes = MathMax(1, FutureEventLeadMinutes);
|
|
|
|
// Alert before session start (today)
|
|
MqlDateTime startDt = dt;
|
|
startDt.hour = MathMax(0, MathMin(23, StartHour));
|
|
startDt.min = 0;
|
|
startDt.sec = 0;
|
|
datetime todayStart = StructToTime(startDt);
|
|
int todayKey = GetDateKey(now);
|
|
int minutesToStart = (int)((todayStart - now) / 60);
|
|
|
|
if(minutesToStart >= 0 && minutesToStart <= leadMinutes && lastFutureStartAlertDateKey != todayKey)
|
|
{
|
|
Print("🔔 FUTURE EVENT ALERT | Session start in ", minutesToStart, " min at ", TimeToString(todayStart, TIME_MINUTES), " ⏳");
|
|
if(EnableVoicePrompts && VoiceCooldown(lastFutureSoundTime, VoiceCooldownSeconds))
|
|
SpeakZira("Session starts in " + IntegerToString(minutesToStart) + " minutes.");
|
|
if(TelegramSendFutureEvents)
|
|
{
|
|
string msg = "🔔 KING GOLD FUTURE EVENT | Session start in " + IntegerToString(minutesToStart) +
|
|
" min | " + _Symbol + " | At " + TimeToString(todayStart, TIME_MINUTES) + " ⏳";
|
|
SendTelegramMessage(msg, lastTelegramFutureTime, TelegramCooldownSeconds);
|
|
}
|
|
lastFutureStartAlertDateKey = todayKey;
|
|
}
|
|
|
|
// Alert before session end (today)
|
|
MqlDateTime endDt = dt;
|
|
endDt.hour = MathMax(0, MathMin(23, EndHour));
|
|
endDt.min = 0;
|
|
endDt.sec = 0;
|
|
datetime todayEnd = StructToTime(endDt);
|
|
int minutesToEnd = (int)((todayEnd - now) / 60);
|
|
|
|
if(minutesToEnd >= 0 && minutesToEnd <= leadMinutes && lastFutureEndAlertDateKey != todayKey)
|
|
{
|
|
Print("🔔 FUTURE EVENT ALERT | Session end in ", minutesToEnd, " min at ", TimeToString(todayEnd, TIME_MINUTES), " ⏳");
|
|
if(EnableVoicePrompts && VoiceCooldown(lastFutureSoundTime, VoiceCooldownSeconds))
|
|
SpeakZira("Session ends in " + IntegerToString(minutesToEnd) + " minutes.");
|
|
if(TelegramSendFutureEvents)
|
|
{
|
|
string msg = "🔔 KING GOLD FUTURE EVENT | Session end in " + IntegerToString(minutesToEnd) +
|
|
" min | " + _Symbol + " | At " + TimeToString(todayEnd, TIME_MINUTES) + " ⏳";
|
|
SendTelegramMessage(msg, lastTelegramFutureTime, TelegramCooldownSeconds);
|
|
}
|
|
lastFutureEndAlertDateKey = todayKey;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void CheckUpcomingEconomicNewsAlerts()
|
|
{
|
|
if(!EnableNewsEventAlerts)
|
|
return;
|
|
|
|
datetime now = TimeCurrent();
|
|
int scanEvery = MathMax(10, NewsScanIntervalSeconds);
|
|
if(lastNewsScanTime != 0 && (now - lastNewsScanTime) < scanEvery)
|
|
return;
|
|
lastNewsScanTime = now;
|
|
|
|
int lookaheadMinutes = MathMax(1, NewsLookaheadMinutes);
|
|
datetime horizon = now + (lookaheadMinutes * 60);
|
|
|
|
MqlCalendarValue values[];
|
|
int total = CalendarValueHistory(values, now, horizon);
|
|
if(total < 0)
|
|
{
|
|
if(!newsCalendarWarnedUnavailable)
|
|
{
|
|
Print("NEWS ALERT: Economic calendar unavailable in this environment.");
|
|
newsCalendarWarnedUnavailable = true;
|
|
}
|
|
return;
|
|
}
|
|
if(total <= 0)
|
|
return;
|
|
|
|
datetime nearest = 0;
|
|
int relevant = 0;
|
|
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
datetime evt = values[i].time;
|
|
if(evt < now || evt > horizon)
|
|
continue;
|
|
relevant++;
|
|
if(nearest == 0 || evt < nearest)
|
|
nearest = evt;
|
|
}
|
|
|
|
if(relevant <= 0)
|
|
return;
|
|
|
|
int newsCooldown = MathMax(10, NewsAlertCooldownSeconds);
|
|
if(lastNewsSoundTime != 0 && (now - lastNewsSoundTime) < newsCooldown)
|
|
return;
|
|
|
|
int mins = (nearest > now) ? (int)((nearest - now) / 60) : 0;
|
|
string scope = NewsAlertAllCurrencies ? "all-currencies" : "symbol-related";
|
|
Print("NEWS ALERT | Upcoming events: ", relevant, " within ", lookaheadMinutes, " min | Nearest in ", mins, " min | Scope=", scope);
|
|
if(EnableVoicePrompts && VoiceCooldown(lastNewsSoundTime, VoiceCooldownSeconds))
|
|
SpeakZira("Sir tudor.High impact news in " + IntegerToString(mins) + " minutes.");
|
|
if(TelegramSendNews)
|
|
{
|
|
string msg = "KING GOLD NEWS ALERT | Events=" + IntegerToString(relevant) +
|
|
" | nearest in " + IntegerToString(mins) + " min | scope=" + scope +
|
|
" | " + _Symbol;
|
|
SendTelegramMessage(msg, lastTelegramNewsTime, newsCooldown);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateMarketCache()
|
|
{
|
|
if(marketCache.needsUpdate())
|
|
{
|
|
marketCache.update();
|
|
}
|
|
}
|
|
|
|
// Utility functions using cached data
|
|
double GetCachedBalance()
|
|
{
|
|
UpdateAccountCache();
|
|
return accountCache.balance;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetCachedEquity()
|
|
{
|
|
UpdateAccountCache();
|
|
return accountCache.equity;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetCachedMargin()
|
|
{
|
|
UpdateAccountCache();
|
|
return accountCache.margin;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetCachedMarginLevel()
|
|
{
|
|
UpdateAccountCache();
|
|
return accountCache.marginLevel;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetCachedBid()
|
|
{
|
|
UpdateMarketCache();
|
|
return marketCache.bid;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetCachedAsk()
|
|
{
|
|
UpdateMarketCache();
|
|
return marketCache.ask;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsDrawdownLimitExceeded()
|
|
{
|
|
UpdateAccountCache();
|
|
if(accountCache.balance <= 0.0)
|
|
return false;
|
|
|
|
double drawdownPct = ((accountCache.balance - accountCache.equity) / accountCache.balance) * 100.0;
|
|
return drawdownPct > MAX_DRAWDOWN_PERCENT;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// VALIDATION FUNCTIONS - Improve error handling and robustness
|
|
//+------------------------------------------------------------------+
|
|
bool ValidateSymbolInfo()
|
|
{
|
|
UpdateMarketCache();
|
|
|
|
// Check critical symbol parameters
|
|
if(marketCache.minLot <= 0.0)
|
|
{
|
|
marketCache.minLot = 0.01;
|
|
Print("WARNING: Invalid SYMBOL_VOLUME_MIN. Using fallback minLot=", marketCache.minLot);
|
|
}
|
|
|
|
if(marketCache.maxLot <= 0.0 || marketCache.maxLot <= marketCache.minLot)
|
|
{
|
|
double fallbackMax = MathMax(marketCache.minLot * 100.0, marketCache.minLot + marketCache.lotStep);
|
|
Print("WARNING: Invalid SYMBOL_VOLUME_MAX: ", marketCache.maxLot, " | Applying fallback maxLot=", fallbackMax);
|
|
marketCache.maxLot = fallbackMax;
|
|
}
|
|
|
|
if(marketCache.lotStep <= 0.0)
|
|
{
|
|
marketCache.lotStep = marketCache.minLot;
|
|
Print("WARNING: Invalid SYMBOL_VOLUME_STEP. Using fallback lotStep=", marketCache.lotStep);
|
|
}
|
|
|
|
if(marketCache.tickSize <= 0.0)
|
|
{
|
|
Print("WARNING: Invalid SYMBOL_TRADE_TICK_SIZE: ", marketCache.tickSize, " - using Point() as fallback");
|
|
marketCache.tickSize = Point();
|
|
}
|
|
|
|
if(marketCache.tickValue <= 0.0)
|
|
{
|
|
marketCache.tickValue = Point();
|
|
Print("WARNING: Invalid SYMBOL_TRADE_TICK_VALUE. Using fallback tickValue=Point(): ", marketCache.tickValue);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool ValidateInputs()
|
|
{
|
|
// Validate risk parameters
|
|
if(RiskPercent < 0.0 || RiskPercent > 10.0)
|
|
{
|
|
Print("ERROR: Invalid RiskPercent: ", RiskPercent, ". Must be between 0.0 and 10.0");
|
|
return false;
|
|
}
|
|
|
|
if(DefaultRiskPercent <= 0.0 || DefaultRiskPercent > 10.0)
|
|
{
|
|
Print("ERROR: Invalid DefaultRiskPercent: ", DefaultRiskPercent, ". Must be between 0.01 and 10.0");
|
|
return false;
|
|
}
|
|
|
|
if(TakeProfit <= 0.0 || StopLoss <= 0.0)
|
|
{
|
|
Print("ERROR: Invalid TP/SL values. TP: ", TakeProfit, ", SL: ", StopLoss);
|
|
return false;
|
|
}
|
|
|
|
if(StartHour < 0 || StartHour > 23 || EndHour < 0 || EndHour > 23)
|
|
{
|
|
Print("ERROR: Invalid trading hours. Start: ", StartHour, ", End: ", EndHour);
|
|
return false;
|
|
}
|
|
|
|
if(MaxOpenTrades <= 0 || MaxOpenTrades > 50)
|
|
{
|
|
Print("ERROR: Invalid MaxOpenTrades: ", MaxOpenTrades, ". Must be between 1 and 50");
|
|
return false;
|
|
}
|
|
|
|
if(BestSessionMinWinRate < 0.0 || BestSessionMinWinRate > 100.0)
|
|
{
|
|
Print("ERROR: Invalid BestSessionMinWinRate: ", BestSessionMinWinRate, ". Must be between 0 and 100");
|
|
return false;
|
|
}
|
|
|
|
if(BestSessionMinTrades < 1 || BestSessionMinTrades > 200)
|
|
{
|
|
Print("ERROR: Invalid BestSessionMinTrades: ", BestSessionMinTrades, ". Must be between 1 and 200");
|
|
return false;
|
|
}
|
|
|
|
if(LondonSessionStartHour < 0 || LondonSessionStartHour > 23 || LondonSessionEndHour < 0 || LondonSessionEndHour > 23)
|
|
{
|
|
Print("ERROR: Invalid London session hours. Start: ", LondonSessionStartHour, ", End: ", LondonSessionEndHour);
|
|
return false;
|
|
}
|
|
|
|
if(NewYorkSessionStartHour < 0 || NewYorkSessionStartHour > 23 || NewYorkSessionEndHour < 0 || NewYorkSessionEndHour > 23)
|
|
{
|
|
Print("ERROR: Invalid New York session hours. Start: ", NewYorkSessionStartHour, ", End: ", NewYorkSessionEndHour);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsValidPrice(double price)
|
|
{
|
|
return (price > 0.0 && price != INVALID_PRICE && price < 999999.0);
|
|
}
|
|
|
|
// Utility: get current time with optional local offset when broker time is not used
|
|
datetime GetCurrentTimeWithOffset()
|
|
{
|
|
return UseBrokerTime ? TimeCurrent() : (TimeLocal() + (TimezoneOffset * 3600));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool WarmupFromHistory()
|
|
{
|
|
int requestedBars = WarmupBars;
|
|
int totalBars = Bars(Symbol(), Period());
|
|
int closedBars = totalBars - 1;
|
|
|
|
if(closedBars < 20)
|
|
{
|
|
Print("WARMUP SKIPPED | Not enough closed bars: ", closedBars);
|
|
return false;
|
|
}
|
|
|
|
int barsToUse = (int)MathMin((double)requestedBars, (double)closedBars);
|
|
barsToUse = (int)MathMin((double)barsToUse, (double)WarmupHardCap);
|
|
barsToUse = (int)MathMin((double)barsToUse, (double)PatternMemoryBars);
|
|
|
|
// Rebuild current streak from latest closed candles (shift 1 and older)
|
|
consecutiveBullishBars = 0;
|
|
consecutiveBearishBars = 0;
|
|
int streakDirection = 0; // 1 bullish, -1 bearish
|
|
|
|
for(int shift = 1; shift <= barsToUse; shift++)
|
|
{
|
|
double o = iOpen(Symbol(), Period(), shift);
|
|
double c = iClose(Symbol(), Period(), shift);
|
|
int dir = 0;
|
|
if(c > o)
|
|
dir = 1;
|
|
else
|
|
if(c < o)
|
|
dir = -1;
|
|
|
|
if(dir == 0)
|
|
continue; // doji is neutral; do not break the current streak
|
|
|
|
if(streakDirection == 0)
|
|
streakDirection = dir;
|
|
|
|
if(dir != streakDirection)
|
|
break;
|
|
|
|
if(streakDirection == 1)
|
|
consecutiveBullishBars++;
|
|
else
|
|
consecutiveBearishBars++;
|
|
}
|
|
|
|
// Volume baseline and trend
|
|
int volumeLookback = MathMin(MathMax(VolumeSignalLookback, 200), barsToUse);
|
|
int recentVolumeLookback = MathMin(200, volumeLookback);
|
|
double sumVolume = 0.0;
|
|
double sumRecentVolume = 0.0;
|
|
|
|
for(int i = 1; i <= volumeLookback; i++)
|
|
{
|
|
long v = UseTickVolume ? iTickVolume(Symbol(), Period(), i) : iVolume(Symbol(), Period(), i);
|
|
sumVolume += (double)v;
|
|
if(i <= recentVolumeLookback)
|
|
sumRecentVolume += (double)v;
|
|
}
|
|
|
|
double avgVolumeAll = (volumeLookback > 0) ? (sumVolume / volumeLookback) : 0.0;
|
|
double avgVolumeRecent = (recentVolumeLookback > 0) ? (sumRecentVolume / recentVolumeLookback) : 0.0;
|
|
marketState.volumeTrend = (avgVolumeAll > 0.0) ? (avgVolumeRecent / avgVolumeAll) : 1.0;
|
|
|
|
// Volatility baseline + current volatility from close prices
|
|
int currentVolWindow = MathMin(VolWindowShort, barsToUse);
|
|
int baselineVolWindow = MathMin(VolWindowLong, barsToUse);
|
|
|
|
double sumCurrent = 0.0;
|
|
for(int i = 1; i <= currentVolWindow; i++)
|
|
sumCurrent += iClose(Symbol(), Period(), i);
|
|
double avgCurrent = sumCurrent / currentVolWindow;
|
|
|
|
double varCurrent = 0.0;
|
|
for(int i = 1; i <= currentVolWindow; i++)
|
|
{
|
|
double px = iClose(Symbol(), Period(), i);
|
|
varCurrent += MathPow(px - avgCurrent, 2);
|
|
}
|
|
marketState.currentVolatility = MathSqrt(varCurrent / currentVolWindow);
|
|
|
|
double sumBaseline = 0.0;
|
|
for(int i = 1; i <= baselineVolWindow; i++)
|
|
sumBaseline += iClose(Symbol(), Period(), i);
|
|
double avgBaseline = sumBaseline / baselineVolWindow;
|
|
|
|
double varBaseline = 0.0;
|
|
for(int i = 1; i <= baselineVolWindow; i++)
|
|
{
|
|
double px = iClose(Symbol(), Period(), i);
|
|
varBaseline += MathPow(px - avgBaseline, 2);
|
|
}
|
|
marketState.avgVolatility = MathSqrt(varBaseline / baselineVolWindow);
|
|
|
|
// Regime preload
|
|
double baselineVol = MathMax(marketState.avgVolatility, SymbolInfoDouble(_Symbol, SYMBOL_POINT));
|
|
if(marketState.currentVolatility > baselineVol * 1.5)
|
|
{
|
|
marketState.currentRegime = "VOLATILE";
|
|
marketState.regimeConfidence = 0.8;
|
|
}
|
|
else
|
|
if(marketState.currentVolatility < baselineVol * 0.5)
|
|
{
|
|
marketState.currentRegime = "QUIET";
|
|
marketState.regimeConfidence = 0.8;
|
|
}
|
|
else
|
|
{
|
|
marketState.currentRegime = "RANGING";
|
|
marketState.regimeConfidence = 0.6;
|
|
}
|
|
|
|
marketState.marketStress = (marketState.currentVolatility > baselineVol * 2.0);
|
|
|
|
// Lightweight price trend preload (normalized points per bar)
|
|
int trendWindow = MathMin(TrendWindowBars, barsToUse);
|
|
double latestClose = iClose(Symbol(), Period(), 1);
|
|
double oldestClose = iClose(Symbol(), Period(), trendWindow);
|
|
marketState.priceTrend = (Point() > 0.0 && trendWindow > 0) ? ((latestClose - oldestClose) / (Point() * trendWindow)) : 0.0;
|
|
|
|
Print("WARMUP DONE | Bars: ", barsToUse,
|
|
" | BullishStreak: ", consecutiveBullishBars,
|
|
" | BearishStreak: ", consecutiveBearishBars,
|
|
" | VolRatio: ", DoubleToString(marketState.volumeTrend, 2),
|
|
" | Volatility: ", DoubleToString(marketState.currentVolatility, 5),
|
|
" | Regime: ", marketState.currentRegime,
|
|
" | Stress: ", (marketState.marketStress ? "YES" : "NO"));
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool WarmupAllSymbolsFromMarketWatch()
|
|
{
|
|
if(totalTrackedSymbols <= 0)
|
|
{
|
|
Print("WARMUP ALL SYMBOLS SKIPPED | No tracked Market Watch symbols.");
|
|
return false;
|
|
}
|
|
|
|
int warmed = 0;
|
|
int skipped = 0;
|
|
datetime now = TimeCurrent();
|
|
|
|
for(int i = 0; i < totalTrackedSymbols; i++)
|
|
{
|
|
string symbol = trackedSymbols[i];
|
|
if(symbol == "")
|
|
{
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
int validTf = 0;
|
|
double sumVolatility = 0.0;
|
|
double sumTrend = 0.0;
|
|
double sumVolumeRatio = 0.0;
|
|
|
|
for(int t = 0; t < 5; t++)
|
|
{
|
|
ENUM_TIMEFRAMES tf = (ENUM_TIMEFRAMES)TIMEFRAMES[t];
|
|
int bars = Bars(symbol, tf);
|
|
if(bars < 50)
|
|
continue;
|
|
|
|
double volatility = calculateVolatility(symbol, TIMEFRAMES[t]);
|
|
double trend = calculateTrendStrength(symbol, TIMEFRAMES[t]);
|
|
double avgVolume = calculateAverageVolume(symbol, TIMEFRAMES[t], 20);
|
|
long currentVolume = UseTickVolume ? iTickVolume(symbol, tf, 1) : iVolume(symbol, tf, 1);
|
|
if(currentVolume <= 0)
|
|
currentVolume = iTickVolume(symbol, tf, 1);
|
|
double volumeRatio = (avgVolume > 0.0) ? ((double)currentVolume / avgVolume) : 1.0;
|
|
|
|
sumVolatility += volatility;
|
|
sumTrend += trend;
|
|
sumVolumeRatio += volumeRatio;
|
|
validTf++;
|
|
}
|
|
|
|
if(validTf <= 0)
|
|
{
|
|
if(i < ArraySize(enhancedSymbolMemory))
|
|
enhancedSymbolMemory[i].isTradable = false;
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
double symbolPoint = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
if(symbolPoint <= 0.0)
|
|
symbolPoint = Point();
|
|
double spreadPoints = (symbolPoint > 0.0) ? ((SymbolInfoDouble(symbol, SYMBOL_ASK) - SymbolInfoDouble(symbol, SYMBOL_BID)) / symbolPoint) : 0.0;
|
|
|
|
if(i < ArraySize(enhancedSymbolMemory))
|
|
{
|
|
enhancedSymbolMemory[i].avgVolatility = sumVolatility / (double)validTf;
|
|
enhancedSymbolMemory[i].trendScore = (int)MathRound(sumTrend / (double)validTf);
|
|
enhancedSymbolMemory[i].liquidityScore = sumVolumeRatio / (double)validTf;
|
|
enhancedSymbolMemory[i].avgSpread = spreadPoints;
|
|
enhancedSymbolMemory[i].lastAnalysis = now;
|
|
enhancedSymbolMemory[i].isTradable = true;
|
|
}
|
|
|
|
warmed++;
|
|
}
|
|
|
|
if(warmed > 0)
|
|
scanner.scanAllSymbols();
|
|
|
|
Print("WARMUP ALL SYMBOLS DONE | Warmed: ", warmed, " | Skipped: ", skipped, " | Tracked: ", totalTrackedSymbols);
|
|
return (warmed > 0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetInstantVolumeBurst(string symbol, ENUM_TIMEFRAMES tf, int lookbackBars)
|
|
{
|
|
int lookback = MathMax(5, MathMin(lookbackBars, 200));
|
|
|
|
long currentVolume = UseTickVolume ? iTickVolume(symbol, tf, 0) : iVolume(symbol, tf, 0);
|
|
if(currentVolume <= 0)
|
|
currentVolume = iTickVolume(symbol, tf, 0);
|
|
|
|
long sumVolume = 0;
|
|
int counted = 0;
|
|
for(int i = 1; i <= lookback; i++)
|
|
{
|
|
long v = UseTickVolume ? iTickVolume(symbol, tf, i) : iVolume(symbol, tf, i);
|
|
if(v <= 0)
|
|
v = iTickVolume(symbol, tf, i);
|
|
if(v <= 0)
|
|
continue;
|
|
sumVolume += v;
|
|
counted++;
|
|
}
|
|
|
|
if(counted <= 0)
|
|
return 1.0;
|
|
|
|
double avgVolume = (double)sumVolume / (double)counted;
|
|
if(avgVolume <= 0.0)
|
|
return 1.0;
|
|
|
|
return (double)currentVolume / avgVolume;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void GetAutoFastSpikeParameters(int &thresholdPoints, uint &windowMs, uint &cooldownMs, bool &preferMomentum)
|
|
{
|
|
long spreadPointsRaw = 0;
|
|
SymbolInfoInteger(_Symbol, SYMBOL_SPREAD, spreadPointsRaw);
|
|
int spreadPoints = (int)MathMax(1, spreadPointsRaw);
|
|
|
|
double volPoints = (point_value > 0.0) ? (marketState.currentVolatility / point_value) : 0.0;
|
|
if(volPoints < 0.0)
|
|
volPoints = 0.0;
|
|
|
|
double threshold = MathMax((double)(spreadPoints * 12), volPoints * 4.0);
|
|
if(marketState.currentRegime == "VOLATILE")
|
|
threshold *= 1.10;
|
|
else
|
|
if(marketState.currentRegime == "QUIET")
|
|
threshold *= 0.85;
|
|
|
|
if(marketState.marketStress)
|
|
threshold *= 1.10;
|
|
|
|
thresholdPoints = (int)ClampValue(MathRound(threshold), 20.0, 5000.0);
|
|
|
|
uint baseWindowMs = 500;
|
|
if(marketState.currentRegime == "VOLATILE")
|
|
baseWindowMs = 280;
|
|
else
|
|
if(marketState.currentRegime == "QUIET")
|
|
baseWindowMs = 800;
|
|
|
|
if(marketState.marketStress)
|
|
baseWindowMs = (uint)MathMax(180, (int)(baseWindowMs * 0.8));
|
|
|
|
windowMs = (uint)MathMax(150, MathMin(1500, (int)baseWindowMs));
|
|
|
|
uint baseCooldownMs = marketState.marketStress ? 1800 : 1200;
|
|
cooldownMs = (uint)MathMax(500, MathMin(8000, (int)baseCooldownMs));
|
|
|
|
// Momentum bias in high-energy regimes (especially useful around news spikes).
|
|
preferMomentum = (marketState.currentRegime == "VOLATILE" || marketState.marketStress);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void ResetFastSpikePendingState()
|
|
{
|
|
fastSpikePending = false;
|
|
fastSpikePendingDirection = 0;
|
|
fastSpikePendingStartMs = 0;
|
|
fastSpikePendingAnchorPrice = 0.0;
|
|
fastSpikePendingThresholdPoints = 0;
|
|
fastSpikePendingMovePoints = 0.0;
|
|
fastSpikePendingHoldTicks = 0;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool DetectFastSpike(int &spikeDirection, double &movePoints, double &anchorPrice, uint &elapsedMs, int &thresholdPointsOut)
|
|
{
|
|
spikeDirection = 0;
|
|
movePoints = 0.0;
|
|
anchorPrice = 0.0;
|
|
elapsedMs = 0;
|
|
thresholdPointsOut = 0;
|
|
|
|
if(!UseFastSpikeReaction || point_value <= 0.0)
|
|
return false;
|
|
|
|
uint nowMs = GetTickCount();
|
|
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
|
double mid = (bid + ask) / 2.0;
|
|
if(mid <= 0.0)
|
|
return false;
|
|
|
|
double spreadPointsNow = MathAbs(ask - bid) / point_value;
|
|
int maxSpikeSpreadPoints = (int)MathMax(1, MaxNewsSpreadPoints);
|
|
if(spreadPointsNow > (double)maxSpikeSpreadPoints)
|
|
return false;
|
|
|
|
int thresholdPoints = 0;
|
|
uint windowMs = 0;
|
|
uint cooldownMs = 0;
|
|
bool preferMomentum = false;
|
|
GetAutoFastSpikeParameters(thresholdPoints, windowMs, cooldownMs, preferMomentum);
|
|
|
|
if(fastSpikeAnchorOpen <= 0.0)
|
|
fastSpikeAnchorOpen = mid;
|
|
if(fastSpikeReferenceMs == 0)
|
|
fastSpikeReferenceMs = nowMs;
|
|
|
|
elapsedMs = nowMs - fastSpikeReferenceMs;
|
|
int accelMs = FastSpikeAccelerationMs;
|
|
if(accelMs > 0 && elapsedMs > (uint)MathMax(50, MathMin(5000, accelMs)))
|
|
return false;
|
|
|
|
movePoints = MathAbs(mid - fastSpikeAnchorOpen) / point_value;
|
|
if(movePoints < (double)MathMax(1, thresholdPoints))
|
|
return false;
|
|
|
|
double volumeBurst = GetInstantVolumeBurst(_Symbol, (ENUM_TIMEFRAMES)Period(), 40);
|
|
double minVolumeBurst = 1.20;
|
|
if(marketState.currentRegime == "VOLATILE")
|
|
minVolumeBurst = 1.05;
|
|
else
|
|
if(marketState.currentRegime == "QUIET")
|
|
minVolumeBurst = 1.30;
|
|
|
|
// If the move is extremely strong, allow it even with moderate volume.
|
|
bool exceptionalMove = (movePoints >= ((double)thresholdPoints * 1.8));
|
|
if(!exceptionalMove && volumeBurst < minVolumeBurst)
|
|
return false;
|
|
|
|
anchorPrice = fastSpikeAnchorOpen;
|
|
thresholdPointsOut = MathMax(1, thresholdPoints);
|
|
spikeDirection = (mid > fastSpikeAnchorOpen) ? 1 : -1; // 1=up spike, -1=down spike
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateAccountHealthDisplayState()
|
|
{
|
|
UpdateAccountCache();
|
|
double balance = accountCache.balance;
|
|
double equity = accountCache.equity;
|
|
double margin = accountCache.margin;
|
|
double freeMargin = accountCache.freeMargin;
|
|
queenMarginLevel = accountCache.marginLevel;
|
|
|
|
if(balance <= 0.0)
|
|
balance = 1.0;
|
|
if(equity <= 0.0)
|
|
equity = 1.0;
|
|
|
|
queenAccountDrawdownPct = ((balance - equity) / balance) * 100.0;
|
|
if(queenAccountDrawdownPct < 0.0)
|
|
queenAccountDrawdownPct = 0.0;
|
|
if(queenAccountDrawdownPct > 100.0)
|
|
queenAccountDrawdownPct = 100.0;
|
|
|
|
double exposureLots = 0.0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong posTicket = PositionGetTicket(i);
|
|
if(posTicket <= 0)
|
|
continue;
|
|
exposureLots += PositionGetDouble(POSITION_VOLUME);
|
|
}
|
|
for(int j = OrdersTotal() - 1; j >= 0; j--)
|
|
{
|
|
ulong ordTicket = OrderGetTicket(j);
|
|
if(ordTicket <= 0)
|
|
continue;
|
|
exposureLots += OrderGetDouble(ORDER_VOLUME_INITIAL);
|
|
}
|
|
queenExposureLots = exposureLots;
|
|
|
|
double equityScore = 1.0 - MathMin(queenAccountDrawdownPct / 25.0, 1.0);
|
|
double marginLevelScore = MathMin(MathMax((queenMarginLevel - 80.0) / 220.0, 0.0), 1.0);
|
|
double freeMarginPct = (equity > 0.0) ? (freeMargin / equity) * 100.0 : 0.0;
|
|
double freeMarginScore = MathMin(MathMax(freeMarginPct / 80.0, 0.0), 1.0);
|
|
double marginUsedPct = (equity > 0.0) ? (margin / equity) * 100.0 : 1000.0;
|
|
double loadScore = 1.0 - MathMin(MathMax(marginUsedPct / 120.0, 0.0), 1.0);
|
|
|
|
queenAccountHealthScore = (equityScore * 0.40) + (marginLevelScore * 0.25) + (freeMarginScore * 0.20) + (loadScore * 0.15);
|
|
queenAccountHealthScore = MathMin(MathMax(queenAccountHealthScore, 0.0), 1.0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// INITIALIZATION
|
|
//+------------------------------------------------------------------+
|
|
int OnInit()
|
|
{
|
|
// Start MicCmd (voice via SAPI) early; rest of init follows
|
|
if(!g_micStarted && EnableVoicePrompts)
|
|
{
|
|
bool grammarOk = EnsureVoiceGrammarFile();
|
|
string micErr = "";
|
|
|
|
if(grammarOk && MicStart(g_voiceGrammarPath))
|
|
{
|
|
g_micStarted = true;
|
|
Print("MicCmd started with grammar: ", g_voiceGrammarPath);
|
|
}
|
|
else
|
|
{
|
|
MicGetLastError(micErr);
|
|
if(!grammarOk)
|
|
Print("VOICE CMD | Grammar unavailable, trying dictation mode (no grammar file).");
|
|
else
|
|
Print("MicCmd grammar start failed: ", micErr, " | retrying dictation mode.");
|
|
|
|
// Fallback: start MicCmd without grammar (dictation catches any phrase)
|
|
if(MicStart(""))
|
|
{
|
|
g_micStarted = true;
|
|
Print("MicCmd started in dictation mode (fallback).");
|
|
}
|
|
else
|
|
{
|
|
MicGetLastError(micErr);
|
|
Print("MicCmd start failed even in dictation mode: ", micErr);
|
|
}
|
|
}
|
|
}
|
|
// Validate inputs first
|
|
if(!ValidateInputs())
|
|
{
|
|
Print("CRITICAL ERROR: Input validation failed. Expert Advisor will not start.");
|
|
return(INIT_FAILED);
|
|
}
|
|
|
|
// Initialize trade object
|
|
trade.SetExpertMagicNumber(MagicNumber);
|
|
|
|
// Initialize external decision DLL (best effort with native fallback).
|
|
g_decisionDllReady = false;
|
|
g_lastDecisionSnapshotLog = 0;
|
|
g_lastDllDecisionAction = "N/A";
|
|
g_lastDllDecisionSymbol = "";
|
|
g_lastDllDecisionRegime = "UNKNOWN";
|
|
g_lastDllDecisionScore = 5;
|
|
g_lastDllDecisionAllowProb = 0.0;
|
|
g_lastDllDecisionRiskMul = 1.0;
|
|
g_lastDllDecisionLatencyUs = 0.0;
|
|
g_lastDllSnapshot = "";
|
|
g_lastDllSnapScoreCalls = -1;
|
|
g_lastDllSnapLearnCalls = -1;
|
|
g_lastDllSnapBasketLearnCalls = -1;
|
|
g_lastDllSnapCacheHits = -1;
|
|
g_lastDllSnapBaselineVol = -1.0;
|
|
g_lastDllSnapBaselineSpread = -1.0;
|
|
if(UseDecisionCoreDll)
|
|
{
|
|
int dcInitRc = dc_init(DecisionCoreDbPath, DecisionCoreSeed);
|
|
if(dcInitRc > 0)
|
|
{
|
|
g_decisionDllReady = true;
|
|
Print("DECISION DLL | Initialized | path=", DecisionCoreDbPath, " | seed=", DecisionCoreSeed);
|
|
}
|
|
else
|
|
{
|
|
Print("DECISION DLL | Init failed rc=", dcInitRc, " | fallback to native memory engine");
|
|
}
|
|
}
|
|
|
|
// Open voice command DB (best-effort)
|
|
g_cmdDb = sqlite_openW(g_cmdDbPath);
|
|
if(g_cmdDb > 0)
|
|
{
|
|
sqlite_reset_stuck(g_cmdDb, 60);
|
|
Print("VOICE CMD | DB opened: ", g_cmdDbPath);
|
|
}
|
|
else
|
|
{
|
|
Print("VOICE CMD | Failed to open DB: ", g_cmdDbPath);
|
|
}
|
|
|
|
// Friendly intro voice on attach (Zira TTS - no .wav needed)
|
|
if(EnableVoicePrompts)
|
|
SpeakZira("good day Sir Tudor .Systems is online. I will trade for you.");
|
|
|
|
// Determine conversion for distance inputs (TP/SL/trailing/breakeven/spread)
|
|
// If InputsArePips=true we convert pips to broker points, otherwise we use raw broker points
|
|
|
|
double pipToPointMultiplier = 1.0;
|
|
if(Digits() == 5 || Digits() == 3)
|
|
{
|
|
pipToPointMultiplier = 10.0; // 5/3-digit symbols: 1 pip = 10 points
|
|
}
|
|
else
|
|
if(Digits() == 4 || Digits() == 2)
|
|
{
|
|
pipToPointMultiplier = 1.0; // 4/2-digit symbols: 1 pip = 1 point
|
|
}
|
|
else
|
|
{
|
|
pipToPointMultiplier = 10.0; // Safe default
|
|
Print("?s??,? Unknown digits format (", Digits(), ") - using default: 1 pip = 10 points");
|
|
}
|
|
|
|
// Inputs can be interpreted either as pips (then we scale by pipToPointMultiplier) or raw broker points
|
|
if(InputsArePips)
|
|
{
|
|
point_value = Point() * pipToPointMultiplier; // price distance per input pip
|
|
pip_value = pipToPointMultiplier; // points per pip for spread conversion
|
|
}
|
|
else
|
|
{
|
|
point_value = Point(); // price distance per broker point
|
|
pip_value = 1.0; // spread inputs are already in points
|
|
}
|
|
|
|
long slippagePoints = (long)(InputsArePips ? (Slippage * pip_value) : Slippage); // convert pips to broker points when needed
|
|
trade.SetDeviationInPoints(slippagePoints); // Apply slippage setting to trade execution
|
|
|
|
string unitLabel = InputsArePips ? "pip" : "point";
|
|
Print("Symbol: ", Symbol(), " | Digits: ", Digits(), " | Price per ", unitLabel, ": ", point_value, " | Points per ", unitLabel, ": ", pip_value);
|
|
Print("TakeProfit: ", TakeProfit, " ", unitLabel, "s = ", (TakeProfit * point_value), " price units");
|
|
Print("StopLoss: ", StopLoss, " ", unitLabel, "s = ", (StopLoss * point_value), " price units");
|
|
|
|
// Load persisted state (optional)
|
|
if(UseFilePersistence)
|
|
LoadStateFromFile();
|
|
|
|
// Initialize Order Book (DOM) if enabled
|
|
if(UseDOMFilter && !ForceDisableDOM)
|
|
{
|
|
// Check account type (Demo/Live)
|
|
ENUM_ACCOUNT_TRADE_MODE accountMode = (ENUM_ACCOUNT_TRADE_MODE)AccountInfoInteger(ACCOUNT_TRADE_MODE);
|
|
bool isDemoAccount = (accountMode == ACCOUNT_TRADE_MODE_DEMO);
|
|
bool isLiveAccount = (accountMode == ACCOUNT_TRADE_MODE_REAL);
|
|
string accountType = isDemoAccount ? "DEMO" : (isLiveAccount ? "LIVE" : "UNKNOWN");
|
|
|
|
// Check if running in Strategy Tester - DOM is not available in tester
|
|
bool isTester = MQLInfoInteger(MQL_TESTER);
|
|
if(isTester)
|
|
{
|
|
Print(" Strategy Tester detected - DOM (Order Book) is NOT available in tester");
|
|
string domModeRequested = DOMAsSignal ? "SIGNAL MODE" : "FILTER MODE";
|
|
Print(" DOM (", domModeRequested, ") will be disabled. Trading will use only Consecutive Bars signals.");
|
|
Print(" Note: DOM requires ECN broker and live/demo account - not available in Strategy Tester");
|
|
domFilterActive = false;
|
|
domAvailable = false;
|
|
}
|
|
// Outside tester: enforce ECN requirement for DOM (auto-disable if non-ECN)
|
|
else
|
|
{
|
|
domAvailable = IsECNBroker();
|
|
if(!domAvailable)
|
|
{
|
|
Print(" ECN Broker NOT detected - DOM auto-disabled");
|
|
Print(" Account Type: ", accountType, " | Broker: ", AccountInfoString(ACCOUNT_COMPANY));
|
|
Print(" Trading continues normally without DOM.");
|
|
if(isDemoAccount)
|
|
Print(" Note: DEMO works with DOM only on ECN brokers.");
|
|
domFilterActive = false;
|
|
}
|
|
else
|
|
{
|
|
Print(" ECN Broker detected - DOM available");
|
|
Print(" Account Type: ", accountType, " (DOM works on both DEMO and LIVE ECN accounts)");
|
|
domFilterActive = true;
|
|
}
|
|
}
|
|
|
|
// Try to initialize DOM if available
|
|
if(domFilterActive && domAvailable)
|
|
{
|
|
if(MarketBookAdd(Symbol()))
|
|
{
|
|
domInitialized = true;
|
|
domFilterActive = true;
|
|
Print(" Order Book (DOM) initialized for ", Symbol());
|
|
string domMode = DOMAsSignal ? "SIGNAL MODE (provides signal, doesn't block)" : "FILTER MODE (blocks trades)";
|
|
ENUM_ACCOUNT_TRADE_MODE accountMode = (ENUM_ACCOUNT_TRADE_MODE)AccountInfoInteger(ACCOUNT_TRADE_MODE);
|
|
string accountType = (accountMode == ACCOUNT_TRADE_MODE_DEMO) ? "DEMO" : ((accountMode == ACCOUNT_TRADE_MODE_REAL) ? "LIVE" : "UNKNOWN");
|
|
Print(" DOM: ENABLED - ", domMode, " (Imbalance: ", MinImbalanceRatio, "x | Min Liquidity: ", MinLiquidity, " lots)");
|
|
Print(" Account Type: ", accountType, " | Broker: ", AccountInfoString(ACCOUNT_COMPANY));
|
|
}
|
|
else
|
|
{
|
|
int error = GetLastError();
|
|
Print(" Failed to initialize Order Book (DOM) - Error: ", error);
|
|
if(error == 4014) // ERR_MARKET_NOT_SELECTED or similar
|
|
{
|
|
Print(" Market Book not available for this symbol or broker");
|
|
}
|
|
Print(" DOM filter disabled - trading will continue without DOM analysis");
|
|
domFilterActive = false;
|
|
domAvailable = false;
|
|
}
|
|
}
|
|
else
|
|
if(UseDOMFilter && !domFilterActive)
|
|
{
|
|
bool isTester = MQLInfoInteger(MQL_TESTER);
|
|
if(isTester)
|
|
{
|
|
Print(" DOM requested but not available (Strategy Tester) - trading without DOM");
|
|
}
|
|
else
|
|
{
|
|
string domModeRequested = DOMAsSignal ? "SIGNAL MODE" : "FILTER MODE";
|
|
ENUM_ACCOUNT_TRADE_MODE accountMode = (ENUM_ACCOUNT_TRADE_MODE)AccountInfoInteger(ACCOUNT_TRADE_MODE);
|
|
string accountType = (accountMode == ACCOUNT_TRADE_MODE_DEMO) ? "DEMO" : ((accountMode == ACCOUNT_TRADE_MODE_REAL) ? "LIVE" : "UNKNOWN");
|
|
Print(" DOM (", domModeRequested, ") requested but not available - trading without DOM");
|
|
Print(" Account Type: ", accountType, " | Broker: ", AccountInfoString(ACCOUNT_COMPANY));
|
|
Print(" Reason: Non-ECN broker detected. DOM requires ECN broker (IC Markets, Pepperstone, FXTM, etc.)");
|
|
if(accountMode == ACCOUNT_TRADE_MODE_DEMO)
|
|
{
|
|
Print(" Note: Even on DEMO accounts, DOM only works with ECN brokers");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
domFilterActive = false;
|
|
if(UseDOMFilter && ForceDisableDOM)
|
|
Print("DOM hard bypass active - DOM is ignored by configuration.");
|
|
}
|
|
|
|
// Validate symbol information
|
|
if(!ValidateSymbolInfo())
|
|
{
|
|
Print("CRITICAL ERROR: Symbol validation failed. Expert Advisor will not start.");
|
|
return(INIT_FAILED);
|
|
}
|
|
|
|
Print("=== King Gold Scalper v9.0 Initialized ===");
|
|
Print("Symbol: ", Symbol());
|
|
ENUM_ACCOUNT_TRADE_MODE accountMode = (ENUM_ACCOUNT_TRADE_MODE)AccountInfoInteger(ACCOUNT_TRADE_MODE);
|
|
string accountType = (accountMode == ACCOUNT_TRADE_MODE_DEMO) ? "DEMO" : ((accountMode == ACCOUNT_TRADE_MODE_REAL) ? "LIVE" : "UNKNOWN");
|
|
Print("Account Type: ", accountType, " | Broker: ", AccountInfoString(ACCOUNT_COMPANY));
|
|
string timeSource = (UseBrokerTime) ? "Broker Server Time" : ("Local PC Time (Offset: " + IntegerToString(TimezoneOffset) + "h)");
|
|
Print("Time Source: ", timeSource);
|
|
Print("Trading Hours: ", StartHour, ":00 - ", EndHour, ":00 (", timeSource, ")");
|
|
double configuredBaseRisk = GetConfiguredBaseRiskPercent();
|
|
Print("Risk: ", configuredBaseRisk, "% (RiskPercent=", RiskPercent, ", Default=", DefaultRiskPercent,
|
|
") | TP: ", TakeProfit, " points | SL: ", StopLoss, " points");
|
|
if(RiskPercent <= 0.0)
|
|
Print("RiskPercent is 0. Using DefaultRiskPercent for lot sizing and adaptive risk.");
|
|
string consecutiveSignalMode = UseMomentumExhaustionSignals ? "Momentum Exhaustion (reversal)" : "Trend Following";
|
|
Print("Signals: Consecutive Bars (", consecutiveSignalMode, ") + Multi-TF scanner/memory per symbol");
|
|
|
|
// Initialize cache systems
|
|
accountCache.update();
|
|
marketCache.update();
|
|
Print("Cache systems initialized - Account data updates every ", ACCOUNT_UPDATE_INTERVAL, " seconds");
|
|
Print("Drawdown protection enabled - Max allowed: ", MAX_DRAWDOWN_PERCENT, "%");
|
|
NormalizeTelegramInputs();
|
|
if(EnableTelegramAlerts)
|
|
{
|
|
Print("TELEGRAM ENABLED | Ensure MT5 WebRequest allow-list contains: https://api.telegram.org");
|
|
if(StringLen(g_telegramBotToken) <= 0 || StringLen(g_telegramChatId) <= 0)
|
|
Print("TELEGRAM WARNING | TelegramBotToken/TelegramChatId are empty - notifications disabled until configured.");
|
|
else
|
|
{
|
|
datetime telegramTestTime = 0;
|
|
bool telegramTestOk = SendTelegramMessage("Test OK - King of Gold", telegramTestTime, 0);
|
|
if(telegramTestOk)
|
|
Print("TELEGRAM TEST | Message sent successfully.");
|
|
else
|
|
Print("TELEGRAM TEST | Failed to send test message. Check token/chat_id and WebRequest allow-list.");
|
|
}
|
|
}
|
|
|
|
// Initialize advanced memory system
|
|
InitializeMemorySystem();
|
|
if(UseWarmupOnInit)
|
|
{
|
|
WarmupFromHistory();
|
|
if(WarmupAllMarketWatchSymbols)
|
|
WarmupAllSymbolsFromMarketWatch();
|
|
}
|
|
Print("Advanced memory system initialized - Learning capabilities activated!");
|
|
if(domFilterActive)
|
|
{
|
|
string domMode = DOMAsSignal ? "SIGNAL MODE (provides signal, doesn't block)" : "FILTER MODE (blocks trades)";
|
|
Print("DOM: ACTIVE - ", domMode, " (Imbalance: ", MinImbalanceRatio, "x | Min Liquidity: ", MinLiquidity, " lots)");
|
|
Print(" DOM works on both DEMO and LIVE ECN accounts");
|
|
}
|
|
else
|
|
if(UseDOMFilter)
|
|
{
|
|
bool isTester = MQLInfoInteger(MQL_TESTER);
|
|
if(isTester)
|
|
{
|
|
string domModeRequested = DOMAsSignal ? "SIGNAL MODE" : "FILTER MODE";
|
|
Print("DOM: REQUESTED (", domModeRequested, ") but not available - Strategy Tester detected (DOM requires live/demo ECN account)");
|
|
}
|
|
else
|
|
{
|
|
string domModeRequested = DOMAsSignal ? "SIGNAL MODE" : "FILTER MODE";
|
|
ENUM_ACCOUNT_TRADE_MODE accountModeFinal = (ENUM_ACCOUNT_TRADE_MODE)AccountInfoInteger(ACCOUNT_TRADE_MODE);
|
|
string accountTypeFinal = (accountModeFinal == ACCOUNT_TRADE_MODE_DEMO) ? "DEMO" : ((accountModeFinal == ACCOUNT_TRADE_MODE_REAL) ? "LIVE" : "UNKNOWN");
|
|
Print("DOM: REQUESTED (", domModeRequested, ") but not available");
|
|
Print(" Account Type: ", accountTypeFinal, " | Broker: ", AccountInfoString(ACCOUNT_COMPANY));
|
|
Print(" Reason: Non-ECN broker or initialization failed");
|
|
if(accountModeFinal == ACCOUNT_TRADE_MODE_DEMO)
|
|
{
|
|
Print(" Note: Even on DEMO accounts, DOM only works with ECN brokers (IC Markets, Pepperstone, FXTM, etc.)");
|
|
}
|
|
else
|
|
{
|
|
Print(" Note: DOM works on both DEMO and LIVE accounts if broker is ECN (IC Markets, Pepperstone, etc.)");
|
|
}
|
|
}
|
|
}
|
|
Print("Magic Number: ", MagicNumber);
|
|
|
|
// Sentiment/adaptive init
|
|
CalculateTotalClosedTrades();
|
|
lastBalance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
currentRiskPercent = GetConfiguredBaseRiskPercent();
|
|
Print("Historical Trades: ", winCount, "W / ", lossCount, "L");
|
|
|
|
// Reset tick-bar state on init
|
|
tickBarIndex = 0;
|
|
tickBarCount = 0;
|
|
tickBarOpen = 0;
|
|
tickBarHigh = 0;
|
|
tickBarLow = 0;
|
|
tickBarClose = 0;
|
|
fastSpikeReferenceMs = GetTickCount();
|
|
fastSpikeReferencePrice = (SymbolInfoDouble(_Symbol, SYMBOL_BID) + SymbolInfoDouble(_Symbol, SYMBOL_ASK)) / 2.0;
|
|
fastSpikeAnchorBarTime = iTime(_Symbol, (ENUM_TIMEFRAMES)Period(), 0);
|
|
fastSpikeAnchorOpen = iOpen(_Symbol, (ENUM_TIMEFRAMES)Period(), 0);
|
|
if(fastSpikeAnchorOpen <= 0.0)
|
|
fastSpikeAnchorOpen = fastSpikeReferencePrice;
|
|
lastFastSpikeSignalMs = 0;
|
|
ResetFastSpikePendingState();
|
|
|
|
return(INIT_SUCCEEDED);
|
|
}
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
// MAIN LOGIC - CALLED ON EVERY TICK
|
|
//+------------------------------------------------------------------+
|
|
void OnTick()
|
|
{
|
|
PollMicCmdQueue();
|
|
EmitHeartbeat();
|
|
EmitDecisionDllSnapshot();
|
|
AutosaveStateIfDue();
|
|
CheckUpcomingEconomicNewsAlerts();
|
|
CheckFutureSessionEventAlerts();
|
|
PollVoiceCommandQueue();
|
|
|
|
// ========== PRIORITY 1: CLOSE ALL TRADES AT END OF TRADING SESSION ==========
|
|
// Only close trades at EndHour if AutoCloseProfit target was hit
|
|
datetime currentTime = GetCurrentTimeWithOffset();
|
|
UpdateRecoveryMode();
|
|
|
|
bool useAutoCloseEffective = UseAutoClose || (UseRecoveryAutoClose && recoveryModeActive);
|
|
|
|
MqlDateTime dt;
|
|
TimeToStruct(currentTime, dt);
|
|
int currentHour = dt.hour;
|
|
|
|
string activeSymbols[];
|
|
GetAutoCloseActiveSymbols(activeSymbols);
|
|
|
|
if(useAutoCloseEffective)
|
|
{
|
|
for(int s = 0; s < ArraySize(activeSymbols); s++)
|
|
{
|
|
string acSymbol = activeSymbols[s];
|
|
int openTrades = CountOpenTradesForSymbolMagic(acSymbol);
|
|
if(openTrades <= 0)
|
|
continue;
|
|
|
|
int stateIdx = FindAutoCloseStateIndex(acSymbol, true);
|
|
double totalProfit = CalculateTotalProfitForSymbol(acSymbol);
|
|
|
|
bool symbolUseAutoCloseEffective = UseAutoClose || (UseRecoveryAutoClose && recoveryModeActive);
|
|
double symbolAutoCloseProfitEffective = AutoCloseProfit;
|
|
bool symbolUseAutoCloseTrailingEffective = UseAutoCloseTrailing;
|
|
double symbolAutoCloseTrailingStartEffective = AutoCloseTrailingStart;
|
|
double symbolAutoCloseTrailingStopEffective = AutoCloseTrailingStop;
|
|
bool recoveryCloseContext = false;
|
|
|
|
if(UseRecoveryAutoClose && recoveryModeActive)
|
|
{
|
|
bool symbolHoldConfiguredTarget = ShouldHoldConfiguredAutoCloseInRecoveryForSymbol(acSymbol);
|
|
LogRecoveryTargetModeForSymbol(acSymbol, symbolHoldConfiguredTarget);
|
|
|
|
if(!symbolHoldConfiguredTarget)
|
|
{
|
|
double recoveryScale = MathMax(0.1, RecoveryRiskMultiplier);
|
|
symbolAutoCloseProfitEffective = MathMax(0.0, RecoveryMinProfitUsd / recoveryScale);
|
|
symbolUseAutoCloseTrailingEffective = RecoveryUseAutoCloseTrailing;
|
|
symbolAutoCloseTrailingStartEffective = RecoveryAutoCloseTrailingStart / recoveryScale;
|
|
symbolAutoCloseTrailingStopEffective = RecoveryAutoCloseTrailingStop / recoveryScale;
|
|
recoveryCloseContext = true;
|
|
}
|
|
}
|
|
|
|
if(!symbolUseAutoCloseEffective)
|
|
continue;
|
|
|
|
double trailingStart = MathMax(symbolAutoCloseProfitEffective, symbolAutoCloseTrailingStartEffective);
|
|
|
|
// Session-end close per symbol, only after target was reached.
|
|
if(currentHour >= EndHour && totalProfit >= symbolAutoCloseProfitEffective)
|
|
{
|
|
Print(" TRADING SESSION ENDED [", acSymbol, "] at hour ", currentHour, " (EndHour=", EndHour,
|
|
") - CLOSING ", openTrades, " TRADES (Profit: $", totalProfit, " >= Target: $", symbolAutoCloseProfitEffective, ")");
|
|
if(EnableVoicePrompts && VoiceCooldown(lastAutoCloseSoundTime, VoiceCooldownSeconds))
|
|
SpeakZira("Auto close triggered on " + acSymbol + ". Closing all trades.");
|
|
if(TelegramSendCloses)
|
|
{
|
|
string msg = "📌 KING GOLD AUTO-CLOSE\n"
|
|
"🚪 Session end trigger\n"
|
|
"📊 Symbol: " + acSymbol + "\n"
|
|
"💰 Profit: $" + DoubleToString(totalProfit, 2) + " / Target: $" + DoubleToString(symbolAutoCloseProfitEffective, 2) + "\n"
|
|
"✅ Action: closing all trades\n"
|
|
"⏰ Time: " + TimeToString(currentTime, TIME_DATE | TIME_SECONDS);
|
|
SendTelegramMessage(msg, lastTelegramCloseTime, TelegramCooldownSeconds);
|
|
}
|
|
RegisterAutoCloseLearningContext(acSymbol, totalProfit, currentTime, openTrades, recoveryModeActive);
|
|
CloseAllTradesForSymbol(acSymbol);
|
|
if(recoveryCloseContext)
|
|
Print(" CLOSED BY RECOVERY AUTOCLOSE [", acSymbol, "] | Profit: $", DoubleToString(totalProfit, 2),
|
|
" | Target: $", DoubleToString(symbolAutoCloseProfitEffective, 2));
|
|
MarkSymbolAutoCloseTriggered(acSymbol, currentTime);
|
|
continue;
|
|
}
|
|
|
|
// ========== TRAILING CLOSE MODE (PER SYMBOL) ==========
|
|
if(symbolUseAutoCloseTrailingEffective && autoCloseSymbolStates[stateIdx].inTrailing)
|
|
{
|
|
if(totalProfit > autoCloseSymbolStates[stateIdx].highestProfitInTrailing)
|
|
{
|
|
autoCloseSymbolStates[stateIdx].highestProfitInTrailing = totalProfit;
|
|
Print(" NEW PROFIT PEAK [", acSymbol, "] in Trailing Mode: $", autoCloseSymbolStates[stateIdx].highestProfitInTrailing);
|
|
}
|
|
|
|
double trailingCloseTarget = autoCloseSymbolStates[stateIdx].highestProfitInTrailing - symbolAutoCloseTrailingStopEffective;
|
|
if(totalProfit <= trailingCloseTarget)
|
|
{
|
|
Print(" Trailing Stop Triggered [", acSymbol, "]: Profit $", totalProfit, " <= Target $", trailingCloseTarget,
|
|
" - CLOSING TRADES");
|
|
if(EnableVoicePrompts && VoiceCooldown(lastAutoCloseSoundTime, VoiceCooldownSeconds))
|
|
SpeakZira("Auto close trailing stop triggered on " + acSymbol + ". Closing all trades.");
|
|
if(TelegramSendCloses)
|
|
{
|
|
string msg = "📌 KING GOLD AUTO-CLOSE\n"
|
|
"🧷 Trailing stop triggered\n"
|
|
"📊 Symbol: " + acSymbol + "\n"
|
|
"💰 Profit: $" + DoubleToString(totalProfit, 2) + "\n"
|
|
"🎯 Trail target: $" + DoubleToString(trailingCloseTarget, 2) + "\n"
|
|
"✅ Action: closing all trades\n"
|
|
"⏰ Time: " + TimeToString(currentTime, TIME_DATE | TIME_SECONDS);
|
|
SendTelegramMessage(msg, lastTelegramCloseTime, TelegramCooldownSeconds);
|
|
}
|
|
RegisterAutoCloseLearningContext(acSymbol, totalProfit, currentTime, openTrades, recoveryModeActive);
|
|
CloseAllTradesForSymbol(acSymbol);
|
|
if(recoveryCloseContext)
|
|
Print(" CLOSED BY RECOVERY AUTOCLOSE (TRAILING) [", acSymbol, "] | Profit: $", DoubleToString(totalProfit, 2),
|
|
" | TrailTarget: $", DoubleToString(trailingCloseTarget, 2));
|
|
MarkSymbolAutoCloseTriggered(acSymbol, currentTime);
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
if(symbolUseAutoCloseTrailingEffective && totalProfit >= trailingStart)
|
|
{
|
|
autoCloseSymbolStates[stateIdx].inTrailing = true;
|
|
autoCloseSymbolStates[stateIdx].highestProfitInTrailing = totalProfit;
|
|
Print(" Target Profit Reached [", acSymbol, "]: $", totalProfit,
|
|
" - ENTERING TRAILING CLOSE MODE (Start: $", trailingStart, ", Trail: $", symbolAutoCloseTrailingStopEffective, ")");
|
|
if(EnableVoicePrompts && VoiceCooldown(lastAutoCloseSoundTime, VoiceCooldownSeconds))
|
|
SpeakZira("Auto close trailing mode activated on " + acSymbol + ".");
|
|
if(TelegramSendCloses)
|
|
{
|
|
string msg = "📌 KING GOLD AUTO-CLOSE\n"
|
|
"📈 Trailing mode activated\n"
|
|
"📊 Symbol: " + acSymbol + "\n"
|
|
"💰 Current profit: $" + DoubleToString(totalProfit, 2) + "\n"
|
|
"🎯 Start: $" + DoubleToString(trailingStart, 2) + " | Trail: $" + DoubleToString(symbolAutoCloseTrailingStopEffective, 2) + "\n"
|
|
"⏰ Time: " + TimeToString(currentTime, TIME_DATE | TIME_SECONDS);
|
|
SendTelegramMessage(msg, lastTelegramCloseTime, TelegramCooldownSeconds);
|
|
}
|
|
SyncChartAutoCloseState();
|
|
continue;
|
|
}
|
|
else
|
|
if(!symbolUseAutoCloseTrailingEffective && totalProfit >= symbolAutoCloseProfitEffective)
|
|
{
|
|
Print(" Target Profit Reached [", acSymbol, "]: ", totalProfit, " >= ", symbolAutoCloseProfitEffective, " - CLOSING TRADES");
|
|
if(EnableVoicePrompts && VoiceCooldown(lastAutoCloseSoundTime, VoiceCooldownSeconds))
|
|
SpeakZira("Sir.Auto close target reached on " + acSymbol + ". Closing all trades.profits book in.");
|
|
if(TelegramSendCloses)
|
|
{
|
|
string msg = "📌 KING GOLD AUTO-CLOSE\n"
|
|
"🎯 Target reached\n"
|
|
"📊 Symbol: " + acSymbol + "\n"
|
|
"💰 Profit: $" + DoubleToString(totalProfit, 2) + " / Target: $" + DoubleToString(symbolAutoCloseProfitEffective, 2) + "\n"
|
|
"✅ Action: closing all trades\n"
|
|
"⏰ Time: " + TimeToString(currentTime, TIME_DATE | TIME_SECONDS);
|
|
SendTelegramMessage(msg, lastTelegramCloseTime, TelegramCooldownSeconds);
|
|
}
|
|
RegisterAutoCloseLearningContext(acSymbol, totalProfit, currentTime, openTrades, recoveryModeActive);
|
|
CloseAllTradesForSymbol(acSymbol);
|
|
if(recoveryCloseContext)
|
|
Print(" CLOSED BY RECOVERY AUTOCLOSE [", acSymbol, "] | Profit: $", DoubleToString(totalProfit, 2),
|
|
" | Target: $", DoubleToString(symbolAutoCloseProfitEffective, 2));
|
|
MarkSymbolAutoCloseTriggered(acSymbol, currentTime);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========== PER-SYMBOL RE-ENTRY GATE AFTER AUTO-CLOSE ==========
|
|
if(!AutoCloseResumeTrading)
|
|
{
|
|
for(int i = 0; i < ArraySize(autoCloseSymbolStates); i++)
|
|
{
|
|
if(!autoCloseSymbolStates[i].triggered)
|
|
continue;
|
|
IsSymbolAutoCloseBlocked(autoCloseSymbolStates[i].symbol, currentTime);
|
|
}
|
|
}
|
|
|
|
// ========== PRIORITY 3: Check if we can trade (normal trading hour restrictions) ==========
|
|
if(!CanTrade())
|
|
return;
|
|
|
|
// Safe mode detection
|
|
safeModeActive = (UseSafeBalanceMode && AccountInfoDouble(ACCOUNT_BALANCE) < SafeBalanceThreshold);
|
|
currentMaxOpenTrades = safeModeActive ? SafeMaxOpenTrades : MaxOpenTrades;
|
|
|
|
// Update consecutive bar counters
|
|
UpdateConsecutiveSignals();
|
|
|
|
// ========== PRIORITY 4: ENHANCED SYSTEM UPDATES ==========
|
|
UpdateMarketState();
|
|
UpdateMarketStressDetection();
|
|
if(marketState.marketStress)
|
|
EmitRiskWarningSound();
|
|
memoryAdaptiveRiskMultiplier = GetAdaptiveRiskMultiplier();
|
|
|
|
// Update enhanced memory system
|
|
static datetime lastMemoryUpdate = 0;
|
|
if(TimeCurrent() - lastMemoryUpdate > 60) // Every minute
|
|
{
|
|
UpdateEnhancedMemorySystem();
|
|
lastMemoryUpdate = TimeCurrent();
|
|
}
|
|
|
|
// Scan for opportunities
|
|
static datetime lastScan = 0;
|
|
if(TimeCurrent() - lastScan > 30) // Every 30 seconds
|
|
{
|
|
scanner.scanAllSymbols();
|
|
lastScan = TimeCurrent();
|
|
|
|
// Print opportunity report
|
|
Print("=== MULTI-SYMBOL SCANNER ===");
|
|
Print(scanner.getTopOpportunities());
|
|
}
|
|
|
|
// Enhanced opportunity gate (multi-symbol, full rules)
|
|
int maxTopSymbols = GetEffectiveMaxScannedOpportunities();
|
|
for(int i = 0; i < maxTopSymbols; i++)
|
|
{
|
|
if(scanner.topSymbols[i] == "")
|
|
continue;
|
|
|
|
string symbol = scanner.topSymbols[i];
|
|
if(!IsSymbolAllowedByScanCategories(symbol))
|
|
continue;
|
|
double symbolBid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
double symbolAsk = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
if(symbolBid <= 0.0 || symbolAsk <= 0.0)
|
|
continue;
|
|
|
|
int symbolCurrentBar = (int)iTime(symbol, Period(), 1); // closed bar id (not live tick bar)
|
|
if(symbolCurrentBar <= 0)
|
|
continue;
|
|
int bearishConsecutiveForBuy = GetConsecutiveBarsMultiTFForSymbol(symbol, false);
|
|
int bullishConsecutiveForSell = GetConsecutiveBarsMultiTFForSymbol(symbol, true);
|
|
bool longConsecutiveOk = true;
|
|
bool shortConsecutiveOk = true;
|
|
if(UseConsecutiveBars)
|
|
{
|
|
if(UseMomentumExhaustionSignals)
|
|
{
|
|
// Reversal mode: BUY after bearish streak, SELL after bullish streak
|
|
longConsecutiveOk = (bearishConsecutiveForBuy >= BearishBarsForBuy);
|
|
shortConsecutiveOk = (bullishConsecutiveForSell >= BullishBarsForSell);
|
|
}
|
|
else
|
|
{
|
|
// Trend mode: BUY after bullish streak, SELL after bearish streak
|
|
longConsecutiveOk = (bullishConsecutiveForSell >= BullishBarsForSell);
|
|
shortConsecutiveOk = (bearishConsecutiveForBuy >= BearishBarsForBuy);
|
|
}
|
|
}
|
|
|
|
enhancedTFAnalyzer.analyzeSymbolWithSignals(symbol);
|
|
|
|
if(enhancedTFAnalyzer.shouldEnterLong(symbol) && longConsecutiveOk)
|
|
{
|
|
if(CountOpenTradesAllSymbols() < currentMaxOpenTrades &&
|
|
CountOpenTradesOfTypeForSymbol(symbol, POSITION_TYPE_BUY) < MaxTradesPerSymbolDirection &&
|
|
CanEnterSymbolDirectionOnBar(symbol, 1, symbolCurrentBar))
|
|
{
|
|
double currentVolumeRatio = GetCurrentVolumeRatioForSymbol(symbol, (ENUM_TIMEFRAMES)Period());
|
|
bool useExploreLot = false;
|
|
int decisionScore = 0;
|
|
bool hardFiltersPassed = CheckAllFiltersForTradeForSymbol(symbol, POSITION_TYPE_BUY, symbolBid, symbolAsk, bearishConsecutiveForBuy);
|
|
if(!hardFiltersPassed)
|
|
continue;
|
|
bool blockedByMemory = ShouldSkipTradeBasedOnMemoryForSymbol(symbol, 1, bearishConsecutiveForBuy, currentVolumeRatio, symbolBid, symbolAsk, useExploreLot, decisionScore);
|
|
if(blockedByMemory)
|
|
{
|
|
MarkSymbolDirectionEnteredOnBar(symbol, 1, symbolCurrentBar);
|
|
continue;
|
|
}
|
|
if(hardFiltersPassed)
|
|
{
|
|
nextTradeUseExploreLot = useExploreLot;
|
|
ExecuteBullishEntry(symbol);
|
|
nextTradeUseExploreLot = false;
|
|
MarkSymbolDirectionEnteredOnBar(symbol, 1, symbolCurrentBar);
|
|
ManageOpenPositions();
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
if(enhancedTFAnalyzer.shouldEnterShort(symbol) && shortConsecutiveOk)
|
|
{
|
|
if(CountOpenTradesAllSymbols() < currentMaxOpenTrades &&
|
|
CountOpenTradesOfTypeForSymbol(symbol, POSITION_TYPE_SELL) < MaxTradesPerSymbolDirection &&
|
|
CanEnterSymbolDirectionOnBar(symbol, -1, symbolCurrentBar))
|
|
{
|
|
double currentVolumeRatio = GetCurrentVolumeRatioForSymbol(symbol, (ENUM_TIMEFRAMES)Period());
|
|
bool useExploreLot = false;
|
|
int decisionScore = 0;
|
|
bool hardFiltersPassed = CheckAllFiltersForTradeForSymbol(symbol, POSITION_TYPE_SELL, symbolBid, symbolAsk, bullishConsecutiveForSell);
|
|
if(!hardFiltersPassed)
|
|
continue;
|
|
bool blockedByMemory = ShouldSkipTradeBasedOnMemoryForSymbol(symbol, -1, bullishConsecutiveForSell, currentVolumeRatio, symbolBid, symbolAsk, useExploreLot, decisionScore);
|
|
if(blockedByMemory)
|
|
{
|
|
MarkSymbolDirectionEnteredOnBar(symbol, -1, symbolCurrentBar);
|
|
continue;
|
|
}
|
|
if(hardFiltersPassed)
|
|
{
|
|
nextTradeUseExploreLot = useExploreLot;
|
|
ExecuteBearishEntry(symbol);
|
|
nextTradeUseExploreLot = false;
|
|
MarkSymbolDirectionEnteredOnBar(symbol, -1, symbolCurrentBar);
|
|
ManageOpenPositions();
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Print signal reports
|
|
static datetime lastSignalReport = 0;
|
|
int signalReportInterval = MathMax(10, SignalReportIntervalSeconds);
|
|
if(TimeCurrent() - lastSignalReport > signalReportInterval)
|
|
{
|
|
for(int i = 0; i < MathMin(3, scanner.opportunityCount); i++)
|
|
{
|
|
Print("=== ", scanner.opportunities[i].symbol, " SIGNAL ANALYSIS ===");
|
|
Print(enhancedTFAnalyzer.generateSignalReport(scanner.opportunities[i].symbol));
|
|
}
|
|
lastSignalReport = TimeCurrent();
|
|
}
|
|
|
|
// Sentiment is updated in OnTradeTransaction to avoid duplicate counting
|
|
if(UseSignalStrengthGreed)
|
|
UpdateSignalStrengthGreed();
|
|
if(UseAdaptiveTrading)
|
|
UpdateAdaptiveTrading();
|
|
|
|
// Learning telemetry (memory + reward) - periodic visibility
|
|
static datetime lastLearningReport = 0;
|
|
int learningInterval = MathMax(30, LearningReportIntervalSeconds);
|
|
if(UseLearningReport && (TimeCurrent() - lastLearningReport > learningInterval))
|
|
{
|
|
PrintLearningTelemetry();
|
|
lastLearningReport = TimeCurrent();
|
|
}
|
|
int bestSessionInterval = MathMax(60, BestSessionReportIntervalSeconds);
|
|
if(UseBestSessionsEngine && (lastBestSessionReport == 0 || (TimeCurrent() - lastBestSessionReport) > bestSessionInterval))
|
|
{
|
|
PrintBestSessionReport();
|
|
lastBestSessionReport = TimeCurrent();
|
|
}
|
|
// Renew pending limit orders if volume momentum weakens
|
|
RenewLimitOrdersByVolume();
|
|
|
|
// Update KogDecisionCore equity tracking
|
|
UpdateEquityMetrics();
|
|
|
|
// Check cooldown after losing trade (revenge trading prevention)
|
|
if(UseCooldown && IsInCooldown())
|
|
return;
|
|
|
|
// Check current price
|
|
double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);
|
|
double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
|
|
|
|
// ========== FAST SPIKE REACTION ==========
|
|
uint nowMs = GetTickCount();
|
|
datetime anchorBarTime = iTime(_Symbol, (ENUM_TIMEFRAMES)Period(), 0);
|
|
if(anchorBarTime != 0 && anchorBarTime != fastSpikeAnchorBarTime)
|
|
{
|
|
fastSpikeAnchorBarTime = anchorBarTime;
|
|
fastSpikeAnchorOpen = iOpen(_Symbol, (ENUM_TIMEFRAMES)Period(), 0);
|
|
if(fastSpikeAnchorOpen <= 0.0)
|
|
fastSpikeAnchorOpen = (bid + ask) * 0.5;
|
|
fastSpikeReferenceMs = nowMs;
|
|
fastSpikeReferencePrice = fastSpikeAnchorOpen;
|
|
ResetFastSpikePendingState();
|
|
}
|
|
else
|
|
if(fastSpikeAnchorOpen <= 0.0)
|
|
{
|
|
fastSpikeAnchorOpen = iOpen(_Symbol, (ENUM_TIMEFRAMES)Period(), 0);
|
|
if(fastSpikeAnchorOpen <= 0.0)
|
|
fastSpikeAnchorOpen = (bid + ask) * 0.5;
|
|
fastSpikeReferenceMs = nowMs;
|
|
fastSpikeReferencePrice = fastSpikeAnchorOpen;
|
|
}
|
|
|
|
int autoThresholdPoints = 0;
|
|
uint autoWindowMs = 0;
|
|
uint cooldownMs = 0;
|
|
bool preferMomentum = false;
|
|
GetAutoFastSpikeParameters(autoThresholdPoints, autoWindowMs, cooldownMs, preferMomentum);
|
|
|
|
double spreadPointsNow = (point_value > 0.0) ? (MathAbs(ask - bid) / point_value) : 0.0;
|
|
int maxSpikeSpreadPoints = (int)MathMax(1, MaxNewsSpreadPoints);
|
|
|
|
if(fastSpikePending)
|
|
{
|
|
int newsConfirmMs = (NewsConfirmMs > 0) ? NewsConfirmMs : (int)autoWindowMs;
|
|
uint confirmWindowMs = (uint)MathMax(120, MathMin(3000, newsConfirmMs));
|
|
uint confirmMinMs = (uint)MathMax(0, MathMin((int)confirmWindowMs, FastSpikeConfirmMinMs));
|
|
int holdTicksRequired = (int)MathMax(1, MathMin(5, FastSpikeHoldTicks));
|
|
double confirmFactor = ClampValue(FastSpikeConfirmThresholdFactor, 0.6, 1.5);
|
|
uint pendingElapsedMs = nowMs - fastSpikePendingStartMs;
|
|
|
|
if(pendingElapsedMs > confirmWindowMs || fastSpikePendingAnchorPrice <= 0.0)
|
|
{
|
|
ResetFastSpikePendingState();
|
|
}
|
|
else
|
|
{
|
|
if(spreadPointsNow > (double)maxSpikeSpreadPoints)
|
|
{
|
|
if(pendingElapsedMs > (confirmWindowMs / 2))
|
|
{
|
|
Print("FAST SPIKE REACTION | pending canceled (high spread ", DoubleToString(spreadPointsNow, 1),
|
|
" > ", maxSpikeSpreadPoints, " points)");
|
|
ResetFastSpikePendingState();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
double breakDistance = (double)MathMax(1, fastSpikePendingThresholdPoints) * confirmFactor * point_value;
|
|
double upLevel = fastSpikePendingAnchorPrice + breakDistance;
|
|
double dnLevel = fastSpikePendingAnchorPrice - breakDistance;
|
|
bool reverted = false;
|
|
bool holdingBreak = false;
|
|
|
|
if(fastSpikePendingDirection == 1)
|
|
{
|
|
reverted = (bid < fastSpikePendingAnchorPrice);
|
|
holdingBreak = (bid > upLevel);
|
|
}
|
|
else
|
|
if(fastSpikePendingDirection == -1)
|
|
{
|
|
reverted = (ask > fastSpikePendingAnchorPrice);
|
|
holdingBreak = (ask < dnLevel);
|
|
}
|
|
else
|
|
reverted = true;
|
|
|
|
if(reverted)
|
|
{
|
|
ResetFastSpikePendingState();
|
|
}
|
|
else
|
|
{
|
|
fastSpikePendingHoldTicks = holdingBreak ? (fastSpikePendingHoldTicks + 1) : 0;
|
|
bool confirmReady = (pendingElapsedMs >= confirmMinMs && fastSpikePendingHoldTicks >= holdTicksRequired);
|
|
if(confirmReady)
|
|
{
|
|
bool spikeCooldownReady = (lastFastSpikeSignalMs == 0 || (nowMs - lastFastSpikeSignalMs) >= cooldownMs);
|
|
if(spikeCooldownReady && CountOpenTradesAllSymbols() < currentMaxOpenTrades)
|
|
{
|
|
int reactionDirection = fastSpikePendingDirection;
|
|
int positionType = (reactionDirection == 1) ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
|
|
bool filtersOk = CheckAllFiltersForTrade(positionType, bid, ask, 1);
|
|
|
|
double spikeVolumeRatio = GetCurrentVolumeRatio();
|
|
bool useExploreLot = false;
|
|
int decisionScore = 0;
|
|
bool memoryOk = !ShouldSkipTradeBasedOnMemory(reactionDirection, 1, spikeVolumeRatio, bid, ask, useExploreLot, decisionScore);
|
|
nextTradeUseExploreLot = useExploreLot;
|
|
|
|
if(filtersOk && memoryOk)
|
|
{
|
|
string reactionSide = (reactionDirection == 1) ? "BUY" : "SELL";
|
|
string spikeSide = (reactionDirection == 1) ? "UP" : "DOWN";
|
|
string regimeBias = preferMomentum ? "VOLATILE_BIAS" : "CALM_BIAS";
|
|
double spikeVolumeBurstNow = GetInstantVolumeBurst(_Symbol, (ENUM_TIMEFRAMES)Period(), 40);
|
|
Print("FAST SPIKE REACTION [BREAK_HOLD] | Spike ", spikeSide, " ", DoubleToString(fastSpikePendingMovePoints, 1),
|
|
" points (confirm ", pendingElapsedMs, "ms, holdTicks ", fastSpikePendingHoldTicks,
|
|
", threshold ", fastSpikePendingThresholdPoints, ", autoThreshold ", autoThresholdPoints,
|
|
", autoWindow ", autoWindowMs, "ms, cooldown ", cooldownMs,
|
|
"ms, volBurst ", DoubleToString(spikeVolumeBurstNow, 2), "x, ", regimeBias,
|
|
") -> opening ", reactionSide, " order");
|
|
|
|
if(reactionDirection == 1)
|
|
OpenBuyTrade(ask);
|
|
else
|
|
OpenSellTrade(bid);
|
|
|
|
nextTradeUseExploreLot = false;
|
|
lastFastSpikeSignalMs = nowMs;
|
|
ResetFastSpikePendingState();
|
|
ManageOpenPositions();
|
|
return; // avoid multiple entries in same tick
|
|
}
|
|
else
|
|
{
|
|
double spikeVolumeBurstNow = GetInstantVolumeBurst(_Symbol, (ENUM_TIMEFRAMES)Period(), 40);
|
|
Print("FAST SPIKE REACTION [BREAK_HOLD] blocked | move=", DoubleToString(fastSpikePendingMovePoints, 1),
|
|
" | threshold=", fastSpikePendingThresholdPoints,
|
|
" | spread=", DoubleToString(spreadPointsNow, 1),
|
|
" | volBurst=", DoubleToString(spikeVolumeBurstNow, 2), "x");
|
|
nextTradeUseExploreLot = false;
|
|
ResetFastSpikePendingState();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!fastSpikePending)
|
|
{
|
|
int spikeDirection = 0; // 1=up spike, -1=down spike
|
|
double spikeMovePoints = 0.0;
|
|
double spikeAnchorPrice = 0.0;
|
|
uint spikeElapsedMs = 0;
|
|
int spikeThresholdPoints = 0;
|
|
bool spikeCooldownReady = (lastFastSpikeSignalMs == 0 || (nowMs - lastFastSpikeSignalMs) >= cooldownMs);
|
|
if(DetectFastSpike(spikeDirection, spikeMovePoints, spikeAnchorPrice, spikeElapsedMs, spikeThresholdPoints)
|
|
&& spikeCooldownReady
|
|
&& CountOpenTradesAllSymbols() < currentMaxOpenTrades)
|
|
{
|
|
fastSpikePending = true;
|
|
fastSpikePendingDirection = spikeDirection;
|
|
fastSpikePendingStartMs = nowMs;
|
|
fastSpikePendingAnchorPrice = spikeAnchorPrice;
|
|
fastSpikePendingThresholdPoints = spikeThresholdPoints;
|
|
fastSpikePendingMovePoints = spikeMovePoints;
|
|
fastSpikePendingHoldTicks = 0;
|
|
string spikeSide = (spikeDirection == 1) ? "UP" : "DOWN";
|
|
Print("FAST SPIKE DETECTED | ", spikeSide, " move=", DoubleToString(spikeMovePoints, 1),
|
|
" points | elapsed=", spikeElapsedMs, "ms | threshold=", spikeThresholdPoints,
|
|
" | autoThreshold=", autoThresholdPoints,
|
|
" | spread=", DoubleToString(spreadPointsNow, 1), " -> pending break+hold confirm");
|
|
EmitSpikeDetectedAlert(spikeDirection, spikeMovePoints, spikeAnchorPrice, spikeThresholdPoints);
|
|
}
|
|
}
|
|
|
|
// Note: Individual filter checks are now done in CheckAllFiltersForTrade()
|
|
// This allows real-time decision making at every tick, not just at bar close
|
|
|
|
// Get consecutive bar signals
|
|
bool consecutiveBullishSignal = (consecutiveBullishBars >= BullishBarsForSell);
|
|
bool consecutiveBearishSignal = (consecutiveBearishBars >= BearishBarsForBuy);
|
|
bool sellSignalActive = UseMomentumExhaustionSignals ? consecutiveBullishSignal : consecutiveBearishSignal;
|
|
int sellSignalBars = UseMomentumExhaustionSignals ? consecutiveBullishBars : consecutiveBearishBars;
|
|
int sellSignalThreshold = UseMomentumExhaustionSignals ? BullishBarsForSell : BearishBarsForBuy;
|
|
string sellSignalLabel = UseMomentumExhaustionSignals ? "Bullish" : "Bearish";
|
|
string sellSignalLabelVoice = UseMomentumExhaustionSignals ? "bullish" : "bearish";
|
|
bool buySignalActive = UseMomentumExhaustionSignals ? consecutiveBearishSignal : consecutiveBullishSignal;
|
|
int buySignalBars = UseMomentumExhaustionSignals ? consecutiveBearishBars : consecutiveBullishBars;
|
|
int buySignalThreshold = UseMomentumExhaustionSignals ? BearishBarsForBuy : BullishBarsForSell;
|
|
string buySignalLabel = UseMomentumExhaustionSignals ? "Bearish" : "Bullish";
|
|
string buySignalLabelVoice = UseMomentumExhaustionSignals ? "bearish" : "bullish";
|
|
|
|
// Total open positions limit
|
|
if(CountOpenTradesAllSymbols() >= currentMaxOpenTrades)
|
|
{
|
|
ManageOpenPositions();
|
|
return;
|
|
}
|
|
|
|
// Get current bar number
|
|
int currentBar = UseTickBarSignals ? tickBarIndex : (int)iTime(Symbol(), Period(), 0);
|
|
|
|
// ========== COMBINED SIGNAL ANALYSIS - Real-time decision making ==========
|
|
// Combines consecutive bars + all filters for real-time trading decisions
|
|
// Checks at EVERY TICK (milliseconds) - not just at bar close
|
|
// Allows entry when ALL conditions are met in real-time
|
|
|
|
// Check SELL signal with all filters combined (real-time)
|
|
if(UseConsecutiveBars && AllowConsecutiveSell && sellSignalActive && sellSignalBars >= sellSignalThreshold)
|
|
{
|
|
if(CountOpenTradesAllSymbols() < currentMaxOpenTrades)
|
|
{
|
|
if(lastMemoryBlockSellBar == currentBar)
|
|
return;
|
|
// Memory-based intelligence check
|
|
double currentVolumeRatio = GetCurrentVolumeRatio();
|
|
bool useExploreLot = false;
|
|
int decisionScore = 0;
|
|
bool hardFiltersPassed = CheckAllFiltersForTrade(POSITION_TYPE_SELL, bid, ask, sellSignalBars);
|
|
if(!hardFiltersPassed)
|
|
return;
|
|
if(ShouldSkipTradeBasedOnMemory(-1, sellSignalBars, currentVolumeRatio, bid, ask, useExploreLot, decisionScore))
|
|
{
|
|
lastMemoryBlockSellBar = currentBar;
|
|
Print(" MEMORY: SELL signal blocked by intelligence system");
|
|
return;
|
|
}
|
|
|
|
// Real-time combined filter check for SELL - checks at every tick
|
|
// Only enter once per bar to avoid multiple entries
|
|
if(lastBullishSignalBar != currentBar && hardFiltersPassed)
|
|
{
|
|
double memoryConfidence = GetMemoryBasedConfidence(-1, sellSignalBars, currentVolumeRatio);
|
|
double adaptiveRisk = memoryAdaptiveRiskMultiplier;
|
|
Print(" SELL SIGNAL CONFIRMED - Consecutive ", sellSignalLabel, ": ", sellSignalBars,
|
|
" | Memory Confidence: ", DoubleToString(memoryConfidence * 100, 1), "%",
|
|
" | Decision Score: ", decisionScore, "/10",
|
|
" | Adaptive Risk: ", DoubleToString(adaptiveRisk, 2), "x",
|
|
" | All filters PASSED - TAKING SELL");
|
|
if(EnableVoicePrompts)
|
|
SpeakZira("Sir.I find consecutive " + sellSignalLabelVoice + " bars on symbol " + Symbol() + ". Preparing sell.");
|
|
nextTradeUseExploreLot = useExploreLot;
|
|
OpenSellTrade(bid);
|
|
nextTradeUseExploreLot = false;
|
|
ManageOpenPositions();
|
|
lastBullishSignalBar = currentBar;
|
|
}
|
|
// If filters fail, signal is blocked (already logged in CheckAllFiltersForTrade)
|
|
// Will retry on next tick when filters might pass
|
|
}
|
|
}
|
|
|
|
// Check BUY signal with all filters combined (real-time)
|
|
if(UseConsecutiveBars && AllowConsecutiveBuy && buySignalActive && buySignalBars >= buySignalThreshold)
|
|
{
|
|
if(CountOpenTradesAllSymbols() < currentMaxOpenTrades)
|
|
{
|
|
if(lastMemoryBlockBuyBar == currentBar)
|
|
return;
|
|
// Memory-based intelligence check
|
|
double currentVolumeRatio = GetCurrentVolumeRatio();
|
|
bool useExploreLot = false;
|
|
int decisionScore = 0;
|
|
bool hardFiltersPassed = CheckAllFiltersForTrade(POSITION_TYPE_BUY, bid, ask, buySignalBars);
|
|
if(!hardFiltersPassed)
|
|
return;
|
|
if(ShouldSkipTradeBasedOnMemory(1, buySignalBars, currentVolumeRatio, bid, ask, useExploreLot, decisionScore))
|
|
{
|
|
lastMemoryBlockBuyBar = currentBar;
|
|
Print(" MEMORY: BUY signal blocked by intelligence system");
|
|
return;
|
|
}
|
|
|
|
// Real-time combined filter check for BUY - checks at every tick
|
|
// Only enter once per bar to avoid multiple entries
|
|
if(lastBearishSignalBar != currentBar && hardFiltersPassed)
|
|
{
|
|
double memoryConfidence = GetMemoryBasedConfidence(1, buySignalBars, currentVolumeRatio);
|
|
double adaptiveRisk = memoryAdaptiveRiskMultiplier;
|
|
Print(" BUY SIGNAL CONFIRMED - Consecutive ", buySignalLabel, ": ", buySignalBars,
|
|
" | Memory Confidence: ", DoubleToString(memoryConfidence * 100, 1), "%",
|
|
" | Decision Score: ", decisionScore, "/10",
|
|
" | Adaptive Risk: ", DoubleToString(adaptiveRisk, 2), "x",
|
|
" | All filters PASSED - TAKING BUY");
|
|
if(EnableVoicePrompts)
|
|
SpeakZira("Sir.I find consecutive " + buySignalLabelVoice + " bars on symbol " + Symbol() + ". Preparing buy.");
|
|
nextTradeUseExploreLot = useExploreLot;
|
|
OpenBuyTrade(ask);
|
|
nextTradeUseExploreLot = false;
|
|
ManageOpenPositions();
|
|
lastBearishSignalBar = currentBar;
|
|
}
|
|
// If filters fail, signal is blocked (already logged in CheckAllFiltersForTrade)
|
|
// Will retry on next tick when filters might pass
|
|
}
|
|
}
|
|
|
|
// Update trailing stops, breakeven stops, and dynamic SL/TP adjustments
|
|
if(UseTrailingStop || UseBreakevenStop || UseDynamicSLTP)
|
|
ManageOpenPositions();
|
|
|
|
// Update sentiment (wins/losses, win rate) and chart display
|
|
TrackSentiment();
|
|
UpdateChartDisplay();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// FORWARD DECLARATIONS
|
|
//+------------------------------------------------------------------+
|
|
void AdjustSLTPBasedOnAnalysis(ulong ticket, int positionType, double entryPrice, double currentSL, double currentTP, double bid, double ask, bool allowSLTightening);
|
|
void CalculateVolumeProfile(double &liquidityZones[], int positionType, double currentPrice);
|
|
double FindNearestLiquidityZone(double originalPrice, int positionType, double &distancePoints);
|
|
bool ShouldRelaxStopManagementForSymbol(string symbol);
|
|
bool GetPositionEntryFromHistory(ulong positionId, string symbol, long magic, datetime &tIn, double &pIn, int &dir);
|
|
|
|
//+------------------------------------------------------------------+
|
|
// MANAGE OPEN POSITIONS (Trailing Stop Loss) - Safer position access
|
|
//+------------------------------------------------------------------+
|
|
void ApplyAdaptiveStopLossByPerformance(ulong ticket, int posType, double entryPrice, double currentSL, double currentTP, double bid, double ask)
|
|
{
|
|
if(!UseAdaptiveStopLoss)
|
|
return;
|
|
|
|
if(ticket <= 0 || point_value <= 0.0)
|
|
return;
|
|
|
|
double priceNow = (posType == POSITION_TYPE_BUY) ? ask : bid;
|
|
double profitPoints = ((posType == POSITION_TYPE_BUY) ? (priceNow - entryPrice) : (entryPrice - priceNow)) / point_value;
|
|
if(profitPoints <= 0.0)
|
|
return;
|
|
|
|
double startPoints = 40.0;
|
|
double lockFactor = 0.30;
|
|
|
|
if(performanceMode == "CONSERVATIVE")
|
|
{
|
|
startPoints = 25.0; // protect earlier in conservative mode
|
|
lockFactor = 0.45;
|
|
}
|
|
else
|
|
if(performanceMode == "AGGRESSIVE")
|
|
{
|
|
startPoints = 55.0; // allow more breathing room in aggressive mode
|
|
lockFactor = 0.20;
|
|
}
|
|
|
|
if(profitPoints < startPoints)
|
|
return;
|
|
|
|
double lockPoints = MathMax(2.0, profitPoints * lockFactor);
|
|
double newSL = currentSL;
|
|
bool shouldModify = false;
|
|
|
|
if(posType == (int)POSITION_TYPE_BUY)
|
|
{
|
|
double candidateSL = entryPrice + (lockPoints * point_value);
|
|
if(candidateSL > currentSL || currentSL <= 0.0)
|
|
{
|
|
newSL = candidateSL;
|
|
shouldModify = true;
|
|
}
|
|
}
|
|
else
|
|
if(posType == (int)POSITION_TYPE_SELL)
|
|
{
|
|
double candidateSL = entryPrice - (lockPoints * point_value);
|
|
if(candidateSL < currentSL || currentSL <= 0.0)
|
|
{
|
|
newSL = candidateSL;
|
|
shouldModify = true;
|
|
}
|
|
}
|
|
|
|
if(shouldModify)
|
|
ModifyPosition(ticket, newSL, currentTP);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool ShouldRelaxStopManagementForSymbol(string symbol)
|
|
{
|
|
// In recovery we prioritize basket-based exits over tight per-position SL.
|
|
if(UseRecoveryAutoClose && recoveryModeActive)
|
|
return true;
|
|
|
|
// If symbol is already in auto-close trailing mode, avoid tightening SL further.
|
|
int idx = FindAutoCloseStateIndex(symbol, false);
|
|
if(idx >= 0 && autoCloseSymbolStates[idx].inTrailing)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void RelaxStopToBreakevenInRecovery(ulong ticket, int posType, double entryPrice, double currentSL, double currentTP)
|
|
{
|
|
if(!(UseRecoveryAutoClose && recoveryModeActive))
|
|
return;
|
|
if(ticket <= 0 || point_value <= 0.0 || currentSL <= 0.0)
|
|
return;
|
|
|
|
double beSL = NormalizeToTick(entryPrice);
|
|
double newSL = currentSL;
|
|
bool shouldModify = false;
|
|
double relaxBand = 2.0 * point_value;
|
|
|
|
if(posType == (int)POSITION_TYPE_BUY)
|
|
{
|
|
if(currentSL > beSL + relaxBand)
|
|
{
|
|
newSL = beSL;
|
|
shouldModify = true;
|
|
}
|
|
}
|
|
else
|
|
if(posType == (int)POSITION_TYPE_SELL)
|
|
{
|
|
if(currentSL < beSL - relaxBand)
|
|
{
|
|
newSL = beSL;
|
|
shouldModify = true;
|
|
}
|
|
}
|
|
|
|
if(shouldModify)
|
|
ModifyPosition(ticket, newSL, currentTP);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void ManageOpenPositions()
|
|
{
|
|
double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);
|
|
double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
|
|
bool relaxSLManagement = ShouldRelaxStopManagementForSymbol(Symbol());
|
|
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket > 0)
|
|
{
|
|
// Safely select position before accessing its data
|
|
if(!PositionSelectByTicket(ticket))
|
|
continue;
|
|
|
|
// Verify this is our position and correct symbol
|
|
if(PositionGetString(POSITION_SYMBOL) != Symbol() ||
|
|
PositionGetInteger(POSITION_MAGIC) != MagicNumber)
|
|
continue;
|
|
|
|
int posType = (int)PositionGetInteger(POSITION_TYPE);
|
|
double entryPrice = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
double currentSL = PositionGetDouble(POSITION_SL);
|
|
double currentTP = PositionGetDouble(POSITION_TP);
|
|
|
|
if(posType == (int)POSITION_TYPE_BUY)
|
|
{
|
|
// Trailing Stop: Only activate after reaching TrailingStartPips profit
|
|
if(UseTrailingStop && !relaxSLManagement)
|
|
{
|
|
double profit = ask - entryPrice;
|
|
if(profit >= (TrailingStartPips * point_value))
|
|
{
|
|
double newSL = ask - (TrailingStopPips * point_value);
|
|
if(newSL > currentSL)
|
|
ModifyPosition(ticket, newSL, currentTP);
|
|
}
|
|
}
|
|
// Breakeven Stop: Move SL to entry after reaching BreakevenTrigger
|
|
if(UseBreakevenStop && !relaxSLManagement && currentSL < entryPrice)
|
|
{
|
|
double profit = ask - entryPrice;
|
|
if(profit >= (BreakevenTrigger * point_value))
|
|
ModifyPosition(ticket, entryPrice, currentTP);
|
|
}
|
|
// Dynamic SL/TP Adjustment: Adjust based on market analysis
|
|
if(UseDynamicSLTP)
|
|
{
|
|
AdjustSLTPBasedOnAnalysis(ticket, posType, entryPrice, currentSL, currentTP, bid, ask, !relaxSLManagement);
|
|
}
|
|
|
|
if(relaxSLManagement)
|
|
RelaxStopToBreakevenInRecovery(ticket, posType, entryPrice, currentSL, currentTP);
|
|
|
|
// Adaptive stop-loss by performance mode
|
|
if(!relaxSLManagement)
|
|
{
|
|
currentSL = PositionGetDouble(POSITION_SL);
|
|
currentTP = PositionGetDouble(POSITION_TP);
|
|
ApplyAdaptiveStopLossByPerformance(ticket, posType, entryPrice, currentSL, currentTP, bid, ask);
|
|
}
|
|
}
|
|
else
|
|
if(posType == (int)POSITION_TYPE_SELL)
|
|
{
|
|
// Trailing Stop: Only activate after reaching TrailingStartPips profit
|
|
if(UseTrailingStop && !relaxSLManagement)
|
|
{
|
|
double profit = entryPrice - bid;
|
|
if(profit >= (TrailingStartPips * point_value))
|
|
{
|
|
double newSL = bid + (TrailingStopPips * point_value);
|
|
if(newSL < currentSL || currentSL == 0)
|
|
ModifyPosition(ticket, newSL, currentTP);
|
|
}
|
|
}
|
|
// Breakeven Stop: Move SL to entry after reaching BreakevenTrigger
|
|
if(UseBreakevenStop && !relaxSLManagement && (currentSL == 0 || currentSL > entryPrice))
|
|
{
|
|
double profit = entryPrice - bid;
|
|
if(profit >= (BreakevenTrigger * point_value))
|
|
ModifyPosition(ticket, entryPrice, currentTP);
|
|
}
|
|
// Dynamic SL/TP Adjustment: Adjust based on market analysis
|
|
if(UseDynamicSLTP)
|
|
{
|
|
AdjustSLTPBasedOnAnalysis(ticket, posType, entryPrice, currentSL, currentTP, bid, ask, !relaxSLManagement);
|
|
}
|
|
|
|
if(relaxSLManagement)
|
|
RelaxStopToBreakevenInRecovery(ticket, posType, entryPrice, currentSL, currentTP);
|
|
|
|
// Adaptive stop-loss by performance mode
|
|
if(!relaxSLManagement)
|
|
{
|
|
currentSL = PositionGetDouble(POSITION_SL);
|
|
currentTP = PositionGetDouble(POSITION_TP);
|
|
ApplyAdaptiveStopLossByPerformance(ticket, posType, entryPrice, currentSL, currentTP, bid, ask);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void ModifyPosition(ulong ticket, double newSL, double newTP)
|
|
{
|
|
// Validate inputs
|
|
if(ticket <= 0)
|
|
{
|
|
Print(" ModifyPosition: Invalid parameters - Ticket: ", ticket, " SL: ", newSL, " TP: ", newTP);
|
|
return;
|
|
}
|
|
if(newSL < 0 || newTP < 0) // allow zero to remove SL/TP; only block negative values
|
|
{
|
|
Print(" ModifyPosition: Negative SL/TP not allowed - SL: ", newSL, " TP: ", newTP);
|
|
return;
|
|
}
|
|
|
|
if(!PositionSelectByTicket(ticket))
|
|
return;
|
|
|
|
double currentSL = PositionGetDouble(POSITION_SL);
|
|
double currentTP = PositionGetDouble(POSITION_TP);
|
|
|
|
int posType = (int)PositionGetInteger(POSITION_TYPE);
|
|
bool isBuy = (posType == POSITION_TYPE_BUY);
|
|
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
|
|
|
if(newSL > 0)
|
|
newSL = NormalizeToTick(newSL);
|
|
if(newTP > 0)
|
|
newTP = NormalizeToTick(newTP);
|
|
|
|
double normCurrentSL = (currentSL > 0) ? NormalizeToTick(currentSL) : currentSL;
|
|
double normCurrentTP = (currentTP > 0) ? NormalizeToTick(currentTP) : currentTP;
|
|
if(ArePricesEqual(newSL, normCurrentSL) && ArePricesEqual(newTP, normCurrentTP))
|
|
return;
|
|
|
|
double currentPriceForSL = isBuy ? bid : ask;
|
|
double currentPriceForTP = isBuy ? ask : bid;
|
|
|
|
if(!IsStopValidForModify(newSL, currentPriceForSL, isBuy, true) ||
|
|
!IsStopValidForModify(newTP, currentPriceForTP, isBuy, false))
|
|
{
|
|
Print(" ModifyPosition skipped - stops too close to market (min distance: ",
|
|
DoubleToString(MathMax(GetMinStopDistance(), GetFreezeDistance()), Digits()), ")");
|
|
return;
|
|
}
|
|
|
|
if(!trade.PositionModify(ticket, newSL, newTP))
|
|
{
|
|
Print(" ModifyPosition FAILED - Ticket: ", ticket, " | Error: ", trade.ResultRetcodeDescription(), " (Code: ", trade.ResultRetcode(), ")");
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// CHECK IF WE CAN TRADE (TIME & DAY RESTRICTIONS with Timezone Support)
|
|
//+------------------------------------------------------------------+
|
|
bool CanTrade()
|
|
{
|
|
// Voice/queue pause gate
|
|
static datetime lastPauseLog = 0;
|
|
if(tradingPaused)
|
|
{
|
|
if(TimeCurrent() - lastPauseLog > 10)
|
|
{
|
|
Print("TRADING PAUSED (voice command)");
|
|
lastPauseLog = TimeCurrent();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Get current time - use broker server time or local time with offset
|
|
datetime currentTime = UseBrokerTime ? TimeCurrent() : (TimeLocal() + (TimezoneOffset * 3600));
|
|
|
|
// Check current hour and day using MqlDateTime struct
|
|
MqlDateTime dt;
|
|
TimeToStruct(currentTime, dt);
|
|
int hour = dt.hour;
|
|
if(hour < StartHour || hour >= EndHour)
|
|
return false;
|
|
|
|
// Check day of week (0=Sunday, 1=Monday, ..., 6=Saturday)
|
|
int dayOfWeek = dt.day_of_week;
|
|
|
|
if((dayOfWeek == 0 && !TradeSunday) ||
|
|
(dayOfWeek == 1 && !TradeMonday) ||
|
|
(dayOfWeek == 2 && !TradeTuesday) ||
|
|
(dayOfWeek == 3 && !TradeWednesday) ||
|
|
(dayOfWeek == 4 && !TradeThursday) ||
|
|
(dayOfWeek == 5 && !TradeFriday) ||
|
|
(dayOfWeek == 6 && !TradeSaturday))
|
|
return false;
|
|
|
|
if(!IsCurrentSessionTradable())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// CHECK IF IN COOLDOWN PERIOD (After Losing Trade)
|
|
// Uses real time-based cooldown instead of bar counting
|
|
//+------------------------------------------------------------------+
|
|
bool IsInCooldown()
|
|
{
|
|
if(!UseCooldown)
|
|
return false;
|
|
|
|
if(lastLossTime == 0)
|
|
return false; // No loss recorded yet
|
|
|
|
// Calculate time elapsed since last loss
|
|
datetime currentTime = UseBrokerTime ? TimeCurrent() : (TimeLocal() + (TimezoneOffset * 3600));
|
|
int timeElapsed = (int)(currentTime - lastLossTime);
|
|
|
|
// Convert CooldownBars to seconds (approximate: 1 bar = period in seconds)
|
|
int periodSeconds = PeriodSeconds(Period());
|
|
int cooldownSeconds = CooldownBars * periodSeconds;
|
|
|
|
if(timeElapsed < cooldownSeconds)
|
|
{
|
|
int barsLeft = (cooldownSeconds - timeElapsed) / periodSeconds;
|
|
if(barsLeft > 0)
|
|
{
|
|
Print(" In Cooldown: ", barsLeft, " bars left (", (cooldownSeconds - timeElapsed), " seconds)");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
// CHECK IF POSITION EXISTS
|
|
//+------------------------------------------------------------------+
|
|
|
|
// Count open positions for this symbol and direction (POSITION_TYPE_BUY / POSITION_TYPE_SELL)
|
|
// ONLY counts trades with OUR magic number
|
|
// (definition moved earlier)
|
|
|
|
//+------------------------------------------------------------------+
|
|
// PRICE HELPERS - normalize to tick size and enforce stop distances
|
|
//+------------------------------------------------------------------+
|
|
double NormalizeToTick(double price)
|
|
{
|
|
return NormalizeToTickForSymbol(_Symbol, price);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double NormalizeToTickForSymbol(string symbol, double price)
|
|
{
|
|
double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
if(tickSize <= 0)
|
|
tickSize = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
if(tickSize <= 0)
|
|
tickSize = Point();
|
|
|
|
double normalized = MathRound(price / tickSize) * tickSize;
|
|
return NormalizeDouble(normalized, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetMinStopDistance()
|
|
{
|
|
return GetMinStopDistanceForSymbol(_Symbol);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetMinStopDistanceForSymbol(string symbol)
|
|
{
|
|
int stopsLevel = (int)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL);
|
|
if(stopsLevel <= 0)
|
|
return 0.0;
|
|
|
|
double pt = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
if(pt <= 0)
|
|
pt = Point();
|
|
return stopsLevel * pt;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetFreezeDistance()
|
|
{
|
|
return GetFreezeDistanceForSymbol(_Symbol);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetFreezeDistanceForSymbol(string symbol)
|
|
{
|
|
int freezeLevel = (int)SymbolInfoInteger(symbol, SYMBOL_TRADE_FREEZE_LEVEL);
|
|
if(freezeLevel <= 0)
|
|
return 0.0;
|
|
|
|
double pt = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
if(pt <= 0)
|
|
pt = Point();
|
|
return freezeLevel * pt;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsPendingPriceValid(double price, double current, bool isBuy)
|
|
{
|
|
double minDistance = GetMinStopDistance();
|
|
if(minDistance <= 0)
|
|
return true;
|
|
|
|
return isBuy ? ((current - price) >= minDistance) : ((price - current) >= minDistance);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsStopValidForModify(double stopPrice, double currentPrice, bool isBuy, bool isSL)
|
|
{
|
|
if(stopPrice <= 0)
|
|
return true;
|
|
|
|
double minDistance = MathMax(GetMinStopDistance(), GetFreezeDistance());
|
|
if(minDistance <= 0)
|
|
return true;
|
|
|
|
if(isBuy)
|
|
return isSL ? (stopPrice <= currentPrice - minDistance) : (stopPrice >= currentPrice + minDistance);
|
|
|
|
return isSL ? (stopPrice >= currentPrice + minDistance) : (stopPrice <= currentPrice - minDistance);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool ArePricesEqual(double a, double b)
|
|
{
|
|
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
if(tickSize <= 0)
|
|
tickSize = Point();
|
|
|
|
return MathAbs(a - b) <= (tickSize / 2.0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// VOLUME AND LIMIT CHECKS
|
|
//+------------------------------------------------------------------+
|
|
bool CheckVolumeValueForSymbol(string symbol, double volume, string &description)
|
|
{
|
|
double min_volume = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
|
if(volume < min_volume)
|
|
{
|
|
description = StringFormat("Volume is less than SYMBOL_VOLUME_MIN=%.2f", min_volume);
|
|
return false;
|
|
}
|
|
|
|
double max_volume = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
|
|
if(volume > max_volume)
|
|
{
|
|
description = StringFormat("Volume is greater than SYMBOL_VOLUME_MAX=%.2f", max_volume);
|
|
return false;
|
|
}
|
|
|
|
double volume_step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
|
|
if(volume_step <= 0)
|
|
{
|
|
description = "Invalid SYMBOL_VOLUME_STEP";
|
|
return false;
|
|
}
|
|
|
|
int ratio = (int)MathRound(volume / volume_step);
|
|
if(MathAbs(ratio * volume_step - volume) > 0.0000001)
|
|
{
|
|
description = StringFormat("Volume not multiple of SYMBOL_VOLUME_STEP=%.2f (closest %.2f)",
|
|
volume_step, ratio * volume_step);
|
|
return false;
|
|
}
|
|
|
|
description = "Correct volume value";
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool CheckVolumeValue(double volume, string &description)
|
|
{
|
|
return CheckVolumeValueForSymbol(Symbol(), volume, description);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double PositionVolumeOnSymbol(string symbol)
|
|
{
|
|
double volume_on_symbol = 0.0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket > 0 && PositionGetString(POSITION_SYMBOL) == symbol)
|
|
volume_on_symbol += PositionGetDouble(POSITION_VOLUME);
|
|
}
|
|
return volume_on_symbol;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double PendingsVolumeOnSymbol(string symbol)
|
|
{
|
|
double volume_on_symbol = 0.0;
|
|
int all_orders = OrdersTotal();
|
|
for(int i = 0; i < all_orders; i++)
|
|
{
|
|
ulong ticket = OrderGetTicket(i);
|
|
if(ticket > 0 && OrderGetString(ORDER_SYMBOL) == symbol)
|
|
volume_on_symbol += OrderGetDouble(ORDER_VOLUME_INITIAL);
|
|
}
|
|
return volume_on_symbol;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double NewOrderAllowedVolume(string symbol)
|
|
{
|
|
double symbol_max_volume = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
|
|
double max_volume = SymbolInfoDouble(symbol, SYMBOL_VOLUME_LIMIT);
|
|
if(max_volume <= 0)
|
|
return symbol_max_volume;
|
|
|
|
double opened_volume = PositionVolumeOnSymbol(symbol);
|
|
if(max_volume - opened_volume <= 0)
|
|
return 0.0;
|
|
|
|
double orders_volume_on_symbol = PendingsVolumeOnSymbol(symbol);
|
|
double allowed_volume = max_volume - opened_volume - orders_volume_on_symbol;
|
|
if(allowed_volume > symbol_max_volume)
|
|
allowed_volume = symbol_max_volume;
|
|
|
|
return allowed_volume;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsNewOrderAllowed()
|
|
{
|
|
int max_allowed_orders = (int)AccountInfoInteger(ACCOUNT_LIMIT_ORDERS);
|
|
if(max_allowed_orders == 0)
|
|
return true;
|
|
|
|
int orders = OrdersTotal();
|
|
return (orders < max_allowed_orders);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool CheckStopLossTakeProfit(ENUM_ORDER_TYPE type, double sl, double tp)
|
|
{
|
|
return CheckStopLossTakeProfitForSymbol(_Symbol, type, sl, tp);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool CheckStopLossTakeProfitForSymbol(string symbol, ENUM_ORDER_TYPE type, double sl, double tp)
|
|
{
|
|
int stops_level = (int)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL);
|
|
if(stops_level <= 0)
|
|
return true;
|
|
|
|
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
double pt = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
if(pt <= 0)
|
|
pt = _Point;
|
|
double min_dist = stops_level * pt;
|
|
|
|
if(type == ORDER_TYPE_BUY)
|
|
{
|
|
bool sl_ok = (sl <= 0) ? true : ((bid - sl) > min_dist);
|
|
bool tp_ok = (tp <= 0) ? true : ((tp - bid) > min_dist);
|
|
if(!sl_ok)
|
|
Print(" StopLoss too close for BUY [", symbol, "]: SL=", sl, " Bid=", bid, " min=", min_dist);
|
|
if(!tp_ok)
|
|
Print(" TakeProfit too close for BUY [", symbol, "]: TP=", tp, " Bid=", bid, " min=", min_dist);
|
|
return (sl_ok && tp_ok);
|
|
}
|
|
if(type == ORDER_TYPE_SELL)
|
|
{
|
|
bool sl_ok = (sl <= 0) ? true : ((sl - ask) > min_dist);
|
|
bool tp_ok = (tp <= 0) ? true : ((ask - tp) > min_dist);
|
|
if(!sl_ok)
|
|
Print(" StopLoss too close for SELL [", symbol, "]: SL=", sl, " Ask=", ask, " min=", min_dist);
|
|
if(!tp_ok)
|
|
Print(" TakeProfit too close for SELL [", symbol, "]: TP=", tp, " Ask=", ask, " min=", min_dist);
|
|
return (sl_ok && tp_ok);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// FUNDS CHECK - ensure enough margin before sending orders
|
|
//+------------------------------------------------------------------+
|
|
bool CheckMoneyForTrade(string symb, double lots, ENUM_ORDER_TYPE type)
|
|
{
|
|
//--- Getting the opening price
|
|
MqlTick mqltick;
|
|
if(!SymbolInfoTick(symb, mqltick))
|
|
{
|
|
Print(" CheckMoneyForTrade: SymbolInfoTick failed for ", symb);
|
|
return false;
|
|
}
|
|
double price = mqltick.ask;
|
|
if(type == ORDER_TYPE_SELL)
|
|
price = mqltick.bid;
|
|
//--- values of the required and free margin
|
|
double margin, free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
|
|
//--- call of the checking function
|
|
if(!OrderCalcMargin(type, symb, lots, price, margin))
|
|
{
|
|
Print(" CheckMoneyForTrade: OrderCalcMargin failed, code=", GetLastError());
|
|
return false;
|
|
}
|
|
//--- if there are insufficient funds to perform the operation
|
|
if(margin > free_margin)
|
|
{
|
|
Print(" Not enough money for ", EnumToString(type), " ", lots, " ", symb, " (margin=", margin, " free=", free_margin, ")");
|
|
return false;
|
|
}
|
|
//--- checking successful
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double EnforceMinStopDistance(double entry, double target, bool isBuy, bool isSL)
|
|
{
|
|
return EnforceMinStopDistanceForSymbol(_Symbol, entry, target, isBuy, isSL);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double EnforceMinStopDistanceForSymbol(string symbol, double entry, double target, bool isBuy, bool isSL)
|
|
{
|
|
double symbolPoint = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
if(symbolPoint <= 0.0)
|
|
symbolPoint = Point();
|
|
double minDistance = MathMax(GetMinStopDistanceForSymbol(symbol), GetFreezeDistanceForSymbol(symbol)) + symbolPoint;
|
|
if(minDistance <= 0)
|
|
return NormalizeToTickForSymbol(symbol, target);
|
|
|
|
double dist = MathAbs(target - entry);
|
|
if(dist >= minDistance)
|
|
return NormalizeToTickForSymbol(symbol, target);
|
|
|
|
if(isSL)
|
|
return NormalizeToTickForSymbol(symbol, isBuy ? (entry - minDistance) : (entry + minDistance));
|
|
|
|
return NormalizeToTickForSymbol(symbol, isBuy ? (entry + minDistance) : (entry - minDistance));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double ConvertInputDistanceToPriceForSymbol(string symbol, double inputDistance)
|
|
{
|
|
double symbolPoint = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
if(symbolPoint <= 0.0)
|
|
symbolPoint = Point();
|
|
|
|
if(!InputsArePips)
|
|
return inputDistance * symbolPoint;
|
|
|
|
long digits = SymbolInfoInteger(symbol, SYMBOL_DIGITS);
|
|
double pipToPointMultiplier = ((digits == 5 || digits == 3) ? 10.0 : 1.0);
|
|
return inputDistance * symbolPoint * pipToPointMultiplier;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void AdjustEntryDistancesSmart(string symbol, int direction, int decisionScore, double bid, double ask,
|
|
double baseTPDistance, double baseSLDistance,
|
|
double &tpDistance, double &slDistance)
|
|
{
|
|
double symbolPoint = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
if(symbolPoint <= 0.0)
|
|
symbolPoint = Point();
|
|
|
|
double spread = MathAbs(ask - bid);
|
|
double stopLevelDistance = ((double)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL)) * symbolPoint;
|
|
double freezeLevelDistance = ((double)SymbolInfoInteger(symbol, SYMBOL_TRADE_FREEZE_LEVEL)) * symbolPoint;
|
|
double brokerMinDistance = MathMax(stopLevelDistance, freezeLevelDistance);
|
|
|
|
tpDistance = MathMax(baseTPDistance, 0.0);
|
|
slDistance = MathMax(baseSLDistance, 0.0);
|
|
|
|
double spreadGuardTP = spread * 2.5;
|
|
double spreadGuardSL = spread * 2.0;
|
|
tpDistance = MathMax(tpDistance, MathMax(spreadGuardTP, brokerMinDistance));
|
|
slDistance = MathMax(slDistance, MathMax(spreadGuardSL, brokerMinDistance));
|
|
|
|
double regimeTpFactor = 1.0;
|
|
double regimeSlFactor = 1.0;
|
|
if(marketState.currentRegime == "VOLATILE")
|
|
{
|
|
regimeTpFactor = 1.25;
|
|
regimeSlFactor = 1.30;
|
|
}
|
|
else
|
|
if(marketState.currentRegime == "QUIET")
|
|
{
|
|
regimeTpFactor = 0.95;
|
|
regimeSlFactor = 0.95;
|
|
}
|
|
|
|
if(marketState.marketStress)
|
|
{
|
|
regimeTpFactor = MathMin(regimeTpFactor, 1.0);
|
|
regimeSlFactor = MathMax(regimeSlFactor, 1.10);
|
|
}
|
|
|
|
tpDistance *= regimeTpFactor;
|
|
slDistance *= regimeSlFactor;
|
|
|
|
double targetRR = 1.2;
|
|
if(decisionScore >= LongTermScoreThreshold)
|
|
targetRR = 1.5;
|
|
else
|
|
if(decisionScore <= ShortTermScoreThreshold)
|
|
targetRR = 1.0;
|
|
if(marketState.marketStress)
|
|
targetRR = MathMin(targetRR, 1.0);
|
|
|
|
if(slDistance > 0.0)
|
|
tpDistance = MathMax(tpDistance, slDistance * targetRR);
|
|
|
|
double maxRR = 2.4;
|
|
if(slDistance > 0.0 && tpDistance > slDistance * maxRR)
|
|
tpDistance = slDistance * maxRR;
|
|
|
|
tpDistance = MathMax(tpDistance, MathMax(spreadGuardTP, brokerMinDistance));
|
|
slDistance = MathMax(slDistance, MathMax(spreadGuardSL, brokerMinDistance));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// OPEN BUY TRADE
|
|
//+------------------------------------------------------------------+
|
|
void OpenBuyTrade(double ask)
|
|
{
|
|
if(IsSymbolAutoCloseBlocked(Symbol(), GetCurrentTimeWithOffset()))
|
|
{
|
|
Print("AUTO CLOSE GATE [", Symbol(), "] - BUY blocked until next session window.");
|
|
return;
|
|
}
|
|
|
|
if(CountOpenTradesAllSymbols() >= currentMaxOpenTrades)
|
|
return;
|
|
|
|
double lots = CalculateLotSize();
|
|
if(!CheckMoneyForTrade(Symbol(), lots, ORDER_TYPE_BUY))
|
|
{
|
|
EmitOrderBlockedSound();
|
|
return;
|
|
}
|
|
string volumeDesc = "";
|
|
if(!CheckVolumeValue(lots, volumeDesc))
|
|
{
|
|
Print(" Volume check failed: ", volumeDesc);
|
|
EmitOrderBlockedSound();
|
|
return;
|
|
}
|
|
double allowedVolume = NewOrderAllowedVolume(Symbol());
|
|
if(allowedVolume <= 0 || lots > allowedVolume)
|
|
{
|
|
Print(" Volume limit exceeded: lots=", lots, " allowed=", allowedVolume);
|
|
EmitOrderBlockedSound();
|
|
return;
|
|
}
|
|
double entryPrice = ask;
|
|
double bidNow = SymbolInfoDouble(Symbol(), SYMBOL_BID);
|
|
double tpMultiplier = 1.0;
|
|
double slMultiplier = 1.0;
|
|
string entryProfile = "NORMAL";
|
|
int entryScore = 5;
|
|
SelectEntryProfile(1, bidNow, ask, tpMultiplier, slMultiplier, entryProfile, entryScore);
|
|
|
|
// Apply Smart Entry - adjust entry price towards liquidity zones
|
|
double liqDistPts = 0.0;
|
|
bool liqUsed = false;
|
|
if(UseSmartEntry)
|
|
{
|
|
entryPrice = FindNearestLiquidityZone(ask, POSITION_TYPE_BUY, liqDistPts);
|
|
liqUsed = (entryPrice != ask);
|
|
}
|
|
|
|
entryPrice = NormalizeToTick(entryPrice);
|
|
double baseTPDistance = ConvertInputDistanceToPriceForSymbol(Symbol(), TakeProfit) * tpMultiplier;
|
|
double baseSLDistance = ConvertInputDistanceToPriceForSymbol(Symbol(), StopLoss) * slMultiplier;
|
|
double tpDistance = baseTPDistance;
|
|
double slDistance = baseSLDistance;
|
|
if(UseEntryProfileTP_SL)
|
|
AdjustEntryDistancesSmart(Symbol(), 1, entryScore, bidNow, ask, baseTPDistance, baseSLDistance, tpDistance, slDistance);
|
|
double tp = EnforceMinStopDistance(entryPrice, entryPrice + tpDistance, true, false);
|
|
double sl = EnforceMinStopDistance(entryPrice, entryPrice - slDistance, true, true);
|
|
if(!CheckStopLossTakeProfit(ORDER_TYPE_BUY, sl, tp))
|
|
return;
|
|
|
|
Print(" ENTRY PROFILE [BUY] - ", entryProfile,
|
|
" | Score: ", entryScore, "/10",
|
|
" | Regime: ", marketState.currentRegime,
|
|
" | TPx: ", DoubleToString(tpMultiplier, 2),
|
|
" | SLx: ", DoubleToString(slMultiplier, 2));
|
|
|
|
// Execute BUY order with slippage control
|
|
bool orderPlaced = false;
|
|
|
|
if(UseSmartEntry && UseLimitOrders && entryPrice < ask)
|
|
{
|
|
// Use LIMIT order when smart entry found a better price below current ask
|
|
if(!IsNewOrderAllowed())
|
|
{
|
|
Print(" BUY LIMIT skipped - pending order limit reached");
|
|
}
|
|
else
|
|
if(!IsPendingPriceValid(entryPrice, ask, true))
|
|
{
|
|
Print(" BUY LIMIT skipped - price too close to market (min distance: ", DoubleToString(GetMinStopDistance(), Digits()), ")");
|
|
}
|
|
else
|
|
{
|
|
if(EnableVoicePrompts && liqUsed)
|
|
SpeakZira("I find a liquidity zone on symbol " + Symbol() + ". I will place a buy limit order.");
|
|
orderPlaced = trade.BuyLimit(lots, entryPrice, Symbol(), sl, tp, ORDER_TIME_GTC, 0, "King Gold Scalper BUY (Limit)");
|
|
}
|
|
if(orderPlaced)
|
|
Print(" BUY LIMIT Order PLACED - Ticket: ", trade.ResultOrder(), " | Lots: ", lots, " | Limit Price: ", entryPrice, " | Current Ask: ", ask, " | TP: ", tp, " | SL: ", sl);
|
|
}
|
|
else
|
|
{
|
|
// Use MARKET order (original behavior or when limit not applicable)
|
|
orderPlaced = trade.Buy(lots, Symbol(), entryPrice, sl, tp, "King Gold Scalper BUY");
|
|
if(orderPlaced)
|
|
Print(" BUY Order EXECUTED - Ticket: ", trade.ResultOrder(), " | Lots: ", lots, " | Entry: ", entryPrice, " | TP: ", tp, " | SL: ", sl);
|
|
}
|
|
|
|
// Verify order success
|
|
if(!orderPlaced)
|
|
{
|
|
Print(" BUY Order FAILED - Error: ", trade.ResultRetcodeDescription(), " (Code: ", trade.ResultRetcode(), ")");
|
|
EmitOrderBlockedSound();
|
|
}
|
|
else
|
|
{
|
|
Print(" BUY order accepted - trade context will be captured on DEAL_ENTRY_IN");
|
|
EmitOrderExecutedSound(1);
|
|
if(TelegramSendEntries)
|
|
{
|
|
string msg = BuildTelegramEntryMessage(Symbol(), "BUY", entryPrice, sl, tp, entryScore, marketState.currentRegime);
|
|
SendTelegramMessage(msg, lastTelegramEntryTime, TelegramCooldownSeconds);
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// OPEN SELL TRADE
|
|
//+------------------------------------------------------------------+
|
|
void OpenSellTrade(double bid)
|
|
{
|
|
if(IsSymbolAutoCloseBlocked(Symbol(), GetCurrentTimeWithOffset()))
|
|
{
|
|
Print("AUTO CLOSE GATE [", Symbol(), "] - SELL blocked until next session window.");
|
|
return;
|
|
}
|
|
|
|
if(CountOpenTradesAllSymbols() >= currentMaxOpenTrades)
|
|
return;
|
|
|
|
double lots = CalculateLotSize();
|
|
if(!CheckMoneyForTrade(Symbol(), lots, ORDER_TYPE_SELL))
|
|
{
|
|
EmitOrderBlockedSound();
|
|
return;
|
|
}
|
|
string volumeDesc = "";
|
|
if(!CheckVolumeValue(lots, volumeDesc))
|
|
{
|
|
Print(" Volume check failed: ", volumeDesc);
|
|
EmitOrderBlockedSound();
|
|
return;
|
|
}
|
|
double allowedVolume = NewOrderAllowedVolume(Symbol());
|
|
if(allowedVolume <= 0 || lots > allowedVolume)
|
|
{
|
|
Print(" Volume limit exceeded: lots=", lots, " allowed=", allowedVolume);
|
|
EmitOrderBlockedSound();
|
|
return;
|
|
}
|
|
double entryPrice = bid;
|
|
double askNow = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
|
|
double tpMultiplier = 1.0;
|
|
double slMultiplier = 1.0;
|
|
string entryProfile = "NORMAL";
|
|
int entryScore = 5;
|
|
SelectEntryProfile(-1, bid, askNow, tpMultiplier, slMultiplier, entryProfile, entryScore);
|
|
|
|
// Apply Smart Entry - adjust entry price towards liquidity zones
|
|
double liqDistPts = 0.0;
|
|
bool liqUsed = false;
|
|
if(UseSmartEntry)
|
|
{
|
|
entryPrice = FindNearestLiquidityZone(bid, POSITION_TYPE_SELL, liqDistPts);
|
|
liqUsed = (entryPrice != bid);
|
|
}
|
|
|
|
entryPrice = NormalizeToTick(entryPrice);
|
|
double baseTPDistance = ConvertInputDistanceToPriceForSymbol(Symbol(), TakeProfit) * tpMultiplier;
|
|
double baseSLDistance = ConvertInputDistanceToPriceForSymbol(Symbol(), StopLoss) * slMultiplier;
|
|
double tpDistance = baseTPDistance;
|
|
double slDistance = baseSLDistance;
|
|
if(UseEntryProfileTP_SL)
|
|
AdjustEntryDistancesSmart(Symbol(), -1, entryScore, bid, askNow, baseTPDistance, baseSLDistance, tpDistance, slDistance);
|
|
double tp = EnforceMinStopDistance(entryPrice, entryPrice - tpDistance, false, false);
|
|
double sl = EnforceMinStopDistance(entryPrice, entryPrice + slDistance, false, true);
|
|
if(!CheckStopLossTakeProfit(ORDER_TYPE_SELL, sl, tp))
|
|
return;
|
|
|
|
Print(" ENTRY PROFILE [SELL] - ", entryProfile,
|
|
" | Score: ", entryScore, "/10",
|
|
" | Regime: ", marketState.currentRegime,
|
|
" | TPx: ", DoubleToString(tpMultiplier, 2),
|
|
" | SLx: ", DoubleToString(slMultiplier, 2));
|
|
|
|
// Execute SELL order with slippage control
|
|
bool orderPlaced = false;
|
|
|
|
if(UseSmartEntry && UseLimitOrders && entryPrice > bid)
|
|
{
|
|
// Use LIMIT order when smart entry found a better price above current bid
|
|
if(!IsNewOrderAllowed())
|
|
{
|
|
Print(" SELL LIMIT skipped - pending order limit reached");
|
|
}
|
|
else
|
|
if(!IsPendingPriceValid(entryPrice, bid, false))
|
|
{
|
|
Print(" SELL LIMIT skipped - price too close to market (min distance: ", DoubleToString(GetMinStopDistance(), Digits()), ")");
|
|
}
|
|
else
|
|
{
|
|
if(EnableVoicePrompts && liqUsed)
|
|
SpeakZira("I find a liquidity zone on symbol " + Symbol() + ". I will place a sell limit order.");
|
|
orderPlaced = trade.SellLimit(lots, entryPrice, Symbol(), sl, tp, ORDER_TIME_GTC, 0, "King Gold Scalper SELL (Limit)");
|
|
}
|
|
if(orderPlaced)
|
|
Print(" SELL LIMIT Order PLACED - Ticket: ", trade.ResultOrder(), " | Lots: ", lots, " | Limit Price: ", entryPrice, " | Current Bid: ", bid, " | TP: ", tp, " | SL: ", sl);
|
|
}
|
|
else
|
|
{
|
|
// Use MARKET order (original behavior or when limit not applicable)
|
|
orderPlaced = trade.Sell(lots, Symbol(), entryPrice, sl, tp, "King Gold Scalper SELL");
|
|
if(orderPlaced)
|
|
Print(" SELL Order EXECUTED - Ticket: ", trade.ResultOrder(), " | Lots: ", lots, " | Entry: ", entryPrice, " | TP: ", tp, " | SL: ", sl);
|
|
}
|
|
|
|
// Verify order success
|
|
if(!orderPlaced)
|
|
{
|
|
Print(" SELL Order FAILED - Error: ", trade.ResultRetcodeDescription(), " (Code: ", trade.ResultRetcode(), ")");
|
|
EmitOrderBlockedSound();
|
|
}
|
|
else
|
|
{
|
|
Print(" SELL order accepted - trade context will be captured on DEAL_ENTRY_IN");
|
|
EmitOrderExecutedSound(-1);
|
|
if(TelegramSendEntries)
|
|
{
|
|
string msg = BuildTelegramEntryMessage(Symbol(), "SELL", entryPrice, sl, tp, entryScore, marketState.currentRegime);
|
|
SendTelegramMessage(msg, lastTelegramEntryTime, TelegramCooldownSeconds);
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// CALCULATE LOT SIZE BASED ON RISK PERCENTAGE OF ACCOUNT BALANCE
|
|
//+------------------------------------------------------------------+
|
|
double CalculateLotSizeForSymbol(string symbol)
|
|
{
|
|
if(LotSize > 0)
|
|
return LotSize; // Fixed lot size mode
|
|
|
|
double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
|
|
// Determine which risk % to use (adaptive + default fallback)
|
|
double baseRisk = GetConfiguredBaseRiskPercent();
|
|
|
|
double riskToUse = UseAdaptiveTrading ? currentRiskPercent : baseRisk;
|
|
if(UseAdaptiveTrading)
|
|
riskToUse *= memoryAdaptiveRiskMultiplier;
|
|
|
|
// Safe mode risk cut
|
|
if(safeModeActive)
|
|
riskToUse *= SafeRiskFactor;
|
|
|
|
// Recovery mode risk cut
|
|
if(recoveryModeActive)
|
|
riskToUse *= ClampValue(RecoveryRiskMultiplier, 0.1, 5.0);
|
|
|
|
// Controlled explore: allow reduced-risk entries for learning
|
|
if(nextTradeUseExploreLot)
|
|
{
|
|
static bool warnedExploreLot = false;
|
|
double exploreMultiplier = ClampValue(ExploreLotMultiplier, 0.1, 1.5);
|
|
if(ExploreLotMultiplier > 1.0 && !warnedExploreLot)
|
|
{
|
|
Print("RISK WARNING | ExploreLotMultiplier=", DoubleToString(ExploreLotMultiplier, 2),
|
|
" > 1.00; forcing 1.2 max for EXPLORE_SMALL consistency");
|
|
warnedExploreLot = true;
|
|
}
|
|
riskToUse *= exploreMultiplier;
|
|
}
|
|
|
|
// Apply greed multiplier
|
|
if(UseSignalStrengthGreed)
|
|
{
|
|
if(greedLevel == "GREEDY")
|
|
riskToUse *= 1.5;
|
|
else
|
|
if(greedLevel == "CONSERVATIVE")
|
|
riskToUse *= 0.75;
|
|
}
|
|
|
|
riskToUse = ClampValue(riskToUse, 0.01, 10.0);
|
|
|
|
double riskMoney = accountBalance * (riskToUse / 100.0);
|
|
|
|
if(accountBalance <= 0 || riskMoney <= 0)
|
|
return SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
|
|
|
// Get symbol properties
|
|
double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
|
|
double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
if(tickSize <= 0.0)
|
|
tickSize = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
|
|
int symbolDigits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
|
|
double symbolPoint = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
double symbolPipMultiplier = ((symbolDigits == 5 || symbolDigits == 3) ? 10.0 : 1.0);
|
|
double pointDistance = InputsArePips ? (symbolPoint * symbolPipMultiplier) : symbolPoint;
|
|
|
|
// Convert StopLoss to price distance in current symbol units
|
|
double slPriceDistance = StopLoss * pointDistance;
|
|
if(slPriceDistance <= 0.0 || tickValue <= 0.0)
|
|
return SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
|
|
|
// Risk per 1 lot = (SL distance / tick size) * tick value
|
|
double riskPerLot = (slPriceDistance / tickSize) * tickValue;
|
|
|
|
if(riskPerLot <= 0)
|
|
return SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
|
|
|
// Calculate lot size: risk money / risk per lot
|
|
double lots = riskMoney / riskPerLot;
|
|
|
|
// Get symbol volume constraints
|
|
double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
|
double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
|
|
double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
|
|
|
|
// Safety check for invalid lotStep
|
|
if(lotStep <= 0 || lotStep > 1.0)
|
|
{
|
|
double originalLotStep = lotStep;
|
|
lotStep = 0.01; // Default fallback
|
|
Print(" Invalid lotStep detected (", originalLotStep, ") - using default 0.01");
|
|
}
|
|
double logStep = MathLog10(lotStep);
|
|
if(MathAbs(logStep - MathRound(logStep)) > 0.0001)
|
|
Print(" lotStep is not a power of 10 (", lotStep, ") - may cause normalization issues");
|
|
|
|
// Normalize volume to meet symbol requirements
|
|
lots = MathMax(lots, minLot);
|
|
double effectiveMaxLot = MathMax(minLot, maxLot);
|
|
lots = MathMin(lots, effectiveMaxLot);
|
|
lots = NormalizeDouble(lots, (int)MathLog10(1.0 / lotStep));
|
|
|
|
if(lots > effectiveMaxLot)
|
|
lots = effectiveMaxLot;
|
|
lots = MathMax(lots, minLot);
|
|
|
|
Print("LOT CALCULATION [", symbol, "] - Balance: $", accountBalance,
|
|
" | Risk: ", riskToUse, "% = $", riskMoney,
|
|
" | Lot Size: ", lots,
|
|
" | MaxLot: ", DoubleToString(effectiveMaxLot, 2),
|
|
" | Greed: ", greedLevel,
|
|
" | Mode: ", performanceMode,
|
|
" | MemoryRisk: ", DoubleToString(memoryAdaptiveRiskMultiplier, 2), "x");
|
|
|
|
return lots;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double CalculateLotSize()
|
|
{
|
|
return CalculateLotSizeForSymbol(Symbol());
|
|
}
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
// GET AVERAGE ENTRY PRICE FOR A DIRECTION
|
|
//+------------------------------------------------------------------+
|
|
double GetAverageEntryPrice(int pos_type)
|
|
{
|
|
double totalPrice = 0;
|
|
int count = 0;
|
|
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket > 0)
|
|
{
|
|
if(PositionGetString(POSITION_SYMBOL) == Symbol() &&
|
|
PositionGetInteger(POSITION_TYPE) == pos_type &&
|
|
PositionGetInteger(POSITION_MAGIC) == MagicNumber)
|
|
{
|
|
totalPrice += PositionGetDouble(POSITION_PRICE_OPEN);
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(count <= 0)
|
|
return 0;
|
|
|
|
return totalPrice / count;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// COUNT OPEN TRADES - Only count trades with OUR magic number
|
|
//+------------------------------------------------------------------+
|
|
int CountOpenTrades()
|
|
{
|
|
int count = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(PositionGetTicket(i) > 0)
|
|
{
|
|
// Only count if symbol matches AND magic number matches
|
|
if(PositionGetString(POSITION_SYMBOL) == Symbol() &&
|
|
PositionGetInteger(POSITION_MAGIC) == MagicNumber)
|
|
{
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// COUNT OPEN TRADES (ALL SYMBOLS) - ONLY OUR MAGIC NUMBER
|
|
//+------------------------------------------------------------------+
|
|
int CountOpenTradesAllSymbols()
|
|
{
|
|
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)
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// CALCULATE TOTAL PROFIT/LOSS (ALL POSITIONS) - ONLY OUR MAGIC NUMBER
|
|
//+------------------------------------------------------------------+
|
|
double CalculateTotalProfit()
|
|
{
|
|
double totalProfit = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket > 0 && PositionGetString(POSITION_SYMBOL) == Symbol() &&
|
|
PositionGetInteger(POSITION_MAGIC) == MagicNumber)
|
|
{
|
|
totalProfit += PositionGetDouble(POSITION_PROFIT);
|
|
}
|
|
}
|
|
return totalProfit;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// CALCULATE PROFIT/LOSS FOR SPECIFIC DIRECTION (BUY or SELL) - ONLY OUR MAGIC NUMBER
|
|
//+------------------------------------------------------------------+
|
|
double CalculateDirectionProfit(int posType)
|
|
{
|
|
double profit = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket > 0 && PositionGetString(POSITION_SYMBOL) == Symbol() &&
|
|
PositionGetInteger(POSITION_MAGIC) == MagicNumber)
|
|
{
|
|
if(PositionGetInteger(POSITION_TYPE) == posType)
|
|
{
|
|
profit += PositionGetDouble(POSITION_PROFIT);
|
|
}
|
|
}
|
|
}
|
|
return profit;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// CLOSE ALL TRADES - Safer position access with ticket verification
|
|
//+------------------------------------------------------------------+
|
|
void CloseAllTrades()
|
|
{
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket > 0)
|
|
{
|
|
// Safely select and verify position before closing
|
|
if(!PositionSelectByTicket(ticket))
|
|
continue;
|
|
|
|
// Only close if it's our symbol and magic number
|
|
if(PositionGetString(POSITION_SYMBOL) == Symbol() &&
|
|
PositionGetInteger(POSITION_MAGIC) == MagicNumber)
|
|
{
|
|
trade.PositionClose(ticket);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// DEINITIALIZATION
|
|
//+------------------------------------------------------------------+
|
|
//+------------------------------------------------------------------+
|
|
// TRADE TRANSACTION HANDLER - Track winning/losing trades for cooldown
|
|
//+------------------------------------------------------------------+
|
|
void OnTradeTransaction(const MqlTradeTransaction& trans,
|
|
const MqlTradeRequest& request,
|
|
const MqlTradeResult& result)
|
|
{
|
|
// Only process deal transactions
|
|
if(trans.type != TRADE_TRANSACTION_DEAL_ADD)
|
|
return;
|
|
|
|
// Get deal ticket
|
|
ulong dealTicket = trans.deal;
|
|
if(dealTicket == 0)
|
|
return;
|
|
|
|
// Check if this is our deal
|
|
if(!HistoryDealSelect(dealTicket))
|
|
return;
|
|
|
|
// Verify it's our magic number (multi-symbol aware)
|
|
string dealSymbol = HistoryDealGetString(dealTicket, DEAL_SYMBOL);
|
|
if(HistoryDealGetInteger(dealTicket, DEAL_MAGIC) != MagicNumber)
|
|
return;
|
|
|
|
// Deal entry type
|
|
ENUM_DEAL_ENTRY dealEntry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(dealTicket, DEAL_ENTRY);
|
|
|
|
// Handle position entry (capture trade context for later real outcome learning)
|
|
ulong positionId = (ulong)HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID);
|
|
if(dealEntry == DEAL_ENTRY_IN)
|
|
{
|
|
ENUM_DEAL_TYPE dealTypeIn = (ENUM_DEAL_TYPE)HistoryDealGetInteger(dealTicket, DEAL_TYPE);
|
|
int direction = 0;
|
|
if(dealTypeIn == DEAL_TYPE_BUY)
|
|
direction = 1;
|
|
else
|
|
if(dealTypeIn == DEAL_TYPE_SELL)
|
|
direction = -1;
|
|
|
|
if(direction != 0)
|
|
{
|
|
datetime entryTime = (datetime)HistoryDealGetInteger(dealTicket, DEAL_TIME);
|
|
double entryPrice = HistoryDealGetDouble(dealTicket, DEAL_PRICE);
|
|
double volumeRatio = GetCurrentVolumeRatioForSymbol(dealSymbol, (ENUM_TIMEFRAMES)Period());
|
|
double symbolPoint = SymbolInfoDouble(dealSymbol, SYMBOL_POINT);
|
|
int symbolDigits = (int)SymbolInfoInteger(dealSymbol, SYMBOL_DIGITS);
|
|
double spreadPointValue = InputsArePips ? ((((symbolDigits == 5 || symbolDigits == 3) ? 10.0 : 1.0) * symbolPoint)) : symbolPoint;
|
|
double spread = (spreadPointValue > 0.0) ? ((SymbolInfoDouble(dealSymbol, SYMBOL_ASK) - SymbolInfoDouble(dealSymbol, SYMBOL_BID)) / spreadPointValue) : 0.0;
|
|
int barsAtEntry = (direction == 1)
|
|
? GetConsecutiveBarsMultiTFForSymbol(dealSymbol, false)
|
|
: GetConsecutiveBarsMultiTFForSymbol(dealSymbol, true);
|
|
double bidNow = SymbolInfoDouble(dealSymbol, SYMBOL_BID);
|
|
double askNow = SymbolInfoDouble(dealSymbol, SYMBOL_ASK);
|
|
string scoreDetails = "";
|
|
int decisionScore = GetDecisionScore10ForSymbol(dealSymbol, direction, barsAtEntry, volumeRatio, bidNow, askNow, scoreDetails);
|
|
int barsBucket = GetBarsBucket(barsAtEntry);
|
|
int volumeBucket = GetVolumeBucket(volumeRatio);
|
|
int entryTf = DetectEntryTimeframeForSymbol(dealSymbol, direction);
|
|
string regimeAtEntry = marketState.currentRegime;
|
|
|
|
// Generate sequence-based dynamic pattern for learning (last 3 closed candles)
|
|
string seqHash = GeneratePatternSequenceHash(dealSymbol, entryTf, 1, 3);
|
|
int seqType = InferSequencePatternType(dealSymbol, entryTf, 1, 3);
|
|
string seqDesc = BuildSequenceDescription(dealSymbol, entryTf, 1, 3);
|
|
int patternIdx = FindOrCreateDynamicPatternMeta(seqHash, seqType, entryTf, seqDesc);
|
|
int decisionSetupIdAtEntry = 0;
|
|
int decisionActionAtEntry = ConsumePendingDecisionActionForSymbol(dealSymbol, decisionSetupIdAtEntry);
|
|
int entryTypeAtEntry = 0;
|
|
ulong sourceOrderTicket = (ulong)HistoryDealGetInteger(dealTicket, DEAL_ORDER);
|
|
int sourceOrderTypeAtEntry = -1;
|
|
bool sourceOrderDetectedFromActivePool = false;
|
|
if(sourceOrderTicket > 0 && HistoryOrderSelect(sourceOrderTicket))
|
|
{
|
|
sourceOrderTypeAtEntry = (int)HistoryOrderGetInteger(sourceOrderTicket, ORDER_TYPE);
|
|
if(IsSmartPendingOrderType(sourceOrderTypeAtEntry))
|
|
entryTypeAtEntry = 1;
|
|
}
|
|
else
|
|
if(sourceOrderTicket > 0 && OrderSelect(sourceOrderTicket))
|
|
{
|
|
sourceOrderDetectedFromActivePool = true;
|
|
sourceOrderTypeAtEntry = (int)OrderGetInteger(ORDER_TYPE);
|
|
if(IsSmartPendingOrderType(sourceOrderTypeAtEntry))
|
|
entryTypeAtEntry = 1;
|
|
}
|
|
|
|
if(LogDecisionLearningDetails && sourceOrderDetectedFromActivePool)
|
|
Print("LEARN LINK [ENTRY_SOURCE] | history_missing=1 | used_orders_pool=1 | orderTicket=", (long)sourceOrderTicket,
|
|
" | orderType=", sourceOrderTypeAtEntry,
|
|
" | entryTypeDetected=", entryTypeAtEntry);
|
|
|
|
double liqDistPts = 0.0;
|
|
double liqLevel = FindNearestLiquidityZone(entryPrice, (direction == 1) ? POSITION_TYPE_BUY : POSITION_TYPE_SELL, liqDistPts);
|
|
bool liqUsed = (UseSmartEntry && MathAbs(liqLevel - entryPrice) > SymbolInfoDouble(dealSymbol, SYMBOL_POINT) * 0.1);
|
|
SavePositionEntryContext(positionId, direction, entryPrice, barsAtEntry, volumeRatio, spread, entryTime,
|
|
decisionScore, decisionActionAtEntry, decisionSetupIdAtEntry, entryTypeAtEntry, sourceOrderTicket, sourceOrderTypeAtEntry,
|
|
barsBucket, volumeBucket, entryTf, regimeAtEntry, seqHash,
|
|
liqLevel, liqDistPts, liqUsed);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check if it's a position close (not open)
|
|
if(dealEntry != DEAL_ENTRY_OUT)
|
|
return; // Only process position closes
|
|
|
|
// Get deal profit
|
|
double dealProfit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT);
|
|
double dealSwap = HistoryDealGetDouble(dealTicket, DEAL_SWAP);
|
|
double dealCommission = HistoryDealGetDouble(dealTicket, DEAL_COMMISSION);
|
|
double totalProfit = dealProfit + dealSwap + dealCommission;
|
|
int entryIndex = FindPositionEntryContextIndex(positionId);
|
|
bool hasEntryContext = (entryIndex != -1);
|
|
if(hasEntryContext)
|
|
{
|
|
positionEntryMemory[entryIndex].accumulatedProfit += totalProfit;
|
|
positionEntryMemory[entryIndex].closeDealCount++;
|
|
}
|
|
|
|
bool positionStillOpen = (positionId > 0 && IsPositionIdentifierStillOpen(positionId));
|
|
|
|
// Learn once per position at final close to avoid duplicate memory entries on partial closes.
|
|
if(!positionStillOpen)
|
|
{
|
|
PositionEntryContext entryContext;
|
|
int direction = 0;
|
|
datetime entryTime = (datetime)HistoryDealGetInteger(dealTicket, DEAL_TIME);
|
|
double entryPrice = HistoryDealGetDouble(dealTicket, DEAL_PRICE);
|
|
int barsAtEntry = 0;
|
|
double volumeRatioAtEntry = 1.0;
|
|
double symbolPoint = SymbolInfoDouble(dealSymbol, SYMBOL_POINT);
|
|
double spreadAtEntry = (symbolPoint > 0.0) ? ((SymbolInfoDouble(dealSymbol, SYMBOL_ASK) - SymbolInfoDouble(dealSymbol, SYMBOL_BID)) / symbolPoint) : 0.0;
|
|
double finalizedProfit = totalProfit;
|
|
|
|
if(hasEntryContext)
|
|
{
|
|
entryContext = positionEntryMemory[entryIndex];
|
|
direction = entryContext.direction;
|
|
entryTime = entryContext.entryTime;
|
|
entryPrice = entryContext.entryPrice;
|
|
barsAtEntry = entryContext.consecutiveBars;
|
|
volumeRatioAtEntry = entryContext.volumeRatio;
|
|
spreadAtEntry = entryContext.spreadAtEntry;
|
|
finalizedProfit = entryContext.accumulatedProfit;
|
|
}
|
|
else
|
|
{
|
|
int tmpDir = 0;
|
|
if(GetPositionEntryFromHistory(positionId, dealSymbol, MagicNumber, entryTime, entryPrice, tmpDir))
|
|
direction = tmpDir;
|
|
|
|
ENUM_DEAL_TYPE closeType = (ENUM_DEAL_TYPE)HistoryDealGetInteger(dealTicket, DEAL_TYPE);
|
|
if(direction == 0)
|
|
{
|
|
if(closeType == DEAL_TYPE_SELL)
|
|
direction = 1; // closing a BUY
|
|
else
|
|
if(closeType == DEAL_TYPE_BUY)
|
|
direction = -1; // closing a SELL
|
|
}
|
|
}
|
|
|
|
if(direction != 0)
|
|
{
|
|
datetime exitTime = (datetime)HistoryDealGetInteger(dealTicket, DEAL_TIME);
|
|
double exitPrice = HistoryDealGetDouble(dealTicket, DEAL_PRICE);
|
|
string exitReason = DealReasonToText(HistoryDealGetInteger(dealTicket, DEAL_REASON));
|
|
bool autoClosePositiveContext = IsAutoCloseLearningPositiveContext(dealSymbol, exitTime);
|
|
double learningProfit = finalizedProfit;
|
|
if(autoClosePositiveContext && learningProfit < 0.0)
|
|
{
|
|
learningProfit = 0.0;
|
|
Print("AUTO-CLOSE LEARNING ADJUST [", dealSymbol, "] | position loss ignored in positive basket close | raw=", DoubleToString(finalizedProfit, 2));
|
|
}
|
|
|
|
RecordTradeToMemory(direction, entryPrice, exitPrice, learningProfit, barsAtEntry, volumeRatioAtEntry, spreadAtEntry, entryTime, exitTime, exitReason, dealSymbol);
|
|
EmitCloseResultSound(finalizedProfit);
|
|
|
|
// Update per-symbol enhanced memory from real outcomes
|
|
int symIndex = getSymbolIndex(dealSymbol);
|
|
if(symIndex >= 0)
|
|
{
|
|
enhancedSymbolMemory[symIndex].totalTrades++;
|
|
if(learningProfit > 0.0)
|
|
enhancedSymbolMemory[symIndex].winningTrades++;
|
|
enhancedSymbolMemory[symIndex].totalProfit += learningProfit;
|
|
enhancedSymbolMemory[symIndex].winRate = (enhancedSymbolMemory[symIndex].totalTrades > 0)
|
|
? ((double)enhancedSymbolMemory[symIndex].winningTrades / (double)enhancedSymbolMemory[symIndex].totalTrades)
|
|
: 0.0;
|
|
|
|
int tfUsed = hasEntryContext ? entryContext.entryTimeframe : (int)Period();
|
|
if(getTimeframeIndex(tfUsed) < 0)
|
|
tfUsed = (int)Period();
|
|
int tfIdx = getTimeframeIndex(tfUsed);
|
|
if(tfIdx >= 0)
|
|
{
|
|
int prevTrades = enhancedSymbolMemory[symIndex].tfTrades[tfIdx];
|
|
double prevAvg = enhancedSymbolMemory[symIndex].tfPerformance[tfIdx];
|
|
int nextTrades = prevTrades + 1;
|
|
enhancedSymbolMemory[symIndex].tfTrades[tfIdx] = nextTrades;
|
|
enhancedSymbolMemory[symIndex].tfPerformance[tfIdx] =
|
|
((prevAvg * (double)prevTrades) + learningProfit) / (double)nextTrades;
|
|
}
|
|
}
|
|
|
|
// Update dynamic pattern learning with outcome
|
|
if(hasEntryContext && entryContext.patternHash != "")
|
|
{
|
|
int tfUsed = hasEntryContext ? entryContext.entryTimeframe : (int)Period();
|
|
if(getTimeframeIndex(tfUsed) < 0)
|
|
tfUsed = (int)Period();
|
|
UpdateDynamicPattern(entryContext.patternHash, learningProfit, tfUsed);
|
|
}
|
|
|
|
int barsBucket = hasEntryContext ? entryContext.barsBucket : GetBarsBucket(barsAtEntry);
|
|
int volumeBucket = hasEntryContext ? entryContext.volumeBucket : GetVolumeBucket(volumeRatioAtEntry);
|
|
string regimeAtEntry = hasEntryContext ? entryContext.regimeAtEntry : marketState.currentRegime;
|
|
int entryTypeAtEntry = hasEntryContext ? entryContext.entryTypeAtEntry : 0;
|
|
double smartLimitOutcomeBonus = 0.0;
|
|
if(UseSmartLimitRewardBoost && entryTypeAtEntry == 1)
|
|
{
|
|
if(learningProfit > 0.0)
|
|
smartLimitOutcomeBonus = MathAbs(SmartLimitRewardWinBonus);
|
|
else
|
|
if(learningProfit < 0.0)
|
|
smartLimitOutcomeBonus = -MathAbs(SmartLimitRewardLossPenalty);
|
|
}
|
|
|
|
UpdateSetupBucketReward(direction, barsBucket, volumeBucket, regimeAtEntry, learningProfit, entryTypeAtEntry);
|
|
ApplyRewardPenaltyToPattern(dealSymbol, direction, barsAtEntry, learningProfit);
|
|
|
|
if(UseDecisionCoreDll && g_decisionDllReady)
|
|
{
|
|
DC_TradeOutcome outcome;
|
|
int learnTf = hasEntryContext ? entryContext.entryTimeframe : (int)Period();
|
|
if(getTimeframeIndex(learnTf) < 0)
|
|
learnTf = (int)Period();
|
|
int setupIdForLearn = hasEntryContext ? entryContext.decisionSetupIdAtEntry : 0;
|
|
int actionForLearn = hasEntryContext ? entryContext.decisionActionAtEntry : 1;
|
|
int wasWinForLearn = (learningProfit > 0.0) ? 1 : 0;
|
|
double drawdownAfterForLearn = GetAccountDrawdownPercentAuto();
|
|
int entryTypeFromContext = hasEntryContext ? entryContext.entryTypeAtEntry : -1;
|
|
|
|
if(LogDecisionLearningDetails)
|
|
{
|
|
Print("LEARN LINK [ENTRY_TYPE] | posTicket=", (long)positionId,
|
|
" | dealTicket=", (long)dealTicket,
|
|
" | orderTicket=", (long)(hasEntryContext ? entryContext.sourceOrderTicketAtEntry : 0),
|
|
" | orderType=", (hasEntryContext ? entryContext.sourceOrderTypeAtEntry : -1),
|
|
" | entryTypeFromContext=", entryTypeFromContext,
|
|
" | entryTypeUsed=", entryTypeAtEntry);
|
|
Print("LEARN LINK [OUTCOME] | setup_id=", setupIdForLearn,
|
|
" | action_taken=", actionForLearn,
|
|
" | profit=", DoubleToString(learningProfit, 2),
|
|
" | was_win=", wasWinForLearn,
|
|
" | drawdown_after=", DoubleToString(drawdownAfterForLearn, 2),
|
|
" | entry_type=", entryTypeAtEntry);
|
|
if(setupIdForLearn <= 0)
|
|
Print("LEARN LINK WARNING | setup_id is 0 (fallback matching may be weaker) | symbol=", dealSymbol);
|
|
}
|
|
|
|
DC_FillTradeOutcomeSafe(outcome,
|
|
dealSymbol,
|
|
learnTf,
|
|
direction,
|
|
actionForLearn,
|
|
learningProfit,
|
|
drawdownAfterForLearn,
|
|
GetSetupRewardScore(direction,
|
|
hasEntryContext ? entryContext.barsBucket : GetBarsBucket(barsAtEntry),
|
|
hasEntryContext ? entryContext.volumeBucket : GetVolumeBucket(volumeRatioAtEntry),
|
|
hasEntryContext ? entryContext.regimeAtEntry : marketState.currentRegime,
|
|
entryTypeAtEntry) + smartLimitOutcomeBonus,
|
|
wasWinForLearn,
|
|
setupIdForLearn,
|
|
entryTypeAtEntry);
|
|
int learnRc = dc_learn(outcome);
|
|
if(learnRc <= 0)
|
|
Print("DECISION DLL | learn failed rc=", learnRc, " | symbol=", dealSymbol);
|
|
else
|
|
if(LogDecisionLearningDetails)
|
|
Print("DECISION DLL | learn rc=", learnRc, " | symbol=", dealSymbol,
|
|
" | setup_id=", setupIdForLearn,
|
|
" | action=", actionForLearn,
|
|
" | entry_type=", entryTypeAtEntry);
|
|
}
|
|
|
|
// Update memory streak state only once per fully closed position
|
|
if(learningProfit > 0.0)
|
|
{
|
|
marketState.consecutiveWins++;
|
|
marketState.consecutiveLosses = 0;
|
|
marketState.recentPerformance = (marketState.recentPerformance * 0.90) + 0.10;
|
|
}
|
|
else
|
|
if(learningProfit < 0.0)
|
|
{
|
|
marketState.consecutiveLosses++;
|
|
marketState.consecutiveWins = 0;
|
|
marketState.recentPerformance = (marketState.recentPerformance * 0.90) - 0.10;
|
|
}
|
|
else
|
|
{
|
|
marketState.consecutiveWins = 0;
|
|
marketState.consecutiveLosses = 0;
|
|
marketState.recentPerformance *= 0.90;
|
|
}
|
|
}
|
|
|
|
if(positionId > 0 && hasEntryContext)
|
|
RemovePositionEntryContext(positionId);
|
|
|
|
}
|
|
|
|
// Check if it's a loss
|
|
datetime closeDealTime = (datetime)HistoryDealGetInteger(dealTicket, DEAL_TIME);
|
|
bool ignoreDealLossForCooldown = IsAutoCloseLearningPositiveContext(dealSymbol, closeDealTime);
|
|
if(totalProfit < 0 && !ignoreDealLossForCooldown)
|
|
{
|
|
// Record loss time for cooldown
|
|
datetime dealTime = closeDealTime;
|
|
lastLossTime = dealTime;
|
|
|
|
// Also update bar-based tracking for compatibility
|
|
int currentBar = (int)iTime(Symbol(), Period(), 0);
|
|
lastLossBar = currentBar;
|
|
|
|
Print(" LOSS DETECTED - Profit: $", DoubleToString(totalProfit, 2), " | Cooldown activated for ", CooldownBars, " bars");
|
|
}
|
|
else
|
|
if(totalProfit > 0)
|
|
{
|
|
Print(" WIN DETECTED - Profit: $", DoubleToString(totalProfit, 2));
|
|
}
|
|
else
|
|
if(totalProfit < 0 && ignoreDealLossForCooldown)
|
|
Print(" AUTO-CLOSE BASKET CONTEXT | Negative deal ignored for cooldown/loss streak: ", DoubleToString(totalProfit, 2));
|
|
|
|
if(!positionStillOpen && CountOpenTradesForSymbolMagic(dealSymbol) <= 0)
|
|
{
|
|
int acCtxIdx = FindAutoCloseLearningContextIndex(dealSymbol, false);
|
|
if(acCtxIdx >= 0 && autoCloseLearningContexts[acCtxIdx].active && !autoCloseLearningContexts[acCtxIdx].learnSent)
|
|
{
|
|
if(UseDecisionCoreDll && g_decisionDllReady)
|
|
{
|
|
DC_BasketOutcome basketOutcome;
|
|
DC_FillBasketOutcomeSafe(basketOutcome,
|
|
dealSymbol,
|
|
(int)Period(),
|
|
0,
|
|
2, // auto-close buckets are treated as controlled/recovery actions
|
|
autoCloseLearningContexts[acCtxIdx].basketProfitAtTrigger,
|
|
autoCloseLearningContexts[acCtxIdx].drawdownPctAtTrigger,
|
|
GetAccountDrawdownPercentAuto(),
|
|
autoCloseLearningContexts[acCtxIdx].tradeCountAtTrigger,
|
|
autoCloseLearningContexts[acCtxIdx].recoveryModeAtTrigger ? 1 : 0,
|
|
(autoCloseLearningContexts[acCtxIdx].basketProfitAtTrigger > 0.0) ? 1 : 0);
|
|
int basketLearnRc = dc_learn_basket(basketOutcome);
|
|
if(basketLearnRc <= 0)
|
|
Print("DECISION DLL | basket learn failed rc=", basketLearnRc, " | symbol=", dealSymbol);
|
|
else
|
|
Print("DECISION DLL | basket learn ok | symbol=", dealSymbol,
|
|
" | profit=", DoubleToString(basketOutcome.basket_profit, 2),
|
|
" | dd_before=", DoubleToString(basketOutcome.basket_drawdown_before, 2),
|
|
" | dd_after=", DoubleToString(basketOutcome.basket_drawdown_after, 2),
|
|
" | trades=", basketOutcome.basket_trade_count,
|
|
" | recovery=", basketOutcome.recovery_mode);
|
|
}
|
|
autoCloseLearningContexts[acCtxIdx].learnSent = true;
|
|
}
|
|
ClearAutoCloseLearningContext(dealSymbol);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsSmartPendingOrderType(long orderType)
|
|
{
|
|
return (orderType == ORDER_TYPE_BUY_LIMIT ||
|
|
orderType == ORDER_TYPE_SELL_LIMIT ||
|
|
orderType == ORDER_TYPE_BUY_STOP_LIMIT ||
|
|
orderType == ORDER_TYPE_SELL_STOP_LIMIT);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// DEINITIALIZATION
|
|
//+------------------------------------------------------------------+
|
|
void OnDeinit(const int reason)
|
|
{
|
|
if(g_micStarted)
|
|
{
|
|
MicStop();
|
|
g_micStarted = false;
|
|
}
|
|
// Persist state on unload
|
|
if(UseFilePersistence)
|
|
SaveStateToFile();
|
|
|
|
// Release Order Book if it was initialized
|
|
if(domInitialized && domAvailable)
|
|
{
|
|
if(!MarketBookRelease(Symbol()))
|
|
{
|
|
Print(" Failed to release Order Book for ", Symbol());
|
|
}
|
|
else
|
|
{
|
|
Print(" Order Book (DOM) released");
|
|
}
|
|
}
|
|
|
|
// Clear arrays and reset state
|
|
ArrayResize(bookArray, 0);
|
|
|
|
string deinitReason = "Unknown";
|
|
switch(reason)
|
|
{
|
|
case REASON_ACCOUNT:
|
|
deinitReason = "Account changed";
|
|
break;
|
|
case REASON_CHARTCHANGE:
|
|
deinitReason = "Chart symbol changed";
|
|
break;
|
|
case REASON_CHARTCLOSE:
|
|
deinitReason = "Chart closed";
|
|
break;
|
|
case REASON_PARAMETERS:
|
|
deinitReason = "Parameters changed";
|
|
break;
|
|
case REASON_RECOMPILE:
|
|
deinitReason = "Recompiled";
|
|
break;
|
|
case REASON_REMOVE:
|
|
deinitReason = "EA removed";
|
|
break;
|
|
case REASON_INITFAILED:
|
|
deinitReason = "Init failed";
|
|
break;
|
|
}
|
|
|
|
Print("=== King Gold Scalper Stopped === (Reason: ", deinitReason, ")");
|
|
|
|
// Close voice DB
|
|
if(g_cmdDb > 0)
|
|
{
|
|
sqlite_close(g_cmdDb);
|
|
g_cmdDb = -1;
|
|
}
|
|
|
|
if(g_decisionDllReady)
|
|
{
|
|
dc_close();
|
|
g_decisionDllReady = false;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// UPDATE CONSECUTIVE BARS COUNTERS
|
|
// Tracks consecutive bullish (green) and bearish (red) bars
|
|
//+------------------------------------------------------------------+
|
|
void UpdateConsecutiveSignals()
|
|
{
|
|
if(!UseConsecutiveBars)
|
|
return;
|
|
|
|
if(UseTickBarSignals)
|
|
{
|
|
// In tick-bar mode, respect synthetic bar construction for streaks.
|
|
UpdateConsecutiveBarsFromTicks();
|
|
return;
|
|
}
|
|
|
|
// In timeframe mode, use closed bars from chart timeframe for local streak gating.
|
|
UpdateConsecutiveBarsFromTimeframe();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// AVERAGE TICK VOLUME HELPER
|
|
//+------------------------------------------------------------------+
|
|
double GetAverageTickVolumeForSymbol(string symbol, ENUM_TIMEFRAMES tf, int lookback)
|
|
{
|
|
if(lookback <= 0)
|
|
return 0.0;
|
|
|
|
int bars = Bars(symbol, tf);
|
|
if(bars <= lookback + 1)
|
|
return 0.0;
|
|
|
|
long sumVol = 0;
|
|
int counted = 0;
|
|
|
|
for(int i = 1; i <= lookback; i++)
|
|
{
|
|
long v = iTickVolume(symbol, tf, i);
|
|
if(v <= 0)
|
|
continue;
|
|
sumVol += v;
|
|
counted++;
|
|
}
|
|
|
|
if(counted == 0)
|
|
return 0.0;
|
|
|
|
return (double)sumVol / (double)counted;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
double GetAverageTickVolume(ENUM_TIMEFRAMES tf, int lookback)
|
|
{
|
|
return GetAverageTickVolumeForSymbol(Symbol(), tf, lookback);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateConsecutiveBarsFromTimeframe()
|
|
{
|
|
datetime currentBarTime = iTime(Symbol(), Period(), 1); // use closed bar (index 1) consistently
|
|
if(currentBarTime == lastProcessedBarTime)
|
|
return;
|
|
|
|
double currentOpen = iOpen(Symbol(), Period(), 1);
|
|
double currentClose = iClose(Symbol(), Period(), 1);
|
|
|
|
bool isBullish = (currentClose > currentOpen);
|
|
bool isBearish = (currentClose < currentOpen);
|
|
|
|
// Optional: validate bar by tick volume
|
|
if(UseVolumeInSignals)
|
|
{
|
|
long currVol = iTickVolume(Symbol(), Period(), 1);
|
|
double avgVol = GetAverageTickVolume(Period(), VolumeSignalLookback);
|
|
double volThreshold = VolumeSignalFactor * avgVol;
|
|
|
|
if(avgVol > 0.0)
|
|
{
|
|
bool strongVol = (double)currVol >= volThreshold;
|
|
if(!strongVol)
|
|
{
|
|
// Weak volume: still count bullish/bearish into streaks, then mark bar as processed
|
|
if(isBullish)
|
|
{
|
|
consecutiveBullishBars++;
|
|
consecutiveBearishBars = 0;
|
|
}
|
|
else
|
|
if(isBearish)
|
|
{
|
|
consecutiveBearishBars++;
|
|
consecutiveBullishBars = 0;
|
|
}
|
|
lastProcessedBarTime = currentBarTime;
|
|
|
|
string barTypeWeak = (currentClose > currentOpen) ? "BULLISH" :
|
|
((currentClose < currentOpen) ? "BEARISH" : "DOJI");
|
|
Print(barTypeWeak, " (WEAK VOLUME) [O:", currentOpen, " C:", currentClose,
|
|
"] Vol: ", currVol, " | Avg: ", avgVol, " | Threshold: ", volThreshold,
|
|
" - Counted | Bullish: ", consecutiveBullishBars, "/", BullishBarsForSell,
|
|
" | Bearish: ", consecutiveBearishBars, "/", BearishBarsForBuy);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(isBullish)
|
|
{
|
|
consecutiveBullishBars++;
|
|
consecutiveBearishBars = 0;
|
|
}
|
|
else
|
|
if(isBearish)
|
|
{
|
|
consecutiveBearishBars++;
|
|
consecutiveBullishBars = 0;
|
|
}
|
|
else // DOJI - neutral: keep current streaks unchanged
|
|
{
|
|
}
|
|
|
|
lastProcessedBarTime = currentBarTime;
|
|
string barType = (currentClose > currentOpen) ? "BULLISH" : ((currentClose < currentOpen) ? "BEARISH" : "DOJI");
|
|
|
|
if(UseVolumeInSignals)
|
|
{
|
|
long currVol = iTickVolume(Symbol(), Period(), 1);
|
|
double avgVol = GetAverageTickVolume(Period(), VolumeSignalLookback);
|
|
double volThreshold = VolumeSignalFactor * avgVol;
|
|
Print(barType, " [O:", currentOpen, " C:", currentClose,
|
|
"] Vol: ", currVol, " | Avg: ", avgVol, " | Threshold: ", volThreshold,
|
|
" - Bullish: ", consecutiveBullishBars, "/", BullishBarsForSell,
|
|
" | Bearish: ", consecutiveBearishBars, "/", BearishBarsForBuy);
|
|
}
|
|
else
|
|
{
|
|
Print(barType, " [O:", currentOpen, " C:", currentClose,
|
|
"] - Bullish: ", consecutiveBullishBars, "/", BullishBarsForSell,
|
|
" | Bearish: ", consecutiveBearishBars, "/", BearishBarsForBuy);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateConsecutiveBarsFromTicks()
|
|
{
|
|
if(TickBarSize <= 0)
|
|
return;
|
|
|
|
MqlTick tick;
|
|
if(!SymbolInfoTick(Symbol(), tick))
|
|
return;
|
|
|
|
double price = (tick.last > 0) ? tick.last : ((tick.bid > 0) ? tick.bid : tick.ask);
|
|
if(price <= 0)
|
|
return;
|
|
|
|
if(tickBarCount == 0)
|
|
{
|
|
tickBarOpen = price;
|
|
tickBarHigh = price;
|
|
tickBarLow = price;
|
|
}
|
|
|
|
tickBarHigh = MathMax(tickBarHigh, price);
|
|
tickBarLow = MathMin(tickBarLow, price);
|
|
tickBarClose = price;
|
|
tickBarCount++;
|
|
|
|
if(tickBarCount < TickBarSize)
|
|
return;
|
|
|
|
// Completed a tick-bar -> optionally filter by volume
|
|
if(UseVolumeInSignals)
|
|
{
|
|
long currVol = iTickVolume(Symbol(), Period(), 1);
|
|
double avgVol = GetAverageTickVolume(Period(), VolumeSignalLookback);
|
|
double volThreshold = VolumeSignalFactor * avgVol;
|
|
|
|
if(avgVol > 0.0)
|
|
{
|
|
bool strongVol = (double)currVol >= volThreshold;
|
|
if(!strongVol)
|
|
{
|
|
// Weak volume: still count bullish/bearish into streaks and continue
|
|
if(tickBarClose > tickBarOpen)
|
|
{
|
|
consecutiveBullishBars++;
|
|
consecutiveBearishBars = 0;
|
|
}
|
|
else
|
|
if(tickBarClose < tickBarOpen)
|
|
{
|
|
consecutiveBearishBars++;
|
|
consecutiveBullishBars = 0;
|
|
}
|
|
|
|
string barTypeWeak = (tickBarClose > tickBarOpen) ? "BULLISH" :
|
|
((tickBarClose < tickBarOpen) ? "BEARISH" : "DOJI");
|
|
Print(barTypeWeak, " [TickBar WEAK VOLUME O:", tickBarOpen, " C:", tickBarClose,
|
|
" H:", tickBarHigh, " L:", tickBarLow, "] Vol: ", currVol,
|
|
" | Avg: ", avgVol, " | Threshold: ", volThreshold,
|
|
" - Counted | Bullish: ", consecutiveBullishBars, "/", BullishBarsForSell,
|
|
" | Bearish: ", consecutiveBearishBars, "/", BearishBarsForBuy,
|
|
" | Size: ", TickBarSize, " ticks");
|
|
|
|
tickBarIndex++;
|
|
tickBarCount = 0;
|
|
|
|
double nextOpenWeak = (tickBarClose > 0) ? tickBarClose : tickBarOpen;
|
|
tickBarOpen = nextOpenWeak;
|
|
tickBarHigh = nextOpenWeak;
|
|
tickBarLow = nextOpenWeak;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(tickBarClose > tickBarOpen)
|
|
{
|
|
consecutiveBullishBars++;
|
|
consecutiveBearishBars = 0;
|
|
}
|
|
else
|
|
if(tickBarClose < tickBarOpen)
|
|
{
|
|
consecutiveBearishBars++;
|
|
consecutiveBullishBars = 0;
|
|
}
|
|
else // DOJI - neutral: keep current streaks unchanged
|
|
{
|
|
}
|
|
|
|
string barType = (tickBarClose > tickBarOpen) ? "BULLISH" : ((tickBarClose < tickBarOpen) ? "BEARISH" : "DOJI");
|
|
|
|
if(UseVolumeInSignals)
|
|
{
|
|
long currVol = iTickVolume(Symbol(), Period(), 1);
|
|
double avgVol = GetAverageTickVolume(Period(), VolumeSignalLookback);
|
|
double volThreshold = VolumeSignalFactor * avgVol;
|
|
Print(barType, " [TickBar O:", tickBarOpen, " C:", tickBarClose,
|
|
" H:", tickBarHigh, " L:", tickBarLow, "] Vol: ", currVol,
|
|
" | Avg: ", avgVol, " | Threshold: ", volThreshold,
|
|
" - Bullish: ", consecutiveBullishBars, "/", BullishBarsForSell,
|
|
" | Bearish: ", consecutiveBearishBars, "/", BearishBarsForBuy,
|
|
" | Size: ", TickBarSize, " ticks");
|
|
}
|
|
else
|
|
{
|
|
Print(barType, " [TickBar O:", tickBarOpen, " C:", tickBarClose,
|
|
" H:", tickBarHigh, " L:", tickBarLow,
|
|
"] - Bullish: ", consecutiveBullishBars, "/", BullishBarsForSell,
|
|
" | Bearish: ", consecutiveBearishBars, "/", BearishBarsForBuy,
|
|
" | Size: ", TickBarSize, " ticks");
|
|
}
|
|
|
|
tickBarIndex++;
|
|
tickBarCount = 0;
|
|
double nextOpen = (tickBarClose > 0) ? tickBarClose : tickBarOpen; // avoid seeding with invalid/old close
|
|
tickBarOpen = nextOpen;
|
|
tickBarHigh = nextOpen;
|
|
tickBarLow = nextOpen;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// VOLUME FILTER - Blocks trades if volume is too low
|
|
// Uses tick volume for compatibility with Strategy Tester
|
|
// Returns true if volume is sufficient, false if too low
|
|
//+------------------------------------------------------------------+
|
|
bool CheckVolumeFilter()
|
|
{
|
|
long currentVolume;
|
|
long sumVolume = 0;
|
|
|
|
// Use tick volume (works in tester) or real volume based on setting
|
|
if(UseTickVolume)
|
|
{
|
|
// Get tick volume - works perfectly in Strategy Tester
|
|
currentVolume = iTickVolume(Symbol(), Period(), 0);
|
|
|
|
// Calculate average tick volume for last 200 bars
|
|
for(int i = 1; i <= 200; i++)
|
|
{
|
|
sumVolume += iTickVolume(Symbol(), Period(), i);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Use real volume (if available from broker)
|
|
currentVolume = iVolume(Symbol(), Period(), 0);
|
|
if(currentVolume <= 0)
|
|
Print("Real volume unavailable (0) - broker did not provide volume for current bar.");
|
|
|
|
// Calculate average real volume for last 200 bars
|
|
for(int i = 1; i <= 200; i++)
|
|
{
|
|
sumVolume += iVolume(Symbol(), Period(), i);
|
|
}
|
|
}
|
|
|
|
long avgVolume = sumVolume / 200;
|
|
|
|
// Calculate volume ratio
|
|
double volumeRatio = 0.0;
|
|
if(avgVolume > 0)
|
|
volumeRatio = (double)currentVolume / avgVolume;
|
|
else
|
|
{
|
|
// All sampled bars have zero volume; treat as insufficient data and block trade
|
|
Print(" Volume check: all sampled bars have zero volume - blocking trade");
|
|
return false;
|
|
}
|
|
|
|
string volumeType = (UseTickVolume) ? "Tick" : "Real";
|
|
|
|
double requiredRatio = MinVolumeRatio;
|
|
if(safeModeActive)
|
|
requiredRatio *= SafeVolumeEntryMultiplier;
|
|
|
|
// Check if volume meets minimum requirement
|
|
if(volumeRatio >= requiredRatio)
|
|
{
|
|
Print(" VOLUME FILTER [", volumeType, "] - Current: ", currentVolume, " | Avg: ", avgVolume, " | Ratio: ", DoubleToString(volumeRatio, 2), "x (>= ", requiredRatio, ") - OK");
|
|
return true; // Volume is sufficient
|
|
}
|
|
else
|
|
{
|
|
Print(" VOLUME FILTER [", volumeType, "] - Current: ", currentVolume, " | Avg: ", avgVolume, " | Ratio: ", DoubleToString(volumeRatio, 2), "x (< ", requiredRatio, ") - BLOCKED");
|
|
return false; // Volume too low - block trade
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// VOLUME MOMENTUM RATIO - used for limit order renewal
|
|
// Returns volume ratio (current/avg). Returns -1 if unavailable.
|
|
//+------------------------------------------------------------------+
|
|
double GetVolumeMomentumRatio()
|
|
{
|
|
long currentVolume = 0;
|
|
long sumVolume = 0;
|
|
|
|
if(UseTickVolume)
|
|
{
|
|
currentVolume = iTickVolume(Symbol(), Period(), 0);
|
|
for(int i = 1; i <= 200; i++)
|
|
sumVolume += iTickVolume(Symbol(), Period(), i);
|
|
}
|
|
else
|
|
{
|
|
currentVolume = iVolume(Symbol(), Period(), 0);
|
|
if(currentVolume <= 0)
|
|
Print("Real volume unavailable (0) - broker did not provide volume for current bar.");
|
|
for(int i = 1; i <= 200; i++)
|
|
sumVolume += iVolume(Symbol(), Period(), i);
|
|
}
|
|
|
|
long avgVolume = sumVolume / 200;
|
|
if(avgVolume <= 0)
|
|
return -1.0;
|
|
|
|
return (double)currentVolume / avgVolume;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// LIMIT ORDER RENEWAL - remove old pending limits when momentum weak
|
|
//+------------------------------------------------------------------+
|
|
void RenewLimitOrdersByVolume()
|
|
{
|
|
if(!UseLimitOrderRenewal || !UseLimitOrders)
|
|
return;
|
|
|
|
datetime now = GetCurrentTimeWithOffset();
|
|
if(lastLimitRenewal != 0 && (now - lastLimitRenewal) < LimitOrderRenewalCooldownSeconds)
|
|
return;
|
|
|
|
double volumeRatio = GetVolumeMomentumRatio();
|
|
if(volumeRatio < 0)
|
|
return;
|
|
|
|
// Use same trigger as ANALYSIS 2: weak momentum => renew limits
|
|
if(volumeRatio >= StrongMomentumThreshold)
|
|
{
|
|
lastLimitRenewal = now;
|
|
return;
|
|
}
|
|
|
|
int deleted = 0;
|
|
int totalOrders = OrdersTotal();
|
|
|
|
for(int i = totalOrders - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = OrderGetTicket(i);
|
|
if(ticket == 0)
|
|
continue;
|
|
if(!OrderSelect(ticket))
|
|
continue;
|
|
|
|
string symbol = OrderGetString(ORDER_SYMBOL);
|
|
long magic = OrderGetInteger(ORDER_MAGIC);
|
|
long type = OrderGetInteger(ORDER_TYPE);
|
|
datetime setupTime = (datetime)OrderGetInteger(ORDER_TIME_SETUP);
|
|
|
|
if(symbol != Symbol() || magic != MagicNumber)
|
|
continue;
|
|
|
|
if(type != ORDER_TYPE_BUY_LIMIT && type != ORDER_TYPE_SELL_LIMIT)
|
|
continue;
|
|
|
|
if(setupTime <= 0)
|
|
continue;
|
|
|
|
int ageMinutes = (int)((now - setupTime) / 60);
|
|
if(ageMinutes < LimitOrderMaxAgeMinutes)
|
|
continue;
|
|
|
|
if(trade.OrderDelete(ticket))
|
|
deleted++;
|
|
else
|
|
Print(" LIMIT RENEWAL FAILED - Ticket: ", ticket, " | ", trade.ResultRetcodeDescription(), " (Code: ", trade.ResultRetcode(), ")");
|
|
}
|
|
|
|
if(deleted > 0)
|
|
Print(" LIMIT RENEWAL: Weak volume momentum (", DoubleToString(volumeRatio, 2), "x) - Deleted ", deleted, " old limit orders (>", LimitOrderMaxAgeMinutes, " min)");
|
|
|
|
lastLimitRenewal = now;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// CHECK IF BROKER IS ECN (Supports Market Book / DOM)
|
|
// Returns true if ECN broker detected (IC Markets, Pepperstone, etc.)
|
|
//+------------------------------------------------------------------+
|
|
bool IsECNBroker()
|
|
{
|
|
// Get broker name
|
|
string brokerName = AccountInfoString(ACCOUNT_COMPANY);
|
|
StringToUpper(brokerName); // Convert to uppercase in place
|
|
|
|
// List of known ECN brokers that support Market Book
|
|
string ecnBrokers[] =
|
|
{
|
|
"IC MARKETS", "ICMARKETS", "INTERACTIVE BROKERS", "INTERACTIVEBROKERS", "IB",
|
|
"PEPPERSTONE", "FXTM", "XM", "FXPRO", "FXCM", "OANDA",
|
|
"DUKASCOPY", "SAXOBANK", "SAXO", "LMAX", "CURRENEX",
|
|
"GAIN CAPITAL", "GAINCAPITAL", "FOREX.COM", "FOREXCOM",
|
|
"ADMIRAL MARKETS", "ADMIRALMARKETS", "ADMIRAL", "EXNESS",
|
|
"INSTAFOREX", "INSTA", "ROBOFOREX", "ROBO", "TICKMILL"
|
|
};
|
|
|
|
// Check if broker name contains any ECN broker name
|
|
for(int i = 0; i < ArraySize(ecnBrokers); i++)
|
|
{
|
|
if(StringFind(brokerName, ecnBrokers[i]) >= 0)
|
|
{
|
|
Print(" ECN Broker detected: ", AccountInfoString(ACCOUNT_COMPANY));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Alternative: Try to test if Market Book is available
|
|
// This is a more reliable method - test if we can add Market Book
|
|
if(MarketBookAdd(Symbol()))
|
|
{
|
|
// Successfully added - this is likely an ECN broker
|
|
MarketBookRelease(Symbol()); // Release it immediately
|
|
Print(" Market Book test successful - ECN broker detected: ", AccountInfoString(ACCOUNT_COMPANY));
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// Failed to add - not an ECN broker or Market Book not available
|
|
int error = GetLastError();
|
|
ResetLastError(); // Clear error for next operations
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// ORDER BOOK (DOM) EVENT HANDLER
|
|
// Called when Order Book is updated
|
|
//+------------------------------------------------------------------+
|
|
void OnBookEvent(const string& symbol)
|
|
{
|
|
if(!UseDOMFilter || ForceDisableDOM || !domFilterActive || !domInitialized || symbol != Symbol())
|
|
return;
|
|
|
|
// Update Order Book data
|
|
if(MarketBookGet(Symbol(), bookArray))
|
|
{
|
|
lastBookUpdate = TimeCurrent();
|
|
// Order Book data is now in bookArray - ready for analysis
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// CHECK DOM FILTER - Main function to validate trade using Order Book
|
|
// Returns true if trade should proceed, false if blocked
|
|
// If DOMAsSignal=true, always returns true (doesn't block), but still analyzes DOM
|
|
//+------------------------------------------------------------------+
|
|
bool CheckDOMFilter(int positionType, double bid, double ask)
|
|
{
|
|
if(!UseDOMFilter || ForceDisableDOM || !domFilterActive || !domInitialized)
|
|
return true; // If DOM not enabled, allow trade
|
|
|
|
// Ensure Order Book data is fresh (max age 5 seconds), refresh if stale
|
|
datetime now = TimeCurrent();
|
|
bool bookStale = (lastBookUpdate == 0 || (now - lastBookUpdate) > 5);
|
|
if(bookStale)
|
|
{
|
|
Print("DOM: Order Book data stale (age: ", (lastBookUpdate == 0 ? -1 : (now - lastBookUpdate)), "s) - refreshing");
|
|
if(MarketBookGet(Symbol(), bookArray))
|
|
lastBookUpdate = now;
|
|
else
|
|
{
|
|
Print(" DOM: Failed to refresh stale Order Book data - allowing trade");
|
|
return true; // fail-safe
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Refresh to use latest snapshot even when not stale
|
|
if(MarketBookGet(Symbol(), bookArray))
|
|
lastBookUpdate = now;
|
|
else
|
|
{
|
|
Print(" DOM: Failed to get Order Book data - allowing trade");
|
|
return true; // If can't get data, allow trade (fail-safe)
|
|
}
|
|
}
|
|
|
|
// Check minimum liquidity requirement
|
|
double totalLiquidity = GetTotalLiquidity();
|
|
if(totalLiquidity < MinLiquidity)
|
|
{
|
|
if(DOMAsSignal)
|
|
{
|
|
Print(" DOM: Insufficient liquidity (", totalLiquidity, " < ", MinLiquidity, " lots) - Signal mode: allowing trade but weak signal");
|
|
LogDOMAnalysis(positionType, totalLiquidity, 0.0);
|
|
return true; // In signal mode, don't block
|
|
}
|
|
else
|
|
{
|
|
Print(" DOM: Insufficient liquidity (", totalLiquidity, " < ", MinLiquidity, " lots) - BLOCKING trade");
|
|
return false; // In filter mode, block
|
|
}
|
|
}
|
|
|
|
// Check imbalance for direction confirmation
|
|
double imbalance = CalculateImbalance();
|
|
bool directionConfirmed = false;
|
|
|
|
if(positionType == POSITION_TYPE_BUY)
|
|
{
|
|
// For BUY: We want BID volume > ASK volume (negative imbalance)
|
|
// Imbalance < 0 means more BID volume (buyers), which is good for BUY
|
|
if(imbalance < 0 && MathAbs(imbalance) >= MinImbalanceRatio)
|
|
{
|
|
directionConfirmed = true;
|
|
Print("DOM: BUY confirmed - BID volume dominance (Imbalance: ", MathAbs(imbalance), "x)");
|
|
}
|
|
else
|
|
{
|
|
if(DOMAsSignal)
|
|
Print("DOM: BUY signal weak - ASK volume too high (Imbalance: ", MathAbs(imbalance), "x < ", MinImbalanceRatio, "x) - Signal mode: allowing trade");
|
|
else
|
|
Print("DOM: BUY NOT confirmed - ASK volume too high (Imbalance: ", MathAbs(imbalance), "x < ", MinImbalanceRatio, "x)");
|
|
}
|
|
}
|
|
else
|
|
if(positionType == POSITION_TYPE_SELL)
|
|
{
|
|
// For SELL: We want ASK volume > BID volume (positive imbalance)
|
|
// Imbalance > 0 means more ASK volume (sellers), which is good for SELL
|
|
if(imbalance > 0 && MathAbs(imbalance) >= MinImbalanceRatio)
|
|
{
|
|
directionConfirmed = true;
|
|
Print("DOM: SELL confirmed - ASK volume dominance (Imbalance: ", imbalance, "x)");
|
|
}
|
|
else
|
|
{
|
|
if(DOMAsSignal)
|
|
Print("DOM: SELL signal weak - BID volume too high (Imbalance: ", imbalance, "x < ", MinImbalanceRatio, "x) - Signal mode: allowing trade");
|
|
else
|
|
Print("DOM: SELL NOT confirmed - BID volume too high (Imbalance: ", imbalance, "x < ", MinImbalanceRatio, "x)");
|
|
}
|
|
}
|
|
|
|
// Log DOM analysis details
|
|
LogDOMAnalysis(positionType, totalLiquidity, imbalance);
|
|
|
|
// In signal mode, always allow trade (DOM provides signal, doesn't block)
|
|
if(DOMAsSignal)
|
|
return true;
|
|
|
|
// In filter mode, block if direction not confirmed
|
|
return directionConfirmed;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// GET DOM SIGNAL STRENGTH - Returns signal strength (0.0 to 1.0) for position type
|
|
// 1.0 = strong confirmation, 0.0 = no confirmation or weak signal
|
|
//+------------------------------------------------------------------+
|
|
double GetDOMSignalStrength(int positionType)
|
|
{
|
|
if(!UseDOMFilter || ForceDisableDOM || !domFilterActive || !domInitialized)
|
|
return 0.0; // No signal if DOM not available
|
|
|
|
// Ensure Order Book data is fresh
|
|
datetime now = TimeCurrent();
|
|
bool bookStale = (lastBookUpdate == 0 || (now - lastBookUpdate) > 5);
|
|
if(bookStale)
|
|
{
|
|
if(MarketBookGet(Symbol(), bookArray))
|
|
lastBookUpdate = now;
|
|
else
|
|
return 0.0; // Can't get data
|
|
}
|
|
else
|
|
{
|
|
if(!MarketBookGet(Symbol(), bookArray))
|
|
return 0.0; // Can't get data
|
|
}
|
|
|
|
// Check liquidity
|
|
double totalLiquidity = GetTotalLiquidity();
|
|
if(totalLiquidity < MinLiquidity)
|
|
return 0.0; // Weak signal if liquidity too low
|
|
|
|
// Calculate imbalance
|
|
double imbalance = CalculateImbalance();
|
|
double signalStrength = 0.0;
|
|
|
|
if(positionType == POSITION_TYPE_BUY)
|
|
{
|
|
// For BUY: negative imbalance (BID > ASK) is good
|
|
if(imbalance < 0)
|
|
{
|
|
double absImbalance = MathAbs(imbalance);
|
|
if(absImbalance >= MinImbalanceRatio)
|
|
{
|
|
// Strong signal: normalize to 0.0-1.0 range
|
|
// If imbalance is exactly MinImbalanceRatio, signal = 0.5
|
|
// If imbalance is 2x MinImbalanceRatio or more, signal = 1.0
|
|
signalStrength = MathMin(1.0, 0.5 + (absImbalance - MinImbalanceRatio) / (MinImbalanceRatio * 2.0));
|
|
}
|
|
else
|
|
{
|
|
// Weak signal: partial confirmation
|
|
signalStrength = absImbalance / MinImbalanceRatio * 0.5;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
if(positionType == POSITION_TYPE_SELL)
|
|
{
|
|
// For SELL: positive imbalance (ASK > BID) is good
|
|
if(imbalance > 0)
|
|
{
|
|
if(imbalance >= MinImbalanceRatio)
|
|
{
|
|
// Strong signal: normalize to 0.0-1.0 range
|
|
signalStrength = MathMin(1.0, 0.5 + (imbalance - MinImbalanceRatio) / (MinImbalanceRatio * 2.0));
|
|
}
|
|
else
|
|
{
|
|
// Weak signal: partial confirmation
|
|
signalStrength = imbalance / MinImbalanceRatio * 0.5;
|
|
}
|
|
}
|
|
}
|
|
|
|
return signalStrength;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// CALCULATE IMBALANCE - Ratio of BID/ASK volumes
|
|
// Returns positive value if ASK > BID, negative if BID > ASK
|
|
//+------------------------------------------------------------------+
|
|
double CalculateImbalance()
|
|
{
|
|
if(ArraySize(bookArray) == 0)
|
|
return 0.0;
|
|
|
|
double totalBidVolume = 0.0;
|
|
double totalAskVolume = 0.0;
|
|
int bidLevels = 0;
|
|
int askLevels = 0;
|
|
|
|
// Analyze top DOMLevels on each side
|
|
for(int i = 0; i < ArraySize(bookArray) && (bidLevels < DOMLevels || askLevels < DOMLevels); i++)
|
|
{
|
|
if(bookArray[i].type == BOOK_TYPE_BUY && bidLevels < DOMLevels)
|
|
{
|
|
totalBidVolume += (double)bookArray[i].volume;
|
|
bidLevels++;
|
|
}
|
|
else
|
|
if(bookArray[i].type == BOOK_TYPE_SELL && askLevels < DOMLevels)
|
|
{
|
|
totalAskVolume += (double)bookArray[i].volume;
|
|
askLevels++;
|
|
}
|
|
}
|
|
|
|
if(totalBidVolume == 0 && totalAskVolume == 0)
|
|
return 0.0;
|
|
|
|
if(totalBidVolume == 0)
|
|
return 999.0; // Only ASK volume (extreme imbalance)
|
|
|
|
if(totalAskVolume == 0)
|
|
return -999.0; // Only BID volume (extreme imbalance)
|
|
|
|
// Return ratio: positive = ASK dominance, negative = BID dominance
|
|
// Formula: (ASK - BID) / BID, or ASK/BID ratio
|
|
double ratio = totalAskVolume / totalBidVolume;
|
|
|
|
// Convert to signed imbalance: if ratio > 1, ASK > BID (positive), else negative
|
|
if(ratio >= 1.0)
|
|
return ratio; // ASK dominance
|
|
else
|
|
if(ratio > 0.001) // Safety check: avoid division by near-zero
|
|
return -1.0 / ratio; // BID dominance (negative)
|
|
else
|
|
return -999.0; // Extreme imbalance protection
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// GET TOTAL LIQUIDITY - Sum of all volumes in Order Book
|
|
//+------------------------------------------------------------------+
|
|
double GetTotalLiquidity()
|
|
{
|
|
if(ArraySize(bookArray) == 0)
|
|
return 0.0;
|
|
|
|
double totalLiquidity = 0.0;
|
|
int bidLevels = 0;
|
|
int askLevels = 0;
|
|
|
|
// Sum volumes from top DOMLevels on each side
|
|
for(int i = 0; i < ArraySize(bookArray) && (bidLevels < DOMLevels || askLevels < DOMLevels); i++)
|
|
{
|
|
if(bookArray[i].type == BOOK_TYPE_BUY && bidLevels < DOMLevels)
|
|
{
|
|
totalLiquidity += (double)bookArray[i].volume;
|
|
bidLevels++;
|
|
}
|
|
else
|
|
if(bookArray[i].type == BOOK_TYPE_SELL && askLevels < DOMLevels)
|
|
{
|
|
totalLiquidity += (double)bookArray[i].volume;
|
|
askLevels++;
|
|
}
|
|
}
|
|
|
|
return totalLiquidity;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// FIND SUPPORT/RESISTANCE LEVELS - Identify zones with large volume accumulation
|
|
// Returns array of price levels with significant volume
|
|
//+------------------------------------------------------------------+
|
|
void FindSupportResistance(double &supportLevels[], double &resistanceLevels[], double currentPrice)
|
|
{
|
|
ArrayResize(supportLevels, 0);
|
|
ArrayResize(resistanceLevels, 0);
|
|
|
|
if(ArraySize(bookArray) == 0)
|
|
return;
|
|
|
|
// Calculate average volume to identify significant levels
|
|
double avgVolume = 0.0;
|
|
int count = 0;
|
|
for(int i = 0; i < ArraySize(bookArray); i++)
|
|
{
|
|
avgVolume += (double)bookArray[i].volume;
|
|
count++;
|
|
}
|
|
if(count > 0)
|
|
avgVolume /= count;
|
|
|
|
double thresholdVolume = avgVolume * 1.5; // Levels with 1.5x average volume
|
|
|
|
// Find support levels (BID side, below current price)
|
|
for(int i = 0; i < ArraySize(bookArray); i++)
|
|
{
|
|
if(bookArray[i].type == BOOK_TYPE_BUY &&
|
|
bookArray[i].price < currentPrice &&
|
|
(double)bookArray[i].volume >= thresholdVolume)
|
|
{
|
|
int size = ArraySize(supportLevels);
|
|
ArrayResize(supportLevels, size + 1);
|
|
supportLevels[size] = bookArray[i].price;
|
|
}
|
|
}
|
|
|
|
// Find resistance levels (ASK side, above current price)
|
|
for(int i = 0; i < ArraySize(bookArray); i++)
|
|
{
|
|
if(bookArray[i].type == BOOK_TYPE_SELL &&
|
|
bookArray[i].price > currentPrice &&
|
|
(double)bookArray[i].volume >= thresholdVolume)
|
|
{
|
|
int size = ArraySize(resistanceLevels);
|
|
ArrayResize(resistanceLevels, size + 1);
|
|
resistanceLevels[size] = bookArray[i].price;
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// VOLUME PROFILE - Identify price zones with high volume from historical bars
|
|
// Returns array of price levels with significant volume accumulation
|
|
//+------------------------------------------------------------------+
|
|
void CalculateVolumeProfile(double &liquidityZones[], int positionType, double currentPrice)
|
|
{
|
|
ArrayResize(liquidityZones, 0);
|
|
|
|
if(!UseVolumeProfile)
|
|
return;
|
|
|
|
// Create price buckets (zones) and accumulate volume
|
|
int numBars = MathMin(VolumeProfileBars, Bars(Symbol(), Period()));
|
|
if(numBars < 10)
|
|
return; // Need at least 10 bars for meaningful analysis
|
|
|
|
double priceRange = 0;
|
|
double minPrice = DBL_MAX;
|
|
double maxPrice = 0;
|
|
|
|
// Find price range from historical bars
|
|
for(int i = 1; i <= numBars; i++)
|
|
{
|
|
double high = iHigh(Symbol(), Period(), i);
|
|
double low = iLow(Symbol(), Period(), i);
|
|
if(high > maxPrice)
|
|
maxPrice = high;
|
|
if(low < minPrice)
|
|
minPrice = low;
|
|
}
|
|
|
|
priceRange = maxPrice - minPrice;
|
|
if(priceRange <= 0)
|
|
return;
|
|
|
|
// Create price buckets (50 zones)
|
|
int numBuckets = 50;
|
|
double bucketSize = priceRange / numBuckets;
|
|
double volumeBuckets[];
|
|
double priceBuckets[];
|
|
ArrayResize(volumeBuckets, numBuckets);
|
|
ArrayResize(priceBuckets, numBuckets);
|
|
ArrayInitialize(volumeBuckets, 0.0);
|
|
|
|
// Initialize price buckets
|
|
for(int i = 0; i < numBuckets; i++)
|
|
{
|
|
priceBuckets[i] = minPrice + (i * bucketSize) + (bucketSize / 2.0);
|
|
}
|
|
|
|
// Accumulate volume in each price bucket
|
|
for(int i = 1; i <= numBars; i++)
|
|
{
|
|
double high = iHigh(Symbol(), Period(), i);
|
|
double low = iLow(Symbol(), Period(), i);
|
|
long volume = UseTickVolume ? iTickVolume(Symbol(), Period(), i) : iVolume(Symbol(), Period(), i);
|
|
|
|
if(volume <= 0)
|
|
continue;
|
|
|
|
// Distribute volume across buckets that this bar covers
|
|
for(int j = 0; j < numBuckets; j++)
|
|
{
|
|
double bucketPrice = priceBuckets[j];
|
|
if(bucketPrice >= low && bucketPrice <= high)
|
|
{
|
|
volumeBuckets[j] += (double)volume; // Explicit conversion from long to double
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate average volume
|
|
double totalVolume = 0;
|
|
for(int i = 0; i < numBuckets; i++)
|
|
{
|
|
totalVolume += volumeBuckets[i];
|
|
}
|
|
double avgVolume = (numBuckets > 0) ? (totalVolume / numBuckets) : 0;
|
|
|
|
if(avgVolume <= 0)
|
|
return;
|
|
|
|
// Find zones with volume >= 1.5x average (significant liquidity zones)
|
|
double thresholdVolume = avgVolume * 1.5;
|
|
|
|
for(int i = 0; i < numBuckets; i++)
|
|
{
|
|
if(volumeBuckets[i] >= thresholdVolume)
|
|
{
|
|
int size = ArraySize(liquidityZones);
|
|
ArrayResize(liquidityZones, size + 1);
|
|
liquidityZones[size] = priceBuckets[i];
|
|
}
|
|
}
|
|
|
|
// Sort zones by distance from current price
|
|
for(int i = 0; i < ArraySize(liquidityZones) - 1; i++)
|
|
{
|
|
for(int j = i + 1; j < ArraySize(liquidityZones); j++)
|
|
{
|
|
double distI = MathAbs(liquidityZones[i] - currentPrice);
|
|
double distJ = MathAbs(liquidityZones[j] - currentPrice);
|
|
if(distJ < distI)
|
|
{
|
|
double temp = liquidityZones[i];
|
|
liquidityZones[i] = liquidityZones[j];
|
|
liquidityZones[j] = temp;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// FIND NEAREST LIQUIDITY ZONE - Get best entry price based on liquidity
|
|
// Returns adjusted entry price, or original price if no zone found
|
|
//+------------------------------------------------------------------+
|
|
double FindNearestLiquidityZone(double originalPrice, int positionType, double &distancePoints)
|
|
{
|
|
if(!UseSmartEntry)
|
|
return originalPrice;
|
|
|
|
if(point_value <= 0.0)
|
|
return originalPrice;
|
|
|
|
double maxDistancePoints = GetLiquidityZoneDistancePointsForSymbol(Symbol());
|
|
if(maxDistancePoints <= 0.0)
|
|
return originalPrice;
|
|
|
|
double adjustedPrice = originalPrice;
|
|
double bestZone = 0;
|
|
double minDistance = DBL_MAX;
|
|
bool zoneFound = false;
|
|
distancePoints = 0.0;
|
|
|
|
// Method 1: Use DOM (Order Book) if available (NOT available in Strategy Tester)
|
|
bool isTester = MQLInfoInteger(MQL_TESTER);
|
|
if(UseDOMFilter && !isTester && domFilterActive && domInitialized)
|
|
{
|
|
double supportLevels[];
|
|
double resistanceLevels[];
|
|
double currentPrice = (positionType == POSITION_TYPE_BUY) ?
|
|
SymbolInfoDouble(Symbol(), SYMBOL_ASK) :
|
|
SymbolInfoDouble(Symbol(), SYMBOL_BID);
|
|
|
|
FindSupportResistance(supportLevels, resistanceLevels, currentPrice);
|
|
|
|
if(positionType == POSITION_TYPE_BUY && ArraySize(supportLevels) > 0)
|
|
{
|
|
// For BUY: find highest support level below current price
|
|
for(int i = 0; i < ArraySize(supportLevels); i++)
|
|
{
|
|
if(supportLevels[i] < originalPrice)
|
|
{
|
|
double distance = originalPrice - supportLevels[i];
|
|
double distancePts = distance / point_value;
|
|
|
|
if(distancePts <= maxDistancePoints && distance < minDistance)
|
|
{
|
|
minDistance = distance;
|
|
bestZone = supportLevels[i];
|
|
zoneFound = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
if(positionType == POSITION_TYPE_SELL && ArraySize(resistanceLevels) > 0)
|
|
{
|
|
// For SELL: find lowest resistance level above current price
|
|
for(int i = 0; i < ArraySize(resistanceLevels); i++)
|
|
{
|
|
if(resistanceLevels[i] > originalPrice)
|
|
{
|
|
double distance = resistanceLevels[i] - originalPrice;
|
|
double distPts = distance / point_value;
|
|
|
|
if(distPts <= maxDistancePoints && distance < minDistance)
|
|
{
|
|
minDistance = distance;
|
|
bestZone = resistanceLevels[i];
|
|
zoneFound = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 2: Use Volume Profile if DOM not available or no zone found
|
|
if(!zoneFound && UseVolumeProfile)
|
|
{
|
|
double liquidityZones[];
|
|
double currentPrice = (positionType == POSITION_TYPE_BUY) ?
|
|
SymbolInfoDouble(Symbol(), SYMBOL_ASK) :
|
|
SymbolInfoDouble(Symbol(), SYMBOL_BID);
|
|
|
|
CalculateVolumeProfile(liquidityZones, positionType, currentPrice);
|
|
|
|
if(ArraySize(liquidityZones) > 0)
|
|
{
|
|
for(int i = 0; i < ArraySize(liquidityZones); i++)
|
|
{
|
|
double distance = MathAbs(liquidityZones[i] - originalPrice);
|
|
double distancePts = distance / point_value;
|
|
|
|
if(distancePts <= maxDistancePoints)
|
|
{
|
|
if(positionType == POSITION_TYPE_BUY && liquidityZones[i] < originalPrice)
|
|
{
|
|
// For BUY: prefer zones below current price
|
|
if(distance < minDistance)
|
|
{
|
|
minDistance = distance;
|
|
bestZone = liquidityZones[i];
|
|
zoneFound = true;
|
|
}
|
|
}
|
|
else
|
|
if(positionType == POSITION_TYPE_SELL && liquidityZones[i] > originalPrice)
|
|
{
|
|
// For SELL: prefer zones above current price
|
|
if(distance < minDistance)
|
|
{
|
|
minDistance = distance;
|
|
bestZone = liquidityZones[i];
|
|
zoneFound = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(zoneFound)
|
|
{
|
|
adjustedPrice = bestZone;
|
|
string direction = (positionType == POSITION_TYPE_BUY) ? "BUY" : "SELL";
|
|
distancePoints = MathAbs(adjustedPrice - originalPrice) / point_value;
|
|
Print(" SMART ENTRY [", direction, "] - Original: ", originalPrice, " Adjusted: ", adjustedPrice, " (", DoubleToString(distancePoints, 1), " points)");
|
|
}
|
|
|
|
return adjustedPrice;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// LOG DOM ANALYSIS - Print detailed Order Book information
|
|
//+------------------------------------------------------------------+
|
|
void LogDOMAnalysis(int positionType, double totalLiquidity, double imbalance)
|
|
{
|
|
if(ArraySize(bookArray) == 0)
|
|
return;
|
|
|
|
double totalBidVolume = 0.0;
|
|
double totalAskVolume = 0.0;
|
|
int bidLevels = 0;
|
|
int askLevels = 0;
|
|
|
|
// Calculate volumes
|
|
for(int i = 0; i < ArraySize(bookArray) && (bidLevels < DOMLevels || askLevels < DOMLevels); i++)
|
|
{
|
|
if(bookArray[i].type == BOOK_TYPE_BUY && bidLevels < DOMLevels)
|
|
{
|
|
totalBidVolume += (double)bookArray[i].volume;
|
|
bidLevels++;
|
|
}
|
|
else
|
|
if(bookArray[i].type == BOOK_TYPE_SELL && askLevels < DOMLevels)
|
|
{
|
|
totalAskVolume += (double)bookArray[i].volume;
|
|
askLevels++;
|
|
}
|
|
}
|
|
|
|
string direction = (positionType == POSITION_TYPE_BUY) ? "BUY" : "SELL";
|
|
string imbalanceStatus = (imbalance > 0) ? "ASK dominance" : "BID dominance";
|
|
|
|
Print(" DOM ANALYSIS [", direction, "] - BID: ", totalBidVolume, " lots (", bidLevels, " levels) | ASK: ", totalAskVolume, " lots (", askLevels, " levels) | Total: ", totalLiquidity, " lots | Imbalance: ", MathAbs(imbalance), "x (", imbalanceStatus, ")");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// CHECK ALL FILTERS FOR TRADE - Combined real-time filter analysis
|
|
// Combines consecutive bars signal with all active filters
|
|
// Returns true if ALL filters pass, false if any filter blocks the trade
|
|
// This function is called on EVERY tick for real-time decision making
|
|
//+------------------------------------------------------------------+
|
|
bool CheckAllFiltersForTrade(int positionType, double bid, double ask, int consecutiveBars)
|
|
{
|
|
string direction = (positionType == POSITION_TYPE_BUY) ? "BUY" : "SELL";
|
|
string filterStatus = "";
|
|
bool allFiltersPassed = true;
|
|
|
|
// ========== FILTER 1: SPREAD CHECK ==========
|
|
long spreadPoints;
|
|
SymbolInfoInteger(_Symbol, SYMBOL_SPREAD, spreadPoints);
|
|
long maxSpreadInPoints = (long)(MaxSpread * pip_value); // Convert points to broker points
|
|
if(spreadPoints > maxSpreadInPoints)
|
|
{
|
|
double spreadInPoints = (double)spreadPoints / pip_value;
|
|
filterStatus += " Spread (" + DoubleToString(spreadInPoints, 1) + " points > " + IntegerToString(MaxSpread) + " points) | ";
|
|
allFiltersPassed = false;
|
|
}
|
|
else
|
|
{
|
|
double spreadInPoints = (double)spreadPoints / pip_value;
|
|
filterStatus += " Spread (" + DoubleToString(spreadInPoints, 1) + " points <= " + IntegerToString(MaxSpread) + " points) | ";
|
|
}
|
|
|
|
// ========== FILTER 2: VOLUME CHECK ==========
|
|
if(UseVolumeFilter)
|
|
{
|
|
// Get current volume for real-time check
|
|
long currentVolume = UseTickVolume ? iTickVolume(Symbol(), Period(), 0) : iVolume(Symbol(), Period(), 0);
|
|
if(!UseTickVolume && currentVolume <= 0)
|
|
Print("Real volume unavailable (0) - broker did not provide volume for current bar.");
|
|
long sumVolume = 0;
|
|
|
|
// Calculate average volume for last 200 bars
|
|
for(int i = 1; i <= 200; i++)
|
|
{
|
|
sumVolume += UseTickVolume ? iTickVolume(Symbol(), Period(), i) : iVolume(Symbol(), Period(), i);
|
|
}
|
|
long avgVolume = sumVolume / 200;
|
|
|
|
double volumeRatio = (avgVolume > 0) ? ((double)currentVolume / avgVolume) : 1.3;
|
|
double requiredVolumeRatio = MinVolumeRatio;
|
|
if(safeModeActive)
|
|
requiredVolumeRatio *= SafeVolumeEntryMultiplier;
|
|
|
|
if(volumeRatio < requiredVolumeRatio)
|
|
{
|
|
filterStatus += " Volume (" + DoubleToString(volumeRatio, 2) + "x < " + DoubleToString(requiredVolumeRatio, 2) + "x) | ";
|
|
allFiltersPassed = false;
|
|
}
|
|
else
|
|
{
|
|
filterStatus += " Volume (" + DoubleToString(volumeRatio, 2) + "x >= " + DoubleToString(requiredVolumeRatio, 2) + "x) | ";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
filterStatus += " Volume (disabled) | ";
|
|
}
|
|
|
|
// ========== FILTER 3: DOM (ORDER BOOK) CHECK ==========
|
|
if(UseDOMFilter && !ForceDisableDOM && domFilterActive && domInitialized)
|
|
{
|
|
if(DOMAsSignal)
|
|
{
|
|
// DOM as signal mode: doesn't block, but provides signal strength
|
|
double domSignalStrength = GetDOMSignalStrength(positionType);
|
|
if(domSignalStrength >= 0.7)
|
|
{
|
|
filterStatus += " DOM (STRONG signal: " + DoubleToString(domSignalStrength * 100, 0) + "%) | ";
|
|
}
|
|
else
|
|
if(domSignalStrength >= 0.3)
|
|
{
|
|
filterStatus += " DOM (MODERATE signal: " + DoubleToString(domSignalStrength * 100, 0) + "%) | ";
|
|
}
|
|
else
|
|
{
|
|
filterStatus += " DOM (WEAK signal: " + DoubleToString(domSignalStrength * 100, 0) + "%) | ";
|
|
}
|
|
// In signal mode, DOM doesn't block trades
|
|
CheckDOMFilter(positionType, bid, ask); // Still analyze DOM for logging
|
|
}
|
|
else
|
|
{
|
|
// DOM as filter mode: blocks trades that don't meet criteria
|
|
if(!CheckDOMFilter(positionType, bid, ask))
|
|
{
|
|
filterStatus += " DOM (imbalance/liquidity) | ";
|
|
allFiltersPassed = false;
|
|
}
|
|
else
|
|
{
|
|
filterStatus += " DOM (confirmed) | ";
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(UseDOMFilter && ForceDisableDOM)
|
|
filterStatus += " DOM (force bypass) | ";
|
|
else
|
|
if(UseDOMFilter)
|
|
filterStatus += " DOM (not available) | ";
|
|
else
|
|
filterStatus += " DOM (disabled) | ";
|
|
}
|
|
|
|
// ========== FILTER 4: CONSECUTIVE BARS SIGNAL ==========
|
|
if(UseAntiExtremeFilter)
|
|
{
|
|
int lookbackBars = MathMax(10, AntiExtremeLookbackBars);
|
|
double minDistancePrice = ConvertInputDistanceToPriceForSymbol(Symbol(), AntiExtremeMinDistance);
|
|
if(minDistancePrice < 0.0)
|
|
minDistancePrice = 0.0;
|
|
double symbolPoint = SymbolInfoDouble(Symbol(), SYMBOL_POINT);
|
|
if(symbolPoint <= 0.0)
|
|
symbolPoint = Point();
|
|
|
|
int highShift = iHighest(Symbol(), (ENUM_TIMEFRAMES)Period(), MODE_HIGH, lookbackBars, 1);
|
|
int lowShift = iLowest(Symbol(), (ENUM_TIMEFRAMES)Period(), MODE_LOW, lookbackBars, 1);
|
|
double recentHigh = (highShift >= 0) ? iHigh(Symbol(), (ENUM_TIMEFRAMES)Period(), highShift) : 0.0;
|
|
double recentLow = (lowShift >= 0) ? iLow(Symbol(), (ENUM_TIMEFRAMES)Period(), lowShift) : 0.0;
|
|
|
|
if(positionType == POSITION_TYPE_BUY && recentHigh > 0.0)
|
|
{
|
|
double distanceToHigh = recentHigh - ask;
|
|
if(ask >= (recentHigh - minDistancePrice))
|
|
{
|
|
filterStatus += " AntiExtreme(BUY near HIGH: " + DoubleToString(distanceToHigh / symbolPoint, 1) + " pts) | ";
|
|
allFiltersPassed = false;
|
|
}
|
|
else
|
|
{
|
|
filterStatus += " AntiExtreme(BUY ok: " + DoubleToString(distanceToHigh / symbolPoint, 1) + " pts) | ";
|
|
}
|
|
}
|
|
else
|
|
if(positionType == POSITION_TYPE_SELL && recentLow > 0.0)
|
|
{
|
|
double distanceToLow = bid - recentLow;
|
|
if(bid <= (recentLow + minDistancePrice))
|
|
{
|
|
filterStatus += " AntiExtreme(SELL near LOW: " + DoubleToString(distanceToLow / symbolPoint, 1) + " pts) | ";
|
|
allFiltersPassed = false;
|
|
}
|
|
else
|
|
{
|
|
filterStatus += " AntiExtreme(SELL ok: " + DoubleToString(distanceToLow / symbolPoint, 1) + " pts) | ";
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
filterStatus += " AntiExtreme(disabled) | ";
|
|
}
|
|
|
|
filterStatus += " Signal (" + IntegerToString(consecutiveBars) + " consecutive bars)";
|
|
|
|
// Log combined filter status
|
|
if(ShouldEmitFilterStatusLog(Symbol(), direction, allFiltersPassed, "chart_filters"))
|
|
{
|
|
if(allFiltersPassed)
|
|
{
|
|
Print(" ALL FILTERS PASSED [", direction, "] - ", filterStatus);
|
|
}
|
|
else
|
|
{
|
|
Print(" FILTERS BLOCKED [", direction, "] - ", filterStatus);
|
|
}
|
|
}
|
|
|
|
return allFiltersPassed;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// ADJUST SL/TP BASED ON MARKET ANALYSIS
|
|
// Analyzes DOM, volume, momentum and adjusts SL/TP to maximize profit
|
|
// Only adjusts when market conditions are favorable
|
|
//+------------------------------------------------------------------+
|
|
void AdjustSLTPBasedOnAnalysis(ulong ticket, int positionType, double entryPrice, double currentSL, double currentTP, double bid, double ask, bool allowSLTightening)
|
|
{
|
|
// Calculate current profit
|
|
double currentProfit = (positionType == POSITION_TYPE_BUY) ? (ask - entryPrice) : (entryPrice - bid);
|
|
double profitInPoints = currentProfit / point_value;
|
|
|
|
// Only adjust if we have some profit (at least 10 points)
|
|
if(profitInPoints < 20)
|
|
return;
|
|
|
|
// Throttle dynamic adjustments to avoid excessive order modifications
|
|
datetime now = TimeCurrent();
|
|
if(lastDynamicAdjust != 0 && (now - lastDynamicAdjust) < dynamicSLTPThrottleSeconds)
|
|
return;
|
|
|
|
// Calculate original TP distance - use initial TP from settings if currentTP is invalid
|
|
double originalTPDistance = 0;
|
|
if(currentTP > 0 && currentTP != 9999526456.42 && currentTP < entryPrice * 20) // Sanity check for absurd values
|
|
{
|
|
originalTPDistance = (positionType == POSITION_TYPE_BUY) ? (currentTP - entryPrice) : (entryPrice - currentTP);
|
|
}
|
|
|
|
// If currentTP is invalid, use initial TP from settings
|
|
if(originalTPDistance <= 0 || originalTPDistance > (TakeProfit * point_value * 20))
|
|
{
|
|
// Use initial TP from settings
|
|
originalTPDistance = TakeProfit * point_value;
|
|
}
|
|
|
|
// Final validation - ensure TP distance is reasonable (max 2x initial TP)
|
|
double maxTPDistance = TakeProfit * point_value * 2;
|
|
if(originalTPDistance > maxTPDistance)
|
|
originalTPDistance = TakeProfit * point_value;
|
|
|
|
double tpMultiplier = 1.0; // Default: no change
|
|
bool veryFavorableConditions = false; // For aggressive SL movement
|
|
bool favorableConditions = false;
|
|
string analysisReason = "";
|
|
double strengthScore = 0.0; // Overall strength score (0-1)
|
|
|
|
// ========== ANALYSIS 1: DOM (Order Book) Analysis ==========
|
|
if(UseDOMFilter && domFilterActive && domInitialized)
|
|
{
|
|
if(MarketBookGet(Symbol(), bookArray))
|
|
{
|
|
double imbalance = CalculateImbalance();
|
|
double totalLiquidity = GetTotalLiquidity();
|
|
|
|
if(positionType == POSITION_TYPE_BUY)
|
|
{
|
|
// For BUY: Strong BID dominance (negative imbalance) is favorable
|
|
if(imbalance < 0 && MathAbs(imbalance) >= StrongImbalanceThreshold && totalLiquidity >= MinLiquidity)
|
|
{
|
|
double imbalanceStrength = MathMin(MathAbs(imbalance) / StrongImbalanceThreshold, 3.0) / 3.0; // 0-1
|
|
strengthScore += imbalanceStrength * 0.4; // 40% weight
|
|
tpMultiplier = MathMin(1.0 + imbalanceStrength * 0.5, MaxTPMultiplier);
|
|
favorableConditions = true;
|
|
if(MathAbs(imbalance) >= StrongImbalanceThreshold * 1.3)
|
|
veryFavorableConditions = true;
|
|
analysisReason += "DOM: Strong BID (" + DoubleToString(MathAbs(imbalance), 2) + "x) | ";
|
|
}
|
|
}
|
|
else
|
|
if(positionType == POSITION_TYPE_SELL)
|
|
{
|
|
// For SELL: Strong ASK dominance (positive imbalance) is favorable
|
|
if(imbalance > 0 && MathAbs(imbalance) >= StrongImbalanceThreshold && totalLiquidity >= MinLiquidity)
|
|
{
|
|
double imbalanceStrength = MathMin(MathAbs(imbalance) / StrongImbalanceThreshold, 3.0) / 3.0; // 0-1
|
|
strengthScore += imbalanceStrength * 0.4; // 40% weight
|
|
tpMultiplier = MathMin(1.0 + imbalanceStrength * 0.5, MaxTPMultiplier);
|
|
favorableConditions = true;
|
|
if(MathAbs(imbalance) >= StrongImbalanceThreshold * 1.3)
|
|
veryFavorableConditions = true;
|
|
analysisReason += "DOM: Strong ASK (" + DoubleToString(MathAbs(imbalance), 2) + "x) | ";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========== ANALYSIS 2: Volume Momentum Analysis ==========
|
|
long currentVolume = UseTickVolume ? iTickVolume(Symbol(), Period(), 0) : iVolume(Symbol(), Period(), 0);
|
|
if(!UseTickVolume && currentVolume <= 0)
|
|
Print("Real volume unavailable (0) - broker did not provide volume for current bar.");
|
|
long sumVolume = 0;
|
|
for(int i = 1; i <= 200; i++)
|
|
{
|
|
sumVolume += UseTickVolume ? iTickVolume(Symbol(), Period(), i) : iVolume(Symbol(), Period(), i);
|
|
}
|
|
long avgVolume = sumVolume / 200;
|
|
double volumeRatio = (avgVolume > 0) ? ((double)currentVolume / avgVolume) : 1.3;
|
|
|
|
if(volumeRatio >= StrongMomentumThreshold)
|
|
{
|
|
double volumeStrength = MathMin((volumeRatio - StrongMomentumThreshold) / StrongMomentumThreshold, 2.0) / 2.0; // 0-1
|
|
strengthScore += volumeStrength * 0.3; // 30% weight
|
|
double volumeBoost = volumeStrength * 0.3; // Up to 30% boost
|
|
tpMultiplier = MathMin(tpMultiplier + volumeBoost, MaxTPMultiplier);
|
|
favorableConditions = true;
|
|
if(volumeRatio >= StrongMomentumThreshold * 1.3)
|
|
veryFavorableConditions = true;
|
|
analysisReason += "Volume: Strong (" + DoubleToString(volumeRatio, 2) + "x) | ";
|
|
}
|
|
|
|
// ========== ANALYSIS 3: Price Momentum (Current Profit) ==========
|
|
if(profitInPoints > (TakeProfit * 0.3))
|
|
{
|
|
double profitStrength = MathMin(profitInPoints / TakeProfit, 1.0); // 0-1
|
|
strengthScore += profitStrength * 0.3; // 30% weight
|
|
double profitBoost = profitStrength * 0.2; // Up to 20% boost
|
|
tpMultiplier = MathMin(tpMultiplier + profitBoost, MaxTPMultiplier);
|
|
favorableConditions = true;
|
|
if(profitInPoints > (TakeProfit * 0.6))
|
|
veryFavorableConditions = true;
|
|
analysisReason += "Profit: " + DoubleToString(profitInPoints, 1) + " points | ";
|
|
}
|
|
|
|
// ========== CALCULATE NEW TP ==========
|
|
double newTPDistance = originalTPDistance * tpMultiplier;
|
|
|
|
// Limit TP distance to reasonable values (max 5x initial TP)
|
|
double maxAllowedTPDistance = TakeProfit * point_value * 5;
|
|
if(newTPDistance > maxAllowedTPDistance)
|
|
newTPDistance = maxAllowedTPDistance;
|
|
|
|
double newTP = (positionType == POSITION_TYPE_BUY) ? (entryPrice + newTPDistance) : (entryPrice - newTPDistance);
|
|
|
|
// Final sanity check - ensure TP is reasonable
|
|
if(newTP <= 0 || newTP > entryPrice * 10 || newTP < entryPrice * 0.1)
|
|
{
|
|
Print(" Invalid TP calculated: ", newTP, " - Skipping adjustment");
|
|
return;
|
|
}
|
|
|
|
// ========== CALCULATE NEW SL - MOVE INTO PROFIT ==========
|
|
double newSL = currentSL; // Initialize with current SL
|
|
bool shouldModifySL = false;
|
|
|
|
if(favorableConditions && allowSLTightening)
|
|
{
|
|
// Calculate minimum SL in profit (60 pips above/below entry)
|
|
double minSLInProfit = (positionType == POSITION_TYPE_BUY) ?
|
|
(entryPrice + (MinSLProfitPips * point_value)) :
|
|
(entryPrice - (MinSLProfitPips * point_value));
|
|
|
|
// Current price
|
|
double currentPrice = (positionType == POSITION_TYPE_BUY) ? ask : bid;
|
|
|
|
if(veryFavorableConditions && strengthScore > 0.7)
|
|
{
|
|
// VERY FAVORABLE: Trail SL close to current price (aggressive)
|
|
double trailDistance = SLTrailDistance * point_value;
|
|
newSL = (positionType == POSITION_TYPE_BUY) ?
|
|
(currentPrice - trailDistance) :
|
|
(currentPrice + trailDistance);
|
|
|
|
// Ensure SL is at least in minimum profit
|
|
if(positionType == POSITION_TYPE_BUY)
|
|
{
|
|
if(newSL < minSLInProfit)
|
|
newSL = minSLInProfit;
|
|
// Can "jump over" current SL if very favorable
|
|
if(newSL > currentSL || currentSL < entryPrice)
|
|
shouldModifySL = true;
|
|
}
|
|
else
|
|
{
|
|
if(newSL > minSLInProfit)
|
|
newSL = minSLInProfit;
|
|
// Can "jump over" current SL if very favorable
|
|
if(newSL < currentSL || currentSL > entryPrice)
|
|
shouldModifySL = true;
|
|
}
|
|
|
|
analysisReason += "VERY FAVORABLE: Trailing SL | ";
|
|
}
|
|
else
|
|
if(profitInPoints >= MinSLProfitPips)
|
|
{
|
|
// FAVORABLE: Move SL to minimum profit level or better
|
|
if(positionType == POSITION_TYPE_BUY)
|
|
{
|
|
newSL = MathMax(minSLInProfit, currentSL < entryPrice ? minSLInProfit : MathMax(currentSL, minSLInProfit));
|
|
if(newSL > currentSL || currentSL < entryPrice)
|
|
shouldModifySL = true;
|
|
}
|
|
else
|
|
{
|
|
newSL = MathMin(minSLInProfit, currentSL > entryPrice ? minSLInProfit : MathMin(currentSL, minSLInProfit));
|
|
if(newSL < currentSL || currentSL > entryPrice)
|
|
shouldModifySL = true;
|
|
}
|
|
|
|
analysisReason += "Moving SL to profit | ";
|
|
}
|
|
}
|
|
|
|
// ========== APPLY ADJUSTMENTS ==========
|
|
bool shouldModifyTP = (tpMultiplier > 1.01 && newTP != currentTP);
|
|
|
|
// Final validation before modifying
|
|
if(newSL <= 0 || newTP <= 0)
|
|
{
|
|
Print(" Invalid SL/TP values - SL: ", newSL, " TP: ", newTP, " - Skipping adjustment");
|
|
return;
|
|
}
|
|
|
|
// Ensure SL is reasonable (within 10x entry price range)
|
|
if(shouldModifySL && (newSL > entryPrice * 10 || newSL < entryPrice * 0.1))
|
|
{
|
|
Print(" Invalid SL value: ", newSL, " - Skipping SL adjustment");
|
|
shouldModifySL = false;
|
|
}
|
|
|
|
// Ensure TP is reasonable (within 10x entry price range)
|
|
if(shouldModifyTP && (newTP > entryPrice * 10 || newTP < entryPrice * 0.1))
|
|
{
|
|
Print(" Invalid TP value: ", newTP, " - Skipping TP adjustment");
|
|
shouldModifyTP = false;
|
|
}
|
|
|
|
if(shouldModifySL || shouldModifyTP)
|
|
{
|
|
// Use current SL/TP if we're not modifying them
|
|
if(!shouldModifySL)
|
|
newSL = currentSL;
|
|
if(!shouldModifyTP)
|
|
newTP = currentTP;
|
|
|
|
// Final check before modification
|
|
if(newSL > 0 && newTP > 0 && newSL != newTP)
|
|
{
|
|
lastDynamicAdjust = now;
|
|
ModifyPosition(ticket, newSL, newTP);
|
|
string direction = (positionType == POSITION_TYPE_BUY) ? "BUY" : "SELL";
|
|
double newSLPoints = (positionType == POSITION_TYPE_BUY) ?
|
|
((newSL - entryPrice) / point_value) :
|
|
((entryPrice - newSL) / point_value);
|
|
Print(" DYNAMIC ADJUSTMENT [", direction, "] - TP: ", DoubleToString(tpMultiplier, 2), "x | SL: ", DoubleToString(newSLPoints, 1), " points in profit | Strength: ", DoubleToString(strengthScore, 2), " | ", analysisReason);
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// SENTIMENT TRACKING - Show emotions on wins/losses
|
|
//+------------------------------------------------------------------+
|
|
bool RefreshHistoryIfStale(int maxAgeSeconds)
|
|
{
|
|
datetime now = TimeCurrent();
|
|
if(lastHistoryUpdate != 0 && (now - lastHistoryUpdate) <= maxAgeSeconds)
|
|
return true; // recent enough
|
|
|
|
if(!HistorySelect(0, now))
|
|
{
|
|
Print(" HistorySelect failed - cannot refresh deal history");
|
|
return false;
|
|
}
|
|
|
|
lastHistoryUpdate = now;
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool IsTicketProcessed(ulong ticket)
|
|
{
|
|
for(int i = 0; i < ArraySize(processedTickets); i++)
|
|
{
|
|
if(processedTickets[i] == ticket)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void AddProcessedTicket(ulong ticket)
|
|
{
|
|
int size = ArraySize(processedTickets);
|
|
ArrayResize(processedTickets, size + 1);
|
|
processedTickets[size] = ticket;
|
|
|
|
int cap = 2000;
|
|
int total = ArraySize(processedTickets);
|
|
if(total > cap)
|
|
{
|
|
for(int i = 1; i < total; i++)
|
|
processedTickets[i - 1] = processedTickets[i];
|
|
ArrayResize(processedTickets, cap);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool GetPositionEntryFromHistory(ulong positionId, string symbol, long magic, datetime &tIn, double &pIn, int &dir)
|
|
{
|
|
if(!HistorySelect(0, TimeCurrent()))
|
|
return false;
|
|
|
|
int total = HistoryDealsTotal();
|
|
datetime earliestTime = 0;
|
|
bool found = false;
|
|
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
ulong tk = HistoryDealGetTicket(i);
|
|
if(tk == 0 || !HistoryDealSelect(tk))
|
|
continue;
|
|
|
|
if((ulong)HistoryDealGetInteger(tk, DEAL_POSITION_ID) != positionId)
|
|
continue;
|
|
if(HistoryDealGetString(tk, DEAL_SYMBOL) != symbol)
|
|
continue;
|
|
if(HistoryDealGetInteger(tk, DEAL_MAGIC) != magic)
|
|
continue;
|
|
if((ENUM_DEAL_ENTRY)HistoryDealGetInteger(tk, DEAL_ENTRY) != DEAL_ENTRY_IN)
|
|
continue;
|
|
|
|
datetime ti = (datetime)HistoryDealGetInteger(tk, DEAL_TIME);
|
|
if(!found || ti < earliestTime)
|
|
{
|
|
earliestTime = ti;
|
|
pIn = HistoryDealGetDouble(tk, DEAL_PRICE);
|
|
ENUM_DEAL_TYPE dt = (ENUM_DEAL_TYPE)HistoryDealGetInteger(tk, DEAL_TYPE);
|
|
dir = (dt == DEAL_TYPE_BUY) ? 1 : ((dt == DEAL_TYPE_SELL) ? -1 : 0);
|
|
tIn = ti;
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
return found;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void CalculateTotalClosedTrades()
|
|
{
|
|
if(!RefreshHistoryIfStale(5)) // Load ALL deals from beginning to now; throttle refresh
|
|
return;
|
|
|
|
winCount = 0;
|
|
lossCount = 0;
|
|
lastProcessedDealTicket = 0;
|
|
ArrayResize(processedTickets, 0); // reset processed ticket cache
|
|
|
|
int totalDeals = HistoryDealsTotal();
|
|
Print(" CalculateTotalClosedTrades: Scanning ", totalDeals, " total deals in history...");
|
|
Print(" Looking for symbol=", Symbol(), " | MagicNumber: ", MagicNumber);
|
|
|
|
int matchingDeals = 0;
|
|
|
|
for(int i = 0; i < totalDeals; i++)
|
|
{
|
|
ulong ticket = HistoryDealGetTicket(i);
|
|
if(ticket > 0)
|
|
{
|
|
string symbol = HistoryDealGetString(ticket, DEAL_SYMBOL);
|
|
double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT);
|
|
long dealEntry = HistoryDealGetInteger(ticket, DEAL_ENTRY);
|
|
long magicNumber = HistoryDealGetInteger(ticket, DEAL_MAGIC);
|
|
|
|
if(symbol == Symbol() && magicNumber == MagicNumber && dealEntry == DEAL_ENTRY_OUT)
|
|
{
|
|
matchingDeals++;
|
|
if(profit > 0)
|
|
{
|
|
winCount++;
|
|
Print(" WIN #", winCount, ": Ticket #", ticket, " | Profit: $", profit);
|
|
}
|
|
else
|
|
if(profit < 0)
|
|
{
|
|
lossCount++;
|
|
Print(" LOSS #", lossCount, ": Ticket #", ticket, " | Loss: $", profit);
|
|
}
|
|
|
|
lastProcessedDealTicket = ticket;
|
|
AddProcessedTicket(ticket);
|
|
}
|
|
}
|
|
}
|
|
|
|
Print(" Found ", matchingDeals, " matching deals | Result: ", winCount, "W / ", lossCount, "L");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void TrackSentiment()
|
|
{
|
|
if(!RefreshHistoryIfStale(5)) // Load ALL deals from beginning to now; throttle refresh
|
|
return;
|
|
|
|
int totalDeals = HistoryDealsTotal();
|
|
ulong maxNewTicket = lastProcessedDealTicket;
|
|
|
|
int newDealsFound = 0;
|
|
|
|
for(int i = 0; i < totalDeals; i++)
|
|
{
|
|
ulong ticket = HistoryDealGetTicket(i);
|
|
if(ticket > 0 && !IsTicketProcessed(ticket))
|
|
{
|
|
string symbol = HistoryDealGetString(ticket, DEAL_SYMBOL);
|
|
double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT);
|
|
long dealEntry = HistoryDealGetInteger(ticket, DEAL_ENTRY);
|
|
long magicNumber = HistoryDealGetInteger(ticket, DEAL_MAGIC);
|
|
|
|
if(ticket > maxNewTicket)
|
|
maxNewTicket = ticket;
|
|
|
|
bool isOurClosedDeal = (symbol == Symbol() && magicNumber == MagicNumber && dealEntry == DEAL_ENTRY_OUT);
|
|
if(!isOurClosedDeal)
|
|
continue;
|
|
|
|
if(newDealsFound == 0)
|
|
Print(" TrackSentiment: Found new deals - Checking Symbol=", symbol, "(need:", Symbol(), ") Magic=", magicNumber, "(need:", MagicNumber, ") Entry=", dealEntry);
|
|
|
|
newDealsFound++;
|
|
|
|
if(profit > 0)
|
|
{
|
|
winCount++;
|
|
if(UsesentimentTracking)
|
|
DisplayWinSentiment(profit);
|
|
Print(" WIN TRACKED: Ticket #", ticket, " | Profit: $", DoubleToString(profit, 2), " | Total: ", winCount, "W / ", lossCount, "L");
|
|
}
|
|
else
|
|
if(profit < 0)
|
|
{
|
|
lossCount++;
|
|
if(UsesentimentTracking)
|
|
DisplayLossSentiment(profit);
|
|
Print(" LOSS TRACKED: Ticket #", ticket, " | Loss: $", DoubleToString(profit, 2), " | Total: ", winCount, "W / ", lossCount, "L");
|
|
}
|
|
else
|
|
{
|
|
Print(" BREAKEVEN: Ticket #", ticket, " | Profit: $0");
|
|
}
|
|
AddProcessedTicket(ticket);
|
|
}
|
|
}
|
|
|
|
lastProcessedDealTicket = maxNewTicket;
|
|
|
|
if(ShowBalanceChanges)
|
|
{
|
|
double currentBalance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
if(currentBalance != lastBalance)
|
|
{
|
|
double balanceChange = currentBalance - lastBalance;
|
|
string changeType = (balanceChange > 0) ? " BALANCE UP" : " BALANCE DOWN";
|
|
Print(changeType, " | Previous: $", lastBalance, " Current: $", currentBalance,
|
|
" | Change: $", NormalizeDouble(balanceChange, 2),
|
|
" | Win Rate: ", (winCount + lossCount > 0 ? NormalizeDouble((winCount * 100.0) / (winCount + lossCount), 1) : 0),
|
|
"% (", winCount, "W / ", lossCount, "L)");
|
|
lastBalance = currentBalance;
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void DisplayWinSentiment(double profit)
|
|
{
|
|
// Happy emojis and messages on winning trades
|
|
string sentiments[] =
|
|
{
|
|
"THE KING HAS SPOKEN.",
|
|
"WE WIN THE BATTLE.",
|
|
"VICTORY IS OURS.",
|
|
"ROYAL ENTRY. ROYAL PROFIT.",
|
|
"THE CROWN STAYS ON TOP.",
|
|
"ANOTHER WIN FOR THE THRONE."
|
|
};
|
|
int sentimentCount = ArraySize(sentiments);
|
|
int randomSentiment = (sentimentCount > 0) ? (rand() % sentimentCount) : 0;
|
|
|
|
// ASCII/emoji happy faces (GREEN)
|
|
string happyFaces[] =
|
|
{
|
|
" \n ^ ^\n \n ",
|
|
" \n \n WW",
|
|
" \n ^ ^ \n \n ",
|
|
" \\___/\n ( ^ )\n \\___/",
|
|
" :D :D\n _/__\\_\n /////"
|
|
};
|
|
int faceCount = ArraySize(happyFaces);
|
|
int randomFace = (faceCount > 0) ? (rand() % faceCount) : 0;
|
|
|
|
string msg = sentiments[randomSentiment] + " WINNING TRADE! Profit: $" + DoubleToString(NormalizeDouble(profit, 2), 2) + "\n\n";
|
|
msg += happyFaces[randomFace] + "\n\n";
|
|
msg += "King of Gold Scalper is HAPPY! Account growing nicely,scanning for next entryto increase account balance,i m hungry!";
|
|
|
|
Print(msg);
|
|
chartComment = msg;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void DisplayLossSentiment(double loss)
|
|
{
|
|
// Sad emojis and messages on losing trades
|
|
string sentiments[] =
|
|
{
|
|
"THE KING HAS SPOKEN: REGROUP.",
|
|
"WE LOST THIS BATTLE, NOT THE WAR.",
|
|
"HOLD THE LINE. NEXT ENTRY WILL STRIKE.",
|
|
"THE THRONE STAYS CALM UNDER PRESSURE.",
|
|
"DISCIPLINE FIRST. REVENGE NEVER."
|
|
};
|
|
int sentimentCount = ArraySize(sentiments);
|
|
int randomSentiment = (sentimentCount > 0) ? (rand() % sentimentCount) : 0;
|
|
|
|
// ASCII/emoji sad faces (RED)
|
|
string sadFaces[] =
|
|
{
|
|
" \n T T\n \n ",
|
|
" o_o\n \n \\/\\/",
|
|
"\n ; ; \n \n ",
|
|
" \\_/\n ( T )\n /---\\",
|
|
" :( :(\n .__.__.\n /////"
|
|
};
|
|
int faceCount = ArraySize(sadFaces);
|
|
int randomFace = (faceCount > 0) ? (rand() % faceCount) : 0;
|
|
|
|
string msg = sentiments[randomSentiment] + " LOSING TRADE... Loss: $" + DoubleToString(NormalizeDouble(loss, 2), 2) + "\n\n";
|
|
msg += sadFaces[randomFace] + "\n\n";
|
|
msg += "No problem Master the King we will recover the loss,next entry will be more acurate!";
|
|
|
|
Print(msg);
|
|
chartComment = msg;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// ADAPTIVE TRADING - Self-adjust based on performance
|
|
//+------------------------------------------------------------------+
|
|
void UpdateSignalStrengthGreed()
|
|
{
|
|
if(!UseSignalStrengthGreed)
|
|
{
|
|
greedLevel = "MODERATE";
|
|
return;
|
|
}
|
|
bool smallAccountProtect = IsSmallAccountForAutoProtection();
|
|
if(safeModeActive || (ForceConservativeGreedOnSmallAccount && smallAccountProtect))
|
|
{
|
|
greedLevel = "CONSERVATIVE";
|
|
trendDirection = 0;
|
|
if(!safeModeActive && ForceConservativeGreedOnSmallAccount)
|
|
Print("AUTO PROTECT | GREED forced to CONSERVATIVE for small account.");
|
|
return;
|
|
}
|
|
|
|
int signalStrength = 0;
|
|
|
|
// Consecutive bars strength
|
|
if(consecutiveBullishBars >= 3 || consecutiveBearishBars >= 3)
|
|
signalStrength += 3;
|
|
else
|
|
if(consecutiveBullishBars >= 1 || consecutiveBearishBars >= 1)
|
|
signalStrength += 2;
|
|
|
|
// Infer simple trend direction from consecutive bars for display
|
|
if(consecutiveBullishBars > consecutiveBearishBars)
|
|
trendDirection = 1;
|
|
else
|
|
if(consecutiveBearishBars > consecutiveBullishBars)
|
|
trendDirection = -1;
|
|
else
|
|
trendDirection = 0;
|
|
|
|
if(trendDirection != 0)
|
|
signalStrength += 2;
|
|
|
|
// Tick volume boost (if enabled)
|
|
if(UseVolumeFilter)
|
|
{
|
|
long currentVolume = UseTickVolume ? iTickVolume(Symbol(), Period(), 0) : iVolume(Symbol(), Period(), 0);
|
|
if(!UseTickVolume && currentVolume <= 0)
|
|
Print("Real volume unavailable (0) - broker did not provide volume for current bar.");
|
|
long sumVolume = 0;
|
|
for(int i = 1; i <= 200; i++)
|
|
sumVolume += UseTickVolume ? iTickVolume(Symbol(), Period(), i) : iVolume(Symbol(), Period(), i);
|
|
long avgVolume = (sumVolume > 0) ? (sumVolume / 200) : 0;
|
|
double volumeRatio = (avgVolume > 0) ? ((double)currentVolume / avgVolume) : 1.5;
|
|
|
|
if(volumeRatio >= (MinVolumeRatio * 1.5))
|
|
signalStrength += 3;
|
|
else
|
|
if(volumeRatio >= MinVolumeRatio)
|
|
signalStrength += 2;
|
|
}
|
|
|
|
string previousGreed = greedLevel;
|
|
bool largeStableAccount = IsLargeAccountForAutoExplore();
|
|
|
|
if(signalStrength >= 3 && largeStableAccount)
|
|
{
|
|
greedLevel = "GREEDY";
|
|
if(previousGreed != "GREEDY")
|
|
Print(" GREEDY MODE ACTIVATED! Strong signal + stable large account - controlled larger positions.");
|
|
}
|
|
else
|
|
if(signalStrength >= 1)
|
|
{
|
|
greedLevel = "MODERATE";
|
|
if(previousGreed != "MODERATE")
|
|
Print(" MODERATE MODE - Normal signal strength - Trading normally!");
|
|
}
|
|
else
|
|
{
|
|
greedLevel = "CONSERVATIVE";
|
|
if(previousGreed != "CONSERVATIVE")
|
|
Print(" CONSERVATIVE MODE - Weak signals detected - Playing it safe!");
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateAdaptiveTrading()
|
|
{
|
|
if(winCount + lossCount < 3)
|
|
return;
|
|
|
|
double winRate = (winCount * 100.0) / (winCount + lossCount);
|
|
double baseRisk = GetConfiguredBaseRiskPercent();
|
|
double previousRisk = currentRiskPercent;
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
double dd = GetAccountDrawdownPercentAuto();
|
|
bool largeStableAccount = IsLargeAccountForAutoExplore();
|
|
|
|
if(winRate >= MinWinRatePercent)
|
|
{
|
|
currentRiskPercent = baseRisk * AggressiveMultiplier;
|
|
if(performanceMode != "AGGRESSIVE")
|
|
{
|
|
performanceMode = "AGGRESSIVE";
|
|
Print(" AGGRESSIVE MODE ACTIVATED! Win Rate: ", NormalizeDouble(winRate, 1),
|
|
"% | Risk increased from ", NormalizeDouble(previousRisk, 2),
|
|
"% to ", NormalizeDouble(currentRiskPercent, 2),
|
|
"% | Taking bigger positions!");
|
|
}
|
|
}
|
|
else
|
|
if(winRate < (MinWinRatePercent - 15))
|
|
{
|
|
currentRiskPercent = baseRisk * ConservativeMultiplier;
|
|
if(performanceMode != "CONSERVATIVE")
|
|
{
|
|
performanceMode = "CONSERVATIVE";
|
|
Print(" CONSERVATIVE MODE ACTIVATED! Win Rate: ", NormalizeDouble(winRate, 1),
|
|
"% | Risk reduced from ", NormalizeDouble(previousRisk, 2),
|
|
"% to ", NormalizeDouble(currentRiskPercent, 2),
|
|
"% | Protecting capital!");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
currentRiskPercent = baseRisk;
|
|
if(performanceMode != "NORMAL")
|
|
{
|
|
performanceMode = "NORMAL";
|
|
Print(" NORMAL MODE ACTIVATED! Win Rate: ", NormalizeDouble(winRate, 1),
|
|
"% | Trading normally with baseline risk!");
|
|
}
|
|
}
|
|
|
|
// Automatic guardrails for large funded balances and stressed conditions.
|
|
if(dd >= 8.0)
|
|
currentRiskPercent *= 0.80;
|
|
|
|
if(balance >= 10000.0)
|
|
{
|
|
double maxFundedRisk = largeStableAccount ? 0.60 : 0.50;
|
|
currentRiskPercent = MathMin(currentRiskPercent, maxFundedRisk);
|
|
}
|
|
|
|
currentRiskPercent = ClampValue(currentRiskPercent, 0.05, 10.0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
// UPDATE CHART DISPLAY - Show sentiment and EA status on chart
|
|
//+------------------------------------------------------------------+
|
|
void UpdateChartDisplay()
|
|
{
|
|
string display = "";
|
|
UpdateAccountHealthDisplayState();
|
|
|
|
double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);
|
|
double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
|
|
int chartDirection = 0;
|
|
if(consecutiveBearishBars > consecutiveBullishBars)
|
|
chartDirection = 1;
|
|
else
|
|
if(consecutiveBullishBars > consecutiveBearishBars)
|
|
chartDirection = -1;
|
|
|
|
int chartBars = (chartDirection == 1) ? consecutiveBearishBars : consecutiveBullishBars;
|
|
double chartVolRatio = GetCurrentVolumeRatio();
|
|
string scoreDetails = "";
|
|
int decisionScore = (chartDirection == 0) ? 5 : GetDecisionScore10(chartDirection, chartBars, chartVolRatio, bid, ask, scoreDetails);
|
|
string chartSignal = "NEUTRAL";
|
|
if(chartDirection == 1)
|
|
chartSignal = "BULLISH";
|
|
else
|
|
if(chartDirection == -1)
|
|
chartSignal = "BEARISH";
|
|
int threshold = marketState.marketStress ? DecisionScoreThresholdStress : DecisionScoreThresholdNormal;
|
|
|
|
double allowRate = (memoryDecisionTotal > 0) ? ((double)memoryDecisionAllow * 100.0 / (double)memoryDecisionTotal) : 0.0;
|
|
double blockRate = (memoryDecisionTotal > 0) ? ((double)memoryDecisionBlock * 100.0 / (double)memoryDecisionTotal) : 0.0;
|
|
double exploreRate = (memoryDecisionTotal > 0) ? ((double)memoryDecisionExplore * 100.0 / (double)memoryDecisionTotal) : 0.0;
|
|
int closedTrades = winCount + lossCount;
|
|
double winRatePct = (closedTrades > 0) ? (((double)winCount * 100.0) / (double)closedTrades) : 0.0;
|
|
|
|
int buyCount = CountOpenTradesOfType(POSITION_TYPE_BUY);
|
|
int sellCount = CountOpenTradesOfType(POSITION_TYPE_SELL);
|
|
int openAll = CountOpenTradesAllSymbols();
|
|
double totalProfit = CalculateTotalProfit();
|
|
|
|
string modeTag = recoveryModeActive ? "RECOVERY" : (safeModeActive ? "SAFE" : "NORMAL");
|
|
string canTradeTag = CanTrade() ? "YES" : "NO";
|
|
|
|
int nextNewsScanSec = (lastNewsScanTime == 0) ? 0 : (MathMax(0, NewsScanIntervalSeconds - (int)(TimeCurrent() - lastNewsScanTime)));
|
|
|
|
display += "==============================\n";
|
|
display += " King of Gold v9.0 Status\n";
|
|
display += "==============================\n";
|
|
if(chartComment != "")
|
|
{
|
|
display += chartComment + "\n";
|
|
display += "------------------------------\n";
|
|
}
|
|
display += "Core | CanTrade=" + canTradeTag + " | Mode=" + modeTag + " | Stress=" + (marketState.marketStress ? "YES" : "NO") + "\n";
|
|
display += "Signal | " + chartSignal + " | Regime=" + marketState.currentRegime + " | Score " + IntegerToString(decisionScore) + "/10 >= " + IntegerToString(threshold) + "\n";
|
|
display += "Multi | Top1=" + scanner.topSymbols[0] + " (" + DoubleToString(scanner.topScores[0], 1) + ")"
|
|
+ " Top2=" + scanner.topSymbols[1] + " (" + DoubleToString(scanner.topScores[1], 1) + ")"
|
|
+ " Top3=" + scanner.topSymbols[2] + " (" + DoubleToString(scanner.topScores[2], 1) + ")\n";
|
|
display += "Memory | Allow " + DoubleToString(allowRate, 1) + "% | Block " + DoubleToString(blockRate, 1) + "% | Explore " + DoubleToString(exploreRate, 1) + "%\n";
|
|
if(UseDecisionCoreDll)
|
|
{
|
|
string dllStatus = g_decisionDllReady ? "READY" : "FALLBACK";
|
|
display += "DLL | " + dllStatus
|
|
+ " | " + g_lastDllDecisionAction
|
|
+ " " + g_lastDllDecisionSymbol
|
|
+ " | pAllow=" + DoubleToString(g_lastDllDecisionAllowProb, 3)
|
|
+ " | risk=" + DoubleToString(g_lastDllDecisionRiskMul, 2)
|
|
+ "x | lat=" + DoubleToString(g_lastDllDecisionLatencyUs, 0) + "us\n";
|
|
if(g_lastDllSnapScoreCalls >= 0)
|
|
{
|
|
display += "DLL Learn | score=" + IntegerToString((int)g_lastDllSnapScoreCalls)
|
|
+ " learn=" + IntegerToString((int)g_lastDllSnapLearnCalls)
|
|
+ " basket=" + IntegerToString((int)g_lastDllSnapBasketLearnCalls)
|
|
+ " cache=" + IntegerToString((int)g_lastDllSnapCacheHits) + "\n";
|
|
}
|
|
if(g_lastDllSnapBaselineVol >= 0.0 && g_lastDllSnapBaselineSpread >= 0.0)
|
|
{
|
|
display += "DLL Baseline | vol=" + DoubleToString(g_lastDllSnapBaselineVol, 3)
|
|
+ " spread=" + DoubleToString(g_lastDllSnapBaselineSpread, 2) + "\n";
|
|
}
|
|
}
|
|
display += "Streak | Wins=" + IntegerToString(marketState.consecutiveWins) + " Losses=" + IntegerToString(marketState.consecutiveLosses)
|
|
+ " | MemRisk=" + DoubleToString(memoryAdaptiveRiskMultiplier, 2) + "x\n";
|
|
display += "Perf | WinRate=" + DoubleToString(winRatePct, 1) + "% | Closed " + IntegerToString(closedTrades)
|
|
+ " (" + IntegerToString(winCount) + "W/" + IntegerToString(lossCount) + "L)\n";
|
|
display += "Learning | DynamicPatterns=" + IntegerToString(dynamicPatternCount) + " | RewardUpdates=" + IntegerToString((int)setupRewardUpdates) + "\n";
|
|
display += "Risk | DD=" + DoubleToString(queenAccountDrawdownPct, 2) + "% | Margin=" + DoubleToString(queenMarginLevel, 0) + "% | Health=" + DoubleToString(queenAccountHealthScore, 2) + "\n";
|
|
display += "Account | Bal=$" + DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2) + " Eq=$" + DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2)
|
|
+ " | PL=$" + DoubleToString(NormalizeDouble(totalProfit, 2), 2) + "\n";
|
|
display += "Trades | Chart B/S=" + IntegerToString(buyCount) + "/" + IntegerToString(sellCount) + " | All=" + IntegerToString(openAll) + "/" + IntegerToString(currentMaxOpenTrades) + "\n";
|
|
display += "Alerts | Voice=" + (EnableVoicePrompts ? "ON" : "OFF")
|
|
+ " News=" + (EnableNewsEventAlerts ? "ON" : "OFF")
|
|
+ " Future=" + (EnableFutureSessionAlerts ? "ON" : "OFF")
|
|
+ " | NewsScanIn=" + IntegerToString(nextNewsScanSec) + "s\n";
|
|
display += "Updated | " + TimeToString(TimeCurrent(), TIME_DATE | TIME_MINUTES);
|
|
|
|
Comment(display);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+//
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
|
//+------------------------------------------------------------------+
|