2714 lines
No EOL
90 KiB
MQL5
2714 lines
No EOL
90 KiB
MQL5
//+------------------------------------------------------------------+
| |
//| 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 <Trade\Trade.mqh>
| |
| |
//+------------------------------------------------------------------+
| |
//| Enumerations |
| |
//+------------------------------------------------------------------+
| |
//| Represents the general compliance condition of the account. |
| |
//+------------------------------------------------------------------+
| |
enum ENUM_COMPLIANCE_STATUS
| |
{
| |
COMPLIANCE_SAFE = 0, // Account is operating within configured limits
| |
COMPLIANCE_WARNING, // Account is approaching a compliance limit
| |
COMPLIANCE_CRITICAL, // Account is very close to a compliance limit
| |
COMPLIANCE_BREACHED // A configured compliance limit has been breached
| |
};
| |
| |
//+------------------------------------------------------------------+
| |
//| Profit target status |
| |
//+------------------------------------------------------------------+
| |
//| Represents progress toward the configured account profit target. |
| |
//+------------------------------------------------------------------+
| |
enum ENUM_TARGET_STATUS
| |
{
| |
TARGET_IN_PROGRESS = 0, // Target is still being pursued
| |
TARGET_NEARING, // At least 90% of the target has been reached
| |
TARGET_REACHED // Required target balance has been achieved
| |
};
| |
| |
//+------------------------------------------------------------------+
| |
//| Overall guard status |
| |
//+------------------------------------------------------------------+
| |
//| Represents the combined state of the Part 3 financial rules. |
| |
//+------------------------------------------------------------------+
| |
enum ENUM_GUARD_STATUS
| |
{
| |
GUARD_NOT_READY = 0, // At least one required financial rule is unavailable
| |
GUARD_SAFE, // All evaluated financial rules are within safe limits
| |
GUARD_TARGET_REACHED, // Profit target is reached with no higher-priority risk
| |
GUARD_WARNING, // At least one drawdown rule is in warning state
| |
GUARD_CRITICAL, // At least one drawdown rule is in critical state
| |
GUARD_BREACHED // At least one drawdown rule has been breached
| |
};
| |
| |
//+------------------------------------------------------------------+
| |
//| 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
| |
};
| |
| |
//+------------------------------------------------------------------+
| |
//| Daily drawdown state |
| |
//+------------------------------------------------------------------+
| |
//| Holds the current runtime result of the selected daily rule. |
| |
//+------------------------------------------------------------------+
| |
struct SDailyDrawdownState
| |
{
| |
bool ready; // Whether the daily rule can be evaluated
| |
double referenceEquity; // Confirmed equity reference for the session
| |
double currentEquity; // Current live account equity
| |
double maximumDailyLoss; // Maximum permitted daily loss
| |
double dailyEquityFloor; // Minimum permitted equity for the daily rule
| |
double currentDailyLoss; // Current loss from the daily equity reference
| |
double drawdownPercent; // Loss as a percentage of reference equity
| |
double limitUsagePercent; // Percentage of daily allowance already used
| |
double remainingAllowance; // Amount remaining before daily breach
| |
ENUM_COMPLIANCE_STATUS status; // Valid only when ready is true
| |
};
| |
| |
//+------------------------------------------------------------------+
| |
//| Overall drawdown state |
| |
//+------------------------------------------------------------------+
| |
//| Holds the current runtime result of the static overall rule. |
| |
//+------------------------------------------------------------------+
| |
struct SOverallDrawdownState
| |
{
| |
bool ready; // Whether the overall rule can be evaluated
| |
double initialBalance; // Fixed initial prop-firm account balance
| |
double currentEquity; // Current live account equity
| |
double maximumOverallLoss; // Maximum permitted overall loss
| |
double overallFloor; // Static minimum permitted account equity
| |
double currentOverallLoss; // Current loss from the initial balance
| |
double limitUsagePercent; // Percentage of overall allowance already used
| |
double remainingAllowance; // Amount remaining before overall breach
| |
ENUM_COMPLIANCE_STATUS status; // Valid only when ready is true
| |
};
| |
| |
//+------------------------------------------------------------------+
| |
//| Profit target state |
| |
//+------------------------------------------------------------------+
| |
//| Holds the current runtime result of the realized-balance target. |
| |
//+------------------------------------------------------------------+
| |
struct SProfitTargetState
| |
{
| |
bool ready; // Whether the target rule can be evaluated
| |
double initialBalance; // Fixed initial prop-firm account balance
| |
double currentBalance; // Current realized account balance
| |
double targetAmount; // Profit required in account currency
| |
double targetBalance; // Balance required to reach the target
| |
double currentProfit; // Realized profit above the initial balance
| |
double progressPercent; // Percentage of the target already achieved
| |
double remainingProfit; // Profit still required to reach the target
| |
ENUM_TARGET_STATUS status; // Valid only when ready is true
| |
};
| |
| |
//+------------------------------------------------------------------+
| |
//| Global variables |
| |
//+------------------------------------------------------------------+
| |
SAccountState g_accountState; // Latest monitored account state
| |
SAccountSettings g_accountSettings; // Persisted prop-firm settings
| |
SDailySession g_dailySession; // Current configured daily session
| |
SDailyDrawdownState g_dailyDrawdown; // Current daily drawdown runtime state
| |
SOverallDrawdownState g_overallDrawdown; // Current overall drawdown runtime state
| |
SProfitTargetState g_profitTarget; // Current profit-target runtime state
| |
| |
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;
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Validate financial reference |
| |
//+------------------------------------------------------------------+
| |
bool IsValidFinancialReference(const double value)
| |
{
| |
//--- Financial references must be finite and strictly positive
| |
if(!MathIsValidNumber(value) ||
| |
value <= 0.0)
| |
return(false);
| |
| |
return(true);
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Classify consumed loss allowance |
| |
//+------------------------------------------------------------------+
| |
bool ClassifyLossUsage(const double usagePercent,
| |
ENUM_COMPLIANCE_STATUS &status)
| |
{
| |
//--- Reject invalid usage before assigning a compliance state
| |
if(!MathIsValidNumber(usagePercent) ||
| |
usagePercent < 0.0)
| |
{
| |
Print("Prop Firm Guard: Cannot classify an invalid loss usage value.");
| |
| |
return(false);
| |
}
| |
| |
//--- Defend the classifier against invalid threshold configuration
| |
if(!MathIsValidNumber(InpWarningLevelPercent) ||
| |
!MathIsValidNumber(InpCriticalLevelPercent) ||
| |
InpWarningLevelPercent <= 0.0 ||
| |
InpWarningLevelPercent >= InpCriticalLevelPercent ||
| |
InpCriticalLevelPercent >= 100.0)
| |
{
| |
Print("Prop Firm Guard: Cannot classify loss usage with invalid warning thresholds.");
| |
| |
return(false);
| |
}
| |
| |
//--- Evaluate the most severe condition first
| |
if(usagePercent >= 100.0)
| |
{
| |
status = COMPLIANCE_BREACHED;
| |
| |
return(true);
| |
}
| |
| |
if(usagePercent >= InpCriticalLevelPercent)
| |
{
| |
status = COMPLIANCE_CRITICAL;
| |
| |
return(true);
| |
}
| |
| |
if(usagePercent >= InpWarningLevelPercent)
| |
{
| |
status = COMPLIANCE_WARNING;
| |
| |
return(true);
| |
}
| |
| |
status = COMPLIANCE_SAFE;
| |
| |
return(true);
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Reset daily drawdown state |
| |
//+------------------------------------------------------------------+
| |
void ResetDailyDrawdownState()
| |
{
| |
//--- Mark the complete daily calculation as unavailable
| |
g_dailyDrawdown.ready = false;
| |
g_dailyDrawdown.referenceEquity = 0.0;
| |
g_dailyDrawdown.currentEquity = 0.0;
| |
g_dailyDrawdown.maximumDailyLoss = 0.0;
| |
g_dailyDrawdown.dailyEquityFloor = 0.0;
| |
g_dailyDrawdown.currentDailyLoss = 0.0;
| |
g_dailyDrawdown.drawdownPercent = 0.0;
| |
g_dailyDrawdown.limitUsagePercent = 0.0;
| |
g_dailyDrawdown.remainingAllowance = 0.0;
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Reset overall drawdown state |
| |
//+------------------------------------------------------------------+
| |
void ResetOverallDrawdownState()
| |
{
| |
//--- Mark the complete overall calculation as unavailable
| |
g_overallDrawdown.ready = false;
| |
g_overallDrawdown.initialBalance = 0.0;
| |
g_overallDrawdown.currentEquity = 0.0;
| |
g_overallDrawdown.maximumOverallLoss = 0.0;
| |
g_overallDrawdown.overallFloor = 0.0;
| |
g_overallDrawdown.currentOverallLoss = 0.0;
| |
g_overallDrawdown.limitUsagePercent = 0.0;
| |
g_overallDrawdown.remainingAllowance = 0.0;
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Reset profit target state |
| |
//+------------------------------------------------------------------+
| |
void ResetProfitTargetState()
| |
{
| |
//--- Mark the complete target calculation as unavailable
| |
g_profitTarget.ready = false;
| |
g_profitTarget.initialBalance = 0.0;
| |
g_profitTarget.currentBalance = 0.0;
| |
g_profitTarget.targetAmount = 0.0;
| |
g_profitTarget.targetBalance = 0.0;
| |
g_profitTarget.currentProfit = 0.0;
| |
g_profitTarget.progressPercent = 0.0;
| |
g_profitTarget.remainingProfit = 0.0;
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Calculate daily drawdown |
| |
//+------------------------------------------------------------------+
| |
void CalculateDailyDrawdown()
| |
{
| |
//--- Start from an explicitly unavailable calculation state
| |
ResetDailyDrawdownState();
| |
| |
//--- Require the current configured daily session
| |
if(!g_hasDailySession)
| |
return;
| |
| |
//--- The daily rule is valid only for an active confirmed session
| |
if(g_dailySession.status != SESSION_ACTIVE ||
| |
!g_dailySession.balanceConfirmed)
| |
return;
| |
| |
//--- Validate the equity reference captured at confirmation
| |
double referenceEquity = g_dailySession.startingEquity;
| |
| |
if(!IsValidFinancialReference(referenceEquity))
| |
{
| |
Print("Prop Firm Guard: Daily drawdown cannot use an invalid reference equity.");
| |
| |
return;
| |
}
| |
| |
//--- Defend against invalid daily rule configuration
| |
double dailyDrawdownPercent =
| |
g_accountSettings.dailyDrawdownPercent;
| |
| |
if(!MathIsValidNumber(dailyDrawdownPercent) ||
| |
dailyDrawdownPercent <= 0.0 ||
| |
dailyDrawdownPercent > 100.0)
| |
{
| |
Print("Prop Firm Guard: Daily drawdown percentage is invalid at runtime.");
| |
| |
return;
| |
}
| |
| |
//--- Current equity may be negative after a severe account loss
| |
double currentEquity = g_accountState.equity;
| |
| |
if(!MathIsValidNumber(currentEquity))
| |
{
| |
Print("Prop Firm Guard: Current equity is invalid for daily drawdown calculation.");
| |
| |
return;
| |
}
| |
| |
//--- Calculate the permitted daily loss from the equity reference
| |
double maximumDailyLoss =
| |
referenceEquity *
| |
dailyDrawdownPercent / 100.0;
| |
| |
if(!IsValidFinancialReference(maximumDailyLoss))
| |
{
| |
Print("Prop Firm Guard: Calculated maximum daily loss is invalid.");
| |
| |
return;
| |
}
| |
| |
//--- Derive the selected model's fixed daily equity floor
| |
double dailyEquityFloor =
| |
referenceEquity -
| |
maximumDailyLoss;
| |
| |
if(!MathIsValidNumber(dailyEquityFloor))
| |
{
| |
Print("Prop Firm Guard: Calculated daily equity floor is invalid.");
| |
| |
return;
| |
}
| |
| |
//--- Measure current equity loss from the confirmed equity reference
| |
double currentDailyLoss =
| |
referenceEquity -
| |
currentEquity;
| |
| |
if(currentDailyLoss < 0.0)
| |
currentDailyLoss = 0.0;
| |
| |
if(!MathIsValidNumber(currentDailyLoss))
| |
{
| |
Print("Prop Firm Guard: Calculated daily loss is invalid.");
| |
| |
return;
| |
}
| |
| |
//--- Express the loss relative to the session equity reference
| |
double drawdownPercent =
| |
currentDailyLoss /
| |
referenceEquity * 100.0;
| |
| |
//--- Express the loss as consumption of the permitted daily allowance
| |
double limitUsagePercent =
| |
currentDailyLoss /
| |
maximumDailyLoss * 100.0;
| |
| |
if(!MathIsValidNumber(drawdownPercent) ||
| |
!MathIsValidNumber(limitUsagePercent))
| |
{
| |
Print("Prop Firm Guard: Calculated daily drawdown percentages are invalid.");
| |
| |
return;
| |
}
| |
| |
//--- Preserve zero usage when equity remains above the reference
| |
if(drawdownPercent < 0.0)
| |
drawdownPercent = 0.0;
| |
| |
if(limitUsagePercent < 0.0)
| |
limitUsagePercent = 0.0;
| |
| |
//--- Calculate money still available before the daily floor is reached
| |
double remainingAllowance =
| |
maximumDailyLoss -
| |
currentDailyLoss;
| |
| |
if(remainingAllowance < 0.0)
| |
remainingAllowance = 0.0;
| |
| |
if(!MathIsValidNumber(remainingAllowance))
| |
{
| |
Print("Prop Firm Guard: Calculated daily remaining allowance is invalid.");
| |
| |
return;
| |
}
| |
| |
//--- Classify only after all required financial values are valid
| |
ENUM_COMPLIANCE_STATUS status;
| |
| |
if(!ClassifyLossUsage(limitUsagePercent,
| |
status))
| |
return;
| |
| |
//--- Publish the complete calculated state only after validation succeeds
| |
g_dailyDrawdown.referenceEquity = referenceEquity;
| |
g_dailyDrawdown.currentEquity = currentEquity;
| |
g_dailyDrawdown.maximumDailyLoss = maximumDailyLoss;
| |
g_dailyDrawdown.dailyEquityFloor = dailyEquityFloor;
| |
g_dailyDrawdown.currentDailyLoss = currentDailyLoss;
| |
g_dailyDrawdown.drawdownPercent = drawdownPercent;
| |
g_dailyDrawdown.limitUsagePercent = limitUsagePercent;
| |
g_dailyDrawdown.remainingAllowance = remainingAllowance;
| |
g_dailyDrawdown.status = status;
| |
g_dailyDrawdown.ready = true;
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Calculate overall drawdown |
| |
//+------------------------------------------------------------------+
| |
void CalculateOverallDrawdown()
| |
{
| |
//--- Start from an explicitly unavailable calculation state
| |
ResetOverallDrawdownState();
| |
| |
//--- Validate the fixed initial account reference
| |
double initialBalance = g_accountSettings.initialBalance;
| |
| |
if(!IsValidFinancialReference(initialBalance))
| |
{
| |
Print("Prop Firm Guard: Overall drawdown cannot use an invalid initial balance.");
| |
| |
return;
| |
}
| |
| |
//--- Defend against invalid overall rule configuration
| |
double overallDrawdownPercent =
| |
g_accountSettings.overallDrawdownPercent;
| |
| |
if(!MathIsValidNumber(overallDrawdownPercent) ||
| |
overallDrawdownPercent <= 0.0 ||
| |
overallDrawdownPercent > 100.0)
| |
{
| |
Print("Prop Firm Guard: Overall drawdown percentage is invalid at runtime.");
| |
| |
return;
| |
}
| |
| |
//--- Current equity may become negative after a severe account loss
| |
double currentEquity = g_accountState.equity;
| |
| |
if(!MathIsValidNumber(currentEquity))
| |
{
| |
Print("Prop Firm Guard: Current equity is invalid for overall drawdown calculation.");
| |
| |
return;
| |
}
| |
| |
//--- Calculate the maximum permitted static overall loss
| |
double maximumOverallLoss =
| |
initialBalance *
| |
overallDrawdownPercent / 100.0;
| |
| |
if(!IsValidFinancialReference(maximumOverallLoss))
| |
{
| |
Print("Prop Firm Guard: Calculated maximum overall loss is invalid.");
| |
| |
return;
| |
}
| |
| |
//--- Derive the static minimum equity permitted by this rule model
| |
double overallFloor =
| |
initialBalance -
| |
maximumOverallLoss;
| |
| |
if(!MathIsValidNumber(overallFloor))
| |
{
| |
Print("Prop Firm Guard: Calculated overall equity floor is invalid.");
| |
| |
return;
| |
}
| |
| |
//--- Measure current equity loss from the fixed initial balance
| |
double currentOverallLoss =
| |
initialBalance -
| |
currentEquity;
| |
| |
if(currentOverallLoss < 0.0)
| |
currentOverallLoss = 0.0;
| |
| |
if(!MathIsValidNumber(currentOverallLoss))
| |
{
| |
Print("Prop Firm Guard: Calculated overall loss is invalid.");
| |
| |
return;
| |
}
| |
| |
//--- Express current loss as consumption of the overall allowance
| |
double limitUsagePercent =
| |
currentOverallLoss /
| |
maximumOverallLoss * 100.0;
| |
| |
if(!MathIsValidNumber(limitUsagePercent))
| |
{
| |
Print("Prop Firm Guard: Calculated overall limit usage is invalid.");
| |
| |
return;
| |
}
| |
| |
if(limitUsagePercent < 0.0)
| |
limitUsagePercent = 0.0;
| |
| |
//--- Calculate money still available before the static floor is reached
| |
double remainingAllowance =
| |
maximumOverallLoss -
| |
currentOverallLoss;
| |
| |
if(remainingAllowance < 0.0)
| |
remainingAllowance = 0.0;
| |
| |
if(!MathIsValidNumber(remainingAllowance))
| |
{
| |
Print("Prop Firm Guard: Calculated overall remaining allowance is invalid.");
| |
| |
return;
| |
}
| |
| |
//--- Classify only after all required financial values are valid
| |
ENUM_COMPLIANCE_STATUS status;
| |
| |
if(!ClassifyLossUsage(limitUsagePercent,
| |
status))
| |
return;
| |
| |
//--- Publish the complete calculated state only after validation succeeds
| |
g_overallDrawdown.initialBalance = initialBalance;
| |
g_overallDrawdown.currentEquity = currentEquity;
| |
g_overallDrawdown.maximumOverallLoss = maximumOverallLoss;
| |
g_overallDrawdown.overallFloor = overallFloor;
| |
g_overallDrawdown.currentOverallLoss = currentOverallLoss;
| |
g_overallDrawdown.limitUsagePercent = limitUsagePercent;
| |
g_overallDrawdown.remainingAllowance = remainingAllowance;
| |
g_overallDrawdown.status = status;
| |
g_overallDrawdown.ready = true;
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Calculate profit target |
| |
//+------------------------------------------------------------------+
| |
void CalculateProfitTarget()
| |
{
| |
//--- Start from an explicitly unavailable calculation state
| |
ResetProfitTargetState();
| |
| |
//--- Validate the fixed initial account reference
| |
double initialBalance = g_accountSettings.initialBalance;
| |
| |
if(!IsValidFinancialReference(initialBalance))
| |
{
| |
Print("Prop Firm Guard: Profit target cannot use an invalid initial balance.");
| |
| |
return;
| |
}
| |
| |
//--- Defend against invalid profit target configuration
| |
double profitTargetPercent =
| |
g_accountSettings.profitTargetPercent;
| |
| |
if(!MathIsValidNumber(profitTargetPercent) ||
| |
profitTargetPercent <= 0.0 ||
| |
profitTargetPercent > 100.0)
| |
{
| |
Print("Prop Firm Guard: Profit target percentage is invalid at runtime.");
| |
| |
return;
| |
}
| |
| |
//--- Read the current realized account balance
| |
double currentBalance = g_accountState.balance;
| |
| |
if(!MathIsValidNumber(currentBalance))
| |
{
| |
Print("Prop Firm Guard: Current balance is invalid for profit target calculation.");
| |
| |
return;
| |
}
| |
| |
//--- Calculate the required realized profit
| |
double targetAmount =
| |
initialBalance *
| |
profitTargetPercent / 100.0;
| |
| |
if(!IsValidFinancialReference(targetAmount))
| |
{
| |
Print("Prop Firm Guard: Calculated profit target amount is invalid.");
| |
| |
return;
| |
}
| |
| |
//--- Derive the realized balance required to reach the target
| |
double targetBalance =
| |
initialBalance +
| |
targetAmount;
| |
| |
if(!MathIsValidNumber(targetBalance))
| |
{
| |
Print("Prop Firm Guard: Calculated target balance is invalid.");
| |
| |
return;
| |
}
| |
| |
//--- Measure realized profit above the initial challenge balance
| |
double currentProfit =
| |
currentBalance -
| |
initialBalance;
| |
| |
if(currentProfit < 0.0)
| |
currentProfit = 0.0;
| |
| |
if(!MathIsValidNumber(currentProfit))
| |
{
| |
Print("Prop Firm Guard: Calculated realized profit is invalid.");
| |
| |
return;
| |
}
| |
| |
//--- Express realized profit as completion of the required target
| |
double progressPercent =
| |
currentProfit /
| |
targetAmount * 100.0;
| |
| |
if(!MathIsValidNumber(progressPercent))
| |
{
| |
Print("Prop Firm Guard: Calculated profit target progress is invalid.");
| |
| |
return;
| |
}
| |
| |
if(progressPercent < 0.0)
| |
progressPercent = 0.0;
| |
| |
if(progressPercent > 100.0)
| |
progressPercent = 100.0;
| |
| |
//--- Calculate realized profit still required to reach the target
| |
double remainingProfit =
| |
targetAmount -
| |
currentProfit;
| |
| |
if(remainingProfit < 0.0)
| |
remainingProfit = 0.0;
| |
| |
if(!MathIsValidNumber(remainingProfit))
| |
{
| |
Print("Prop Firm Guard: Calculated remaining profit is invalid.");
| |
| |
return;
| |
}
| |
| |
//--- Classify the valid realized-balance target state
| |
ENUM_TARGET_STATUS status;
| |
| |
if(currentBalance >= targetBalance)
| |
status = TARGET_REACHED;
| |
else if(progressPercent >= 90.0)
| |
status = TARGET_NEARING;
| |
else
| |
status = TARGET_IN_PROGRESS;
| |
| |
//--- Publish the complete calculated state only after validation succeeds
| |
g_profitTarget.initialBalance = initialBalance;
| |
g_profitTarget.currentBalance = currentBalance;
| |
g_profitTarget.targetAmount = targetAmount;
| |
g_profitTarget.targetBalance = targetBalance;
| |
g_profitTarget.currentProfit = currentProfit;
| |
g_profitTarget.progressPercent = progressPercent;
| |
g_profitTarget.remainingProfit = remainingProfit;
| |
g_profitTarget.status = status;
| |
g_profitTarget.ready = true;
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Convert compliance status to text |
| |
//+------------------------------------------------------------------+
| |
string ComplianceStatusToString(const ENUM_COMPLIANCE_STATUS status)
| |
{
| |
//--- Convert only valid calculated compliance states
| |
switch(status)
| |
{
| |
case COMPLIANCE_WARNING:
| |
return("WARNING");
| |
| |
case COMPLIANCE_CRITICAL:
| |
return("CRITICAL");
| |
| |
case COMPLIANCE_BREACHED:
| |
return("BREACHED");
| |
| |
case COMPLIANCE_SAFE:
| |
return("SAFE");
| |
}
| |
| |
return("UNKNOWN");
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Convert target status to text |
| |
//+------------------------------------------------------------------+
| |
string TargetStatusToString(const ENUM_TARGET_STATUS status)
| |
{
| |
//--- Convert only valid calculated profit-target states
| |
switch(status)
| |
{
| |
case TARGET_NEARING:
| |
return("NEARING TARGET");
| |
| |
case TARGET_REACHED:
| |
return("TARGET REACHED");
| |
| |
case TARGET_IN_PROGRESS:
| |
return("IN PROGRESS");
| |
}
| |
| |
return("UNKNOWN");
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Get overall guard status |
| |
//+------------------------------------------------------------------+
| |
ENUM_GUARD_STATUS GetOverallGuardStatus()
| |
{
| |
//--- Do not report a complete guard state while any rule is unavailable
| |
if(!g_dailyDrawdown.ready ||
| |
!g_overallDrawdown.ready ||
| |
!g_profitTarget.ready)
| |
return(GUARD_NOT_READY);
| |
| |
//--- Give highest financial priority to any drawdown breach
| |
if(g_dailyDrawdown.status == COMPLIANCE_BREACHED ||
| |
g_overallDrawdown.status == COMPLIANCE_BREACHED)
| |
return(GUARD_BREACHED);
| |
| |
//--- Check for a critical drawdown condition
| |
if(g_dailyDrawdown.status == COMPLIANCE_CRITICAL ||
| |
g_overallDrawdown.status == COMPLIANCE_CRITICAL)
| |
return(GUARD_CRITICAL);
| |
| |
//--- Check for a warning drawdown condition
| |
if(g_dailyDrawdown.status == COMPLIANCE_WARNING ||
| |
g_overallDrawdown.status == COMPLIANCE_WARNING)
| |
return(GUARD_WARNING);
| |
| |
//--- Report target completion only after all risk rules are available
| |
if(g_profitTarget.status == TARGET_REACHED)
| |
return(GUARD_TARGET_REACHED);
| |
| |
//--- All required calculations are ready and no higher state applies
| |
return(GUARD_SAFE);
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Convert guard status to text |
| |
//+------------------------------------------------------------------+
| |
string GuardStatusToString(const ENUM_GUARD_STATUS status)
| |
{
| |
//--- Convert the complete account-level guard state
| |
switch(status)
| |
{
| |
case GUARD_NOT_READY:
| |
return("NOT READY");
| |
| |
case GUARD_TARGET_REACHED:
| |
return("TARGET REACHED");
| |
| |
case GUARD_WARNING:
| |
return("WARNING");
| |
| |
case GUARD_CRITICAL:
| |
return("CRITICAL");
| |
| |
case GUARD_BREACHED:
| |
return("BREACHED");
| |
| |
case GUARD_SAFE:
| |
return("SAFE");
| |
}
| |
| |
return("UNKNOWN");
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| 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 exposing dependent calculations
| |
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);
| |
}
| |
| |
//--- Refresh all financial states from the persisted session baseline
| |
CalculateDailyDrawdown();
| |
CalculateOverallDrawdown();
| |
CalculateProfitTarget();
| |
| |
//--- Report an unavailable financial state without reversing persistence
| |
if(!g_dailyDrawdown.ready ||
| |
!g_overallDrawdown.ready ||
| |
!g_profitTarget.ready)
| |
{
| |
Print("Prop Firm Guard: Starting balance was confirmed and saved, "
| |
"but one or more financial calculations are not ready.");
| |
}
| |
| |
//--- 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 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 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);
| |
}
| |
| |
//--- Build the initial Part 3 financial runtime state
| |
CalculateDailyDrawdown();
| |
CalculateOverallDrawdown();
| |
CalculateProfitTarget();
| |
| |
//--- 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 session maintenance and financial monitoring
| |
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 financial engines
| |
UpdateAccountState();
| |
| |
//--- Maintain the configured daily session boundary before calculation
| |
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.");
| |
}
| |
| |
//--- Recalculate the current Part 3 financial state
| |
CalculateDailyDrawdown();
| |
CalculateOverallDrawdown();
| |
CalculateProfitTarget();
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| 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();
| |
}
| |
//+------------------------------------------------------------------+ |