preview
Building a Prop Firm Compliance Monitor in MQL5 (Part 2): Daily Sessions and Restart-Safe Starting Balances

Building a Prop Firm Compliance Monitor in MQL5 (Part 2): Daily Sessions and Restart-Safe Starting Balances

MetaTrader 5 — Trading |
154 0
Chacha Ian Maroa
Chacha Ian Maroa

Introduction

When an Expert Advisor starts, the balance it reads reflects the account at that moment. If trades have already been opened or closed earlier in the session, that value may no longer represent the balance that should serve as the daily reference. Reusing the current balance after every restart or reattachment would therefore allow the baseline to shift during the same trading day.

Part 1 established the account-level foundation for Prop Firm Guard by validating the configured prop firm rules and persisting those settings in SQLite. Part 2 extends that foundation with state specific to a single daily session. The objective is to preserve a trader-confirmed starting balance so the same reference can be restored after the EA is removed, reattached, or restarted.

A new session does not automatically accept the current balance as final. Instead, the EA presents it as a suggested value and requires explicit confirmation. Once confirmed, the session stores the starting balance and the equity captured at that moment and can restore them later.

The measurable goal for Part 2 is straightforward: for the same account and the same configured session day, reattaching the EA should restore the same confirmed baseline rather than create a new one. A new session should begin only after the configured reset boundary identifies a different session day.

This persistence provides continuity, not independent historical verification. Prop Firm Guard can preserve the value that the trader confirms, but it does not reconstruct past account history to prove that the value is objectively correct. Part 2 therefore establishes a stable daily reference that later risk calculations can use consistently.


Building the Daily Session Model

Part 1 already separates persistent account configuration from live account state. SAccountSettings stores the prop firm rules associated with the account, while SAccountState holds values that can change during runtime, such as balance and equity. Part 2 needs a third structure because the confirmed daily baseline belongs to one specific session and must remain stable even when live account values change.

In PropFirmGuard.mq5, locate the Enumerations section. Add the following two enumerations immediately after ENUM_PROTECTION_ACTION and before the Inputs section:

//+------------------------------------------------------------------+
//| 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
  };

ENUM_DAILY_SESSION_STATUS gives the session three explicit lifecycle states: awaiting confirmation, active, and closed. Using an enum avoids relying on free-form status strings and makes it easier to validate whether the stored session state is consistent.

A new session begins in SESSION_AWAITING_CONFIRMATION. After the trader confirms the daily baseline, it becomes SESSION_ACTIVE. When the configured session boundary moves to a new day, the previous session becomes SESSION_CLOSED. A session can also close without ever being confirmed if rollover occurs first.

ENUM_DAILY_SESSION_LOAD_RESULT separates three database outcomes that should not be treated as the same condition. A session may be found, it may not yet exist, or a database operation may fail. The "not found" result is normal and will later tell the EA to create a new session, while an actual database error should stop that initialization path.

Next, add the daily-session structure immediately after the complete SAccountSettings structure and before the Global variables block:

//+------------------------------------------------------------------+
//| 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
  };

SDailySession contains only the state required in Part 2: account identity, the session key, the confirmed baseline, its confirmation status, the session lifecycle state, and timestamps. Fields such as daily loss, target progress, latest balance, and latest equity are intentionally excluded because those measurements are not maintained until Part 3.

Finally, replace the entire existing Global variables block with the following version:

//+------------------------------------------------------------------+
//| 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

int  g_database        = INVALID_HANDLE; // Active SQLite database handle
bool g_hasDailySession = false;          // Whether a daily session is currently loaded

g_dailySession holds the current session state in memory, while g_hasDailySession indicates whether a valid session has been restored or created. The two object names are reserved for the confirmation controls that will be added later.


Defining the Configurable Session Boundary

A daily session needs a repeatable rule for deciding when one trading day ends and the next begins. Using the broker server's calendar date alone is not sufficient because a prop firm may define its daily reset using UTC, New York time, the broker's end-of-day schedule, or another firm-specific boundary. Two traders connected to the same broker could therefore be subject to different daily-reset rules depending on the prop firm program they are following.

Prop Firm Guard avoids treating broker midnight as a universal boundary. Instead, Part 2 introduces a configurable reset hour and minute expressed in broker server time. The trader can therefore translate the relevant prop firm reset rule into the time used by the connected trading server and configure the EA accordingly.

In PropFirmGuard.mq5, locate the Inputs section. Insert the following new input group immediately after the existing General group and before Prop Firm Rules:

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

