//+------------------------------------------------------------------+ //| PropFirmGuard.mq5 | //| Copyright 2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| Standard Library | //+------------------------------------------------------------------+ #include //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+ //| Represents the general compliance condition of the account. | //+------------------------------------------------------------------+ enum ENUM_COMPLIANCE_STATUS { COMPLIANCE_SAFE = 0, // Account is operating within configured limits COMPLIANCE_WARNING, // Account is approaching a compliance limit COMPLIANCE_CRITICAL, // Account is very close to a compliance limit COMPLIANCE_BREACHED // A configured compliance limit has been breached }; //+------------------------------------------------------------------+ //| Profit target status | //+------------------------------------------------------------------+ //| Represents progress toward the configured account profit target. | //+------------------------------------------------------------------+ enum ENUM_TARGET_STATUS { TARGET_IN_PROGRESS = 0, // Target is still being pursued TARGET_NEARING, // At least 90% of the target has been reached TARGET_REACHED // Required target balance has been achieved }; //+------------------------------------------------------------------+ //| 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 }; //+------------------------------------------------------------------+ //| Daily session status | //+------------------------------------------------------------------+ //| Represents the lifecycle state of one persistent daily session. | //+------------------------------------------------------------------+ enum ENUM_DAILY_SESSION_STATUS { SESSION_AWAITING_CONFIRMATION = 0, // Daily starting balance is not yet confirmed SESSION_ACTIVE, // Daily starting balance has been confirmed SESSION_CLOSED // Session has been finalized after day rollover }; //+------------------------------------------------------------------+ //| Daily session load result | //+------------------------------------------------------------------+ //| Distinguishes a restored session from absence and database error. | //+------------------------------------------------------------------+ enum ENUM_DAILY_SESSION_LOAD_RESULT { SESSION_LOAD_ERROR = -1, // Database operation failed SESSION_LOAD_NOT_FOUND = 0, // No matching daily session exists SESSION_LOAD_FOUND = 1 // Matching daily session was restored }; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "General" input int InpTimerSeconds = 1; // Dashboard update interval in seconds input int InpDashboardX = 10; // Dashboard horizontal position input int InpDashboardY = 105; // Dashboard vertical position input bool InpEnableAlerts = true; // Enable terminal alerts input bool InpEnablePushNotifications = false; // Enable mobile push notifications input group "Daily Session" input int InpDailyResetHour = 0; // Daily reset hour in broker server time input int InpDailyResetMinute = 0; // Daily reset minute in broker server time input group "Prop Firm Rules" input double InpInitialAccountBalance = 20000.0; // Initial prop-firm account balance input double InpDailyDrawdownPercent = 5.0; // Maximum permitted daily drawdown input double InpOverallDrawdownPercent = 10.0; // Maximum permitted overall drawdown input double InpProfitTargetPercent = 8.0; // Required profit target input group "Warning Levels" input double InpWarningLevelPercent = 70.0; // Loss limit usage that triggers warning input double InpCriticalLevelPercent = 90.0; // Loss limit usage that triggers critical state input group "News Filter" input bool InpEnableNewsFilter = true; // Enable economic news monitoring input ENUM_CALENDAR_EVENT_IMPORTANCE InpNewsImportance = CALENDAR_IMPORTANCE_HIGH; // Minimum monitored importance input int InpMinutesBeforeNews = 10; // Restricted minutes before news input int InpMinutesAfterNews = 10; // Restricted minutes after news input bool InpAllowNewsHolding = true; // Allow positions to remain open during news input group "Protection Actions" input ENUM_PROTECTION_ACTION InpDailyDrawdownAction = PROTECTION_WARN_ONLY; // Daily breach action input ENUM_PROTECTION_ACTION InpOverallDrawdownAction = PROTECTION_WARN_ONLY; // Overall breach action input ENUM_PROTECTION_ACTION InpNewsRestrictionAction = PROTECTION_WARN_ONLY; // News restriction action //+------------------------------------------------------------------+ //| Account state structure | //+------------------------------------------------------------------+ //| Holds the live account values required by the monitoring logic. | //+------------------------------------------------------------------+ struct SAccountState { long login; // Trading account login number double balance; // Current account balance double equity; // Current account equity double floatingPL; // Current floating profit or loss ENUM_COMPLIANCE_STATUS status; // Current overall compliance status }; //+------------------------------------------------------------------+ //| Account settings structure | //+------------------------------------------------------------------+ //| Stores the main prop-firm rules associated with this account. | //+------------------------------------------------------------------+ struct SAccountSettings { long accountLogin; // Trading account login string accountServer; // Broker trading server double initialBalance; // Initial prop-firm account balance double dailyDrawdownPercent; // Maximum daily drawdown double overallDrawdownPercent; // Maximum overall drawdown double profitTargetPercent; // Required profit target datetime updatedAt; // Last settings update time }; //+------------------------------------------------------------------+ //| Daily session structure | //+------------------------------------------------------------------+ //| Stores the persistent reference for one configured trading day. | //+------------------------------------------------------------------+ struct SDailySession { long accountLogin; // Trading account login string accountServer; // Broker trading server string sessionDate; // Configured trading-day session key double startingBalance; // Trader-confirmed daily starting balance double startingEquity; // Equity captured when confirmation occurs bool balanceConfirmed; // Whether the daily baseline is confirmed ENUM_DAILY_SESSION_STATUS status; // Current daily session lifecycle state datetime createdAt; // Time the session record was created datetime updatedAt; // Time the session record was last updated }; //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ SAccountState g_accountState; // Latest monitored account state SAccountSettings g_accountSettings; // Persisted prop-firm settings SDailySession g_dailySession; // Current configured daily session 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 string g_instanceLockName = ""; // Account-specific terminal instance lock int g_database = INVALID_HANDLE; // Active SQLite database handle bool g_hasDailySession = false; // Whether a daily session is currently loaded bool g_instanceLockAcquired = false; // Whether this EA owns the instance lock //+------------------------------------------------------------------+ //| Validate compliance inputs | //+------------------------------------------------------------------+ bool ValidateComplianceInputs() { //--- Validate the monitoring interval if(InpTimerSeconds < 1) { Print("Prop Firm Guard: Timer interval must be at least 1 second."); return(false); } //--- Validate the configured daily reset hour if(InpDailyResetHour < 0 || InpDailyResetHour > 23) { Print("Prop Firm Guard: Daily reset hour must be between 0 and 23."); return(false); } //--- Validate the configured daily reset minute if(InpDailyResetMinute < 0 || InpDailyResetMinute > 59) { Print("Prop Firm Guard: Daily reset minute must be between 0 and 59."); return(false); } //--- Validate the initial account balance if(InpInitialAccountBalance <= 0.0) { Print("Prop Firm Guard: Initial account balance must be greater than zero."); return(false); } //--- Validate the daily drawdown percentage if(InpDailyDrawdownPercent <= 0.0 || InpDailyDrawdownPercent > 100.0) { Print("Prop Firm Guard: Daily drawdown percentage must be between 0 and 100."); return(false); } //--- Validate the overall drawdown percentage if(InpOverallDrawdownPercent <= 0.0 || InpOverallDrawdownPercent > 100.0) { Print("Prop Firm Guard: Overall drawdown percentage must be between 0 and 100."); return(false); } //--- Validate the profit target percentage if(InpProfitTargetPercent <= 0.0 || InpProfitTargetPercent > 100.0) { Print("Prop Firm Guard: Profit target percentage must be between 0 and 100."); return(false); } //--- Validate the warning threshold if(InpWarningLevelPercent <= 0.0 || InpWarningLevelPercent >= InpCriticalLevelPercent) { Print("Prop Firm Guard: Warning level must be greater than 0 and below the critical level."); return(false); } //--- Validate the critical threshold if(InpCriticalLevelPercent <= InpWarningLevelPercent || InpCriticalLevelPercent >= 100.0) { Print("Prop Firm Guard: Critical level must be above the warning level and below 100."); return(false); } //--- Validate the news restriction windows if(InpMinutesBeforeNews < 0 || InpMinutesAfterNews < 0) { Print("Prop Firm Guard: News restriction minutes cannot be negative."); return(false); } return(true); } //+------------------------------------------------------------------+ //| Update account state | //+------------------------------------------------------------------+ void UpdateAccountState() { //--- Read the current account values g_accountState.login = AccountInfoInteger(ACCOUNT_LOGIN); g_accountState.balance = AccountInfoDouble(ACCOUNT_BALANCE); g_accountState.equity = AccountInfoDouble(ACCOUNT_EQUITY); //--- Derive the current floating profit or loss g_accountState.floatingPL = g_accountState.equity - g_accountState.balance; //--- Initialize compliance status before rule calculations are added g_accountState.status = COMPLIANCE_SAFE; } //+------------------------------------------------------------------+ //| Acquire instance lock | //+------------------------------------------------------------------+ bool AcquireInstanceLock() { //--- Require a valid trading account identity if(g_accountState.login <= 0) { Print("Prop Firm Guard: Cannot acquire an instance lock without a valid account login."); return(false); } //--- Build one terminal-wide lock name for the current account g_instanceLockName = StringFormat("PFG_INSTANCE_LOCK_%I64d", g_accountState.login); //--- Create the temporary lock only when it does not already exist if(!GlobalVariableCheck(g_instanceLockName)) { ResetLastError(); if(!GlobalVariableTemp(g_instanceLockName)) { Print("Prop Firm Guard: Could not create the terminal instance lock. Error: ", GetLastError()); return(false); } } //--- Atomically claim an available lock whose current value is zero ResetLastError(); if(!GlobalVariableSetOnCondition(g_instanceLockName, 1.0, 0.0)) { int errorCode = GetLastError(); if(errorCode == 0) { Print("Prop Firm Guard: Another instance is already active for account ", g_accountState.login, ". Only one instance per trading account is allowed."); } else { Print("Prop Firm Guard: Could not acquire the terminal instance lock. Error: ", errorCode); } return(false); } //--- Record ownership only after the atomic acquisition succeeds g_instanceLockAcquired = true; Print("Prop Firm Guard: Instance lock acquired for account ", g_accountState.login, "."); return(true); } //+------------------------------------------------------------------+ //| Release instance lock | //+------------------------------------------------------------------+ bool ReleaseInstanceLock() { //--- Ignore cleanup when this EA never acquired the lock if(!g_instanceLockAcquired) return(true); //--- Treat a missing lock variable as already released if(!GlobalVariableCheck(g_instanceLockName)) { g_instanceLockAcquired = false; return(true); } //--- Atomically return this owned lock to the available state ResetLastError(); if(!GlobalVariableSetOnCondition(g_instanceLockName, 0.0, 1.0)) { Print("Prop Firm Guard: Could not release the terminal instance lock. Error: ", GetLastError()); return(false); } //--- Clear local ownership after successful release g_instanceLockAcquired = false; Print("Prop Firm Guard: Instance lock released for account ", g_accountState.login, "."); return(true); } //+------------------------------------------------------------------+ //| Get current daily session date | //+------------------------------------------------------------------+ string GetCurrentSessionDate() { //--- Read the server time currently known by the terminal datetime serverTime = TimeCurrent(); //--- Convert the configured reset boundary to seconds from midnight int resetOffsetSeconds = (InpDailyResetHour * 60 * 60) + (InpDailyResetMinute * 60); //--- Shift the server time so the configured reset becomes midnight datetime sessionTime = serverTime - resetOffsetSeconds; //--- Use the shifted calendar date as the persistent session key return(TimeToString(sessionTime, TIME_DATE)); } //+------------------------------------------------------------------+ //| Get account currency digits | //+------------------------------------------------------------------+ int GetAccountCurrencyDigits() { //--- Read the precision configured for the account currency ResetLastError(); long currencyDigits = AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS); int errorCode = GetLastError(); if(errorCode != 0) { Print("Prop Firm Guard: Could not read account currency digits. Error: ", errorCode); return(-1); } //--- Reject an invalid precision returned by the terminal if(currencyDigits < 0 || currencyDigits > 8) { Print("Prop Firm Guard: Account currency digits are outside the supported range."); return(-1); } return((int)currencyDigits); } //+------------------------------------------------------------------+ //| Format account monetary value | //+------------------------------------------------------------------+ bool FormatAccountMoney(const double value, string &formattedValue) { //--- Reject invalid numeric input if(!MathIsValidNumber(value)) { Print("Prop Firm Guard: Cannot format an invalid monetary value."); return(false); } //--- Read the account currency precision int currencyDigits = GetAccountCurrencyDigits(); if(currencyDigits < 0) return(false); //--- Format the value using the account currency precision formattedValue = DoubleToString(value, currencyDigits); return(true); } //+------------------------------------------------------------------+ //| Parse starting balance | //+------------------------------------------------------------------+ bool ParseStartingBalance(const string balanceText, double &startingBalance) { //--- Start with a neutral output value startingBalance = 0.0; //--- Remove surrounding whitespace before validation string normalizedText = balanceText; StringTrimLeft(normalizedText); StringTrimRight(normalizedText); int textLength = StringLen(normalizedText); if(textLength == 0) return(false); bool decimalPointFound = false; int digitCount = 0; int fractionalDigits = 0; //--- Validate the complete input before numeric conversion for(int i = 0; i < textLength; i++) { ushort character = (ushort)StringGetCharacter(normalizedText, i); if(character >= '0' && character <= '9') { digitCount++; if(decimalPointFound) fractionalDigits++; continue; } if(character == '.') { if(decimalPointFound) return(false); if(i == 0 || i == textLength - 1) return(false); decimalPointFound = true; continue; } return(false); } if(digitCount == 0) return(false); //--- Enforce the account currency precision int currencyDigits = GetAccountCurrencyDigits(); if(currencyDigits < 0) return(false); if(fractionalDigits > currencyDigits) return(false); //--- Convert only after the whole string has passed validation double parsedValue = StringToDouble(normalizedText); if(!MathIsValidNumber(parsedValue) || parsedValue <= 0.0) return(false); startingBalance = parsedValue; return(true); } //+------------------------------------------------------------------+ //| Check daily session state consistency | //+------------------------------------------------------------------+ bool IsDailySessionStateConsistent(const SDailySession &session) { //--- An unconfirmed session must remain in the waiting state if(session.status == SESSION_AWAITING_CONFIRMATION) return(!session.balanceConfirmed); //--- An active session requires a confirmed daily baseline if(session.status == SESSION_ACTIVE) return(session.balanceConfirmed); //--- A closed session may have ended with or without confirmation if(session.status == SESSION_CLOSED) return(true); return(false); } //+------------------------------------------------------------------+ //| Open database | //+------------------------------------------------------------------+ bool OpenDatabase() { //--- Open or create the SQLite database ResetLastError(); g_database = DatabaseOpen(g_databaseName, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE); if(g_database == INVALID_HANDLE) { Print("Prop Firm Guard: Could not open database ", g_databaseName, ". Error: ", GetLastError()); return(false); } Print("Prop Firm Guard: Database opened successfully: ", g_databaseName); return(true); } //+------------------------------------------------------------------+ //| Close database | //+------------------------------------------------------------------+ void CloseDatabase() { //--- Ignore the request when no database connection is active if(g_database == INVALID_HANDLE) return; //--- Close the active SQLite connection ResetLastError(); DatabaseClose(g_database); int errorCode = GetLastError(); if(errorCode != 0) { Print("Prop Firm Guard: Database close reported error: ", errorCode); } else { Print("Prop Firm Guard: Database closed successfully."); } //--- Clear the stored database handle g_database = INVALID_HANDLE; } //+------------------------------------------------------------------+ //| Ensure database tables exist | //+------------------------------------------------------------------+ bool EnsureDatabaseTables() { //--- Require an active SQLite connection before creating tables if(g_database == INVALID_HANDLE) { Print("Prop Firm Guard: Cannot ensure database tables without an active database."); return(false); } //--- Define persistent storage for each account/server pair string settingsTable = "CREATE TABLE IF NOT EXISTS settings (" "account_login INTEGER NOT NULL," "account_server TEXT NOT NULL," "initial_balance REAL NOT NULL," "daily_drawdown_percent REAL NOT NULL," "overall_drawdown_percent REAL NOT NULL," "profit_target_percent REAL NOT NULL," "updated_at INTEGER NOT NULL," "PRIMARY KEY(account_login, account_server)" ");"; //--- Define persistent storage for each configured daily session string dailySessionsTable = "CREATE TABLE IF NOT EXISTS daily_sessions (" "account_login INTEGER NOT NULL," "account_server TEXT NOT NULL," "session_date TEXT NOT NULL," "starting_balance REAL NOT NULL," "starting_equity REAL NOT NULL," "balance_confirmed INTEGER NOT NULL," "session_status INTEGER NOT NULL," "created_at INTEGER NOT NULL," "updated_at INTEGER NOT NULL," "PRIMARY KEY(account_login, account_server, session_date)" ");"; //--- Ensure the existing account settings table is available ResetLastError(); if(!DatabaseExecute(g_database, settingsTable)) { Print("Prop Firm Guard: Could not create settings table. Error: ", GetLastError()); return(false); } //--- Ensure the daily session table is available ResetLastError(); if(!DatabaseExecute(g_database, dailySessionsTable)) { Print("Prop Firm Guard: Could not create daily_sessions table. Error: ", GetLastError()); return(false); } return(true); } //+------------------------------------------------------------------+ //| Finalize database request | //+------------------------------------------------------------------+ void FinalizeDatabaseRequest(const int request) { //--- Ignore invalid database requests if(request == INVALID_HANDLE) return; //--- Release the prepared database request ResetLastError(); DatabaseFinalize(request); int errorCode = GetLastError(); if(errorCode != 0) { Print("Prop Firm Guard: Could not finalize database request. Error: ", errorCode); } } //+------------------------------------------------------------------+ //| Execute prepared database statement | //+------------------------------------------------------------------+ bool ExecuteDatabaseRequest(const int request, const string operation) { //--- Execute the prepared database statement ResetLastError(); bool result = DatabaseRead(request); int errorCode = GetLastError(); //--- Accept normal completion when no result row is returned bool success = (result || errorCode == ERR_DATABASE_NO_MORE_DATA); if(!success) { Print("Prop Firm Guard: Database error while ", operation, ". Error: ", errorCode); } //--- Release the request after execution FinalizeDatabaseRequest(request); return(success); } //+------------------------------------------------------------------+ //| Save account settings | //+------------------------------------------------------------------+ bool SaveAccountSettings() { //--- Prepare the account-settings write statement string sql = "INSERT OR REPLACE INTO settings (" "account_login," "account_server," "initial_balance," "daily_drawdown_percent," "overall_drawdown_percent," "profit_target_percent," "updated_at" ") VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);"; int request = DatabasePrepare(g_database, sql); if(request == INVALID_HANDLE) { Print("Prop Firm Guard: Could not prepare settings save request. Error: ", GetLastError()); return(false); } //--- Bind the current account settings to the prepared statement if(!DatabaseBind(request, 0, g_accountSettings.accountLogin) || !DatabaseBind(request, 1, g_accountSettings.accountServer) || !DatabaseBind(request, 2, g_accountSettings.initialBalance) || !DatabaseBind(request, 3, g_accountSettings.dailyDrawdownPercent) || !DatabaseBind(request, 4, g_accountSettings.overallDrawdownPercent) || !DatabaseBind(request, 5, g_accountSettings.profitTargetPercent) || !DatabaseBind(request, 6, g_accountSettings.updatedAt)) { Print("Prop Firm Guard: Could not bind settings values. Error: ", GetLastError()); FinalizeDatabaseRequest(request); return(false); } //--- Execute and finalize the prepared write request if(!ExecuteDatabaseRequest(request, "saving account settings")) return(false); return(true); } //+------------------------------------------------------------------+ //| Load account settings | //+------------------------------------------------------------------+ bool LoadAccountSettings() { //--- Build the current account/server lookup string accountServer = AccountInfoString(ACCOUNT_SERVER); string sql = "SELECT " "account_login," "account_server," "initial_balance," "daily_drawdown_percent," "overall_drawdown_percent," "profit_target_percent," "updated_at " "FROM settings " "WHERE account_login = ?1 AND account_server = ?2;"; int request = DatabasePrepare(g_database, sql); if(request == INVALID_HANDLE) { Print("Prop Firm Guard: Could not prepare settings load request. Error: ", GetLastError()); return(false); } //--- Bind the current account identity if(!DatabaseBind(request, 0, g_accountState.login) || !DatabaseBind(request, 1, accountServer)) { Print("Prop Firm Guard: Could not bind settings search values. Error: ", GetLastError()); FinalizeDatabaseRequest(request); return(false); } //--- Read the matching settings record ResetLastError(); if(!DatabaseRead(request)) { int errorCode = GetLastError(); FinalizeDatabaseRequest(request); //--- No row means this account/server pair has not been saved yet if(errorCode == ERR_DATABASE_NO_MORE_DATA) return(false); Print("Prop Firm Guard: Could not read account settings. Error: ", errorCode); return(false); } //--- Read and validate the stored column values long accountLogin; string storedServer; double initialBalance; double dailyDrawdown; double overallDrawdown; double profitTarget; long updatedAt; if(!DatabaseColumnLong(request, 0, accountLogin) || !DatabaseColumnText(request, 1, storedServer) || !DatabaseColumnDouble(request, 2, initialBalance) || !DatabaseColumnDouble(request, 3, dailyDrawdown) || !DatabaseColumnDouble(request, 4, overallDrawdown) || !DatabaseColumnDouble(request, 5, profitTarget) || !DatabaseColumnLong(request, 6, updatedAt)) { Print("Prop Firm Guard: Could not read stored settings columns. Error: ", GetLastError()); FinalizeDatabaseRequest(request); return(false); } //--- Restore the stored values into the account settings structure g_accountSettings.accountLogin = accountLogin; g_accountSettings.accountServer = storedServer; g_accountSettings.initialBalance = initialBalance; g_accountSettings.dailyDrawdownPercent = dailyDrawdown; g_accountSettings.overallDrawdownPercent = overallDrawdown; g_accountSettings.profitTargetPercent = profitTarget; g_accountSettings.updatedAt = (datetime)updatedAt; //--- Release the completed read request FinalizeDatabaseRequest(request); return(true); } //+------------------------------------------------------------------+ //| Initialize account settings | //+------------------------------------------------------------------+ bool InitializeAccountSettings() { //--- Check whether this account/server pair already has stored settings bool settingsFound = LoadAccountSettings(); //--- Associate the settings with the current trading account g_accountSettings.accountLogin = g_accountState.login; g_accountSettings.accountServer = AccountInfoString(ACCOUNT_SERVER); //--- Synchronize persistence with the current Inputs values g_accountSettings.initialBalance = InpInitialAccountBalance; g_accountSettings.dailyDrawdownPercent = InpDailyDrawdownPercent; g_accountSettings.overallDrawdownPercent = InpOverallDrawdownPercent; g_accountSettings.profitTargetPercent = InpProfitTargetPercent; g_accountSettings.updatedAt = TimeCurrent(); //--- Save the synchronized configuration if(!SaveAccountSettings()) return(false); //--- Report whether the record was created or synchronized if(settingsFound) Print("Prop Firm Guard: Account settings restored and synchronized."); else Print("Prop Firm Guard: New account settings saved."); return(true); } //+------------------------------------------------------------------+ //| Save daily session | //+------------------------------------------------------------------+ bool SaveDailySession() { //--- Require an active SQLite connection if(g_database == INVALID_HANDLE) { Print("Prop Firm Guard: Cannot save daily session without an active database."); return(false); } //--- Reject an incomplete session identity if(g_dailySession.accountLogin <= 0 || g_dailySession.accountServer == "" || g_dailySession.sessionDate == "") { Print("Prop Firm Guard: Cannot save daily session with an incomplete identity."); return(false); } //--- Prevent contradictory confirmation and lifecycle states if(!IsDailySessionStateConsistent(g_dailySession)) { Print("Prop Firm Guard: Cannot save an inconsistent daily session state."); return(false); } //--- Insert a new session or update only its mutable fields string sql = "INSERT INTO daily_sessions (" "account_login," "account_server," "session_date," "starting_balance," "starting_equity," "balance_confirmed," "session_status," "created_at," "updated_at" ") VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) " "ON CONFLICT(account_login, account_server, session_date) " "DO UPDATE SET " "starting_balance = excluded.starting_balance," "starting_equity = excluded.starting_equity," "balance_confirmed = excluded.balance_confirmed," "session_status = excluded.session_status," "updated_at = excluded.updated_at;"; ResetLastError(); int request = DatabasePrepare(g_database, sql); if(request == INVALID_HANDLE) { Print("Prop Firm Guard: Could not prepare daily session save request. Error: ", GetLastError()); return(false); } //--- Bind the complete daily session to the prepared statement if(!DatabaseBind(request, 0, g_dailySession.accountLogin) || !DatabaseBind(request, 1, g_dailySession.accountServer) || !DatabaseBind(request, 2, g_dailySession.sessionDate) || !DatabaseBind(request, 3, g_dailySession.startingBalance) || !DatabaseBind(request, 4, g_dailySession.startingEquity) || !DatabaseBind(request, 5, g_dailySession.balanceConfirmed ? 1 : 0) || !DatabaseBind(request, 6, (int)g_dailySession.status) || !DatabaseBind(request, 7, (long)g_dailySession.createdAt) || !DatabaseBind(request, 8, (long)g_dailySession.updatedAt)) { Print("Prop Firm Guard: Could not bind daily session values. Error: ", GetLastError()); FinalizeDatabaseRequest(request); return(false); } //--- Execute the UPSERT and release the prepared request if(!ExecuteDatabaseRequest(request, "saving daily session")) return(false); return(true); } //+------------------------------------------------------------------+ //| Load daily session | //+------------------------------------------------------------------+ ENUM_DAILY_SESSION_LOAD_RESULT LoadDailySession(const string sessionDate) { //--- Require an active SQLite connection if(g_database == INVALID_HANDLE) { Print("Prop Firm Guard: Cannot load daily session without an active database."); return(SESSION_LOAD_ERROR); } //--- Reject an empty session key if(sessionDate == "") { Print("Prop Firm Guard: Cannot load a daily session with an empty session date."); return(SESSION_LOAD_ERROR); } //--- Build the current account and session lookup string accountServer = AccountInfoString(ACCOUNT_SERVER); string sql = "SELECT " "account_login," "account_server," "session_date," "starting_balance," "starting_equity," "balance_confirmed," "session_status," "created_at," "updated_at " "FROM daily_sessions " "WHERE account_login = ?1 " "AND account_server = ?2 " "AND session_date = ?3 " "LIMIT 1;"; ResetLastError(); int request = DatabasePrepare(g_database, sql); if(request == INVALID_HANDLE) { Print("Prop Firm Guard: Could not prepare daily session load request. Error: ", GetLastError()); return(SESSION_LOAD_ERROR); } //--- Bind the complete session identity if(!DatabaseBind(request, 0, g_accountState.login) || !DatabaseBind(request, 1, accountServer) || !DatabaseBind(request, 2, sessionDate)) { Print("Prop Firm Guard: Could not bind daily session search values. Error: ", GetLastError()); FinalizeDatabaseRequest(request); return(SESSION_LOAD_ERROR); } //--- Attempt to read the matching session in the same database request ResetLastError(); if(!DatabaseRead(request)) { int errorCode = GetLastError(); FinalizeDatabaseRequest(request); //--- Absence is a normal result when the configured day is new if(errorCode == ERR_DATABASE_NO_MORE_DATA) return(SESSION_LOAD_NOT_FOUND); Print("Prop Firm Guard: Could not read daily session. Error: ", errorCode); return(SESSION_LOAD_ERROR); } //--- Read every column before changing the active in-memory session long accountLogin; string storedServer; string storedSessionDate; double startingBalance; double startingEquity; int balanceConfirmed; int sessionStatus; long createdAt; long updatedAt; bool loaded = DatabaseColumnLong(request, 0, accountLogin) && DatabaseColumnText(request, 1, storedServer) && DatabaseColumnText(request, 2, storedSessionDate) && DatabaseColumnDouble(request, 3, startingBalance) && DatabaseColumnDouble(request, 4, startingEquity) && DatabaseColumnInteger(request, 5, balanceConfirmed) && DatabaseColumnInteger(request, 6, sessionStatus) && DatabaseColumnLong(request, 7, createdAt) && DatabaseColumnLong(request, 8, updatedAt); if(!loaded) { Print("Prop Firm Guard: Could not read daily session columns. Error: ", GetLastError()); FinalizeDatabaseRequest(request); return(SESSION_LOAD_ERROR); } //--- Release the completed read request FinalizeDatabaseRequest(request); //--- Validate persisted boolean and enum representations if(balanceConfirmed != 0 && balanceConfirmed != 1) { Print("Prop Firm Guard: Stored daily session has an invalid confirmation value."); return(SESSION_LOAD_ERROR); } if(sessionStatus < (int)SESSION_AWAITING_CONFIRMATION || sessionStatus > (int)SESSION_CLOSED) { Print("Prop Firm Guard: Stored daily session has an invalid status value."); return(SESSION_LOAD_ERROR); } //--- Build a temporary session before replacing the active state SDailySession loadedSession; loadedSession.accountLogin = accountLogin; loadedSession.accountServer = storedServer; loadedSession.sessionDate = storedSessionDate; loadedSession.startingBalance = startingBalance; loadedSession.startingEquity = startingEquity; loadedSession.balanceConfirmed = (balanceConfirmed == 1); loadedSession.status = (ENUM_DAILY_SESSION_STATUS)sessionStatus; loadedSession.createdAt = (datetime)createdAt; loadedSession.updatedAt = (datetime)updatedAt; //--- Reject a persisted state that contradicts its confirmation flag if(!IsDailySessionStateConsistent(loadedSession)) { Print("Prop Firm Guard: Stored daily session contains an inconsistent state."); return(SESSION_LOAD_ERROR); } //--- Restore the validated session only after the complete read succeeds g_dailySession = loadedSession; return(SESSION_LOAD_FOUND); } //+------------------------------------------------------------------+ //| Initialize daily session | //+------------------------------------------------------------------+ bool InitializeDailySession() { //--- Derive the session key from the configured reset boundary string sessionDate = GetCurrentSessionDate(); if(sessionDate == "") { Print("Prop Firm Guard: Could not derive the current daily session date."); return(false); } //--- Attempt to restore the session for the current account and day ENUM_DAILY_SESSION_LOAD_RESULT loadResult = LoadDailySession(sessionDate); if(loadResult == SESSION_LOAD_FOUND) { //--- Mark the restored session as available to the runtime g_hasDailySession = true; Print("Prop Firm Guard: Daily session restored for ", sessionDate, "."); return(true); } //--- Stop initialization when the database lookup failed if(loadResult == SESSION_LOAD_ERROR) { Print("Prop Firm Guard: Could not restore the current daily session."); return(false); } //--- Build a new unconfirmed session when no stored record exists datetime currentTime = TimeCurrent(); g_dailySession.accountLogin = g_accountState.login; g_dailySession.accountServer = AccountInfoString(ACCOUNT_SERVER); g_dailySession.sessionDate = sessionDate; g_dailySession.startingBalance = 0.0; g_dailySession.startingEquity = 0.0; g_dailySession.balanceConfirmed = false; g_dailySession.status = SESSION_AWAITING_CONFIRMATION; g_dailySession.createdAt = currentTime; g_dailySession.updatedAt = currentTime; //--- Persist the newly created session before exposing it as active if(!SaveDailySession()) { Print("Prop Firm Guard: Could not save the new daily session."); g_hasDailySession = false; return(false); } //--- Mark the persisted session as available to the runtime g_hasDailySession = true; Print("Prop Firm Guard: New daily session created for ", sessionDate, "."); return(true); } //+------------------------------------------------------------------+ //| Delete daily session controls | //+------------------------------------------------------------------+ bool DeleteDailySessionControls() { bool success = true; //--- Remove the starting balance edit field when present if(ObjectFind(0, g_startBalanceEdit) >= 0) { ResetLastError(); if(!ObjectDelete(0, g_startBalanceEdit)) { Print("Prop Firm Guard: Could not delete starting balance field. Error: ", GetLastError()); success = false; } } //--- Remove the confirmation button when present if(ObjectFind(0, g_confirmButton) >= 0) { ResetLastError(); if(!ObjectDelete(0, g_confirmButton)) { Print("Prop Firm Guard: Could not delete confirmation button. Error: ", GetLastError()); success = false; } } return(success); } //+------------------------------------------------------------------+ //| Create starting balance edit | //+------------------------------------------------------------------+ bool CreateStartingBalanceEdit(const int x, const int y, const string suggestedBalance) { //--- Create the editable starting balance field ResetLastError(); if(!ObjectCreate(0, g_startBalanceEdit, OBJ_EDIT, 0, 0, 0)) { Print("Prop Firm Guard: Could not create starting balance field. Error: ", GetLastError()); return(false); } //--- Configure the field used to review or change the suggestion 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()); if(!ObjectDelete(0, g_startBalanceEdit)) Print("Prop Firm Guard: Could not remove incomplete starting balance field."); return(false); } //--- Apply the suggested balance and font 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()); if(!ObjectDelete(0, g_startBalanceEdit)) Print("Prop Firm Guard: Could not remove incomplete starting balance field."); return(false); } return(true); } //+------------------------------------------------------------------+ //| Create confirm balance button | //+------------------------------------------------------------------+ bool CreateConfirmBalanceButton(const int x, const int y) { //--- Create the action control for daily balance confirmation ResetLastError(); if(!ObjectCreate(0, g_confirmButton, OBJ_BUTTON, 0, 0, 0)) { Print("Prop Firm Guard: Could not create confirmation button. Error: ", GetLastError()); return(false); } //--- Configure the confirmation button 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()); if(!ObjectDelete(0, g_confirmButton)) Print("Prop Firm Guard: Could not remove incomplete confirmation button."); return(false); } //--- Apply the button label and font if(!ObjectSetString(0, g_confirmButton, OBJPROP_FONT, "Tahoma") || !ObjectSetString(0, g_confirmButton, OBJPROP_TEXT, "Confirm")) { Print("Prop Firm Guard: Could not initialize confirmation button. Error: ", GetLastError()); if(!ObjectDelete(0, g_confirmButton)) Print("Prop Firm Guard: Could not remove incomplete confirmation button."); return(false); } return(true); } //+------------------------------------------------------------------+ //| Synchronize daily session controls | //+------------------------------------------------------------------+ bool SynchronizeDailySessionControls() { //--- Require a loaded session before changing its interface if(!g_hasDailySession) { Print("Prop Firm Guard: Cannot synchronize controls without a daily session."); return(false); } //--- Confirmed sessions must not expose confirmation controls if(g_dailySession.balanceConfirmed) return(DeleteDailySessionControls()); //--- Preserve an existing complete control set and any edited text bool editExists = (ObjectFind(0, g_startBalanceEdit) >= 0); bool buttonExists = (ObjectFind(0, g_confirmButton) >= 0); if(editExists && buttonExists) return(true); //--- Remove a partial or stale owned control set before rebuilding if(editExists || buttonExists) { if(!DeleteDailySessionControls()) return(false); } //--- Format the current balance only when a new field is required string suggestedBalance; if(!FormatAccountMoney(g_accountState.balance, suggestedBalance)) { Print("Prop Firm Guard: Could not format the suggested starting balance."); return(false); } int controlX = InpDashboardX + 18; int controlY = InpDashboardY + 120; //--- Build the complete confirmation interface if(!CreateStartingBalanceEdit(controlX, controlY, suggestedBalance)) return(false); if(!CreateConfirmBalanceButton(controlX + 130, controlY)) { DeleteDailySessionControls(); return(false); } //--- Refresh the chart after the complete control set is created ChartRedraw(0); return(true); } //+------------------------------------------------------------------+ //| Confirm daily starting balance | //+------------------------------------------------------------------+ bool ConfirmDailyStartingBalance() { //--- Require an initialized daily session if(!g_hasDailySession) { Print("Prop Firm Guard: Cannot confirm a starting balance without a daily session."); return(false); } //--- Accept confirmation only while the session is awaiting it if(g_dailySession.status != SESSION_AWAITING_CONFIRMATION) { Print("Prop Firm Guard: Current daily session is not awaiting balance confirmation."); return(false); } //--- Protect an already confirmed baseline from accidental overwrite if(g_dailySession.balanceConfirmed) { Print("Prop Firm Guard: Daily starting balance is already confirmed."); return(false); } //--- Require the starting balance field to exist if(ObjectFind(0, g_startBalanceEdit) < 0) { Print("Prop Firm Guard: Starting balance field is not available."); return(false); } //--- Read the complete text entered by the trader string balanceText; ResetLastError(); if(!ObjectGetString(0, g_startBalanceEdit, OBJPROP_TEXT, 0, balanceText)) { Print("Prop Firm Guard: Could not read starting balance input. Error: ", GetLastError()); return(false); } //--- Validate the complete input before changing session state double startingBalance = 0.0; if(!ParseStartingBalance(balanceText, startingBalance)) { Print("Prop Firm Guard: Starting balance must be a valid positive monetary value."); return(false); } //--- Preserve the previous session state for persistence rollback SDailySession previousSession = g_dailySession; //--- Refresh live account values immediately before capturing equity UpdateAccountState(); //--- Convert the waiting session into a confirmed active session g_dailySession.startingBalance = startingBalance; g_dailySession.startingEquity = g_accountState.equity; g_dailySession.balanceConfirmed = true; g_dailySession.status = SESSION_ACTIVE; g_dailySession.updatedAt = TimeCurrent(); //--- Persist the confirmed state before changing presentation state if(!SaveDailySession()) { //--- Restore the previous in-memory state after persistence failure g_dailySession = previousSession; Print("Prop Firm Guard: Starting balance confirmation was not saved. " "The previous session state has been restored."); return(false); } //--- Format the confirmed monetary values for reporting string formattedBalance; string formattedEquity; if(!FormatAccountMoney(g_dailySession.startingBalance, formattedBalance)) formattedBalance = DoubleToString(g_dailySession.startingBalance); if(!FormatAccountMoney(g_dailySession.startingEquity, formattedEquity)) formattedEquity = DoubleToString(g_dailySession.startingEquity); //--- Synchronize presentation only after persistence succeeds if(!SynchronizeDailySessionControls()) { Print("Prop Firm Guard: Starting balance was confirmed and saved, " "but the confirmation controls could not be synchronized."); return(true); } Print("Prop Firm Guard: Daily starting balance confirmed at ", formattedBalance, ". Equity captured at confirmation: ", formattedEquity, "."); return(true); } //+------------------------------------------------------------------+ //| Check for new trading day | //+------------------------------------------------------------------+ bool CheckForNewTradingDay() { //--- Require an initialized daily session before checking rollover if(!g_hasDailySession) { Print("Prop Firm Guard: Cannot check session rollover without a daily session."); return(false); } //--- Derive the session key from the currently known server time string currentSessionDate = GetCurrentSessionDate(); if(currentSessionDate == "") { Print("Prop Firm Guard: Could not derive the current session date."); return(false); } //--- Keep the current session while its configured day is unchanged if(currentSessionDate == g_dailySession.sessionDate) return(true); //--- Preserve the active state in case closing persistence fails SDailySession previousSession = g_dailySession; //--- Close the previous session before changing the active day g_dailySession.status = SESSION_CLOSED; g_dailySession.updatedAt = TimeCurrent(); if(!SaveDailySession()) { g_dailySession = previousSession; Print("Prop Firm Guard: Could not close daily session ", previousSession.sessionDate, ". Rollover has been postponed."); return(false); } //--- Preserve the exact closed state that was successfully persisted SDailySession closedSession = g_dailySession; g_hasDailySession = false; //--- Restore or create the session for the new configured day if(!InitializeDailySession()) { g_dailySession = closedSession; g_hasDailySession = true; if(!DeleteDailySessionControls()) Print("Prop Firm Guard: Could not remove stale session controls after rollover failure."); Print("Prop Firm Guard: Previous session was closed, but the new daily session could not be initialized."); return(false); } Print("Prop Firm Guard: Daily session rollover completed from ", previousSession.sessionDate, " to ", g_dailySession.sessionDate, "."); return(true); } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Validate the complete Part 1 and Part 2 input configuration if(!ValidateComplianceInputs()) return(INIT_PARAMETERS_INCORRECT); //--- Refresh the account identity and current financial state UpdateAccountState(); //--- Enforce one Prop Firm Guard instance for the trading account if(!AcquireInstanceLock()) return(INIT_FAILED); //--- Open the SQLite persistence layer if(!OpenDatabase()) { Print("Prop Firm Guard: Database initialization failed."); if(!ReleaseInstanceLock()) Print("Prop Firm Guard: Instance lock cleanup was incomplete."); return(INIT_FAILED); } //--- Ensure all persistence tables required through Part 2 exist if(!EnsureDatabaseTables()) { Print("Prop Firm Guard: Could not initialize database tables."); CloseDatabase(); if(!ReleaseInstanceLock()) Print("Prop Firm Guard: Instance lock cleanup was incomplete."); return(INIT_FAILED); } //--- Restore and synchronize the Part 1 account settings if(!InitializeAccountSettings()) { Print("Prop Firm Guard: Could not initialize account settings."); CloseDatabase(); if(!ReleaseInstanceLock()) Print("Prop Firm Guard: Instance lock cleanup was incomplete."); return(INIT_FAILED); } //--- Restore or create the current configured daily session if(!InitializeDailySession()) { Print("Prop Firm Guard: Could not initialize daily session."); CloseDatabase(); if(!ReleaseInstanceLock()) Print("Prop Firm Guard: Instance lock cleanup was incomplete."); return(INIT_FAILED); } //--- Match the confirmation controls to the current session state if(!SynchronizeDailySessionControls()) { Print("Prop Firm Guard: Could not synchronize daily session controls."); if(!DeleteDailySessionControls()) Print("Prop Firm Guard: Daily session control cleanup was incomplete."); CloseDatabase(); if(!ReleaseInstanceLock()) Print("Prop Firm Guard: Instance lock cleanup was incomplete."); return(INIT_FAILED); } //--- Start periodic daily session maintenance ResetLastError(); if(!EventSetTimer(InpTimerSeconds)) { Print("Prop Firm Guard: Could not start the session timer. Error: ", GetLastError()); if(!DeleteDailySessionControls()) Print("Prop Firm Guard: Daily session control cleanup was incomplete."); CloseDatabase(); if(!ReleaseInstanceLock()) Print("Prop Firm Guard: Instance lock cleanup was incomplete."); return(INIT_FAILED); } Print("Prop Firm Guard initialized successfully."); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Stop periodic session maintenance first EventKillTimer(); //--- Remove chart objects owned by the Part 2 session workflow if(!DeleteDailySessionControls()) { Print("Prop Firm Guard: Daily session control cleanup was incomplete."); } //--- Close the SQLite persistence layer CloseDatabase(); //--- Release this trading account's terminal instance lock if(!ReleaseInstanceLock()) { Print("Prop Firm Guard: Instance lock cleanup was incomplete."); } //--- Record the MetaTrader deinitialization reason Print("Prop Firm Guard removed. Deinitialization reason: ", reason); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { } //+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer() { //--- Refresh the account snapshot used by the session workflow UpdateAccountState(); //--- Maintain the configured daily session boundary if(!CheckForNewTradingDay()) { Print("Prop Firm Guard: Daily session maintenance could not be completed."); return; } //--- Keep chart controls aligned with the authoritative session state if(!SynchronizeDailySessionControls()) { Print("Prop Firm Guard: Daily session controls could not be synchronized."); } } //+------------------------------------------------------------------+ //| Chart event function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Ignore events that are not graphical object clicks if(id != CHARTEVENT_OBJECT_CLICK) return; //--- Ignore every chart object except the owned confirmation button if(sparam != g_confirmButton) return; //--- Release the button's pressed visual state when it still exists if(ObjectFind(0, g_confirmButton) >= 0) { ResetLastError(); if(!ObjectSetInteger(0, g_confirmButton, OBJPROP_STATE, false)) { Print("Prop Firm Guard: Could not reset confirmation button state. Error: ", GetLastError()); } } //--- Delegate validation, state transition, and persistence ConfirmDailyStartingBalance(); } //+------------------------------------------------------------------+