//+------------------------------------------------------------------+ //| 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. | //+------------------------------------------------------------------+ 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 | //+------------------------------------------------------------------+ 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 | //+------------------------------------------------------------------+ 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 = 100000.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 //+------------------------------------------------------------------+ //| Account state structure | //+------------------------------------------------------------------+ //| Holds the live account values required by the monitoring logic. | //+------------------------------------------------------------------+ 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 the persistent state associated with one trading day. | //+------------------------------------------------------------------+ 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 | //+------------------------------------------------------------------+ struct SNewsState { bool hasExposure; // Whether supported open positions exist int monitoredCurrencies; // Number of unique monitored currencies string currencyList; // Monitored currency list 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; // Signed distance to event 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; // Affected positions requiring protection ENUM_NEWS_STATUS status; // Current news-monitoring status ENUM_NEWS_HOLDING_STATUS holdingStatus; // Current news-holding condition }; //+------------------------------------------------------------------+ //| News position match structure | //+------------------------------------------------------------------+ 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 protection is required }; //+------------------------------------------------------------------+ //| 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; // Alert transition memory CTrade g_trade; // Synchronous trade execution helper 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; // Whether a daily session is loaded 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; // Last successful runtime database update string g_objectPrefix = "PFG_"; // Prefix used by dashboard objects //+------------------------------------------------------------------+ //| Validate compliance inputs | //+------------------------------------------------------------------+ bool ValidateComplianceInputs() { //--- Validate the initial account balance if(InpInitialAccountBalance <= 0.0) { Print("Prop Firm Guard: Initial account balance must be greater than zero."); return(false); } //--- Validate the daily drawdown percentage if(InpDailyDrawdownPercent <= 0.0 || InpDailyDrawdownPercent > 100.0) { Print("Prop Firm Guard: Daily drawdown percentage must be between 0 and 100."); return(false); } //--- Validate the overall drawdown percentage if(InpOverallDrawdownPercent <= 0.0 || InpOverallDrawdownPercent > 100.0) { Print("Prop Firm Guard: Overall drawdown percentage must be between 0 and 100."); return(false); } //--- Validate the profit target percentage if(InpProfitTargetPercent <= 0.0 || InpProfitTargetPercent > 100.0) { Print("Prop Firm Guard: Profit target percentage must be between 0 and 100."); return(false); } //--- Validate the warning threshold if(InpWarningLevelPercent <= 0.0 || InpWarningLevelPercent >= InpCriticalLevelPercent) { Print("Prop Firm Guard: Warning level must be greater than 0 and below the critical level."); return(false); } //--- Validate the critical threshold if(InpCriticalLevelPercent <= InpWarningLevelPercent || InpCriticalLevelPercent >= 100.0) { Print("Prop Firm Guard: Critical level must be above the warning level and below 100."); return(false); } //--- Validate the news restriction windows 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) { //--- Apply the requested property to the current chart 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 background with dark chart text if(!SetChartIntegerProperty(CHART_COLOR_BACKGROUND, clrWhiteSmoke)) return(false); if(!SetChartIntegerProperty(CHART_COLOR_FOREGROUND, clrBlack)) return(false); //--- Remove the chart grid behind the dashboard if(!SetChartIntegerProperty(CHART_SHOW_GRID, false)) return(false); //--- Display price movement as candlesticks if(!SetChartIntegerProperty(CHART_MODE, CHART_CANDLES)) return(false); //--- Use green for bullish candles and bars if(!SetChartIntegerProperty(CHART_COLOR_CANDLE_BULL, clrGreen)) return(false); if(!SetChartIntegerProperty(CHART_COLOR_CHART_UP, clrGreen)) return(false); //--- Use red for bearish candles and bars if(!SetChartIntegerProperty(CHART_COLOR_CANDLE_BEAR, clrRed)) return(false); if(!SetChartIntegerProperty(CHART_COLOR_CHART_DOWN, clrRed)) return(false); //--- Keep the line chart representation neutral if(!SetChartIntegerProperty(CHART_COLOR_CHART_LINE, clrBlack)) return(false); //--- Distinguish Bid and Ask prices if(!SetChartIntegerProperty(CHART_COLOR_BID, clrGreen)) return(false); if(!SetChartIntegerProperty(CHART_COLOR_ASK, clrRed)) return(false); //--- Keep the last price visually neutral if(!SetChartIntegerProperty(CHART_COLOR_LAST, clrBlack)) return(false); //--- Highlight Stop Loss and Take Profit levels if(!SetChartIntegerProperty(CHART_COLOR_STOP_LEVEL, clrRed)) return(false); //--- Display and allow interaction with trade levels if(!SetChartIntegerProperty(CHART_SHOW_TRADE_LEVELS, true)) return(false); if(!SetChartIntegerProperty(CHART_DRAG_TRADE_LEVELS, true)) return(false); //--- Keep the One Click Trading panel available if(!SetChartIntegerProperty(CHART_SHOW_ONE_CLICK, true)) return(false); //--- Apply the completed chart presentation ChartRedraw(0); return(true); } //+------------------------------------------------------------------+ //| Update account state | //+------------------------------------------------------------------+ void UpdateAccountState() { //--- Read the current account values g_accountState.login = AccountInfoInteger(ACCOUNT_LOGIN); g_accountState.balance = AccountInfoDouble(ACCOUNT_BALANCE); g_accountState.equity = AccountInfoDouble(ACCOUNT_EQUITY); //--- Derive the current floating profit or loss g_accountState.floatingPL = g_accountState.equity - g_accountState.balance; //--- Initialize compliance status before rule calculations are added g_accountState.status = COMPLIANCE_SAFE; } //+------------------------------------------------------------------+ //| Calculate daily drawdown | //+------------------------------------------------------------------+ void CalculateDailyDrawdown() { //--- Reset the state until the daily starting balance is confirmed 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; } //--- Read the active daily reference and current equity g_dailyDrawdown.startingBalance = g_dailySession.startingBalance; g_dailyDrawdown.currentEquity = g_accountState.equity; //--- Calculate the permitted daily loss g_dailyDrawdown.maximumDailyLoss = g_dailyDrawdown.startingBalance * g_accountSettings.dailyDrawdownPercent / 100.0; //--- Calculate the current daily loss g_dailyDrawdown.currentDailyLoss = g_dailyDrawdown.startingBalance - g_dailyDrawdown.currentEquity; if(g_dailyDrawdown.currentDailyLoss < 0.0) g_dailyDrawdown.currentDailyLoss = 0.0; //--- Calculate the loss percentage g_dailyDrawdown.drawdownPercent = g_dailyDrawdown.currentDailyLoss / g_dailyDrawdown.startingBalance * 100.0; //--- Calculate daily limit usage 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 remaining allowance g_dailyDrawdown.remainingAllowance = g_dailyDrawdown.maximumDailyLoss - g_dailyDrawdown.currentDailyLoss; if(g_dailyDrawdown.remainingAllowance < 0.0) g_dailyDrawdown.remainingAllowance = 0.0; //--- Classify the current daily drawdown state 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() { //--- Read the fixed challenge reference and current equity g_overallDrawdown.initialBalance = g_accountSettings.initialBalance; g_overallDrawdown.currentEquity = g_accountState.equity; //--- Calculate the maximum permitted overall loss g_overallDrawdown.maximumOverallLoss = g_overallDrawdown.initialBalance * g_accountSettings.overallDrawdownPercent / 100.0; //--- Calculate the static overall equity floor g_overallDrawdown.overallFloor = g_overallDrawdown.initialBalance - g_overallDrawdown.maximumOverallLoss; //--- Calculate the current loss from the initial balance g_overallDrawdown.currentOverallLoss = g_overallDrawdown.initialBalance - g_overallDrawdown.currentEquity; if(g_overallDrawdown.currentOverallLoss < 0.0) g_overallDrawdown.currentOverallLoss = 0.0; //--- Calculate overall limit usage if(g_overallDrawdown.maximumOverallLoss > 0.0) { g_overallDrawdown.limitUsagePercent = g_overallDrawdown.currentOverallLoss / g_overallDrawdown.maximumOverallLoss * 100.0; } else { g_overallDrawdown.limitUsagePercent = 0.0; } //--- Calculate the remaining overall allowance g_overallDrawdown.remainingAllowance = g_overallDrawdown.maximumOverallLoss - g_overallDrawdown.currentOverallLoss; if(g_overallDrawdown.remainingAllowance < 0.0) g_overallDrawdown.remainingAllowance = 0.0; //--- Classify the current overall drawdown state 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() { //--- Read the fixed challenge reference and realized balance g_profitTarget.initialBalance = g_accountSettings.initialBalance; g_profitTarget.currentBalance = g_accountState.balance; //--- Calculate the required profit and target balance g_profitTarget.targetAmount = g_profitTarget.initialBalance * g_accountSettings.profitTargetPercent / 100.0; g_profitTarget.targetBalance = g_profitTarget.initialBalance + g_profitTarget.targetAmount; //--- Calculate realized profit above the initial balance g_profitTarget.currentProfit = g_profitTarget.currentBalance - g_profitTarget.initialBalance; if(g_profitTarget.currentProfit < 0.0) g_profitTarget.currentProfit = 0.0; //--- Calculate target progress if(g_profitTarget.targetAmount > 0.0) { g_profitTarget.progressPercent = g_profitTarget.currentProfit / g_profitTarget.targetAmount * 100.0; } else { g_profitTarget.progressPercent = 0.0; } if(g_profitTarget.progressPercent > 100.0) g_profitTarget.progressPercent = 100.0; //--- Calculate the remaining required profit g_profitTarget.remainingProfit = g_profitTarget.targetAmount - g_profitTarget.currentProfit; if(g_profitTarget.remainingProfit < 0.0) g_profitTarget.remainingProfit = 0.0; //--- Classify 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() { //--- Clear account-exposure information g_newsState.hasExposure = false; g_newsState.monitoredCurrencies = 0; g_newsState.currencyList = ""; //--- Clear the previously selected calendar event g_newsState.eventFound = false; g_newsState.eventCurrency = ""; g_newsState.eventId = 0; g_newsState.eventName = ""; g_newsState.importance = CALENDAR_IMPORTANCE_NONE; g_newsState.eventTime = 0; //--- Reset event timing and restriction-window state g_newsState.secondsToEvent = 0; g_newsState.restrictionStart = 0; g_newsState.restrictionEnd = 0; g_newsState.restrictedWindow = false; //--- Set the initial monitoring condition g_newsState.status = InpEnableNewsFilter ? NEWS_NO_EXPOSURE : NEWS_DISABLED; } //+------------------------------------------------------------------+ //| Initialize alert state | //+------------------------------------------------------------------+ void InitializeAlertState() { //--- Synchronize financial alert memory with the current rule states g_alertState.dailyDrawdownStatus = g_dailyDrawdown.status; g_alertState.overallDrawdownStatus = g_overallDrawdown.status; g_alertState.profitTargetStatus = g_profitTarget.status; //--- Clear Economic Calendar notification tracking g_alertState.newsEventId = 0; g_alertState.newsApproachAlerted = false; g_alertState.newsRestrictedAlerted = false; //--- Start the dashboard without a retained alert message g_alertState.dashboardMessage = ""; } //+------------------------------------------------------------------+ //| Send guard notification | //+------------------------------------------------------------------+ void SendGuardNotification(const string message) { //--- Record every generated notification in the terminal log Print("Prop Firm Guard: ", message); //--- Show a terminal alert when enabled if(InpEnableAlerts) Alert("Prop Firm Guard: ", message); //--- Stop here when mobile push notifications are disabled if(!InpEnablePushNotifications) return; //--- Confirm that push notifications are enabled in MetaTrader if(!TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)) { Print("Prop Firm Guard: Push notifications are enabled in the EA ", "but disabled in the terminal settings."); return; } //--- Send the optional mobile notification ResetLastError(); if(!SendNotification("Prop Firm Guard: " + message)) { Print("Prop Firm Guard: Could not send push notification. Error: ", GetLastError()); } } //+------------------------------------------------------------------+ //| 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"); } } //+------------------------------------------------------------------+ //| 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() { //--- Daily alerts require a confirmed session baseline if(!g_dailySession.balanceConfirmed) return; //--- An unchanged state has already been observed if(g_dailyDrawdown.status == g_alertState.dailyDrawdownStatus) return; //--- Remember the new state before deciding whether it needs a message g_alertState.dailyDrawdownStatus = g_dailyDrawdown.status; string message = BuildDailyDrawdownAlert(g_dailyDrawdown.status); if(message == "") return; //--- Report the newly entered drawdown state SendGuardNotification(message); } //+------------------------------------------------------------------+ //| Check overall drawdown alert | //+------------------------------------------------------------------+ void CheckOverallDrawdownAlert() { //--- Ignore an unchanged overall drawdown state if(g_overallDrawdown.status == g_alertState.overallDrawdownStatus) return; //--- Store the transition before generating its message g_alertState.overallDrawdownStatus = g_overallDrawdown.status; string message = BuildOverallDrawdownAlert(g_overallDrawdown.status); if(message == "") return; //--- Report the newly entered drawdown state SendGuardNotification(message); } //+------------------------------------------------------------------+ //| Check profit target alert | //+------------------------------------------------------------------+ void CheckProfitTargetAlert() { //--- Ignore an unchanged target state if(g_profitTarget.status == g_alertState.profitTargetStatus) return; //--- Remember the new target state g_alertState.profitTargetStatus = g_profitTarget.status; //--- Only completion produces a target notification if(g_profitTarget.status != TARGET_REACHED) return; SendGuardNotification("Profit target reached."); } //+------------------------------------------------------------------+ //| Check economic news alerts | //+------------------------------------------------------------------+ void CheckNewsAlert() { //--- News alerts require a relevant event and affected exposure if(!InpEnableNewsFilter || !g_newsState.eventFound || g_newsState.affectedPositions == 0) return; //--- Give every newly selected event a fresh notification 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 inside 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; SendGuardNotification(message); } //--- Report prohibited holding once the active restriction requires protection 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; SendGuardNotification(message); } } //+------------------------------------------------------------------+ //| Manage alerts | //+------------------------------------------------------------------+ void ManageAlerts() { //--- Check financial-rule transitions CheckDailyDrawdownAlert(); CheckOverallDrawdownAlert(); CheckProfitTargetAlert(); //--- Check event-specific economic-news transitions CheckNewsAlert(); } //+------------------------------------------------------------------+ //| Check whether currency already exists in array | //+------------------------------------------------------------------+ bool CurrencyExists(const string ¤cies[], const string currency) { int count = ArraySize(currencies); //--- Search the current currency set 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) { //--- Ignore an empty currency value if(currency == "") return(true); //--- Keep each monitored currency only once if(CurrencyExists(currencies, currency)) return(true); int currentSize = ArraySize(currencies); //--- Extend the currency array 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); } //--- Treat XAUUSD as USD calendar exposure if(baseCurrency == "XAU" && profitCurrency == "USD") { currency1 = "USD"; return(true); } //--- Read the symbol's trading calculation model ResetLastError(); if(!SymbolInfoInteger(symbol, SYMBOL_TRADE_CALC_MODE, calculationMode)) { Print("Prop Firm Guard: Could not read calculation mode for ", symbol, ". Error: ", GetLastError()); return(false); } //--- Ignore instruments outside the supported Forex calculation modes if(calculationMode != SYMBOL_CALC_MODE_FOREX && calculationMode != SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE) return(true); currency1 = baseCurrency; currency2 = profitCurrency; return(true); } //+------------------------------------------------------------------+ //| 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(); //--- Classify every currently open account position 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; } 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; //--- Classify the position against the selected event currency 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); //--- Store the ticket-level classification 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() { //--- Reset the account-level holding result g_newsState.affectedPositions = 0; g_newsState.protectionPositions = 0; g_newsState.holdingStatus = NEWS_HOLDING_CLEAR; int totalMatches = ArraySize(g_newsPositionMatches); //--- Stop when no selected event or classified positions exist if(!g_newsState.eventFound || totalMatches == 0) return; //--- Count positions affected by the selected event for(int i = 0; i < totalMatches; i++) { g_newsPositionMatches[i].protectionRequired = false; if(g_newsPositionMatches[i].status == NEWS_POSITION_AFFECTED) g_newsState.affectedPositions++; } //--- No affected positions means no holding restriction applies if(g_newsState.affectedPositions == 0) return; //--- Respect configurations that permit positions to remain open if(InpAllowNewsHolding) { if(g_newsState.restrictedWindow) g_newsState.holdingStatus = NEWS_HOLDING_ALLOWED; else g_newsState.holdingStatus = NEWS_HOLDING_WARNING; return; } //--- Warn while affected positions approach the restricted window if(!g_newsState.restrictedWindow) { g_newsState.holdingStatus = NEWS_HOLDING_WARNING; return; } //--- Mark affected positions when holding is prohibited in the window 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; } //+------------------------------------------------------------------+ //| 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(); } //+------------------------------------------------------------------+ //| Add currencies exposed by position symbol | //+------------------------------------------------------------------+ bool AddPositionCurrencies(const string symbol, string ¤cies[]) { string currency1; string currency2; //--- Determine the supported news currencies for the position symbol if(!GetSymbolNewsCurrencies(symbol, currency1, currency2)) return(false); //--- Ignore instruments outside the supported monitoring scope if(currency1 == "" && currency2 == "") return(true); //--- Add the exposed currencies without creating duplicates if(!AddUniqueCurrency(currencies, currency1)) return(false); if(!AddUniqueCurrency(currencies, currency2)) return(false); return(true); } //+------------------------------------------------------------------+ //| Collect currencies from active positions | //+------------------------------------------------------------------+ bool CollectExposureCurrencies(string ¤cies[]) { //--- Start each collection with an empty exposure set ArrayResize(currencies, 0); int totalPositions = PositionsTotal(); //--- No open positions means there is currently no news exposure if(totalPositions == 0) return(true); //--- Inspect every currently open account position for(int i = 0; i < totalPositions; i++) { ResetLastError(); 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); //--- Append each currency using a comma-separated format 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 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; //--- Inspect the calendar values returned for this currency for(int i = 0; i < valueCount; i++) { MqlCalendarEvent event; ResetLastError(); if(!CalendarEventById(values[i].event_id, event)) { Print("Prop Firm Guard: Could not retrieve calendar event ", values[i].event_id, ". Error: ", GetLastError()); continue; } //--- Reject events below the configured minimum importance if(event.importance < InpNewsImportance) continue; //--- Require a calendar event with an exact release time if(event.time_mode != CALENDAR_TIMEMODE_DATETIME) continue; //--- Keep the earliest qualifying 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 the previous exposure and event state InitializeNewsState(); //--- Stop immediately when economic-news monitoring is disabled if(!InpEnableNewsFilter) { g_newsState.status = NEWS_DISABLED; return; } string currencies[]; //--- Collect one unique currency set from all supported open positions if(!CollectExposureCurrencies(currencies)) { g_newsState.status = NEWS_UNAVAILABLE; return; } int currencyCount = ArraySize(currencies); //--- No supported exposure means there is nothing to search if(currencyCount == 0) { g_newsState.hasExposure = false; g_newsState.monitoredCurrencies = 0; g_newsState.currencyList = ""; g_newsState.status = NEWS_NO_EXPOSURE; return; } //--- Store the current account-exposure summary g_newsState.hasExposure = true; g_newsState.monitoredCurrencies = currencyCount; g_newsState.currencyList = BuildCurrencyList(currencies); //--- Use trade-server time for calendar and restriction calculations datetime currentTime = TimeTradeServer(); datetime searchFrom = currentTime - InpMinutesAfterNews * 60; datetime searchTo = currentTime + 24 * 60 * 60; bool foundAny = false; ulong selectedEventId = 0; string selectedEventName = ""; string selectedCurrency = ""; ENUM_CALENDAR_EVENT_IMPORTANCE selectedImportance = CALENDAR_IMPORTANCE_NONE; datetime selectedEventTime = 0; //--- Search each exposed currency and retain the nearest event for(int i = 0; i < currencyCount; i++) { MqlCalendarValue candidateValue; MqlCalendarEvent candidateEvent; if(!FindNextNewsForCurrency(currencies[i], searchFrom, searchTo, candidateValue, candidateEvent)) continue; if(!foundAny || candidateValue.time < selectedEventTime) { selectedEventId = candidateEvent.id; selectedEventName = candidateEvent.name; selectedCurrency = currencies[i]; selectedImportance = candidateEvent.importance; selectedEventTime = candidateValue.time; foundAny = true; } } //--- Report an exposed account with no qualifying event if(!foundAny) { g_newsState.status = NEWS_NO_EVENT; return; } //--- Store the nearest qualifying event across all exposed currencies g_newsState.eventFound = true; g_newsState.eventCurrency = selectedCurrency; g_newsState.eventId = selectedEventId; g_newsState.eventName = selectedEventName; g_newsState.importance = selectedImportance; g_newsState.eventTime = selectedEventTime; //--- Calculate the signed distance from the current server time g_newsState.secondsToEvent = (long)(g_newsState.eventTime - currentTime); //--- Build the configured restriction interval g_newsState.restrictionStart = g_newsState.eventTime - InpMinutesBeforeNews * 60; g_newsState.restrictionEnd = g_newsState.eventTime + InpMinutesAfterNews * 60; //--- Determine whether the current time is inside that interval 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; } //+------------------------------------------------------------------+ //| Format news countdown | //+------------------------------------------------------------------+ string FormatNewsCountdown(const long secondsToEvent) { long absoluteSeconds = secondsToEvent; //--- Convert the signed value into a duration for formatting 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); //--- Prefix post-event elapsed time (secondsToEvent < 0) with a plus sign if(secondsToEvent >= 0) return(formatted); return("+" + formatted); } //+------------------------------------------------------------------+ //| Get trading date | //+------------------------------------------------------------------+ string GetTradingDate() { //--- Build the session date from trading-server time datetime serverTime = TimeCurrent(); return(TimeToString(serverTime, TIME_DATE)); } //+------------------------------------------------------------------+ //| Open database | //+------------------------------------------------------------------+ bool OpenDatabase() { //--- Open or create the SQLite database 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); } //+------------------------------------------------------------------+ //| Close database | //+------------------------------------------------------------------+ void CloseDatabase() { //--- Ignore the request when no database connection is active if(g_database == INVALID_HANDLE) return; //--- Close the active SQLite connection 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."); } //--- Clear the stored database handle g_database = INVALID_HANDLE; } //+------------------------------------------------------------------+ //| Check whether database column exists | //+------------------------------------------------------------------+ bool DatabaseColumnExists(const string tableName, const string columnName) { //--- Inspect the current table schema 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; //--- Search the returned schema rows for the requested column 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; } } //--- Release the schema request FinalizeDatabaseRequest(request); return(columnFound); } //+------------------------------------------------------------------+ //| Ensure database column exists | //+------------------------------------------------------------------+ bool EnsureDatabaseColumn(const string tableName, const string columnName, const string columnDefinition) { //--- Leave an existing column unchanged if(DatabaseColumnExists(tableName, columnName)) return(true); //--- Add the missing column 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() { //--- Add runtime persistence columns when missing 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() { //--- Define account-level settings storage 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)" ");"; //--- Define persistent trading-day storage 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)" ");"; //--- Ensure the account settings table exists ResetLastError(); if(!DatabaseExecute(g_database, settingsTable)) { Print("Prop Firm Guard: Could not create settings table. Error: ", GetLastError()); return(false); } //--- Ensure the daily-session table exists ResetLastError(); if(!DatabaseExecute(g_database, dailySessionsTable)) { Print("Prop Firm Guard: Could not create daily_sessions table. Error: ", GetLastError()); return(false); } //--- Extend an existing daily-session table when required if(!EnsureDailySessionRuntimeColumns()) return(false); return(true); } //+------------------------------------------------------------------+ //| Check for new trading day | //+------------------------------------------------------------------+ void CheckForNewTradingDay() { //--- Read the current server-based trading date string currentDate = GetTradingDate(); //--- Keep the active session when it already belongs to today if(g_hasDailySession && g_dailySession.tradingDate == currentDate) return; Print("Prop Firm Guard: New trading day detected: ", currentDate); //--- Restore or create the session for the new trading date if(!InitializeDailySession()) { Print("Prop Firm Guard: Could not initialize the new daily session."); return; } //--- Rebuild the confirmation controls for the newly loaded session DeleteDailySessionControls(); if(!CreateDailySessionControls()) { Print("Prop Firm Guard: Could not create daily session controls."); } } //+------------------------------------------------------------------+ //| Finalize database request | //+------------------------------------------------------------------+ void FinalizeDatabaseRequest(const int request) { //--- Ignore invalid database requests if(request == INVALID_HANDLE) return; //--- Release the prepared database request ResetLastError(); DatabaseFinalize(request); int errorCode = GetLastError(); if(errorCode != 0) { Print("Prop Firm Guard: Could not finalize database request. Error: ", errorCode); } } //+------------------------------------------------------------------+ //| Execute prepared database statement | //+------------------------------------------------------------------+ bool ExecuteDatabaseRequest(const int request, const string operation) { //--- Execute the prepared database statement ResetLastError(); bool result = DatabaseRead(request); int errorCode = GetLastError(); //--- Accept normal completion when no result row is returned bool success = (result || errorCode == ERR_DATABASE_NO_MORE_DATA); if(!success) { Print("Prop Firm Guard: Database error while ", operation, ". Error: ", errorCode); } //--- Release the request after execution FinalizeDatabaseRequest(request); return(success); } //+------------------------------------------------------------------+ //| Save account settings | //+------------------------------------------------------------------+ bool SaveAccountSettings() { //--- Prepare the account-settings write statement 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 the current account settings to the prepared 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); } //--- Execute and finalize the prepared write request if(!ExecuteDatabaseRequest(request, "saving account settings")) return(false); return(true); } //+------------------------------------------------------------------+ //| Load account settings | //+------------------------------------------------------------------+ bool LoadAccountSettings() { //--- Build the current account/server lookup 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); } //--- Bind the current account identity 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); } //--- Read the matching settings record ResetLastError(); if(!DatabaseRead(request)) { int errorCode = GetLastError(); FinalizeDatabaseRequest(request); //--- No row means this account/server pair has not been saved yet if(errorCode == ERR_DATABASE_NO_MORE_DATA) return(false); Print("Prop Firm Guard: Could not read account settings. Error: ", errorCode); return(false); } //--- Read and validate the stored column values long accountLogin; string storedServer; double initialBalance; double dailyDrawdown; double overallDrawdown; double profitTarget; long updatedAt; 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); } //--- Restore the stored values into the account settings structure 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; //--- Release the completed read request FinalizeDatabaseRequest(request); return(true); } //+------------------------------------------------------------------+ //| Initialize daily session | //+------------------------------------------------------------------+ bool InitializeDailySession() { //--- Identify the current trading day string tradingDate = GetTradingDate(); //--- Restore today's session when it already exists if(DailySessionExists(tradingDate)) { if(!LoadDailySession()) return(false); g_hasDailySession = true; Print("Prop Firm Guard: Daily session restored for ", tradingDate, "."); return(true); } //--- Create a new unconfirmed session for the current trading day 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(); //--- Persist the new daily-session record if(!SaveDailySession()) return(false); g_hasDailySession = true; Print("Prop Firm Guard: New daily session created for ", tradingDate, "."); return(true); } //+------------------------------------------------------------------+ //| Initialize account settings | //+------------------------------------------------------------------+ bool InitializeAccountSettings() { //--- Check whether this account/server pair already has stored settings bool settingsFound = LoadAccountSettings(); //--- Associate the settings with the current trading account g_accountSettings.accountLogin = g_accountState.login; g_accountSettings.accountServer = AccountInfoString(ACCOUNT_SERVER); //--- Synchronize persistence with the current Inputs values g_accountSettings.initialBalance = InpInitialAccountBalance; g_accountSettings.dailyDrawdownPercent = InpDailyDrawdownPercent; g_accountSettings.overallDrawdownPercent = InpOverallDrawdownPercent; g_accountSettings.profitTargetPercent = InpProfitTargetPercent; g_accountSettings.updatedAt = TimeCurrent(); //--- Save the synchronized configuration if(!SaveAccountSettings()) return(false); //--- Report whether the record was created or synchronized if(settingsFound) Print("Prop Firm Guard: Account settings restored and synchronized."); else Print("Prop Firm Guard: New account settings saved."); return(true); } //+------------------------------------------------------------------+ //| Save daily session | //+------------------------------------------------------------------+ bool SaveDailySession() { //--- Prepare the complete daily-session record 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); } //--- Execute and finalize the prepared write request return(ExecuteDatabaseRequest(request, "saving daily session")); } //+------------------------------------------------------------------+ //| Load daily session | //+------------------------------------------------------------------+ bool LoadDailySession() { //--- Select the session belonging to the current account and date 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); } //--- Read the matching record 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); } //--- Read all stored values before updating the in-memory session 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); } //--- Restore the complete daily-session state 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; //--- Release the completed read request FinalizeDatabaseRequest(request); return(true); } //+------------------------------------------------------------------+ //| Check daily session existence | //+------------------------------------------------------------------+ bool DailySessionExists(const string tradingDate) { //--- Build the session identity for the requested trading date 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); } //--- Bind the current account, server, and requested date 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); } //--- Check whether one matching row exists 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); } //+------------------------------------------------------------------+ //| Create starting balance edit | //+------------------------------------------------------------------+ bool CreateStartingBalanceEdit(const int x, const int y, const string suggestedBalance) { //--- Create the editable starting-balance field if(!ObjectCreate(0, g_startBalanceEdit, OBJ_EDIT, 0, 0, 0)) { Print("Prop Firm Guard: Could not create starting balance field. Error: ", GetLastError()); return(false); } //--- Configure the field position and appearance 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); } //--- Apply the suggested balance and text properties 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 daily-balance confirmation button if(!ObjectCreate(0, g_confirmButton, OBJ_BUTTON, 0, 0, 0)) { Print("Prop Firm Guard: Could not create confirmation button. Error: ", GetLastError()); return(false); } //--- Configure the button position and appearance 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); } //--- Apply the button text and font 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); } //+------------------------------------------------------------------+ //| Create daily session controls | //+------------------------------------------------------------------+ bool CreateDailySessionControls() { //--- Skip the controls when today's balance is already confirmed if(g_dailySession.balanceConfirmed) return(true); int controlX = InpDashboardX + 18; int controlY = InpDashboardY + 120; //--- Suggest the current balance without confirming it string suggestedBalance = DoubleToString(g_accountState.balance, 2); //--- Create the editable starting-balance field if(!CreateStartingBalanceEdit(controlX, controlY, suggestedBalance)) return(false); //--- Create the confirmation button beside the edit field if(!CreateConfirmBalanceButton(controlX + 130, controlY)) return(false); return(true); } //+------------------------------------------------------------------+ //| Delete daily session controls | //+------------------------------------------------------------------+ void DeleteDailySessionControls() { //--- Remove the starting-balance field when present if(ObjectFind(0, g_startBalanceEdit) >= 0) { if(!ObjectDelete(0, g_startBalanceEdit)) { Print("Prop Firm Guard: Could not delete starting balance field. Error: ", GetLastError()); } } //--- Remove the confirmation button when present if(ObjectFind(0, g_confirmButton) >= 0) { if(!ObjectDelete(0, g_confirmButton)) { Print("Prop Firm Guard: Could not delete confirmation button. Error: ", GetLastError()); } } ChartRedraw(); } //+------------------------------------------------------------------+ //| Confirm daily starting balance | //+------------------------------------------------------------------+ bool ConfirmDailyStartingBalance() { //--- Keep an existing confirmed baseline unchanged 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 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); //--- Validate the entered starting balance if(startingBalance <= 0.0) { Alert("Prop Firm Guard: Enter a valid starting balance greater than zero."); return(false); } //--- Store the confirmed daily reference g_dailySession.startingBalance = startingBalance; g_dailySession.startingEquity = g_accountState.equity; g_dailySession.balanceConfirmed = true; g_dailySession.updatedAt = TimeCurrent(); //--- Persist the confirmed session immediately if(!SaveDailySession()) { Print("Prop Firm Guard: Could not save confirmed daily session."); //--- Keep the in memory state consistent with the database result g_dailySession.balanceConfirmed = false; return(false); } Print("Prop Firm Guard: Daily starting balance confirmed at ", DoubleToString(g_dailySession.startingBalance, 2), " for ", g_dailySession.tradingDate, "."); //--- Remove the confirmation controls after a successful save DeleteDailySessionControls(); //--- Refresh the dashboard with the confirmed session state UpdateDashboard(); return(true); } //+------------------------------------------------------------------+ //| 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"); } } //+------------------------------------------------------------------+ //| Get overall guard status | //+------------------------------------------------------------------+ ENUM_GUARD_STATUS GetOverallGuardStatus() { //--- Give the highest priority to any drawdown breach if(g_dailyDrawdown.status == COMPLIANCE_BREACHED || g_overallDrawdown.status == COMPLIANCE_BREACHED) return(GUARD_BREACHED); //--- Check for a critical drawdown condition if(g_dailyDrawdown.status == COMPLIANCE_CRITICAL || g_overallDrawdown.status == COMPLIANCE_CRITICAL) return(GUARD_CRITICAL); //--- Check for a warning condition if(g_dailyDrawdown.status == COMPLIANCE_WARNING || g_overallDrawdown.status == COMPLIANCE_WARNING) return(GUARD_WARNING); //--- Report target completion only when no risk condition has priority 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 news trading window text | //+------------------------------------------------------------------+ string GetNewsTradingWindowText() { //--- Report the configured filter state if(!InpEnableNewsFilter) return("DISABLED"); //--- A missing relevant event leaves the current window safe if(!g_newsState.eventFound) return("SAFE"); //--- Report the active restricted interval if(g_newsState.restrictedWindow) return("RESTRICTED"); return("SAFE"); } //+------------------------------------------------------------------+ //| Get configured news holding rule | //+------------------------------------------------------------------+ string GetNewsHoldingRuleText() { //--- Convert the configured holding rule into dashboard text if(InpAllowNewsHolding) return("ALLOWED"); return("NOT ALLOWED"); } //+------------------------------------------------------------------+ //| 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); } } //+------------------------------------------------------------------+ //| Get daily session status | //+------------------------------------------------------------------+ string GetDailySessionStatus() { //--- An unconfirmed starting balance keeps the session in setup if(!g_dailySession.balanceConfirmed) return("AWAITING_CONFIRMATION"); //--- A drawdown breach has the highest session priority if(g_dailyDrawdown.status == COMPLIANCE_BREACHED || g_overallDrawdown.status == COMPLIANCE_BREACHED) return("BREACHED"); //--- Record completion when the profit target has been reached if(g_profitTarget.status == TARGET_REACHED) return("TARGET_REACHED"); //--- Preserve the highest active drawdown warning level 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() { //--- Runtime persistence requires an active daily session if(!g_hasDailySession) return(false); //--- Copy the latest account and target values into the session 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(); //--- Update daily loss only after the session baseline is confirmed if(g_dailySession.balanceConfirmed) { g_dailySession.dailyLoss = g_dailyDrawdown.currentDailyLoss; //--- Preserve the greatest daily 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; } //--- Save the synchronized runtime snapshot return(SaveDailySession()); } //+------------------------------------------------------------------+ //| Update runtime database when due | //+------------------------------------------------------------------+ void UpdateRuntimeDatabase() { //--- Read the current terminal time datetime currentTime = TimeCurrent(); //--- Limit runtime session writes to one update every 10 seconds if(g_lastRuntimeDatabaseUpdate != 0 && currentTime - g_lastRuntimeDatabaseUpdate < 10) return; //--- Save the latest runtime snapshot and remember a successful update if(UpdateDailySessionRuntimeState()) g_lastRuntimeDatabaseUpdate = currentTime; } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Validate the timer interval if(InpTimerSeconds < 1) { Print("Prop Firm Guard: Timer interval must be at least 1 second."); return(INIT_PARAMETERS_INCORRECT); } //--- Validate the configured compliance inputs if(!ValidateComplianceInputs()) return(INIT_PARAMETERS_INCORRECT); //--- Prepare the chart before creating dashboard objects if(!ConfigureChartAppearance()) { Print("Prop Firm Guard: Could not configure chart appearance."); return(INIT_FAILED); } //--- Use synchronous trade execution for protection actions g_trade.SetAsyncMode(false); //--- Read the current account state UpdateAccountState(); //--- Open the SQLite database if(!OpenDatabase()) { Print("Prop Firm Guard: Database initialization failed."); return(INIT_FAILED); } //--- Ensure the required database tables exist if(!EnsureDatabaseTables()) { Print("Prop Firm Guard: Could not initialize database tables."); CloseDatabase(); return(INIT_FAILED); } //--- Restore account specific settings if(!InitializeAccountSettings()) { Print("Prop Firm Guard: Could not initialize account settings."); CloseDatabase(); return(INIT_FAILED); } //--- Restore or create the current daily session if(!InitializeDailySession()) { Print("Prop Firm Guard: Could not initialize daily session."); CloseDatabase(); return(INIT_FAILED); } //--- Calculate the initial financial compliance state CalculateDailyDrawdown(); CalculateOverallDrawdown(); CalculateProfitTarget(); //--- Calculate the initial Economic Calendar state UpdateNewsState(); UpdateNewsPositionMatches(); //--- Establish the initial alert baseline InitializeAlertState(); //--- Create the complete monitoring dashboard if(!CreateDashboard()) { Print("Prop Firm Guard: Failed to create the dashboard."); CloseDatabase(); return(INIT_FAILED); } //--- Populate the dashboard from the restored monitoring state UpdateDashboard(); //--- Start periodic monitoring 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 periodic monitoring EventKillTimer(); //--- Remove all chart objects owned by Prop Firm Guard DeleteDashboard(); //--- Close the SQLite database CloseDatabase(); Print("Prop Firm Guard removed. Deinitialization reason: ", reason); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { } //+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer() { //--- Refresh the current account state UpdateAccountState(); //--- Maintain the current trading day session CheckForNewTradingDay(); //--- Recalculate the financial compliance state CalculateDailyDrawdown(); CalculateOverallDrawdown(); CalculateProfitTarget(); //--- Refresh Economic Calendar exposure and position classification UpdateNewsState(); UpdateNewsPositionMatches(); //--- Process alert transitions and configured protection ManageAlerts(); ManageProtectionActions(); //--- Refresh financial values after any protection action UpdateAccountState(); CalculateDailyDrawdown(); CalculateOverallDrawdown(); CalculateProfitTarget(); //--- Persist the latest daily session snapshot when due UpdateRuntimeDatabase(); //--- Present the resulting monitoring state UpdateDashboard(); } //+------------------------------------------------------------------+ //| Chart event function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Process chart-object clicks only if(id != CHARTEVENT_OBJECT_CLICK) return; //--- Confirm the entered balance when the confirmation button is clicked if(sparam == g_confirmButton) { if(!ConfirmDailyStartingBalance()) Print("Prop Firm Guard: Starting balance confirmation failed."); } } //+------------------------------------------------------------------+ //| Check successful position-close return code | //+------------------------------------------------------------------+ bool IsSuccessfulCloseRetcode(const uint retcode) { //--- Accept only return codes that confirm complete closure 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) { //--- Confirm that the position still exists ResetLastError(); if(!PositionSelectByTicket(ticket)) { int errorCode = GetLastError(); Print("Prop Firm Guard: Position ", ticket, " is no longer available. Error: ", errorCode); return(true); } string symbol; //--- Read the symbol associated with the selected ticket if(!PositionGetString(POSITION_SYMBOL, symbol)) { Print("Prop Firm Guard: Could not read symbol for position ", ticket, ". Error: ", GetLastError()); return(false); } //--- Use the filling policy supported by this symbol if(!g_trade.SetTypeFillingBySymbol(symbol)) { Print("Prop Firm Guard: Could not determine filling policy for ", symbol, "."); return(false); } //--- Submit the close request ResetLastError(); 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); } //--- Require explicit trade-server confirmation of complete closure 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 no longer exists 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[]; //--- Allocate storage for the current position set if(ArrayResize(tickets, totalPositions) != totalPositions) { Print("Prop Firm Guard: Could not allocate position ticket array."); return(false); } int ticketCount = 0; //--- Capture every open position ticket 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; //--- Close positions from the stable ticket list 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; //--- Collect only positions marked for protection 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; //--- Close only the selected protection tickets 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: 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: 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() { if(g_newsState.holdingStatus != NEWS_HOLDING_PROTECTION || !g_newsState.eventFound) return; switch(InpNewsRestrictionAction) { case PROTECTION_WARN_ONLY: 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() { //--- Process account-wide financial protection first HandleDailyDrawdownProtection(); HandleOverallDrawdownProtection(); //--- Apply news protection to any remaining exposure HandleNewsProtection(); } //+------------------------------------------------------------------+ //| Create dashboard | //+------------------------------------------------------------------+ bool CreateDashboard() { //--- Define the panel and two column positions string panelName = g_objectPrefix + "Panel"; int leftX = InpDashboardX + 18; int rightX = InpDashboardX + 275; //--- Create the dashboard background if(!ObjectCreate(0, panelName, OBJ_RECTANGLE_LABEL, 0, 0, 0)) { Print("Prop Firm Guard: Could not create dashboard panel. Error: ", GetLastError()); return(false); } //--- Configure the dashboard panel 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); } //--- Create the main title and overall status 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); //--- Create the session section 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); //--- Create the account section 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); //--- Create the daily drawdown section 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); //--- Create the overall drawdown section 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); //--- Create the profit target section 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); //--- Create the news section 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); //--- Create the combined status section 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); //--- Integrate the existing daily session controls if(!CreateDailySessionControls()) return(false); //--- Draw the completed static interface 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 label relative to the upper left 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 the label position and visual properties 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); } //--- Set the common dashboard font and initial text 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) { //--- Confirm that the requested dashboard object exists if(ObjectFind(0, objectName) < 0) { Print("Prop Firm Guard: Dashboard object not found: ", objectName); return; } //--- Replace the displayed text 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) { //--- Confirm that the requested dashboard object exists if(ObjectFind(0, objectName) < 0) { Print("Prop Firm Guard: Dashboard object not found: ", objectName); return; } //--- Apply the requested text color 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(); //--- Prepare the current session and main status text 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"; } //--- Update session information UpdateDashboardText(g_objectPrefix + "TradingDate", "Trading Date: " + g_dailySession.tradingDate); UpdateDashboardText(g_objectPrefix + "SessionState", sessionState); UpdateDashboardText(g_objectPrefix + "DailyStart", dailyStart); //--- Update account information 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)); //--- Update daily drawdown information 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"); } //--- Update overall drawdown information 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)); //--- Update profit target information 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)); //--- Update Economic Calendar information 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)); } //--- Update the current news holding condition UpdateDashboardText(g_objectPrefix + "NewsStatus", "Status: " + NewsHoldingStatusToString(g_newsState.holdingStatus)); //--- Update the combined guard result UpdateDashboardText(g_objectPrefix + "OverallStatus", headerStatus); if(g_dailySession.balanceConfirmed) UpdateDashboardColor(g_objectPrefix + "OverallStatus", GetGuardStatusColor(guardStatus)); else UpdateDashboardColor(g_objectPrefix + "OverallStatus", clrDarkOrange); //--- Update the compact 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)); //--- Present all updated chart objects 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()); } //--- Refresh the chart after removing the interface ChartRedraw(); } //+------------------------------------------------------------------+