//+------------------------------------------------------------------+ //| PropFirmGuard.mq5 | //| Copyright 2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| Standard Library | //+------------------------------------------------------------------+ #include //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+ //| Represents the general compliance condition of the account. | //| Additional states will be used as more rules are implemented. | //+------------------------------------------------------------------+ enum ENUM_COMPLIANCE_STATUS { COMPLIANCE_SAFE = 0, // Account is operating within configured limits COMPLIANCE_WARNING, // Account is approaching a compliance limit COMPLIANCE_CRITICAL, // Account is very close to a compliance limit COMPLIANCE_BREACHED // A configured compliance limit has been breached }; //+------------------------------------------------------------------+ //| Profit target status | //+------------------------------------------------------------------+ //| Represents progress toward the configured account profit target. | //+------------------------------------------------------------------+ enum ENUM_TARGET_STATUS { TARGET_IN_PROGRESS = 0, // Target is still being pursued TARGET_NEARING, // At least 90% of the target has been reached TARGET_REACHED // Required target balance has been achieved }; //+------------------------------------------------------------------+ //| Overall guard status | //+------------------------------------------------------------------+ //| Combines the monitored rules into one account-level condition. | //+------------------------------------------------------------------+ enum ENUM_GUARD_STATUS { GUARD_SAFE = 0, // All monitored rules are within safe limits GUARD_TARGET_REACHED, // Profit target has been achieved GUARD_WARNING, // At least one rule is in warning state GUARD_CRITICAL, // At least one rule is in critical state GUARD_BREACHED // At least one drawdown rule has been breached }; //+------------------------------------------------------------------+ //| News monitoring status | //+------------------------------------------------------------------+ //| Represents the current condition of the economic news filter. | //+------------------------------------------------------------------+ enum ENUM_NEWS_STATUS { NEWS_DISABLED = 0, // Economic news monitoring is disabled NEWS_NO_EXPOSURE, // No supported open positions require news monitoring NEWS_NO_EVENT, // No relevant event was found NEWS_SAFE, // Relevant event exists but restriction is inactive NEWS_RESTRICTED, // Current time is inside the restricted news window NEWS_UNAVAILABLE // Calendar data could not be retrieved }; //+------------------------------------------------------------------+ //| News position match status | //+------------------------------------------------------------------+ //| Describes whether an open position is affected by the selected | //| Economic Calendar event. | //+------------------------------------------------------------------+ enum ENUM_NEWS_POSITION_STATUS { NEWS_POSITION_NOT_AFFECTED = 0, // Position does not use the event currency NEWS_POSITION_AFFECTED, // Position is exposed to the event currency NEWS_POSITION_UNSUPPORTED // Position is outside current Forex/XAUUSD scope }; //+------------------------------------------------------------------+ //| News holding status | //+------------------------------------------------------------------+ //| Describes the account's current holding-rule condition around | //| the selected Economic Calendar event. | //+------------------------------------------------------------------+ enum ENUM_NEWS_HOLDING_STATUS { NEWS_HOLDING_CLEAR = 0, // No active holding restriction NEWS_HOLDING_ALLOWED, // Restricted window active but holding is permitted NEWS_HOLDING_WARNING, // Event is approaching and affected positions exist NEWS_HOLDING_PROTECTION // Restricted window active and protection is required }; //+------------------------------------------------------------------+ //| Protection action | //+------------------------------------------------------------------+ //| Defines what Prop Firm Guard should do when a monitored rule | //| requires protective action. | //+------------------------------------------------------------------+ enum ENUM_PROTECTION_ACTION { PROTECTION_WARN_ONLY = 0, // Report the condition without closing positions PROTECTION_CLOSE_AFFECTED, // Close positions affected by the triggering rule PROTECTION_CLOSE_ALL // Close every open account position }; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "General" input int InpTimerSeconds = 1; // Dashboard update interval in seconds input int InpDashboardX = 10; // Dashboard horizontal position input int InpDashboardY = 105; // Dashboard vertical position input bool InpEnableAlerts = true; // Enable terminal alerts input bool InpEnablePushNotifications = false; // Enable mobile push notifications input group "Prop Firm Rules" input double InpInitialAccountBalance = 0.0; // Initial prop-firm account balance input double InpDailyDrawdownPercent = 5.0; // Maximum permitted daily drawdown input double InpOverallDrawdownPercent = 10.0; // Maximum permitted overall drawdown input double InpProfitTargetPercent = 8.0; // Required profit target input group "Warning Levels" input double InpWarningLevelPercent = 70.0; // Loss limit usage that triggers warning input double InpCriticalLevelPercent = 90.0; // Loss limit usage that triggers critical state input group "News Filter" input bool InpEnableNewsFilter = true; // Enable economic news monitoring input ENUM_CALENDAR_EVENT_IMPORTANCE InpNewsImportance = CALENDAR_IMPORTANCE_HIGH; // Minimum monitored importance input int InpMinutesBeforeNews = 10; // Restricted minutes before news input int InpMinutesAfterNews = 10; // Restricted minutes after news input bool InpAllowNewsHolding = true; // Allow positions to remain open during news input group "Protection Actions" input ENUM_PROTECTION_ACTION InpDailyDrawdownAction = PROTECTION_WARN_ONLY; // Daily breach action input ENUM_PROTECTION_ACTION InpOverallDrawdownAction = PROTECTION_WARN_ONLY; // Overall breach action input ENUM_PROTECTION_ACTION InpNewsRestrictionAction = PROTECTION_WARN_ONLY; // News restriction action //+------------------------------------------------------------------+ //| Structures | //+------------------------------------------------------------------+ //| Holds the live account values required by the monitoring logic. | //| Keeping them together will make later calculations easier. | //+------------------------------------------------------------------+ struct SAccountState { long login; // Trading account login number double balance; // Current account balance double equity; // Current account equity double floatingPL; // Current floating profit or loss ENUM_COMPLIANCE_STATUS status; // Current overall compliance status }; //+------------------------------------------------------------------+ //| Account settings structure | //+------------------------------------------------------------------+ //| Stores the main prop-firm rules associated with this account. | //+------------------------------------------------------------------+ struct SAccountSettings { long accountLogin; // Trading account login string accountServer; // Broker trading server double initialBalance; // Initial prop-firm account balance double dailyDrawdownPercent; // Maximum daily drawdown double overallDrawdownPercent; // Maximum overall drawdown double profitTargetPercent; // Required profit target datetime updatedAt; // Last settings update time }; //+------------------------------------------------------------------+ //| Daily session structure | //+------------------------------------------------------------------+ //| Represents one monitored trading day. Starting values remain | //| unconfirmed until the user explicitly confirms them in Phase 3. | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Daily session structure | //+------------------------------------------------------------------+ struct SDailySession { long accountLogin; string accountServer; string tradingDate; double startingBalance; double startingEquity; bool balanceConfirmed; double latestBalance; double latestEquity; double dailyLoss; double maximumDailyLossReached; double targetProgress; string sessionStatus; datetime createdAt; datetime updatedAt; }; //+------------------------------------------------------------------+ //| Daily drawdown structure | //+------------------------------------------------------------------+ //| Holds the calculated daily risk values for the active session. | //+------------------------------------------------------------------+ struct SDailyDrawdownState { double startingBalance; // Confirmed daily starting balance double currentEquity; // Current live account equity double maximumDailyLoss; // Maximum money that may be lost today double currentDailyLoss; // Current loss measured from starting balance double drawdownPercent; // Current loss as percentage of starting balance double limitUsagePercent; // Percentage of daily loss allowance already used double remainingAllowance; // Money remaining before daily breach ENUM_COMPLIANCE_STATUS status; // Current daily drawdown status }; //+------------------------------------------------------------------+ //| Overall drawdown structure | //+------------------------------------------------------------------+ //| Holds the live calculations for the static overall loss rule. | //+------------------------------------------------------------------+ struct SOverallDrawdownState { double initialBalance; // Initial prop-firm account balance double currentEquity; // Current live account equity double maximumOverallLoss; // Maximum permitted overall loss double overallFloor; // Minimum permitted account equity double currentOverallLoss; // Current loss from initial balance double limitUsagePercent; // Percentage of overall allowance used double remainingAllowance; // Money remaining before breach ENUM_COMPLIANCE_STATUS status; // Current overall drawdown status }; //+------------------------------------------------------------------+ //| Profit target structure | //+------------------------------------------------------------------+ //| Holds the live calculations used to track challenge progress. | //+------------------------------------------------------------------+ struct SProfitTargetState { double initialBalance; // Initial prop-firm account balance double currentBalance; // Current realized account balance double targetAmount; // Profit required in account currency double targetBalance; // Balance required to complete the target double currentProfit; // Realized profit above initial balance double progressPercent; // Percentage of target already completed double remainingProfit; // Profit still required to reach target ENUM_TARGET_STATUS status; // Current target progress state }; //+------------------------------------------------------------------+ //| News monitoring structure | //+------------------------------------------------------------------+ //| Holds account exposure, the next relevant economic event, and | //| the current news-holding condition. | //+------------------------------------------------------------------+ struct SNewsState { bool hasExposure; // Whether supported open positions exist int monitoredCurrencies; // Number of unique currencies being monitored string currencyList; // Comma-separated monitored currencies bool eventFound; // Whether a relevant event was located string eventCurrency; // Currency affected by selected event ulong eventId; // Economic Calendar event identifier string eventName; // Economic event name ENUM_CALENDAR_EVENT_IMPORTANCE importance; // Event importance datetime eventTime; // Event time in trade-server time long secondsToEvent; // Seconds until event, negative after release datetime restrictionStart; // Beginning of restricted news window datetime restrictionEnd; // End of restricted news window bool restrictedWindow; // Whether restriction is currently active int affectedPositions; // Positions affected by selected event int protectionPositions; // Positions exposed to Phase 12 protection ENUM_NEWS_STATUS status; // Current news subsystem status ENUM_NEWS_HOLDING_STATUS holdingStatus; // Current news-holding rule state }; //+------------------------------------------------------------------+ //| News position match structure | //+------------------------------------------------------------------+ //| Stores the classification and holding-rule state of one open | //| position for the currently selected Economic Calendar event. | //+------------------------------------------------------------------+ struct SNewsPositionMatch { ulong ticket; // Position ticket string symbol; // Position symbol string currency1; // First relevant currency string currency2; // Second relevant currency ENUM_NEWS_POSITION_STATUS status; // Event-currency classification bool protectionRequired; // Whether Phase 12 may act on this position }; //+------------------------------------------------------------------+ //| Alert state | //+------------------------------------------------------------------+ //| Remembers which conditions have already generated notifications. | //+------------------------------------------------------------------+ struct SAlertState { ENUM_COMPLIANCE_STATUS dailyDrawdownStatus; ENUM_COMPLIANCE_STATUS overallDrawdownStatus; ENUM_TARGET_STATUS profitTargetStatus; ulong newsEventId; bool newsApproachAlerted; bool newsRestrictedAlerted; string dashboardMessage; }; //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ SAccountState g_accountState; // Latest monitored account state SAccountSettings g_accountSettings; // Persisted prop-firm settings SDailySession g_dailySession; // Current trading-day record SDailyDrawdownState g_dailyDrawdown; // Current daily drawdown calculations SOverallDrawdownState g_overallDrawdown; // Current overall drawdown calculations SProfitTargetState g_profitTarget; // Current profit-target calculations SNewsState g_newsState; // Current economic news state SNewsPositionMatch g_newsPositionMatches[]; // Classification of open positions SAlertState g_alertState; CTrade g_trade; // Synchronous trade execution helper string g_objectPrefix = "PFG_"; // Prefix used by dashboard objects string g_startBalanceEdit = "PFG_StartBalanceEdit"; // Starting balance input field string g_confirmButton = "PFG_ConfirmBalance"; // Daily balance confirmation button string g_databaseName = "prop_firm_compliance.sqlite"; // SQLite database file int g_database = INVALID_HANDLE; // Active SQLite database handle bool g_hasDailySession = false; // Current-day session availability ulong g_lastMatchedEventId = 0; // Last event printed to the Journal ENUM_NEWS_HOLDING_STATUS g_lastNewsHoldingStatus = NEWS_HOLDING_CLEAR; // Last notified holding condition bool g_dailyBreachWarned = false; // Prevent repeated daily breach warnings bool g_overallBreachWarned = false; // Prevent repeated overall breach warnings ulong g_lastNewsWarnedEventId = 0; // Prevent repeated warning for same news event datetime g_lastRuntimeDatabaseUpdate = 0; //+------------------------------------------------------------------+ //| Validate compliance inputs | //+------------------------------------------------------------------+ bool ValidateComplianceInputs() { // Financial calculations require a valid initial challenge balance. if(InpInitialAccountBalance <= 0.0) { Print("Prop Firm Guard: Initial account balance must be greater than zero."); return(false); } if(InpDailyDrawdownPercent <= 0.0 || InpDailyDrawdownPercent > 100.0) { Print("Prop Firm Guard: Daily drawdown percentage must be between 0 and 100."); return(false); } if(InpOverallDrawdownPercent <= 0.0 || InpOverallDrawdownPercent > 100.0) { Print("Prop Firm Guard: Overall drawdown percentage must be between 0 and 100."); return(false); } if(InpProfitTargetPercent <= 0.0 || InpProfitTargetPercent > 100.0) { Print("Prop Firm Guard: Profit target percentage must be between 0 and 100."); return(false); } if(InpWarningLevelPercent <= 0.0 || InpWarningLevelPercent >= InpCriticalLevelPercent) { Print("Prop Firm Guard: Warning level must be greater than 0 and below the critical level."); return(false); } if(InpCriticalLevelPercent <= InpWarningLevelPercent || InpCriticalLevelPercent >= 100.0) { Print("Prop Firm Guard: Critical level must be above the warning level and below 100."); return(false); } // News restriction windows cannot use negative durations. if(InpMinutesBeforeNews < 0 || InpMinutesAfterNews < 0) { Print("Prop Firm Guard: News restriction minutes cannot be negative."); return(false); } return(true); } //+------------------------------------------------------------------+ //| Set chart integer property | //+------------------------------------------------------------------+ bool SetChartIntegerProperty(const ENUM_CHART_PROPERTY_INTEGER property, const long value) { ResetLastError(); if(!ChartSetInteger(0, property, value)) { Print("Prop Firm Guard: Could not set chart property ", EnumToString(property), ". Error: ", GetLastError()); return(false); } return(true); } //+------------------------------------------------------------------+ //| Configure Prop Firm Guard chart appearance | //+------------------------------------------------------------------+ bool ConfigureChartAppearance() { // Use a light neutral background that keeps the dashboard readable. if(!SetChartIntegerProperty(CHART_COLOR_BACKGROUND, clrWhiteSmoke)) return(false); // Keep axes, scales and chart text clearly visible. if(!SetChartIntegerProperty(CHART_COLOR_FOREGROUND, clrBlack)) return(false); // The dashboard is cleaner without the default chart grid. if(!SetChartIntegerProperty(CHART_SHOW_GRID, false)) return(false); // Display the market as Japanese candlesticks. if(!SetChartIntegerProperty(CHART_MODE, CHART_CANDLES)) return(false); // Bullish candles and bars use green consistently. if(!SetChartIntegerProperty(CHART_COLOR_CANDLE_BULL, clrGreen)) return(false); if(!SetChartIntegerProperty(CHART_COLOR_CHART_UP, clrGreen)) return(false); // Bearish candles and bars use red consistently. if(!SetChartIntegerProperty(CHART_COLOR_CANDLE_BEAR, clrRed)) return(false); if(!SetChartIntegerProperty(CHART_COLOR_CHART_DOWN, clrRed)) return(false); // Keep the line-chart representation neutral and readable. if(!SetChartIntegerProperty(CHART_COLOR_CHART_LINE, clrBlack)) return(false); // Use the same directional convention for live price lines. if(!SetChartIntegerProperty(CHART_COLOR_BID, clrGreen)) return(false); if(!SetChartIntegerProperty(CHART_COLOR_ASK, clrRed)) return(false); // Keep the last traded price visually neutral. if(!SetChartIntegerProperty(CHART_COLOR_LAST, clrBlack)) return(false); // Stop Loss, Take Profit and related stop levels use red because // they represent important protection boundaries. if(!SetChartIntegerProperty(CHART_COLOR_STOP_LEVEL, clrRed)) return(false); // Show open-position, pending-order, SL and TP levels. if(!SetChartIntegerProperty(CHART_SHOW_TRADE_LEVELS, true)) return(false); // Allow trade levels to remain interactively draggable. if(!SetChartIntegerProperty(CHART_DRAG_TRADE_LEVELS, true)) return(false); // Keep MetaTrader's One Click Trading panel visible. if(!SetChartIntegerProperty(CHART_SHOW_ONE_CLICK, true)) return(false); ChartRedraw(0); return(true); } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { if(InpTimerSeconds < 1) { Print("Prop Firm Guard: Timer interval must be at least 1 second."); return(INIT_PARAMETERS_INCORRECT); } if(!ValidateComplianceInputs()) return(INIT_PARAMETERS_INCORRECT); // Apply the visual chart theme before creating dashboard objects. if(!ConfigureChartAppearance()) { Print("Prop Firm Guard: Could not configure chart appearance."); return(INIT_FAILED); } g_trade.SetAsyncMode(false); UpdateAccountState(); if(!OpenDatabase()) { Print("Prop Firm Guard: Database initialization failed."); return(INIT_FAILED); } if(!EnsureDatabaseTables()) { Print("Prop Firm Guard: Could not initialize database tables."); CloseDatabase(); return(INIT_FAILED); } if(!InitializeAccountSettings()) { Print("Prop Firm Guard: Could not initialize account settings."); CloseDatabase(); return(INIT_FAILED); } if(!InitializeDailySession()) { Print("Prop Firm Guard: Could not initialize daily session."); CloseDatabase(); return(INIT_FAILED); } CalculateDailyDrawdown(); CalculateOverallDrawdown(); CalculateProfitTarget(); UpdateNewsState(); UpdateNewsPositionMatches(); // Store the current monitoring state so alerts begin from subsequent // state transitions rather than from historical conditions. InitializeAlertState(); if(!CreateDashboard()) { Print("Prop Firm Guard: Failed to create the dashboard."); CloseDatabase(); return(INIT_FAILED); } if(!EventSetTimer(InpTimerSeconds)) { Print("Prop Firm Guard: Failed to start the timer. Error: ", GetLastError()); DeleteDashboard(); CloseDatabase(); return(INIT_FAILED); } Print("Prop Firm Guard initialized successfully."); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { // Stop timer events before shutting down other EA resources. EventKillTimer(); // Remove only chart objects created by Prop Firm Guard. DeleteDashboard(); // Close the SQLite connection after all database work is complete. CloseDatabase(); Print("Prop Firm Guard removed. Deinitialization reason: ", reason); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { // Phase 1 does not require tick-by-tick processing. // Account monitoring currently runs once per second through OnTimer(). } //+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer() { // Refresh live account information. UpdateAccountState(); // Detect a new daily monitoring session when necessary. CheckForNewTradingDay(); // Recalculate all financial compliance conditions. CalculateDailyDrawdown(); CalculateOverallDrawdown(); CalculateProfitTarget(); // Update economic news exposure and affected positions. UpdateNewsState(); UpdateNewsPositionMatches(); // Notify only when monitored conditions change. ManageAlerts(); // Execute configured protection behavior. ManageProtectionActions(); // Protection actions may change account values or positions. UpdateAccountState(); CalculateDailyDrawdown(); CalculateOverallDrawdown(); CalculateProfitTarget(); // Persist the latest structured daily-session snapshot. UpdateRuntimeDatabase(); // Refresh the dashboard with the resulting state. UpdateDashboard(); } //+------------------------------------------------------------------+ //| Chart event function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { // We are interested only in clicks on chart objects. if(id != CHARTEVENT_OBJECT_CLICK) return; // Confirm the entered starting balance when the user presses // the confirmation button. if(sparam == g_confirmButton) { if(!ConfirmDailyStartingBalance()) Print("Prop Firm Guard: Starting balance confirmation failed."); } } //+------------------------------------------------------------------+ //| Update account state | //+------------------------------------------------------------------+ void UpdateAccountState() { // Read the account identifier so future persisted data can be tied // to the correct trading account. g_accountState.login = AccountInfoInteger(ACCOUNT_LOGIN); // Balance represents realized funds currently available on the account. g_accountState.balance = AccountInfoDouble(ACCOUNT_BALANCE); // Equity includes the effect of all currently open positions. g_accountState.equity = AccountInfoDouble(ACCOUNT_EQUITY); // The difference between equity and balance represents the current // floating profit or loss of open positions. g_accountState.floatingPL = g_accountState.equity - g_accountState.balance; // No compliance rules exist yet, so the initial state remains safe. g_accountState.status = COMPLIANCE_SAFE; } //+------------------------------------------------------------------+ //| Calculate daily drawdown | //+------------------------------------------------------------------+ void CalculateDailyDrawdown() { // Reset the live state when today's starting balance has not yet // been confirmed by the trader. if(!g_dailySession.balanceConfirmed || g_dailySession.startingBalance <= 0.0) { g_dailyDrawdown.startingBalance = 0.0; g_dailyDrawdown.currentEquity = g_accountState.equity; g_dailyDrawdown.maximumDailyLoss = 0.0; g_dailyDrawdown.currentDailyLoss = 0.0; g_dailyDrawdown.drawdownPercent = 0.0; g_dailyDrawdown.limitUsagePercent = 0.0; g_dailyDrawdown.remainingAllowance = 0.0; g_dailyDrawdown.status = COMPLIANCE_SAFE; return; } // Use the confirmed starting balance as the fixed daily reference. g_dailyDrawdown.startingBalance = g_dailySession.startingBalance; // Current equity reflects the effect of currently open positions. g_dailyDrawdown.currentEquity = g_accountState.equity; // Convert the configured daily percentage limit into account currency. g_dailyDrawdown.maximumDailyLoss = g_dailyDrawdown.startingBalance * g_accountSettings.dailyDrawdownPercent / 100.0; // A loss exists only when equity falls below the daily starting balance. g_dailyDrawdown.currentDailyLoss = g_dailyDrawdown.startingBalance - g_dailyDrawdown.currentEquity; if(g_dailyDrawdown.currentDailyLoss < 0.0) g_dailyDrawdown.currentDailyLoss = 0.0; // Express the current loss as a percentage of the starting balance. g_dailyDrawdown.drawdownPercent = g_dailyDrawdown.currentDailyLoss / g_dailyDrawdown.startingBalance * 100.0; // Measure how much of the permitted daily loss has already been used. if(g_dailyDrawdown.maximumDailyLoss > 0.0) { g_dailyDrawdown.limitUsagePercent = g_dailyDrawdown.currentDailyLoss / g_dailyDrawdown.maximumDailyLoss * 100.0; } else { g_dailyDrawdown.limitUsagePercent = 0.0; } // Calculate the amount still available before the daily limit is breached. g_dailyDrawdown.remainingAllowance = g_dailyDrawdown.maximumDailyLoss - g_dailyDrawdown.currentDailyLoss; if(g_dailyDrawdown.remainingAllowance < 0.0) g_dailyDrawdown.remainingAllowance = 0.0; // Assign the most severe state first so a breach can never be // incorrectly classified as merely critical or warning. if(g_dailyDrawdown.limitUsagePercent >= 100.0) g_dailyDrawdown.status = COMPLIANCE_BREACHED; else if(g_dailyDrawdown.limitUsagePercent >= InpCriticalLevelPercent) g_dailyDrawdown.status = COMPLIANCE_CRITICAL; else if(g_dailyDrawdown.limitUsagePercent >= InpWarningLevelPercent) g_dailyDrawdown.status = COMPLIANCE_WARNING; else g_dailyDrawdown.status = COMPLIANCE_SAFE; } //+------------------------------------------------------------------+ //| Calculate overall drawdown | //+------------------------------------------------------------------+ void CalculateOverallDrawdown() { // Use the initial challenge balance as the permanent reference. g_overallDrawdown.initialBalance = g_accountSettings.initialBalance; // Equity is used because open positions can move the account toward // a breach before their profit or loss becomes realized. g_overallDrawdown.currentEquity = g_accountState.equity; // Convert the configured percentage into the maximum monetary loss. g_overallDrawdown.maximumOverallLoss = g_overallDrawdown.initialBalance * g_accountSettings.overallDrawdownPercent / 100.0; // The static equity floor never moves as the account gains profit. g_overallDrawdown.overallFloor = g_overallDrawdown.initialBalance - g_overallDrawdown.maximumOverallLoss; // Measure the current loss relative to the initial challenge balance. g_overallDrawdown.currentOverallLoss = g_overallDrawdown.initialBalance - g_overallDrawdown.currentEquity; // Equity above the initial balance means no overall drawdown is in use. if(g_overallDrawdown.currentOverallLoss < 0.0) g_overallDrawdown.currentOverallLoss = 0.0; // Calculate how much of the permitted overall loss has been consumed. if(g_overallDrawdown.maximumOverallLoss > 0.0) { g_overallDrawdown.limitUsagePercent = g_overallDrawdown.currentOverallLoss / g_overallDrawdown.maximumOverallLoss * 100.0; } else { g_overallDrawdown.limitUsagePercent = 0.0; } // Remaining allowance is the distance between current equity and // the static overall drawdown floor. g_overallDrawdown.remainingAllowance = g_overallDrawdown.maximumOverallLoss - g_overallDrawdown.currentOverallLoss; if(g_overallDrawdown.remainingAllowance < 0.0) g_overallDrawdown.remainingAllowance = 0.0; // Evaluate the current severity using the common warning thresholds. if(g_overallDrawdown.limitUsagePercent >= 100.0) g_overallDrawdown.status = COMPLIANCE_BREACHED; else if(g_overallDrawdown.limitUsagePercent >= InpCriticalLevelPercent) g_overallDrawdown.status = COMPLIANCE_CRITICAL; else if(g_overallDrawdown.limitUsagePercent >= InpWarningLevelPercent) g_overallDrawdown.status = COMPLIANCE_WARNING; else g_overallDrawdown.status = COMPLIANCE_SAFE; } //+------------------------------------------------------------------+ //| Calculate profit target | //+------------------------------------------------------------------+ void CalculateProfitTarget() { // Use the original challenge balance as the fixed target reference. g_profitTarget.initialBalance = g_accountSettings.initialBalance; // Profit-target completion is based on realized account balance. g_profitTarget.currentBalance = g_accountState.balance; // Convert the configured target percentage into account currency. g_profitTarget.targetAmount = g_profitTarget.initialBalance * g_accountSettings.profitTargetPercent / 100.0; // Add the required profit to the initial balance. g_profitTarget.targetBalance = g_profitTarget.initialBalance + g_profitTarget.targetAmount; // Measure realized profit above the original account balance. g_profitTarget.currentProfit = g_profitTarget.currentBalance - g_profitTarget.initialBalance; // A balance below the initial value represents no target progress. if(g_profitTarget.currentProfit < 0.0) g_profitTarget.currentProfit = 0.0; // Calculate the percentage of the target already completed. if(g_profitTarget.targetAmount > 0.0) { g_profitTarget.progressPercent = g_profitTarget.currentProfit / g_profitTarget.targetAmount * 100.0; } else { g_profitTarget.progressPercent = 0.0; } // Target progress does not need to display more than 100%. if(g_profitTarget.progressPercent > 100.0) g_profitTarget.progressPercent = 100.0; // Calculate how much additional realized profit is still required. g_profitTarget.remainingProfit = g_profitTarget.targetAmount - g_profitTarget.currentProfit; if(g_profitTarget.remainingProfit < 0.0) g_profitTarget.remainingProfit = 0.0; // Determine the current target state. if(g_profitTarget.currentBalance >= g_profitTarget.targetBalance) g_profitTarget.status = TARGET_REACHED; else if(g_profitTarget.progressPercent >= 90.0) g_profitTarget.status = TARGET_NEARING; else g_profitTarget.status = TARGET_IN_PROGRESS; } //+------------------------------------------------------------------+ //| Initialize news state | //+------------------------------------------------------------------+ void InitializeNewsState() { g_newsState.hasExposure = false; g_newsState.monitoredCurrencies = 0; g_newsState.currencyList = ""; g_newsState.eventFound = false; g_newsState.eventCurrency = ""; g_newsState.eventId = 0; g_newsState.eventName = ""; g_newsState.importance = CALENDAR_IMPORTANCE_NONE; g_newsState.eventTime = 0; g_newsState.secondsToEvent = 0; g_newsState.restrictionStart = 0; g_newsState.restrictionEnd = 0; g_newsState.restrictedWindow = false; g_newsState.affectedPositions = 0; g_newsState.protectionPositions = 0; g_newsState.status = InpEnableNewsFilter ? NEWS_NO_EXPOSURE : NEWS_DISABLED; g_newsState.holdingStatus = NEWS_HOLDING_CLEAR; } //+------------------------------------------------------------------+ //| Initialize alert state | //+------------------------------------------------------------------+ void InitializeAlertState() { g_alertState.dailyDrawdownStatus = g_dailyDrawdown.status; g_alertState.overallDrawdownStatus = g_overallDrawdown.status; g_alertState.profitTargetStatus = g_profitTarget.status; g_alertState.newsEventId = 0; g_alertState.newsApproachAlerted = false; g_alertState.newsRestrictedAlerted = false; g_alertState.dashboardMessage = ""; } //+------------------------------------------------------------------+ //| Check whether currency already exists in array | //+------------------------------------------------------------------+ bool CurrencyExists(const string ¤cies[], const string currency) { int count = ArraySize(currencies); for(int i = 0; i < count; i++) { if(currencies[i] == currency) return(true); } return(false); } //+------------------------------------------------------------------+ //| Add unique currency to array | //+------------------------------------------------------------------+ bool AddUniqueCurrency(string ¤cies[], const string currency) { // Empty currencies do not represent useful Economic Calendar exposure. if(currency == "") return(true); // Do not duplicate currencies already collected from another position. if(CurrencyExists(currencies, currency)) return(true); int currentSize = ArraySize(currencies); // Extend the dynamic array by one element. if(ArrayResize(currencies, currentSize + 1) != currentSize + 1) { Print("Prop Firm Guard: Could not expand monitored currency array."); return(false); } currencies[currentSize] = currency; return(true); } //+------------------------------------------------------------------+ //| Get symbol currencies relevant to news | //+------------------------------------------------------------------+ bool GetSymbolNewsCurrencies(const string symbol, string ¤cy1, string ¤cy2) { string baseCurrency; string profitCurrency; long calculationMode = 0; currency1 = ""; currency2 = ""; // Read the currencies defined by the broker for this symbol. ResetLastError(); if(!SymbolInfoString(symbol, SYMBOL_CURRENCY_BASE, baseCurrency)) { Print("Prop Firm Guard: Could not read base currency for ", symbol, ". Error: ", GetLastError()); return(false); } ResetLastError(); if(!SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT, profitCurrency)) { Print("Prop Firm Guard: Could not read profit currency for ", symbol, ". Error: ", GetLastError()); return(false); } // XAUUSD receives special treatment. Gold itself has no corresponding // currency-calendar stream, so USD news represents its macro exposure. if(baseCurrency == "XAU" && profitCurrency == "USD") { currency1 = "USD"; return(true); } // Confirm that the symbol uses one of MetaTrader's Forex calculation // models before treating its currencies as Forex news exposure. ResetLastError(); if(!SymbolInfoInteger(symbol, SYMBOL_TRADE_CALC_MODE, calculationMode)) { Print("Prop Firm Guard: Could not read calculation mode for ", symbol, ". Error: ", GetLastError()); return(false); } if(calculationMode != SYMBOL_CALC_MODE_FOREX && calculationMode != SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE) return(true); currency1 = baseCurrency; currency2 = profitCurrency; return(true); } //+------------------------------------------------------------------+ //| Add currencies exposed by position symbol | //+------------------------------------------------------------------+ bool AddPositionCurrencies(const string symbol, string ¤cies[]) { string currency1; string currency2; if(!GetSymbolNewsCurrencies(symbol, currency1, currency2)) return(false); // An empty result means this instrument is intentionally unsupported. if(currency1 == "" && currency2 == "") return(true); if(!AddUniqueCurrency(currencies, currency1)) return(false); if(!AddUniqueCurrency(currencies, currency2)) return(false); return(true); } //+------------------------------------------------------------------+ //| Collect currencies from active positions | //+------------------------------------------------------------------+ bool CollectExposureCurrencies(string ¤cies[]) { ArrayResize(currencies, 0); int totalPositions = PositionsTotal(); // No open positions means there is currently no news exposure. if(totalPositions == 0) return(true); for(int i = 0; i < totalPositions; i++) { ResetLastError(); // PositionGetSymbol() returns the symbol at the requested position // index and selects that position for subsequent position access. string symbol = PositionGetSymbol(i); if(symbol == "") { Print("Prop Firm Guard: Could not retrieve position symbol at index ", i, ". Error: ", GetLastError()); return(false); } if(!AddPositionCurrencies(symbol, currencies)) return(false); } return(true); } //+------------------------------------------------------------------+ //| Build monitored currency list | //+------------------------------------------------------------------+ string BuildCurrencyList(const string ¤cies[]) { string result = ""; int count = ArraySize(currencies); for(int i = 0; i < count; i++) { if(i > 0) result += ", "; result += currencies[i]; } return(result); } //+------------------------------------------------------------------+ //| Find next news event for currency | //+------------------------------------------------------------------+ bool FindNextNewsForCurrency(const string currency, const datetime timeFrom, const datetime timeTo, MqlCalendarValue &bestValue, MqlCalendarEvent &bestEvent) { MqlCalendarValue values[]; ResetLastError(); // Request calendar values only for the specified currency and time range. int valueCount = CalendarValueHistory(values, timeFrom, timeTo, NULL, currency); if(valueCount < 0) { Print("Prop Firm Guard: Could not retrieve calendar values for ", currency, ". Error: ", GetLastError()); return(false); } bool eventFound = false; datetime earliestTime = 0; for(int i = 0; i < valueCount; i++) { MqlCalendarEvent event; ResetLastError(); // Retrieve the descriptive information associated with this value. if(!CalendarEventById(values[i].event_id, event)) { Print("Prop Firm Guard: Could not retrieve calendar event ", values[i].event_id, ". Error: ", GetLastError()); continue; } // Ignore events below the minimum importance selected by the user. if(event.importance < InpNewsImportance) continue; // Ignore events that do not have a useful exact release time. if(event.time_mode != CALENDAR_TIMEMODE_DATETIME) continue; // Keep the earliest matching event in the search interval. if(!eventFound || values[i].time < earliestTime) { bestValue = values[i]; bestEvent = event; earliestTime = values[i].time; eventFound = true; } } return(eventFound); } //+------------------------------------------------------------------+ //| Update news state | //+------------------------------------------------------------------+ void UpdateNewsState() { // Clear previous event information before running a fresh search. InitializeNewsState(); if(!InpEnableNewsFilter) { g_newsState.status = NEWS_DISABLED; return; } string currencies[]; // Build one deduplicated currency list from every supported open // position on the trading account. if(!CollectExposureCurrencies(currencies)) { g_newsState.status = NEWS_UNAVAILABLE; return; } int currencyCount = ArraySize(currencies); // No supported currency exposure means there is nothing for the // Economic Calendar engine to monitor. if(currencyCount == 0) { g_newsState.hasExposure = false; g_newsState.monitoredCurrencies = 0; g_newsState.currencyList = ""; g_newsState.status = NEWS_NO_EXPOSURE; return; } g_newsState.hasExposure = true; g_newsState.monitoredCurrencies = currencyCount; g_newsState.currencyList = BuildCurrencyList(currencies); // Economic Calendar functions operate using trade-server time. datetime currentTime = TimeTradeServer(); // Include the recent past so an event remains selected throughout its // configured post-news restriction window. datetime searchFrom = currentTime - InpMinutesAfterNews * 60; // Search up to one day ahead for the next relevant event. datetime searchTo = currentTime + 24 * 60 * 60; bool foundAny = false; // Store the selected event in initialized scalar values. This avoids // possible-uninitialized compiler warnings for MqlCalendarValue objects. ulong selectedEventId = 0; string selectedEventName = ""; string selectedCurrency = ""; ENUM_CALENDAR_EVENT_IMPORTANCE selectedImportance = CALENDAR_IMPORTANCE_NONE; datetime selectedEventTime = 0; // Search every unique currency currently represented by an open // supported account position. for(int i = 0; i < currencyCount; i++) { MqlCalendarValue candidateValue; MqlCalendarEvent candidateEvent; if(!FindNextNewsForCurrency(currencies[i], searchFrom, searchTo, candidateValue, candidateEvent)) continue; // Keep the first qualifying event, then replace it only when another // exposed currency has an earlier qualifying event. if(!foundAny || candidateValue.time < selectedEventTime) { selectedEventId = candidateEvent.id; selectedEventName = candidateEvent.name; selectedCurrency = currencies[i]; selectedImportance = candidateEvent.importance; selectedEventTime = candidateValue.time; foundAny = true; } } if(!foundAny) { g_newsState.status = NEWS_NO_EVENT; return; } // Store the nearest qualifying event across the complete account exposure. g_newsState.eventFound = true; g_newsState.eventCurrency = selectedCurrency; g_newsState.eventId = selectedEventId; g_newsState.eventName = selectedEventName; g_newsState.importance = selectedImportance; g_newsState.eventTime = selectedEventTime; // Positive values mean the event is still ahead. Negative values mean // the event has already occurred. g_newsState.secondsToEvent = (long)(g_newsState.eventTime - currentTime); // Build the complete prohibited/restricted interval. g_newsState.restrictionStart = g_newsState.eventTime - InpMinutesBeforeNews * 60; g_newsState.restrictionEnd = g_newsState.eventTime + InpMinutesAfterNews * 60; // Determine whether the current trade-server time is inside that window. g_newsState.restrictedWindow = (currentTime >= g_newsState.restrictionStart && currentTime <= g_newsState.restrictionEnd); if(g_newsState.restrictedWindow) g_newsState.status = NEWS_RESTRICTED; else g_newsState.status = NEWS_SAFE; } //+------------------------------------------------------------------+ //| Match open positions to selected news event | //+------------------------------------------------------------------+ void MatchOpenPositionsToNews() { // Remove classifications from the previous monitoring cycle. ArrayResize(g_newsPositionMatches, 0); // Position matching requires an actual selected calendar event. if(!g_newsState.eventFound || g_newsState.eventCurrency == "") return; int totalPositions = PositionsTotal(); for(int i = 0; i < totalPositions; i++) { ResetLastError(); // PositionGetTicket() selects the indexed position so its remaining // properties can be read immediately afterward. ulong ticket = PositionGetTicket(i); if(ticket == 0) { Print("Prop Firm Guard: Could not retrieve position ticket at index ", i, ". Error: ", GetLastError()); continue; } string symbol; if(!PositionGetString(POSITION_SYMBOL, symbol)) { Print("Prop Firm Guard: Could not retrieve symbol for position ", ticket, ". Error: ", GetLastError()); continue; } string currency1; string currency2; if(!GetSymbolNewsCurrencies(symbol, currency1, currency2)) continue; SNewsPositionMatch match; match.ticket = ticket; match.symbol = symbol; match.currency1 = currency1; match.currency2 = currency2; match.status = NEWS_POSITION_NOT_AFFECTED; match.protectionRequired = false; // Empty currencies identify instruments outside the current // Forex and XAUUSD news-monitoring scope. if(currency1 == "" && currency2 == "") { match.status = NEWS_POSITION_UNSUPPORTED; } else if(currency1 == g_newsState.eventCurrency || currency2 == g_newsState.eventCurrency) { match.status = NEWS_POSITION_AFFECTED; } int currentSize = ArraySize(g_newsPositionMatches); if(ArrayResize(g_newsPositionMatches, currentSize + 1) != currentSize + 1) { Print("Prop Firm Guard: Could not expand news position match array."); return; } g_newsPositionMatches[currentSize] = match; } } //+------------------------------------------------------------------+ //| Evaluate news holding rules | //+------------------------------------------------------------------+ void EvaluateNewsHoldingRules() { g_newsState.affectedPositions = 0; g_newsState.protectionPositions = 0; g_newsState.holdingStatus = NEWS_HOLDING_CLEAR; int totalMatches = ArraySize(g_newsPositionMatches); // Nothing needs holding-rule evaluation without a selected event. if(!g_newsState.eventFound || totalMatches == 0) return; // First count every position affected by the selected event. for(int i = 0; i < totalMatches; i++) { // Always reset protection state before evaluating the current window. g_newsPositionMatches[i].protectionRequired = false; if(g_newsPositionMatches[i].status == NEWS_POSITION_AFFECTED) g_newsState.affectedPositions++; } // No active position is exposed to this particular event. if(g_newsState.affectedPositions == 0) return; // When holding is allowed, affected positions remain untouched even // while the event is inside its configured restricted window. if(InpAllowNewsHolding) { if(g_newsState.restrictedWindow) g_newsState.holdingStatus = NEWS_HOLDING_ALLOWED; else g_newsState.holdingStatus = NEWS_HOLDING_WARNING; return; } // Holding is prohibited. Outside the restricted window we warn the // trader that affected positions are exposed to an approaching event. if(!g_newsState.restrictedWindow) { g_newsState.holdingStatus = NEWS_HOLDING_WARNING; return; } // The restricted window is now active and holding is not permitted. // Mark only affected positions for the protection subsystem. for(int i = 0; i < totalMatches; i++) { if(g_newsPositionMatches[i].status != NEWS_POSITION_AFFECTED) continue; g_newsPositionMatches[i].protectionRequired = true; g_newsState.protectionPositions++; } g_newsState.holdingStatus = NEWS_HOLDING_PROTECTION; } //+------------------------------------------------------------------+ //| Count news-affected positions | //+------------------------------------------------------------------+ int CountAffectedNewsPositions() { int affectedCount = 0; int totalMatches = ArraySize(g_newsPositionMatches); for(int i = 0; i < totalMatches; i++) { if(g_newsPositionMatches[i].status == NEWS_POSITION_AFFECTED) affectedCount++; } return(affectedCount); } //+------------------------------------------------------------------+ //| Build affected symbol list | //+------------------------------------------------------------------+ string BuildAffectedSymbolList() { string result = ""; int totalMatches = ArraySize(g_newsPositionMatches); for(int i = 0; i < totalMatches; i++) { if(g_newsPositionMatches[i].status != NEWS_POSITION_AFFECTED) continue; // Avoid repeating the same symbol when a hedging account contains // several positions for that instrument. string token = g_newsPositionMatches[i].symbol; if(StringFind("," + result + ",", "," + token + ",") >= 0) continue; if(result != "") result += ","; result += token; } if(result == "") return("NONE"); return(result); } //+------------------------------------------------------------------+ //| Print news position matches | //+------------------------------------------------------------------+ void PrintNewsPositionMatches() { if(!g_newsState.eventFound) return; Print("--------------------------------------------------"); Print("Prop Firm Guard: News position classification"); Print("Event: ", g_newsState.eventCurrency, " | ", g_newsState.eventName); int totalMatches = ArraySize(g_newsPositionMatches); if(totalMatches == 0) { Print("No open positions available for classification."); Print("--------------------------------------------------"); return; } for(int i = 0; i < totalMatches; i++) { Print("Ticket ", g_newsPositionMatches[i].ticket, " | ", g_newsPositionMatches[i].symbol, " | ", NewsPositionStatusToString(g_newsPositionMatches[i].status)); } Print("--------------------------------------------------"); } //+------------------------------------------------------------------+ //| Update news position matches | //+------------------------------------------------------------------+ void UpdateNewsPositionMatches() { // Classify all currently open positions against the selected event. MatchOpenPositionsToNews(); // Apply the configured holding rule to those classifications. EvaluateNewsHoldingRules(); // Reset event tracking when no calendar event is currently selected. if(!g_newsState.eventFound) { g_lastMatchedEventId = 0; return; } // Print the complete classification only when a different calendar // event becomes active. if(g_newsState.eventId != g_lastMatchedEventId) { PrintNewsPositionMatches(); g_lastMatchedEventId = g_newsState.eventId; } } //+------------------------------------------------------------------+ //| Convert news status to text | //+------------------------------------------------------------------+ string NewsStatusToString(const ENUM_NEWS_STATUS status) { switch(status) { case NEWS_DISABLED: return("DISABLED"); case NEWS_NO_EXPOSURE: return("NO EXPOSURE"); case NEWS_NO_EVENT: return("NO EVENT"); case NEWS_SAFE: return("SAFE"); case NEWS_RESTRICTED: return("RESTRICTED"); case NEWS_UNAVAILABLE: default: return("UNAVAILABLE"); } } //+------------------------------------------------------------------+ //| Convert news position status to text | //+------------------------------------------------------------------+ string NewsPositionStatusToString(const ENUM_NEWS_POSITION_STATUS status) { switch(status) { case NEWS_POSITION_AFFECTED: return("AFFECTED"); case NEWS_POSITION_UNSUPPORTED: return("UNSUPPORTED"); case NEWS_POSITION_NOT_AFFECTED: default: return("NOT AFFECTED"); } } //+------------------------------------------------------------------+ //| Convert calendar importance to text | //+------------------------------------------------------------------+ string CalendarImportanceToString(const ENUM_CALENDAR_EVENT_IMPORTANCE importance) { switch(importance) { case CALENDAR_IMPORTANCE_HIGH: return("HIGH"); case CALENDAR_IMPORTANCE_MODERATE: return("MODERATE"); case CALENDAR_IMPORTANCE_LOW: return("LOW"); case CALENDAR_IMPORTANCE_NONE: default: return("NONE"); } } //+------------------------------------------------------------------+ //| Format news countdown | //+------------------------------------------------------------------+ string FormatNewsCountdown(const long secondsToEvent) { long absoluteSeconds = secondsToEvent; if(absoluteSeconds < 0) absoluteSeconds = -absoluteSeconds; int hours = (int)(absoluteSeconds / 3600); int minutes = (int)((absoluteSeconds % 3600) / 60); int seconds = (int)(absoluteSeconds % 60); string formatted = StringFormat("%02d:%02d:%02d", hours, minutes, seconds); if(secondsToEvent >= 0) return(formatted); return("+" + formatted); } //+------------------------------------------------------------------+ //| Convert compliance status to text | //+------------------------------------------------------------------+ string ComplianceStatusToString(const ENUM_COMPLIANCE_STATUS status) { switch(status) { case COMPLIANCE_WARNING: return("WARNING"); case COMPLIANCE_CRITICAL: return("CRITICAL"); case COMPLIANCE_BREACHED: return("BREACHED"); case COMPLIANCE_SAFE: default: return("SAFE"); } } //+------------------------------------------------------------------+ //| Convert target status to text | //+------------------------------------------------------------------+ string TargetStatusToString(const ENUM_TARGET_STATUS status) { switch(status) { case TARGET_NEARING: return("NEARING TARGET"); case TARGET_REACHED: return("TARGET REACHED"); case TARGET_IN_PROGRESS: default: return("IN PROGRESS"); } } //+------------------------------------------------------------------+ //| Convert news holding status to text | //+------------------------------------------------------------------+ string NewsHoldingStatusToString(const ENUM_NEWS_HOLDING_STATUS status) { switch(status) { case NEWS_HOLDING_ALLOWED: return("HOLDING ALLOWED"); case NEWS_HOLDING_WARNING: return("EVENT APPROACHING"); case NEWS_HOLDING_PROTECTION: return("PROTECTION REQUIRED"); case NEWS_HOLDING_CLEAR: default: return("CLEAR"); } } //+------------------------------------------------------------------+ //| Check successful position-close return code | //+------------------------------------------------------------------+ bool IsSuccessfulCloseRetcode(const uint retcode) { if(retcode == TRADE_RETCODE_DONE || retcode == TRADE_RETCODE_POSITION_CLOSED) return(true); return(false); } //+------------------------------------------------------------------+ //| Close position by ticket | //+------------------------------------------------------------------+ bool ClosePositionByTicket(const ulong ticket, const string reason) { ResetLastError(); // Confirm that the position still exists before attempting to close it. if(!PositionSelectByTicket(ticket)) { int errorCode = GetLastError(); // A position may already have disappeared because another protection // action or manual operation closed it. Print("Prop Firm Guard: Position ", ticket, " is no longer available. Error: ", errorCode); return(true); } string symbol; if(!PositionGetString(POSITION_SYMBOL, symbol)) { Print("Prop Firm Guard: Could not read symbol for position ", ticket, ". Error: ", GetLastError()); return(false); } // Configure CTrade using the filling policies allowed for this symbol. if(!g_trade.SetTypeFillingBySymbol(symbol)) { Print("Prop Firm Guard: Could not determine filling policy for ", symbol, "."); return(false); } ResetLastError(); // PositionClose() performs the basic request submission, but its Boolean // result alone is not sufficient to confirm execution by the trade server. bool requestAccepted = g_trade.PositionClose(ticket); uint retcode = g_trade.ResultRetcode(); string retcodeDescription = g_trade.ResultRetcodeDescription(); if(!requestAccepted) { Print("Prop Firm Guard: Close request failed for ticket ", ticket, " [", symbol, "]. Reason: ", reason, ". Retcode: ", retcode, " - ", retcodeDescription, ". Runtime error: ", GetLastError()); return(false); } // Always inspect the server return code after PositionClose(). if(!IsSuccessfulCloseRetcode(retcode)) { Print("Prop Firm Guard: Trade server did not confirm complete closure of ticket ", ticket, " [", symbol, "]. Reason: ", reason, ". Retcode: ", retcode, " - ", retcodeDescription); return(false); } // Verify that the position is no longer present after the synchronous // close operation. ResetLastError(); if(PositionSelectByTicket(ticket)) { Print("Prop Firm Guard: Position ", ticket, " [", symbol, "] still exists after close request."); return(false); } Print("Prop Firm Guard: Position ", ticket, " [", symbol, "] closed successfully. Reason: ", reason, "."); return(true); } //+------------------------------------------------------------------+ //| Close all open positions | //+------------------------------------------------------------------+ bool CloseAllPositions(const string reason) { int totalPositions = PositionsTotal(); if(totalPositions == 0) return(true); ulong tickets[]; if(ArrayResize(tickets, totalPositions) != totalPositions) { Print("Prop Firm Guard: Could not allocate position ticket array."); return(false); } int ticketCount = 0; // Build a stable ticket list before modifying the open-position pool. for(int i = 0; i < totalPositions; i++) { ResetLastError(); ulong ticket = PositionGetTicket(i); if(ticket == 0) { Print("Prop Firm Guard: Could not retrieve position ticket at index ", i, ". Error: ", GetLastError()); continue; } tickets[ticketCount] = ticket; ticketCount++; } bool allClosed = true; for(int i = 0; i < ticketCount; i++) { if(!ClosePositionByTicket(tickets[i], reason)) allClosed = false; } return(allClosed); } //+------------------------------------------------------------------+ //| Close news-affected positions | //+------------------------------------------------------------------+ bool CloseNewsAffectedPositions() { ulong tickets[]; int totalMatches = ArraySize(g_newsPositionMatches); int ticketCount = 0; // First collect all affected tickets so closing one position cannot // invalidate our traversal of the match array. for(int i = 0; i < totalMatches; i++) { if(!g_newsPositionMatches[i].protectionRequired) continue; int newSize = ticketCount + 1; if(ArrayResize(tickets, newSize) != newSize) { Print("Prop Firm Guard: Could not expand affected-position ticket array."); return(false); } tickets[ticketCount] = g_newsPositionMatches[i].ticket; ticketCount++; } if(ticketCount == 0) return(true); bool allClosed = true; string reason = "News restriction: " + g_newsState.eventCurrency + " " + g_newsState.eventName; for(int i = 0; i < ticketCount; i++) { if(!ClosePositionByTicket(tickets[i], reason)) allClosed = false; } return(allClosed); } //+------------------------------------------------------------------+ //| Handle daily drawdown protection | //+------------------------------------------------------------------+ void HandleDailyDrawdownProtection() { if(!g_dailySession.balanceConfirmed) return; if(g_dailyDrawdown.status != COMPLIANCE_BREACHED) { g_dailyBreachWarned = false; return; } switch(InpDailyDrawdownAction) { case PROTECTION_WARN_ONLY: if(!g_dailyBreachWarned) { Alert("Prop Firm Guard: Daily drawdown limit breached."); Print("Prop Firm Guard: Daily drawdown breached. Action: WARN ONLY."); g_dailyBreachWarned = true; } break; case PROTECTION_CLOSE_AFFECTED: case PROTECTION_CLOSE_ALL: // Daily drawdown is an account-wide rule, therefore every open // position contributes to the breached account condition. if(PositionsTotal() > 0) { if(!CloseAllPositions("Daily drawdown breach")) Print("Prop Firm Guard: One or more positions could not be closed after daily drawdown breach."); } break; } } //+------------------------------------------------------------------+ //| Handle overall drawdown protection | //+------------------------------------------------------------------+ void HandleOverallDrawdownProtection() { if(g_overallDrawdown.status != COMPLIANCE_BREACHED) { g_overallBreachWarned = false; return; } switch(InpOverallDrawdownAction) { case PROTECTION_WARN_ONLY: if(!g_overallBreachWarned) { Alert("Prop Firm Guard: Overall drawdown limit breached."); Print("Prop Firm Guard: Overall drawdown breached. Action: WARN ONLY."); g_overallBreachWarned = true; } break; case PROTECTION_CLOSE_AFFECTED: case PROTECTION_CLOSE_ALL: // Overall drawdown applies to the complete trading account. if(PositionsTotal() > 0) { if(!CloseAllPositions("Overall drawdown breach")) Print("Prop Firm Guard: One or more positions could not be closed after overall drawdown breach."); } break; } } //+------------------------------------------------------------------+ //| Handle news protection | //+------------------------------------------------------------------+ void HandleNewsProtection() { // Protection is needed only when Phase 11 determined that holding is // prohibited and the restricted window is currently active. if(g_newsState.holdingStatus != NEWS_HOLDING_PROTECTION) return; if(!g_newsState.eventFound) return; switch(InpNewsRestrictionAction) { case PROTECTION_WARN_ONLY: // Warn only once for this particular Economic Calendar event. if(g_lastNewsWarnedEventId != g_newsState.eventId) { Alert("Prop Firm Guard: News holding restriction active for ", g_newsState.eventCurrency, " ", g_newsState.eventName, "."); Print("Prop Firm Guard: News protection required. Action: WARN ONLY."); g_lastNewsWarnedEventId = g_newsState.eventId; } break; case PROTECTION_CLOSE_AFFECTED: if(g_newsState.protectionPositions > 0) { if(!CloseNewsAffectedPositions()) Print("Prop Firm Guard: One or more news-affected positions could not be closed."); } break; case PROTECTION_CLOSE_ALL: if(PositionsTotal() > 0) { string reason = "News holding restriction: " + g_newsState.eventCurrency + " " + g_newsState.eventName; if(!CloseAllPositions(reason)) Print("Prop Firm Guard: One or more positions could not be closed during news restriction."); } break; } } //+------------------------------------------------------------------+ //| Manage protection actions | //+------------------------------------------------------------------+ void ManageProtectionActions() { // Financial breaches receive priority because they represent direct // account-rule violations. HandleDailyDrawdownProtection(); HandleOverallDrawdownProtection(); // News protection then applies to any positions that remain open. HandleNewsProtection(); } //+------------------------------------------------------------------+ //| Convert protection action to text | //+------------------------------------------------------------------+ string ProtectionActionToString(const ENUM_PROTECTION_ACTION action) { switch(action) { case PROTECTION_CLOSE_AFFECTED: return("CLOSE AFFECTED"); case PROTECTION_CLOSE_ALL: return("CLOSE ALL"); case PROTECTION_WARN_ONLY: default: return("WARN ONLY"); } } //+------------------------------------------------------------------+ //| Get news trading-window text | //+------------------------------------------------------------------+ string GetNewsTradingWindowText() { if(!InpEnableNewsFilter) return("DISABLED"); if(!g_newsState.eventFound) return("SAFE"); if(g_newsState.restrictedWindow) return("RESTRICTED"); return("SAFE"); } //+------------------------------------------------------------------+ //| Get configured news holding rule | //+------------------------------------------------------------------+ string GetNewsHoldingRuleText() { if(InpAllowNewsHolding) return("ALLOWED"); return("NOT ALLOWED"); } //+------------------------------------------------------------------+ //| Get overall guard status | //+------------------------------------------------------------------+ ENUM_GUARD_STATUS GetOverallGuardStatus() { // A breach always has the highest priority. if(g_dailyDrawdown.status == COMPLIANCE_BREACHED || g_overallDrawdown.status == COMPLIANCE_BREACHED) return(GUARD_BREACHED); // Critical conditions take priority over warnings and target success. if(g_dailyDrawdown.status == COMPLIANCE_CRITICAL || g_overallDrawdown.status == COMPLIANCE_CRITICAL) return(GUARD_CRITICAL); // A warning still takes priority over a reached profit target because // the account may be profitable while simultaneously approaching a loss limit. if(g_dailyDrawdown.status == COMPLIANCE_WARNING || g_overallDrawdown.status == COMPLIANCE_WARNING) return(GUARD_WARNING); // Report target completion only when no higher-risk condition exists. if(g_profitTarget.status == TARGET_REACHED) return(GUARD_TARGET_REACHED); return(GUARD_SAFE); } //+------------------------------------------------------------------+ //| Convert guard status to text | //+------------------------------------------------------------------+ string GuardStatusToString(const ENUM_GUARD_STATUS status) { switch(status) { case GUARD_BREACHED: return("BREACHED"); case GUARD_CRITICAL: return("CRITICAL"); case GUARD_WARNING: return("WARNING"); case GUARD_TARGET_REACHED: return("TARGET REACHED"); case GUARD_SAFE: default: return("SAFE"); } } //+------------------------------------------------------------------+ //| Get guard status color | //+------------------------------------------------------------------+ color GetGuardStatusColor(const ENUM_GUARD_STATUS status) { switch(status) { case GUARD_BREACHED: case GUARD_CRITICAL: return(clrRed); case GUARD_WARNING: return(clrDarkOrange); case GUARD_TARGET_REACHED: return(clrGreen); case GUARD_SAFE: default: return(clrGreen); } } //+------------------------------------------------------------------+ //| Open database | //+------------------------------------------------------------------+ bool OpenDatabase() { // Open the database for reading and writing, creating the file // automatically when Prop Firm Guard runs for the first time. ResetLastError(); g_database = DatabaseOpen(g_databaseName, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE); if(g_database == INVALID_HANDLE) { Print("Prop Firm Guard: Could not open database ", g_databaseName, ". Error: ", GetLastError()); return(false); } Print("Prop Firm Guard: Database opened successfully: ", g_databaseName); return(true); } /* //+------------------------------------------------------------------+ //| Ensure database tables | //+------------------------------------------------------------------+ bool EnsureDatabaseTables() { // Account settings persist the main prop-firm rules independently // for each combination of account login and trading server. string settingsSql = "CREATE TABLE IF NOT EXISTS settings (" "account_login INTEGER NOT NULL," "account_server TEXT NOT NULL," "initial_balance REAL NOT NULL," "daily_drawdown_percent REAL NOT NULL," "overall_drawdown_percent REAL NOT NULL," "profit_target_percent REAL NOT NULL," "updated_at INTEGER NOT NULL," "PRIMARY KEY(account_login, account_server)" ");"; // Daily sessions hold values that belong to one specific trading day. // The daily starting values remain zero until Phase 3 confirms them. string sessionsSql = "CREATE TABLE IF NOT EXISTS daily_sessions (" "account_login INTEGER NOT NULL," "account_server TEXT NOT NULL," "trading_date TEXT NOT NULL," "starting_balance REAL NOT NULL DEFAULT 0," "starting_equity REAL NOT NULL DEFAULT 0," "balance_confirmed INTEGER NOT NULL DEFAULT 0," "created_at INTEGER NOT NULL," "updated_at INTEGER NOT NULL," "PRIMARY KEY(account_login, account_server, trading_date)" ");"; ResetLastError(); if(!DatabaseExecute(g_database, settingsSql)) { Print("Prop Firm Guard: Could not create settings table. Error: ", GetLastError()); return(false); } ResetLastError(); if(!DatabaseExecute(g_database, sessionsSql)) { Print("Prop Firm Guard: Could not create daily_sessions table. Error: ", GetLastError()); return(false); } Print("Prop Firm Guard: Database tables are ready."); return(true); } */ //+------------------------------------------------------------------+ //| Close database | //+------------------------------------------------------------------+ void CloseDatabase() { // Do nothing if no valid database connection currently exists. if(g_database == INVALID_HANDLE) return; ResetLastError(); DatabaseClose(g_database); int errorCode = GetLastError(); if(errorCode != 0) { Print("Prop Firm Guard: Database close reported error: ", errorCode); } else { Print("Prop Firm Guard: Database closed successfully."); } // Never keep a handle after the database has been closed. g_database = INVALID_HANDLE; } //+------------------------------------------------------------------+ //| Check whether database column exists | //+------------------------------------------------------------------+ bool DatabaseColumnExists(const string tableName, const string columnName) { string sql = "PRAGMA table_info(" + tableName + ");"; ResetLastError(); int request = DatabasePrepare(g_database, sql); if(request == INVALID_HANDLE) { Print("Prop Firm Guard: Could not inspect table ", tableName, ". Error: ", GetLastError()); return(false); } bool columnFound = false; while(true) { ResetLastError(); if(!DatabaseRead(request)) { int errorCode = GetLastError(); if(errorCode != ERR_DATABASE_NO_MORE_DATA) { Print("Prop Firm Guard: Could not read schema for ", tableName, ". Error: ", errorCode); } break; } string currentColumn; // PRAGMA table_info returns the column name at index 1. if(!DatabaseColumnText(request, 1, currentColumn)) { Print("Prop Firm Guard: Could not read database column name. Error: ", GetLastError()); break; } if(currentColumn == columnName) { columnFound = true; break; } } FinalizeDatabaseRequest(request); return(columnFound); } //+------------------------------------------------------------------+ //| Ensure database column exists | //+------------------------------------------------------------------+ bool EnsureDatabaseColumn(const string tableName, const string columnName, const string columnDefinition) { if(DatabaseColumnExists(tableName, columnName)) return(true); string sql = "ALTER TABLE " + tableName + " ADD COLUMN " + columnName + " " + columnDefinition + ";"; ResetLastError(); if(!DatabaseExecute(g_database, sql)) { Print("Prop Firm Guard: Could not add column ", columnName, " to ", tableName, ". Error: ", GetLastError()); return(false); } Print("Prop Firm Guard: Added database column ", tableName, ".", columnName, "."); return(true); } //+------------------------------------------------------------------+ //| Ensure runtime daily-session columns exist | //+------------------------------------------------------------------+ bool EnsureDailySessionRuntimeColumns() { if(!EnsureDatabaseColumn("daily_sessions", "latest_balance", "REAL NOT NULL DEFAULT 0")) return(false); if(!EnsureDatabaseColumn("daily_sessions", "latest_equity", "REAL NOT NULL DEFAULT 0")) return(false); if(!EnsureDatabaseColumn("daily_sessions", "daily_loss", "REAL NOT NULL DEFAULT 0")) return(false); if(!EnsureDatabaseColumn("daily_sessions", "maximum_daily_loss_reached", "REAL NOT NULL DEFAULT 0")) return(false); if(!EnsureDatabaseColumn("daily_sessions", "target_progress", "REAL NOT NULL DEFAULT 0")) return(false); if(!EnsureDatabaseColumn("daily_sessions", "session_status", "TEXT NOT NULL DEFAULT 'AWAITING_CONFIRMATION'")) return(false); return(true); } //+------------------------------------------------------------------+ //| Ensure database tables exist | //+------------------------------------------------------------------+ bool EnsureDatabaseTables() { string settingsTable = "CREATE TABLE IF NOT EXISTS settings (" "account_login INTEGER NOT NULL," "account_server TEXT NOT NULL," "initial_balance REAL NOT NULL," "daily_drawdown_percent REAL NOT NULL," "overall_drawdown_percent REAL NOT NULL," "profit_target_percent REAL NOT NULL," "updated_at INTEGER NOT NULL," "PRIMARY KEY(account_login, account_server)" ");"; string dailySessionsTable = "CREATE TABLE IF NOT EXISTS daily_sessions (" "account_login INTEGER NOT NULL," "account_server TEXT NOT NULL," "trading_date TEXT NOT NULL," "starting_balance REAL NOT NULL," "starting_equity REAL NOT NULL," "balance_confirmed INTEGER NOT NULL," "latest_balance REAL NOT NULL DEFAULT 0," "latest_equity REAL NOT NULL DEFAULT 0," "daily_loss REAL NOT NULL DEFAULT 0," "maximum_daily_loss_reached REAL NOT NULL DEFAULT 0," "target_progress REAL NOT NULL DEFAULT 0," "session_status TEXT NOT NULL DEFAULT 'AWAITING_CONFIRMATION'," "created_at INTEGER NOT NULL," "updated_at INTEGER NOT NULL," "PRIMARY KEY(account_login, account_server, trading_date)" ");"; ResetLastError(); if(!DatabaseExecute(g_database, settingsTable)) { Print("Prop Firm Guard: Could not create settings table. Error: ", GetLastError()); return(false); } ResetLastError(); if(!DatabaseExecute(g_database, dailySessionsTable)) { Print("Prop Firm Guard: Could not create daily_sessions table. Error: ", GetLastError()); return(false); } // CREATE TABLE IF NOT EXISTS does not add missing columns to an older // table, so migrate databases created by earlier EA versions. if(!EnsureDailySessionRuntimeColumns()) return(false); return(true); } //+------------------------------------------------------------------+ //| Check for new trading day | //+------------------------------------------------------------------+ void CheckForNewTradingDay() { string currentDate = GetTradingDate(); // The current database session already belongs to today. if(g_hasDailySession && g_dailySession.tradingDate == currentDate) return; Print("Prop Firm Guard: New trading day detected: ", currentDate); // Load today's record if one already exists, or create a new // unconfirmed session when this is the first launch today. if(!InitializeDailySession()) { Print("Prop Firm Guard: Could not initialize the new daily session."); return; } // Rebuild only the starting-balance controls so their state matches // the newly loaded daily session. DeleteDailySessionControls(); if(!CreateDailySessionControls()) { Print("Prop Firm Guard: Could not create daily session controls."); } } //+------------------------------------------------------------------+ //| Confirm daily starting balance | //+------------------------------------------------------------------+ bool ConfirmDailyStartingBalance() { // Never allow normal confirmation logic to overwrite a value that // has already been confirmed for the current trading day. if(g_dailySession.balanceConfirmed) { Print("Prop Firm Guard: Today's starting balance is already confirmed."); return(true); } string balanceText; ResetLastError(); // Read the amount entered in the dashboard edit field. if(!ObjectGetString(0, g_startBalanceEdit, OBJPROP_TEXT, 0, balanceText)) { Print("Prop Firm Guard: Could not read starting balance field. Error: ", GetLastError()); return(false); } double startingBalance = StringToDouble(balanceText); if(startingBalance <= 0.0) { Alert("Prop Firm Guard: Enter a valid starting balance greater than zero."); return(false); } // Store the user-confirmed starting values in memory. g_dailySession.startingBalance = startingBalance; g_dailySession.startingEquity = g_accountState.equity; g_dailySession.balanceConfirmed = true; g_dailySession.updatedAt = TimeCurrent(); // Persist the confirmed values immediately. if(!SaveDailySession()) { Print("Prop Firm Guard: Could not save confirmed daily session."); // Do not leave memory showing a confirmed state that SQLite failed // to preserve. g_dailySession.balanceConfirmed = false; return(false); } Print("Prop Firm Guard: Daily starting balance confirmed at ", DoubleToString(g_dailySession.startingBalance, 2), " for ", g_dailySession.tradingDate, "."); // The confirmation controls are no longer required after the value // has been safely stored. DeleteDailySessionControls(); UpdateDashboard(); return(true); } //+------------------------------------------------------------------+ //| Create daily session controls | //+------------------------------------------------------------------+ bool CreateDailySessionControls() { // A confirmed session does not require editable controls. if(g_dailySession.balanceConfirmed) return(true); int controlX = InpDashboardX + 18; int controlY = InpDashboardY + 120; // Suggest the current balance without treating it as confirmed. string suggestedBalance = DoubleToString(g_accountState.balance, 2); if(!CreateStartingBalanceEdit(controlX, controlY, suggestedBalance)) return(false); if(!CreateConfirmBalanceButton(controlX + 130, controlY)) return(false); return(true); } //+------------------------------------------------------------------+ //| Create starting balance edit | //+------------------------------------------------------------------+ bool CreateStartingBalanceEdit(const int x, const int y, const string suggestedBalance) { // Create an editable field where the trader may accept or change // the suggested starting balance. if(!ObjectCreate(0, g_startBalanceEdit, OBJ_EDIT, 0, 0, 0)) { Print("Prop Firm Guard: Could not create starting balance field. Error: ", GetLastError()); return(false); } if(!ObjectSetInteger(0, g_startBalanceEdit, OBJPROP_CORNER, CORNER_LEFT_UPPER) || !ObjectSetInteger(0, g_startBalanceEdit, OBJPROP_XDISTANCE, x) || !ObjectSetInteger(0, g_startBalanceEdit, OBJPROP_YDISTANCE, y) || !ObjectSetInteger(0, g_startBalanceEdit, OBJPROP_XSIZE, 120) || !ObjectSetInteger(0, g_startBalanceEdit, OBJPROP_YSIZE, 20) || !ObjectSetInteger(0, g_startBalanceEdit, OBJPROP_FONTSIZE, 9) || !ObjectSetInteger(0, g_startBalanceEdit, OBJPROP_COLOR, clrBlack) || !ObjectSetInteger(0, g_startBalanceEdit, OBJPROP_BGCOLOR, clrWhite) || !ObjectSetInteger(0, g_startBalanceEdit, OBJPROP_BORDER_COLOR, clrSilver) || !ObjectSetInteger(0, g_startBalanceEdit, OBJPROP_ALIGN, ALIGN_RIGHT) || !ObjectSetInteger(0, g_startBalanceEdit, OBJPROP_HIDDEN, true)) { Print("Prop Firm Guard: Could not configure starting balance field. Error: ", GetLastError()); return(false); } if(!ObjectSetString(0, g_startBalanceEdit, OBJPROP_FONT, "Tahoma") || !ObjectSetString(0, g_startBalanceEdit, OBJPROP_TEXT, suggestedBalance)) { Print("Prop Firm Guard: Could not initialize starting balance field. Error: ", GetLastError()); return(false); } return(true); } //+------------------------------------------------------------------+ //| Create confirm balance button | //+------------------------------------------------------------------+ bool CreateConfirmBalanceButton(const int x, const int y) { // Create the button used to persist the daily starting balance. if(!ObjectCreate(0, g_confirmButton, OBJ_BUTTON, 0, 0, 0)) { Print("Prop Firm Guard: Could not create confirmation button. Error: ", GetLastError()); return(false); } if(!ObjectSetInteger(0, g_confirmButton, OBJPROP_CORNER, CORNER_LEFT_UPPER) || !ObjectSetInteger(0, g_confirmButton, OBJPROP_XDISTANCE, x) || !ObjectSetInteger(0, g_confirmButton, OBJPROP_YDISTANCE, y) || !ObjectSetInteger(0, g_confirmButton, OBJPROP_XSIZE, 100) || !ObjectSetInteger(0, g_confirmButton, OBJPROP_YSIZE, 20) || !ObjectSetInteger(0, g_confirmButton, OBJPROP_FONTSIZE, 9) || !ObjectSetInteger(0, g_confirmButton, OBJPROP_COLOR, clrBlack) || !ObjectSetInteger(0, g_confirmButton, OBJPROP_BGCOLOR, clrWhite) || !ObjectSetInteger(0, g_confirmButton, OBJPROP_BORDER_COLOR, clrSilver) || !ObjectSetInteger(0, g_confirmButton, OBJPROP_HIDDEN, true)) { Print("Prop Firm Guard: Could not configure confirmation button. Error: ", GetLastError()); return(false); } if(!ObjectSetString(0, g_confirmButton, OBJPROP_FONT, "Tahoma") || !ObjectSetString(0, g_confirmButton, OBJPROP_TEXT, "Confirm")) { Print("Prop Firm Guard: Could not initialize confirmation button. Error: ", GetLastError()); return(false); } return(true); } //+------------------------------------------------------------------+ //| Delete daily session controls | //+------------------------------------------------------------------+ void DeleteDailySessionControls() { // Delete the editable balance field only when it currently exists. if(ObjectFind(0, g_startBalanceEdit) >= 0) { if(!ObjectDelete(0, g_startBalanceEdit)) { Print("Prop Firm Guard: Could not delete starting balance field. Error: ", GetLastError()); } } // Delete the confirmation button only when it currently exists. if(ObjectFind(0, g_confirmButton) >= 0) { if(!ObjectDelete(0, g_confirmButton)) { Print("Prop Firm Guard: Could not delete confirmation button. Error: ", GetLastError()); } } ChartRedraw(); } //+------------------------------------------------------------------+ //| Get trading date | //+------------------------------------------------------------------+ string GetTradingDate() { // TimeCurrent() represents the trading server time known by the terminal. // Phase 3 will use this server-based date for daily session management. datetime serverTime = TimeCurrent(); return(TimeToString(serverTime, TIME_DATE)); } //+------------------------------------------------------------------+ //| Initialize account settings | //+------------------------------------------------------------------+ bool InitializeAccountSettings() { bool settingsFound = LoadAccountSettings(); // Associate these settings with the current trading account. g_accountSettings.accountLogin = g_accountState.login; g_accountSettings.accountServer = AccountInfoString(ACCOUNT_SERVER); // Synchronize the current EA inputs with persistent account settings. g_accountSettings.initialBalance = InpInitialAccountBalance; g_accountSettings.dailyDrawdownPercent = InpDailyDrawdownPercent; g_accountSettings.overallDrawdownPercent = InpOverallDrawdownPercent; g_accountSettings.profitTargetPercent = InpProfitTargetPercent; g_accountSettings.updatedAt = TimeCurrent(); // Store the current configuration so SQLite remains synchronized. if(!SaveAccountSettings()) return(false); if(settingsFound) Print("Prop Firm Guard: Account settings restored and synchronized."); else Print("Prop Firm Guard: New account settings saved."); return(true); } //+------------------------------------------------------------------+ //| Save account settings | //+------------------------------------------------------------------+ bool SaveAccountSettings() { string sql = "INSERT OR REPLACE INTO settings (" "account_login," "account_server," "initial_balance," "daily_drawdown_percent," "overall_drawdown_percent," "profit_target_percent," "updated_at" ") VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);"; int request = DatabasePrepare(g_database, sql); if(request == INVALID_HANDLE) { Print("Prop Firm Guard: Could not prepare settings save request. Error: ", GetLastError()); return(false); } // Bind each value separately so text and numeric values remain data // rather than being concatenated directly into the SQL statement. if(!DatabaseBind(request, 0, g_accountSettings.accountLogin) || !DatabaseBind(request, 1, g_accountSettings.accountServer) || !DatabaseBind(request, 2, g_accountSettings.initialBalance) || !DatabaseBind(request, 3, g_accountSettings.dailyDrawdownPercent) || !DatabaseBind(request, 4, g_accountSettings.overallDrawdownPercent) || !DatabaseBind(request, 5, g_accountSettings.profitTargetPercent) || !DatabaseBind(request, 6, g_accountSettings.updatedAt)) { Print("Prop Firm Guard: Could not bind settings values. Error: ", GetLastError()); FinalizeDatabaseRequest(request); return(false); } // INSERT/UPDATE statements do not return rows. In MQL5 a successful // DatabaseRead() therefore finishes with ERR_DATABASE_NO_MORE_DATA. if(!ExecuteDatabaseRequest(request, "saving account settings")) return(false); return(true); } //+------------------------------------------------------------------+ //| Load account settings | //+------------------------------------------------------------------+ bool LoadAccountSettings() { string accountServer = AccountInfoString(ACCOUNT_SERVER); string sql = "SELECT " "account_login," "account_server," "initial_balance," "daily_drawdown_percent," "overall_drawdown_percent," "profit_target_percent," "updated_at " "FROM settings " "WHERE account_login = ?1 AND account_server = ?2;"; int request = DatabasePrepare(g_database, sql); if(request == INVALID_HANDLE) { Print("Prop Firm Guard: Could not prepare settings load request. Error: ", GetLastError()); return(false); } if(!DatabaseBind(request, 0, g_accountState.login) || !DatabaseBind(request, 1, accountServer)) { Print("Prop Firm Guard: Could not bind settings search values. Error: ", GetLastError()); FinalizeDatabaseRequest(request); return(false); } ResetLastError(); // A true result means the SELECT statement returned its first row. if(!DatabaseRead(request)) { int errorCode = GetLastError(); FinalizeDatabaseRequest(request); // No row simply means this account has not been saved before. if(errorCode == ERR_DATABASE_NO_MORE_DATA) return(false); Print("Prop Firm Guard: Could not read account settings. Error: ", errorCode); return(false); } long accountLogin; string storedServer; double initialBalance; double dailyDrawdown; double overallDrawdown; double profitTarget; long updatedAt; // Read every selected column and verify every database conversion. if(!DatabaseColumnLong(request, 0, accountLogin) || !DatabaseColumnText(request, 1, storedServer) || !DatabaseColumnDouble(request, 2, initialBalance) || !DatabaseColumnDouble(request, 3, dailyDrawdown) || !DatabaseColumnDouble(request, 4, overallDrawdown) || !DatabaseColumnDouble(request, 5, profitTarget) || !DatabaseColumnLong(request, 6, updatedAt)) { Print("Prop Firm Guard: Could not read stored settings columns. Error: ", GetLastError()); FinalizeDatabaseRequest(request); return(false); } g_accountSettings.accountLogin = accountLogin; g_accountSettings.accountServer = storedServer; g_accountSettings.initialBalance = initialBalance; g_accountSettings.dailyDrawdownPercent = dailyDrawdown; g_accountSettings.overallDrawdownPercent = overallDrawdown; g_accountSettings.profitTargetPercent = profitTarget; g_accountSettings.updatedAt = (datetime)updatedAt; FinalizeDatabaseRequest(request); return(true); } //+------------------------------------------------------------------+ //| Initialize daily session | //+------------------------------------------------------------------+ bool InitializeDailySession() { string tradingDate = GetTradingDate(); if(DailySessionExists(tradingDate)) { if(!LoadDailySession()) return(false); g_hasDailySession = true; Print("Prop Firm Guard: Daily session restored for ", tradingDate, "."); return(true); } g_dailySession.accountLogin = g_accountState.login; g_dailySession.accountServer = AccountInfoString(ACCOUNT_SERVER); g_dailySession.tradingDate = tradingDate; g_dailySession.startingBalance = 0.0; g_dailySession.startingEquity = 0.0; g_dailySession.balanceConfirmed = false; g_dailySession.latestBalance = g_accountState.balance; g_dailySession.latestEquity = g_accountState.equity; g_dailySession.dailyLoss = 0.0; g_dailySession.maximumDailyLossReached = 0.0; g_dailySession.targetProgress = 0.0; g_dailySession.sessionStatus = "AWAITING_CONFIRMATION"; g_dailySession.createdAt = TimeCurrent(); g_dailySession.updatedAt = TimeCurrent(); if(!SaveDailySession()) return(false); g_hasDailySession = true; Print("Prop Firm Guard: New daily session created for ", tradingDate, "."); return(true); } //+------------------------------------------------------------------+ //| Save daily session | //+------------------------------------------------------------------+ bool SaveDailySession() { string sql = "INSERT OR REPLACE INTO daily_sessions (" "account_login," "account_server," "trading_date," "starting_balance," "starting_equity," "balance_confirmed," "latest_balance," "latest_equity," "daily_loss," "maximum_daily_loss_reached," "target_progress," "session_status," "created_at," "updated_at" ") VALUES (" "?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14" ");"; ResetLastError(); int request = DatabasePrepare(g_database, sql, g_dailySession.accountLogin, g_dailySession.accountServer, g_dailySession.tradingDate, g_dailySession.startingBalance, g_dailySession.startingEquity, g_dailySession.balanceConfirmed ? 1 : 0, g_dailySession.latestBalance, g_dailySession.latestEquity, g_dailySession.dailyLoss, g_dailySession.maximumDailyLossReached, g_dailySession.targetProgress, g_dailySession.sessionStatus, (long)g_dailySession.createdAt, (long)g_dailySession.updatedAt); if(request == INVALID_HANDLE) { Print("Prop Firm Guard: Could not prepare daily session save request. Error: ", GetLastError()); return(false); } return(ExecuteDatabaseRequest(request, "saving daily session")); } //+------------------------------------------------------------------+ //| Load daily session | //+------------------------------------------------------------------+ bool LoadDailySession() { string sql = "SELECT " "account_login," "account_server," "trading_date," "starting_balance," "starting_equity," "balance_confirmed," "latest_balance," "latest_equity," "daily_loss," "maximum_daily_loss_reached," "target_progress," "session_status," "created_at," "updated_at " "FROM daily_sessions " "WHERE account_login=?1 " "AND account_server=?2 " "AND trading_date=?3;"; string tradingDate = GetTradingDate(); string accountServer = AccountInfoString(ACCOUNT_SERVER); ResetLastError(); int request = DatabasePrepare(g_database, sql, g_accountState.login, accountServer, tradingDate); if(request == INVALID_HANDLE) { Print("Prop Firm Guard: Could not prepare daily session load request. Error: ", GetLastError()); return(false); } ResetLastError(); if(!DatabaseRead(request)) { int errorCode = GetLastError(); if(errorCode != ERR_DATABASE_NO_MORE_DATA) { Print("Prop Firm Guard: Could not load daily session. Error: ", errorCode); } FinalizeDatabaseRequest(request); return(false); } long accountLogin; string loadedServer; string loadedDate; double startingBalance; double startingEquity; int balanceConfirmed; double latestBalance; double latestEquity; double dailyLoss; double maximumDailyLossReached; double targetProgress; string sessionStatus; long createdAt; long updatedAt; bool loaded = DatabaseColumnLong(request, 0, accountLogin) && DatabaseColumnText(request, 1, loadedServer) && DatabaseColumnText(request, 2, loadedDate) && DatabaseColumnDouble(request, 3, startingBalance) && DatabaseColumnDouble(request, 4, startingEquity) && DatabaseColumnInteger(request, 5, balanceConfirmed) && DatabaseColumnDouble(request, 6, latestBalance) && DatabaseColumnDouble(request, 7, latestEquity) && DatabaseColumnDouble(request, 8, dailyLoss) && DatabaseColumnDouble(request, 9, maximumDailyLossReached) && DatabaseColumnDouble(request, 10, targetProgress) && DatabaseColumnText(request, 11, sessionStatus) && DatabaseColumnLong(request, 12, createdAt) && DatabaseColumnLong(request, 13, updatedAt); if(!loaded) { Print("Prop Firm Guard: Could not read daily session columns. Error: ", GetLastError()); FinalizeDatabaseRequest(request); return(false); } g_dailySession.accountLogin = accountLogin; g_dailySession.accountServer = loadedServer; g_dailySession.tradingDate = loadedDate; g_dailySession.startingBalance = startingBalance; g_dailySession.startingEquity = startingEquity; g_dailySession.balanceConfirmed = (balanceConfirmed != 0); g_dailySession.latestBalance = latestBalance; g_dailySession.latestEquity = latestEquity; g_dailySession.dailyLoss = dailyLoss; g_dailySession.maximumDailyLossReached = maximumDailyLossReached; g_dailySession.targetProgress = targetProgress; g_dailySession.sessionStatus = sessionStatus; g_dailySession.createdAt = (datetime)createdAt; g_dailySession.updatedAt = (datetime)updatedAt; FinalizeDatabaseRequest(request); return(true); } //+------------------------------------------------------------------+ //| Check daily session existence | //+------------------------------------------------------------------+ bool DailySessionExists(const string tradingDate) { string accountServer = AccountInfoString(ACCOUNT_SERVER); string sql = "SELECT 1 " "FROM daily_sessions " "WHERE account_login = ?1 " "AND account_server = ?2 " "AND trading_date = ?3 " "LIMIT 1;"; int request = DatabasePrepare(g_database, sql); if(request == INVALID_HANDLE) { Print("Prop Firm Guard: Could not prepare session existence request. Error: ", GetLastError()); return(false); } if(!DatabaseBind(request, 0, g_accountState.login) || !DatabaseBind(request, 1, accountServer) || !DatabaseBind(request, 2, tradingDate)) { Print("Prop Firm Guard: Could not bind session existence values. Error: ", GetLastError()); FinalizeDatabaseRequest(request); return(false); } ResetLastError(); bool exists = DatabaseRead(request); int errorCode = GetLastError(); FinalizeDatabaseRequest(request); if(!exists && errorCode != ERR_DATABASE_NO_MORE_DATA) { Print("Prop Firm Guard: Could not check daily session existence. Error: ", errorCode); } return(exists); } //+------------------------------------------------------------------+ //| Execute prepared database request | //+------------------------------------------------------------------+ bool ExecuteDatabaseRequest(const int request, const string operation) { ResetLastError(); bool result = DatabaseRead(request); int errorCode = GetLastError(); // INSERT and UPDATE statements return no result rows. MQL5 therefore // reports ERR_DATABASE_NO_MORE_DATA after successful execution. bool success = (result || errorCode == ERR_DATABASE_NO_MORE_DATA); if(!success) { Print("Prop Firm Guard: Database error while ", operation, ". Error: ", errorCode); } FinalizeDatabaseRequest(request); return(success); } //+------------------------------------------------------------------+ //| Finalize database request | //+------------------------------------------------------------------+ void FinalizeDatabaseRequest(const int request) { if(request == INVALID_HANDLE) return; ResetLastError(); DatabaseFinalize(request); int errorCode = GetLastError(); if(errorCode != 0) { Print("Prop Firm Guard: Could not finalize database request. Error: ", errorCode); } } //+------------------------------------------------------------------+ //| Create dashboard | //+------------------------------------------------------------------+ bool CreateDashboard() { string panelName = g_objectPrefix + "Panel"; int leftX = InpDashboardX + 18; int rightX = InpDashboardX + 275; if(!ObjectCreate(0, panelName, OBJ_RECTANGLE_LABEL, 0, 0, 0)) { Print("Prop Firm Guard: Could not create dashboard panel. Error: ", GetLastError()); return(false); } if(!ObjectSetInteger(0, panelName, OBJPROP_CORNER, CORNER_LEFT_UPPER) || !ObjectSetInteger(0, panelName, OBJPROP_XDISTANCE, InpDashboardX) || !ObjectSetInteger(0, panelName, OBJPROP_YDISTANCE, InpDashboardY) || !ObjectSetInteger(0, panelName, OBJPROP_XSIZE, 520) || !ObjectSetInteger(0, panelName, OBJPROP_YSIZE, 500) || !ObjectSetInteger(0, panelName, OBJPROP_BGCOLOR, clrWhite) || !ObjectSetInteger(0, panelName, OBJPROP_BORDER_COLOR, clrSilver) || !ObjectSetInteger(0, panelName, OBJPROP_SELECTABLE, false) || !ObjectSetInteger(0, panelName, OBJPROP_HIDDEN, true)) { Print("Prop Firm Guard: Could not configure dashboard panel. Error: ", GetLastError()); return(false); } //--- Main title if(!CreateDashboardLabel(g_objectPrefix + "Title", "PROP FIRM GUARD", leftX, InpDashboardY + 12, 11, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "OverallStatus", "", rightX, InpDashboardY + 14, 10, clrBlack)) return(false); //--- SESSION if(!CreateDashboardLabel(g_objectPrefix + "SessionHeader", "SESSION", leftX, InpDashboardY + 42, 9, clrDimGray)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "TradingDate", "", leftX, InpDashboardY + 62, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "SessionState", "", leftX, InpDashboardY + 82, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "DailyStart", "", leftX, InpDashboardY + 102, 9, clrBlack)) return(false); //--- ACCOUNT if(!CreateDashboardLabel(g_objectPrefix + "AccountHeader", "ACCOUNT", leftX, InpDashboardY + 145, 9, clrDimGray)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "AccountLogin", "", leftX, InpDashboardY + 165, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "Balance", "", leftX, InpDashboardY + 185, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "Equity", "", leftX, InpDashboardY + 205, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "FloatingPL", "", leftX, InpDashboardY + 225, 9, clrBlack)) return(false); //--- DAILY DRAWDOWN if(!CreateDashboardLabel(g_objectPrefix + "DailyDrawdownHeader", "DAILY DRAWDOWN", leftX, InpDashboardY + 255, 9, clrDimGray)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "DailyLossLimit", "", leftX, InpDashboardY + 275, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "DailyCurrentLoss", "", leftX, InpDashboardY + 295, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "DailyLimitUsage", "", leftX, InpDashboardY + 315, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "DailyRemaining", "", leftX, InpDashboardY + 335, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "DailyDrawdownStatus", "", leftX, InpDashboardY + 355, 9, clrBlack)) return(false); //--- OVERALL DRAWDOWN if(!CreateDashboardLabel(g_objectPrefix + "OverallDrawdownHeader", "OVERALL DRAWDOWN", rightX, InpDashboardY + 42, 9, clrDimGray)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "OverallInitialBalance", "", rightX, InpDashboardY + 62, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "OverallFloor", "", rightX, InpDashboardY + 82, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "OverallEquity", "", rightX, InpDashboardY + 102, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "OverallLoss", "", rightX, InpDashboardY + 122, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "OverallLimitUsage", "", rightX, InpDashboardY + 142, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "OverallRemaining", "", rightX, InpDashboardY + 162, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "OverallDrawdownStatus", "", rightX, InpDashboardY + 182, 9, clrBlack)) return(false); //--- PROFIT TARGET if(!CreateDashboardLabel(g_objectPrefix + "ProfitTargetHeader", "PROFIT TARGET", rightX, InpDashboardY + 220, 9, clrDimGray)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "TargetBalance", "", rightX, InpDashboardY + 240, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "TargetCurrentProfit", "", rightX, InpDashboardY + 260, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "TargetProgress", "", rightX, InpDashboardY + 280, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "TargetRemaining", "", rightX, InpDashboardY + 300, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "ProfitTargetStatus", "", rightX, InpDashboardY + 320, 9, clrBlack)) return(false); //--- NEWS if(!CreateDashboardLabel(g_objectPrefix + "NewsHeader", "NEWS", rightX, InpDashboardY + 350, 9, clrDimGray)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "NewsEvent", "", rightX, InpDashboardY + 370, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "NewsTime", "", rightX, InpDashboardY + 390, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "NewsCountdown", "", rightX, InpDashboardY + 410, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "NewsRule", "", rightX, InpDashboardY + 430, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "NewsStatus", "", rightX, InpDashboardY + 450, 9, clrBlack)) return(false); //--- STATUS if(!CreateDashboardLabel(g_objectPrefix + "StatusHeader", "STATUS", leftX, InpDashboardY + 390, 9, clrDimGray)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "StatusDaily", "", leftX, InpDashboardY + 410, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "StatusOverall", "", leftX, InpDashboardY + 430, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "StatusTarget", "", leftX, InpDashboardY + 450, 9, clrBlack)) return(false); if(!CreateDashboardLabel(g_objectPrefix + "StatusNews", "", leftX, InpDashboardY + 470, 9, clrBlack)) return(false); if(!CreateDailySessionControls()) return(false); UpdateDashboard(); ChartRedraw(); return(true); } //+------------------------------------------------------------------+ //| Create dashboard label | //+------------------------------------------------------------------+ bool CreateDashboardLabel(const string name, const string text, const int x, const int y, const int fontSize, const color textColor) { // Create a fixed text label positioned relative to the chart corner. if(!ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0)) { Print("Prop Firm Guard: Could not create label ", name, ". Error: ", GetLastError()); return(false); } // Configure all visual and positioning properties of the label. if(!ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER) || !ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x) || !ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y) || !ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize) || !ObjectSetInteger(0, name, OBJPROP_COLOR, textColor) || !ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false) || !ObjectSetInteger(0, name, OBJPROP_HIDDEN, true)) { Print("Prop Firm Guard: Could not configure label ", name, ". Error: ", GetLastError()); return(false); } if(!ObjectSetString(0, name, OBJPROP_FONT, "Tahoma") || !ObjectSetString(0, name, OBJPROP_TEXT, text)) { Print("Prop Firm Guard: Could not initialize label ", name, ". Error: ", GetLastError()); return(false); } return(true); } //+------------------------------------------------------------------+ //| Update dashboard text | //+------------------------------------------------------------------+ void UpdateDashboardText(const string objectName, const string text) { // Update only objects that currently exist on the chart. if(ObjectFind(0, objectName) < 0) { Print("Prop Firm Guard: Dashboard object not found: ", objectName); return; } if(!ObjectSetString(0, objectName, OBJPROP_TEXT, text)) { Print("Prop Firm Guard: Could not update dashboard object ", objectName, ". Error: ", GetLastError()); } } //+------------------------------------------------------------------+ //| Update dashboard color | //+------------------------------------------------------------------+ void UpdateDashboardColor(const string objectName, const color textColor) { // Change the color only when the dashboard object exists. if(ObjectFind(0, objectName) < 0) { Print("Prop Firm Guard: Dashboard object not found: ", objectName); return; } if(!ObjectSetInteger(0, objectName, OBJPROP_COLOR, textColor)) { Print("Prop Firm Guard: Could not update dashboard color for ", objectName, ". Error: ", GetLastError()); } } //+------------------------------------------------------------------+ //| Update dashboard | //+------------------------------------------------------------------+ void UpdateDashboard() { string sessionState; string dailyStart; string headerStatus; ENUM_GUARD_STATUS guardStatus = GetOverallGuardStatus(); if(g_dailySession.balanceConfirmed) { sessionState = "Session: ACTIVE"; dailyStart = "Starting Balance: " + DoubleToString(g_dailySession.startingBalance, 2); headerStatus = "STATUS: " + GuardStatusToString(guardStatus); } else { sessionState = "Session: AWAITING CONFIRMATION"; dailyStart = "Starting Balance: Not confirmed"; headerStatus = "STATUS: SETUP REQUIRED"; } //--- Session UpdateDashboardText(g_objectPrefix + "TradingDate", "Trading Date: " + g_dailySession.tradingDate); UpdateDashboardText(g_objectPrefix + "SessionState", sessionState); UpdateDashboardText(g_objectPrefix + "DailyStart", dailyStart); //--- Account UpdateDashboardText(g_objectPrefix + "AccountLogin", "Account: " + IntegerToString(g_accountState.login)); UpdateDashboardText(g_objectPrefix + "Balance", "Balance: " + DoubleToString(g_accountState.balance, 2)); UpdateDashboardText(g_objectPrefix + "Equity", "Equity: " + DoubleToString(g_accountState.equity, 2)); UpdateDashboardText(g_objectPrefix + "FloatingPL", "Floating P/L: " + DoubleToString(g_accountState.floatingPL, 2)); //--- Daily drawdown if(g_dailySession.balanceConfirmed) { UpdateDashboardText(g_objectPrefix + "DailyLossLimit", "Maximum Loss: " + DoubleToString(g_dailyDrawdown.maximumDailyLoss, 2) + " (" + DoubleToString(g_accountSettings.dailyDrawdownPercent, 2) + "%)"); UpdateDashboardText(g_objectPrefix + "DailyCurrentLoss", "Current Loss: " + DoubleToString(g_dailyDrawdown.currentDailyLoss, 2) + " (" + DoubleToString(g_dailyDrawdown.drawdownPercent, 2) + "%)"); UpdateDashboardText(g_objectPrefix + "DailyLimitUsage", "Limit Used: " + DoubleToString(g_dailyDrawdown.limitUsagePercent, 2) + "%"); UpdateDashboardText(g_objectPrefix + "DailyRemaining", "Remaining: " + DoubleToString(g_dailyDrawdown.remainingAllowance, 2)); UpdateDashboardText(g_objectPrefix + "DailyDrawdownStatus", "Status: " + ComplianceStatusToString(g_dailyDrawdown.status)); } else { UpdateDashboardText(g_objectPrefix + "DailyLossLimit", "Maximum Loss: --"); UpdateDashboardText(g_objectPrefix + "DailyCurrentLoss", "Current Loss: --"); UpdateDashboardText(g_objectPrefix + "DailyLimitUsage", "Limit Used: --"); UpdateDashboardText(g_objectPrefix + "DailyRemaining", "Remaining: --"); UpdateDashboardText(g_objectPrefix + "DailyDrawdownStatus", "Status: WAITING"); } //--- Overall drawdown UpdateDashboardText(g_objectPrefix + "OverallInitialBalance", "Initial Balance: " + DoubleToString(g_overallDrawdown.initialBalance, 2)); UpdateDashboardText(g_objectPrefix + "OverallFloor", "Equity Floor: " + DoubleToString(g_overallDrawdown.overallFloor, 2)); UpdateDashboardText(g_objectPrefix + "OverallEquity", "Current Equity: " + DoubleToString(g_overallDrawdown.currentEquity, 2)); UpdateDashboardText(g_objectPrefix + "OverallLoss", "Overall Loss: " + DoubleToString(g_overallDrawdown.currentOverallLoss, 2)); UpdateDashboardText(g_objectPrefix + "OverallLimitUsage", "Limit Used: " + DoubleToString(g_overallDrawdown.limitUsagePercent, 2) + "%"); UpdateDashboardText(g_objectPrefix + "OverallRemaining", "Remaining: " + DoubleToString(g_overallDrawdown.remainingAllowance, 2)); UpdateDashboardText(g_objectPrefix + "OverallDrawdownStatus", "Status: " + ComplianceStatusToString(g_overallDrawdown.status)); //--- Profit target UpdateDashboardText(g_objectPrefix + "TargetBalance", "Target Balance: " + DoubleToString(g_profitTarget.targetBalance, 2)); UpdateDashboardText(g_objectPrefix + "TargetCurrentProfit", "Current Profit: " + DoubleToString(g_profitTarget.currentProfit, 2)); UpdateDashboardText(g_objectPrefix + "TargetProgress", "Progress: " + DoubleToString(g_profitTarget.progressPercent, 2) + "%"); UpdateDashboardText(g_objectPrefix + "TargetRemaining", "Remaining: " + DoubleToString(g_profitTarget.remainingProfit, 2)); UpdateDashboardText(g_objectPrefix + "ProfitTargetStatus", "Status: " + TargetStatusToString(g_profitTarget.status)); //--- News if(g_newsState.status == NEWS_DISABLED) { UpdateDashboardText(g_objectPrefix + "NewsEvent", "Filter: Disabled"); UpdateDashboardText(g_objectPrefix + "NewsTime", "Trading Window: DISABLED"); UpdateDashboardText(g_objectPrefix + "NewsCountdown", "Countdown: --"); UpdateDashboardText(g_objectPrefix + "NewsRule", "Holding: --"); } else if(g_newsState.status == NEWS_NO_EXPOSURE) { UpdateDashboardText(g_objectPrefix + "NewsEvent", "No supported active exposure"); UpdateDashboardText(g_objectPrefix + "NewsTime", "Trading Window: SAFE"); UpdateDashboardText(g_objectPrefix + "NewsCountdown", "Countdown: --"); UpdateDashboardText(g_objectPrefix + "NewsRule", "Holding: " + GetNewsHoldingRuleText()); } else if(!g_newsState.eventFound) { UpdateDashboardText(g_objectPrefix + "NewsEvent", "No relevant event found"); UpdateDashboardText(g_objectPrefix + "NewsTime", "Trading Window: SAFE"); UpdateDashboardText(g_objectPrefix + "NewsCountdown", "Currencies: " + g_newsState.currencyList); UpdateDashboardText(g_objectPrefix + "NewsRule", "Holding: " + GetNewsHoldingRuleText()); } else { UpdateDashboardText(g_objectPrefix + "NewsEvent", g_newsState.eventCurrency + " | " + g_newsState.eventName + " | " + CalendarImportanceToString(g_newsState.importance)); UpdateDashboardText(g_objectPrefix + "NewsTime", "Trading Window: " + GetNewsTradingWindowText()); UpdateDashboardText(g_objectPrefix + "NewsCountdown", "In: " + FormatNewsCountdown(g_newsState.secondsToEvent)); UpdateDashboardText(g_objectPrefix + "NewsRule", "Holding: " + GetNewsHoldingRuleText() + " | Affected: " + IntegerToString(g_newsState.affectedPositions)); } UpdateDashboardText(g_objectPrefix + "NewsStatus", "Status: " + NewsHoldingStatusToString(g_newsState.holdingStatus)); //--- Main header UpdateDashboardText(g_objectPrefix + "OverallStatus", headerStatus); if(g_dailySession.balanceConfirmed) UpdateDashboardColor(g_objectPrefix + "OverallStatus", GetGuardStatusColor(guardStatus)); else UpdateDashboardColor(g_objectPrefix + "OverallStatus", clrDarkOrange); //--- Status summary UpdateDashboardText(g_objectPrefix + "StatusDaily", "Daily DD: " + (g_dailySession.balanceConfirmed ? ComplianceStatusToString(g_dailyDrawdown.status) : "WAITING")); UpdateDashboardText(g_objectPrefix + "StatusOverall", "Overall DD: " + ComplianceStatusToString(g_overallDrawdown.status)); UpdateDashboardText(g_objectPrefix + "StatusTarget", "Target: " + TargetStatusToString(g_profitTarget.status)); UpdateDashboardText(g_objectPrefix + "StatusNews", "News: " + NewsHoldingStatusToString(g_newsState.holdingStatus)); ChartRedraw(); } //+------------------------------------------------------------------+ //| Delete dashboard | //+------------------------------------------------------------------+ void DeleteDashboard() { // Delete only chart objects created by Prop Firm Guard. int deletedObjects = ObjectsDeleteAll(0, g_objectPrefix); if(deletedObjects < 0) { Print("Prop Firm Guard: Could not delete dashboard objects. Error: ", GetLastError()); } ChartRedraw(); } //+------------------------------------------------------------------+ //| Send guard notification | //+------------------------------------------------------------------+ void SendGuardNotification(const string message) { Print("Prop Firm Guard: ", message); if(InpEnableAlerts) Alert("Prop Firm Guard: ", message); if(!InpEnablePushNotifications) return; if(!TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)) { Print("Prop Firm Guard: Push notifications are enabled in the EA ", "but disabled in the terminal settings."); return; } ResetLastError(); if(!SendNotification("Prop Firm Guard: " + message)) { Print("Prop Firm Guard: Could not send push notification. Error: ", GetLastError()); } } //+------------------------------------------------------------------+ //| Build daily drawdown alert | //+------------------------------------------------------------------+ string BuildDailyDrawdownAlert(const ENUM_COMPLIANCE_STATUS status) { switch(status) { case COMPLIANCE_WARNING: return("Daily drawdown reached WARNING level."); case COMPLIANCE_CRITICAL: return("CRITICAL: Only " + DoubleToString(g_dailyDrawdown.remainingAllowance, 2) + " remains before the daily loss limit."); case COMPLIANCE_BREACHED: return("Daily drawdown limit breached."); case COMPLIANCE_SAFE: default: return(""); } } //+------------------------------------------------------------------+ //| Build overall drawdown alert | //+------------------------------------------------------------------+ string BuildOverallDrawdownAlert(const ENUM_COMPLIANCE_STATUS status) { switch(status) { case COMPLIANCE_WARNING: return("Overall drawdown reached WARNING level."); case COMPLIANCE_CRITICAL: return("CRITICAL: Only " + DoubleToString(g_overallDrawdown.remainingAllowance, 2) + " remains before the overall loss limit."); case COMPLIANCE_BREACHED: return("Overall drawdown limit breached."); case COMPLIANCE_SAFE: default: return(""); } } //+------------------------------------------------------------------+ //| Check daily drawdown alert | //+------------------------------------------------------------------+ void CheckDailyDrawdownAlert() { if(!g_dailySession.balanceConfirmed) return; if(g_dailyDrawdown.status == g_alertState.dailyDrawdownStatus) return; g_alertState.dailyDrawdownStatus = g_dailyDrawdown.status; string message = BuildDailyDrawdownAlert(g_dailyDrawdown.status); if(message == "") return; g_alertState.dashboardMessage = message; SendGuardNotification(message); } //+------------------------------------------------------------------+ //| Check overall drawdown alert | //+------------------------------------------------------------------+ void CheckOverallDrawdownAlert() { if(g_overallDrawdown.status == g_alertState.overallDrawdownStatus) return; g_alertState.overallDrawdownStatus = g_overallDrawdown.status; string message = BuildOverallDrawdownAlert(g_overallDrawdown.status); if(message == "") return; g_alertState.dashboardMessage = message; SendGuardNotification(message); } //+------------------------------------------------------------------+ //| Check profit target alert | //+------------------------------------------------------------------+ void CheckProfitTargetAlert() { if(g_profitTarget.status == g_alertState.profitTargetStatus) return; g_alertState.profitTargetStatus = g_profitTarget.status; if(g_profitTarget.status != TARGET_REACHED) return; string message = "Profit target reached."; g_alertState.dashboardMessage = message; SendGuardNotification(message); } //+------------------------------------------------------------------+ //| Check economic news alerts | //+------------------------------------------------------------------+ void CheckNewsAlert() { if(!InpEnableNewsFilter || !g_newsState.eventFound || g_newsState.affectedPositions == 0) return; // A new calendar event receives a fresh alert state. if(g_alertState.newsEventId != g_newsState.eventId) { g_alertState.newsEventId = g_newsState.eventId; g_alertState.newsApproachAlerted = false; g_alertState.newsRestrictedAlerted = false; } long warningThreshold = (long)InpMinutesBeforeNews * 60; // Issue one advance warning when the event reaches the configured // pre-news interval. if(g_newsState.secondsToEvent > 0 && g_newsState.secondsToEvent <= warningThreshold && !g_alertState.newsApproachAlerted) { string message = CalendarImportanceToString(g_newsState.importance) + "-impact " + g_newsState.eventCurrency + " news in " + IntegerToString((int)MathCeil((double)g_newsState.secondsToEvent / 60.0)) + " minute(s): " + g_newsState.eventName + "."; g_alertState.newsApproachAlerted = true; g_alertState.dashboardMessage = message; SendGuardNotification(message); } // If holding is prohibited, report entry into the restricted window. if(g_newsState.holdingStatus == NEWS_HOLDING_PROTECTION && !g_alertState.newsRestrictedAlerted) { string message = "News holding restriction active for " + g_newsState.eventCurrency + " " + g_newsState.eventName + "."; g_alertState.newsRestrictedAlerted = true; g_alertState.dashboardMessage = message; SendGuardNotification(message); } } //+------------------------------------------------------------------+ //| Manage alerts | //+------------------------------------------------------------------+ void ManageAlerts() { CheckDailyDrawdownAlert(); CheckOverallDrawdownAlert(); CheckProfitTargetAlert(); CheckNewsAlert(); } //+------------------------------------------------------------------+ //| Get dashboard alert message | //+------------------------------------------------------------------+ string GetDashboardAlertMessage() { if(g_alertState.dashboardMessage != "") return(g_alertState.dashboardMessage); return("Monitoring account..."); } //+------------------------------------------------------------------+ //| Get daily session status | //+------------------------------------------------------------------+ string GetDailySessionStatus() { if(!g_dailySession.balanceConfirmed) return("AWAITING_CONFIRMATION"); if(g_dailyDrawdown.status == COMPLIANCE_BREACHED || g_overallDrawdown.status == COMPLIANCE_BREACHED) return("BREACHED"); if(g_profitTarget.status == TARGET_REACHED) return("TARGET_REACHED"); if(g_dailyDrawdown.status == COMPLIANCE_CRITICAL || g_overallDrawdown.status == COMPLIANCE_CRITICAL) return("CRITICAL"); if(g_dailyDrawdown.status == COMPLIANCE_WARNING || g_overallDrawdown.status == COMPLIANCE_WARNING) return("WARNING"); return("ACTIVE"); } //+------------------------------------------------------------------+ //| Update runtime daily-session state | //+------------------------------------------------------------------+ bool UpdateDailySessionRuntimeState() { if(!g_hasDailySession) return(false); g_dailySession.latestBalance = g_accountState.balance; g_dailySession.latestEquity = g_accountState.equity; g_dailySession.targetProgress = g_profitTarget.progressPercent; g_dailySession.sessionStatus = GetDailySessionStatus(); g_dailySession.updatedAt = TimeCurrent(); if(g_dailySession.balanceConfirmed) { g_dailySession.dailyLoss = g_dailyDrawdown.currentDailyLoss; // Preserve the worst actual loss observed during this trading day. if(g_dailyDrawdown.currentDailyLoss > g_dailySession.maximumDailyLossReached) { g_dailySession.maximumDailyLossReached = g_dailyDrawdown.currentDailyLoss; } } else { g_dailySession.dailyLoss = 0.0; } return(SaveDailySession()); } //+------------------------------------------------------------------+ //| Update runtime database when due | //+------------------------------------------------------------------+ void UpdateRuntimeDatabase() { datetime currentTime = TimeCurrent(); // Runtime snapshots do not need the dashboard's one-second frequency. if(g_lastRuntimeDatabaseUpdate != 0 && currentTime - g_lastRuntimeDatabaseUpdate < 10) return; if(UpdateDailySessionRuntimeState()) g_lastRuntimeDatabaseUpdate = currentTime; } //+------------------------------------------------------------------+