The completed source places these two inputs between the general EA settings and the existing prop firm rule inputs. InpDailyResetHour accepts hours from 0 through 23, while InpDailyResetMinute accepts minutes from 0 through 59. These values describe the daily reset boundary in broker server time. A value of 0:00 therefore preserves a midnight reset, while a firm whose reset corresponds to 17:00 on the broker server can be represented with an hour of 17 and a minute of 0.

Because these values affect the identity of every daily session, invalid settings must be rejected before the EA initializes its persistence layer. The existing ValidateComplianceInputs() function is therefore extended to validate the timer interval and the new session boundary before continuing with the Part 1 rule checks.

Replace the entire existing ValidateComplianceInputs() function with the following version:

//+------------------------------------------------------------------+
//| 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);
  }

The reset boundary can now be converted into the session-date key stored by SDailySession. Add the following new function immediately after UpdateAccountState() and before the later account-currency and persistence helper functions:

//+------------------------------------------------------------------+
//| 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));
  }

The function first converts the configured reset time into seconds from midnight. That offset is subtracted from the server time currently known by the terminal, effectively moving the configured reset boundary to midnight for the purpose of deriving the session date. The date portion of the shifted value then becomes the persistent session key. This is the implementation used by the completed Part 2 source.

For example, suppose the broker server reports 2026.10.14 16:30 and the configured reset time is 17:00. Subtracting 17 hours produces a shifted time belonging to 2026.10.13, so the EA still treats the account as belonging to the previous session. Once the known server time reaches or passes 17:00, the shifted value belongs to 2026.10.14, producing the next session-date key.

This approach makes the boundary configurable, but it does not automatically convert an external timezone rule into broker server time. If a prop firm specifies its reset in UTC or New York time, the corresponding broker-server hour and minute must be configured correctly. Any seasonal timezone changes that affect that relationship must also be reflected in the configured values.

There is also an important timing distinction. The periodic session check introduced later will run through OnTimer(), so it does not depend on tick events from the chart symbol itself. However, GetCurrentSessionDate() uses TimeCurrent(), which represents the server time currently known by the terminal. The EA therefore recognizes rollover when that known server time first reflects the new configured session day. It should not be described as guaranteeing detection at the exact wall-clock instant of the reset.


Persisting Daily Sessions in SQLite

The session model now needs persistent storage so that the same record can be restored after EA removal or terminal restart. Each daily session is identified by three values: the trading account login, the broker server, and the configured sessionDate. The combination allows the same account to accumulate separate session records over time without confusing one configured trading day with another.

Part 1 already creates the settings table, so Part 2 extends the existing database with a daily_sessions table. Replace the entire current EnsureDatabaseTables() function with the following version:

//+------------------------------------------------------------------+
//| 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);
  }

The new table mirrors the Part 2 session model: account and session identity, the confirmed reference values, confirmation state, lifecycle state, and timestamps. balance_confirmed and session_status are stored as integers and converted back to their MQL5 representations when the row is loaded. The composite primary key prevents more than one row from representing the same account, server, and configured session day.

CREATE TABLE IF NOT EXISTS is sufficient for the scope of this article, but it is not a general migration mechanism. It creates a missing table with the expected schema; it does not inspect or repair an existing incompatible schema.

Before a session is written, its confirmation flag and lifecycle state should agree. Add the following new function immediately after GetCurrentSessionDate() and before OpenDatabase():

//+------------------------------------------------------------------+
//| 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);
  }

This prevents contradictory states such as an active session with no confirmed baseline or a session that is still awaiting confirmation while balanceConfirmed is already true. A closed session may be either confirmed or unconfirmed because rollover can occur before confirmation.

Next, add SaveDailySession() immediately after the complete InitializeAccountSettings() function:

//+------------------------------------------------------------------+
//| 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);
  }

The statement uses SQLite UPSERT rather than INSERT OR REPLACE. A new key inserts the complete session. If the same account, server, and session date already exists, only the mutable session fields and updated_at are changed. created_at is deliberately excluded from the update clause so that the original creation time remains intact.

Immediately after SaveDailySession(), add LoadDailySession():

//+------------------------------------------------------------------+
//| 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);
  }

One query now distinguishes the three outcomes defined earlier. A valid row returns SESSION_LOAD_FOUND, absence returns SESSION_LOAD_NOT_FOUND, and database or validation failures return SESSION_LOAD_ERROR. The loaded values are first assembled in a temporary structure so that g_dailySession is replaced only after the complete record has been read and validated.


