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