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