Initializing and Restoring the Current Session

With the persistence functions in place, the EA can now decide whether the current configured session already exists or whether a new one must be created. Add the following new function immediately after LoadDailySession() and before the chart-control functions:

//+------------------------------------------------------------------+
//| 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);
  }

The function has two normal startup paths. If LoadDailySession() returns SESSION_LOAD_FOUND, the previously stored record already matches the current account, server, and configured session date, so it is restored directly into g_dailySession.

If no row exists, the EA creates a new session in SESSION_AWAITING_CONFIRMATION. Both startingBalance and startingEquity begin at zero, and balanceConfirmed remains false. The current account balance is deliberately not written into the session as the final baseline. That value will only be proposed to the trader later and becomes authoritative for Part 2 only after explicit confirmation.

A restored session provides continuity for the same account, broker server, and configured session date. It restores the values that were previously persisted for that key. This does not mean the EA has reconstructed historical account activity or independently verified that the stored baseline matches broker history. The restoration logic only guarantees that the same saved session state can be recovered.

The order of operations is also important when creating a new session. g_hasDailySession is set to true only after SaveDailySession() succeeds. If persistence fails, the EA does not expose the new in-memory values as a valid current session. This keeps runtime availability aligned with successful storage.


Validating the Starting Balance

Before the confirmation interface can accept a daily starting balance, the EA needs a reliable way to format and validate monetary input. This begins with the account currency precision rather than assuming that every account uses two decimal places.

In PropFirmGuard.mq5, insert the following new function immediately after GetCurrentSessionDate() and before IsDailySessionStateConsistent():

//+------------------------------------------------------------------+
//| 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);
  }

GetAccountCurrencyDigits() reads the monetary precision configured for the account and returns it to the caller. This gives the rest of the Part 2 code one consistent source for deciding how many fractional digits a balance may contain. A negative return value indicates that the precision could not be obtained or was outside the supported range.

Immediately after GetAccountCurrencyDigits(), add the formatting helper:

//+------------------------------------------------------------------+
//| 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);
  }

FormatAccountMoney() uses that precision when converting a monetary value to text. Later, the confirmation controls and diagnostic messages can use the same formatting rule instead of hard-coding two decimal places.

The final helper validates the text entered by the trader. Add ParseStartingBalance() immediately after FormatAccountMoney():

//+------------------------------------------------------------------+
//| 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);
  }

The parser checks the complete string before calling StringToDouble(). This is important because the confirmation field should accept only a valid monetary value, not partially interpretable text.

Surrounding spaces are allowed and removed first. The remaining text must contain digits, with no more than one decimal point. The decimal point cannot be the first or last character. Signs, commas, letters, and trailing text are rejected. The number of fractional digits must also fit the account currency precision, and the converted balance must be greater than zero. 

Input Result
20000 Accepted
20000.50 Accepted if account precision allows
0 Rejected
-20000 Rejected
20000abc Rejected
20000 Rejected


Building and Synchronizing the Confirmation Controls

A newly created session should not silently accept the current account balance as its daily baseline. The current balance is useful as a suggested value, but the trader must be able to review or correct it before the session becomes confirmed. Part 2 therefore adds an editable balance field and a confirmation button.

The controls also need predictable ownership. Prop Firm Guard uses the fixed names PFG_StartBalanceEdit and PFG_ConfirmBalance, so its cleanup logic touches only the objects created for this workflow.

Add the following new function immediately after InitializeDailySession(). It removes either control when present and can be called repeatedly without treating an already-absent object as an error:

//+------------------------------------------------------------------+
//| 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);
  }

The edit field is created separately because it has a different purpose from the button. Add CreateStartingBalanceEdit() immediately after DeleteDailySessionControls():

//+------------------------------------------------------------------+
//| 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);
  }

The current account balance is written into this field only as a suggestion. Creating the control does not alter g_dailySession.startingBalance and does not change the confirmation state.

Next, add CreateConfirmBalanceButton() immediately after CreateStartingBalanceEdit():

//+------------------------------------------------------------------+
//| 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);
  }

The button provides the action point for the confirmation workflow. Its click behavior is intentionally left for the next section, where the entered value will be validated and persisted.

The final control function keeps the chart interface synchronized with the persistent session state. Add SynchronizeDailySessionControls() immediately after CreateConfirmBalanceButton():

//+------------------------------------------------------------------+
//| 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);
  }

This establishes the interface rule used by the rest of Part 2: an unconfirmed session has the edit field and confirmation button, while a confirmed session has neither. More importantly, an existing complete control set is left unchanged. Repeated synchronization therefore does not recreate the edit field or overwrite a value that the trader has already typed.

If only one of the two owned objects exists, the incomplete set is removed and rebuilt. This makes synchronization safe to repeat without creating duplicate chart objects. The persistent SDailySession remains the source of truth; the controls are only its user interface.


Confirming the Daily Baseline Safely

The confirmation step is where an unconfirmed session becomes active, so the function must protect both the session state and the persisted record. Add the following new function immediately after SynchronizeDailySessionControls() and before the lifecycle handlers:

//+------------------------------------------------------------------+
//| 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);
  }

Confirmation is accepted only for a loaded session that is still awaiting confirmation and has no confirmed baseline. The complete text from the edit field is passed through the strict parser before any session state is changed.

Immediately before capturing startingEquity, the EA refreshes the live account snapshot with UpdateAccountState(). This avoids relying on an older equity value that may have been collected earlier. The confirmed balance and refreshed equity are then stored together as the session moves to SESSION_ACTIVE.

The previous SDailySession is preserved before that transition. If SaveDailySession() fails, the complete in-memory session is restored, so the EA does not retain a confirmed state that SQLite failed to preserve.

After a successful save, the persistent state is treated as authoritative. The confirmation controls are synchronized afterward, but a chart-object cleanup failure does not undo the stored confirmation.

The result is a persistent trader-approved baseline that can be restored after reattachment or restart. It provides continuity for the session, not independent verification against historical broker records.


Handling Session Rollover

The active session must eventually be replaced when the configured daily boundary moves the EA into a different session key. This comparison uses GetCurrentSessionDate(), so rollover follows the reset hour and minute configured earlier rather than assuming that a new session always begins at calendar midnight.

Add the following new function immediately after ConfirmDailyStartingBalance():

//+------------------------------------------------------------------+
//| 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);
  }

The ordering here is important. When a different session key is detected, the existing session first moves to SESSION_CLOSED and that state is persisted. Only after the save succeeds does the EA attempt to restore or create the session for the new configured day.

If the closing write fails, the previous in-memory state is restored and rollover is postponed. This prevents the EA from moving forward while SQLite still records the old session as active or awaiting confirmation.

The new day then reuses InitializeDailySession(). If a record already exists for the new account, server, and session-date combination, it is restored. Otherwise, the same function creates the unconfirmed session defined earlier. If this second stage fails, the successfully persisted closed session remains the current recovery state rather than reconstructing it with different values.

Rollover also needs periodic maintenance while the EA remains attached. Add the following OnTimer() handler immediately after 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.");
     }
  }

Each timer event refreshes the account snapshot, checks whether the configured session key has changed, and then synchronizes the confirmation controls with the resulting session state.

OnTimer() is independent of ticks on the chart symbol, but GetCurrentSessionDate() uses TimeCurrent(), which represents the server time currently known by the terminal. The rollover is therefore recognized when that known server time reflects the configured new session day; the implementation does not claim an exact wall-clock transition when no newer server time has reached the terminal.


Enforcing One EA Instance and Completing the Lifecycle

Part 2 now has persistent session state, chart controls, and timer-based maintenance. Running several copies for the same trading account would allow them to compete for the same controls, write the same session record, and perform duplicate rollover checks. The EA therefore allows only one active instance per account within the same MetaTrader terminal.

First, extend the existing global-variable block with the lock state. Replace the complete current global-variable block with:

//+------------------------------------------------------------------+
//| 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

The lock uses a terminal global variable whose name contains the account login. A value of 0.0 means available and 1.0 means occupied. Add AcquireInstanceLock() immediately after UpdateAccountState() and before GetCurrentSessionDate():

//+------------------------------------------------------------------+
//| 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);
  }

GlobalVariableSetOnCondition() performs the claim atomically, so a second instance cannot also change an available lock from 0.0 to 1.0. Immediately after AcquireInstanceLock(), add the matching release function:

//+------------------------------------------------------------------+
//| 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);
  }

The local g_instanceLockAcquired flag is important. An EA instance that failed to acquire the lock never marks itself as the owner, so its cleanup path cannot release a lock belonging to another instance. The lifecycle can now be completed. Replace the entire current OnInit() with:

//+------------------------------------------------------------------+
//| 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);
  }

Initialization now proceeds in dependency order: validate inputs, refresh the account identity, acquire the lock, open SQLite, prepare persistence, initialize account settings and the daily session, synchronize the controls, and finally start the timer. OnTimer() was introduced in the previous section. Keep its final Part 2 version as follows:

//+------------------------------------------------------------------+
//| 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.");
     }
  }

The remaining runtime event is the confirmation-button click. Add OnChartEvent() immediately after OnTimer():

//+------------------------------------------------------------------+
//| 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();
  }

The handler remains deliberately narrow. It ignores unrelated chart events and delegates the actual confirmation rules to ConfirmDailyStartingBalance() rather than duplicating them.

Finally, replace the entire current OnDeinit() with:

//+------------------------------------------------------------------+
//| 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);
  }

Shutdown reverses the main runtime dependencies: timer events stop first, owned chart controls are removed, SQLite is closed, and the account lock is released.

Part 2 ends at session management. It does not yet calculate daily drawdown, overall drawdown, or profit-target progress, and it does not execute any protection action. Those features depend on the persistent baseline established here and belong to later parts of the series.


Conclusion

Part 2 establishes a persistent daily-session reference for Prop Firm Guard. For the same trading account, broker server, and configured session day, the EA can store one confirmed starting balance and restore that same baseline after removal, reattachment, or terminal restart.

When the configured session boundary produces a new session key, the current session is first changed to SESSION_CLOSE and persisted. Only after that succeeds does the EA restore or create the record for the new session day. This prevents the baseline from being silently replaced simply because the EA was restarted or because calendar midnight passed.

The persistence layer therefore provides continuity across the session lifecycle, but it does not independently prove that the confirmed balance matches historical broker records. The stored value remains a trader-approved reference, not a reconstruction of past account activity.

With that reference now stable, Part 3 can begin using it for the actual compliance calculations: daily drawdown, overall drawdown, and profit-target progress.


Attachments

The attachment for this article is PropFirmGuard.mq5, containing the revised Part 2 source code. If your implementation does not compile correctly or the daily-session persistence, starting-balance confirmation, rollover behavior, or single-instance handling differs from what was demonstrated, you can download the file and compare it with your current version. The Part 2 source has also been published as an Algo Forge release:

https://forge.mql5.io/CHACHAIAN/PropFirmGuard/releases/tag/v1.0-part-2-revised

Attached files |
PropFirmGuard.mq5 (65.23 KB)
Neural Networks in Trading: A Spiking-Semantic Approach to Spatiotemporal Identification (Key Components) Neural Networks in Trading: A Spiking-Semantic Approach to Spatiotemporal Identification (Key Components)
In this article, we took a detailed look at the integration of the SSAM module into the SEW-ResNeXt block, demonstrating how the S3CE-Net framework enables the effective combination of spiking attention with residual blocks. This architecture ensures accurate processing of temporal and spatial data streams and high training stability. The modularity and flexibility of the components make it easier to extend the model and reuse proven methods.
Building Your Personal Expert Advisor (Part 7): Risk Management VI (Final)—Consolidating the Risk Framework Building Your Personal Expert Advisor (Part 7): Risk Management VI (Final)—Consolidating the Risk Framework
The article consolidates trade, basket, and account risk into a modular framework for MQL5 EAs. Risk logic moves into reusable classes and shared types, leaving the EA with its inputs, signal, configuration, and call sequence. The result is a shorter strategy file and a second EA that reuses the basket tier and the shared conversions.
Online False Discovery Rate Control: Putting an Error Budget on a Live Signal Stream Online False Discovery Rate Control: Putting an Error Budget on a Live Signal Stream
The article addresses online false discovery rate control for streaming signals, where fixed thresholds inflate noise discoveries. We port LORD3, LORD++ and SAFFRON into MQL5, construct a causal bootstrap p‑value source without a resolution floor, and verify correctness before market data. Dependence stress tests, a budget/level indicator and a two-arm Strategy Tester show how to configure, monitor and throttle streams to keep the acted-on noise fraction within target.
Beyond Clone and Commit: The Advanced Side of MQL5 Algo Forge Beyond Clone and Commit: The Advanced Side of MQL5 Algo Forge
We map the parts of Algo Forge that impact day-to-day MQL5 work: opening repos in MetaEditor, README translation, Units, branch/tag protection, browser patching, packages, and CI on a Windows host. The article shows exact settings, required runner labels, and how to gate merges on builds. You leave with reproducible steps to protect code, automate compiles, and ship artifacts.