mql5/Include/Escape/InputValidation.mqh
2025-08-05 01:57:33 -04:00

289 lines
9.5 KiB
MQL5

//+------------------------------------------------------------------+
//| InputValidation.mqh |
//| Copyright 2025, EscapeEA |
//| https://www.escapeea.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, EscapeEA"
#property link "https://www.escapeea.com"
#property version "1.10" // Added string validation and rate limiting
#property strict
#include <Trade\SymbolInfo.mqh>
#include "SecureMemory.mqh"
// Pattern definitions for input validation
#define PATTERN_ALPHANUMERIC "^[a-zA-Z0-9_\\-\\. ]+$"
#define PATTERN_SYMBOL_NAME "^[A-Z0-9]+$"
#define PATTERN_COMMENT "^[a-zA-Z0-9_\\-\\.\\s]+$"
//+------------------------------------------------------------------+
//| Input validation functions |
//+------------------------------------------------------------------+
class CInputValidator {
private:
CSymbolInfo m_symbol;
// Rate limiting
struct RateLimit {
int count;
datetime lastReset;
int maxRequests;
int timeWindow; // in seconds
};
RateLimit m_rateLimits[];
// Private constructor for singleton pattern
CInputValidator() {
// Initialize rate limits (5 requests per minute by default)
ArrayResize(m_rateLimits, 1);
m_rateLimits[0].count = 0;
m_rateLimits[0].lastReset = TimeCurrent();
m_rateLimits[0].maxRequests = 5;
m_rateLimits[0].timeWindow = 60;
}
// Prevent copying
CInputValidator(const CInputValidator&);
void operator=(const CInputValidator&);
// Check rate limiting
bool CheckRateLimit(int limitIndex = 0) {
if(limitIndex < 0 || limitIndex >= ArraySize(m_rateLimits)) return false;
datetime currentTime = TimeCurrent();
if(currentTime - m_rateLimits[limitIndex].lastReset > m_rateLimits[limitIndex].timeWindow) {
m_rateLimits[limitIndex].count = 0;
m_rateLimits[limitIndex].lastReset = currentTime;
}
if(m_rateLimits[limitIndex].count >= m_rateLimits[limitIndex].maxRequests) {
return false;
}
m_rateLimits[limitIndex].count++;
return true;
}
public:
// Get singleton instance
static CInputValidator* Instance() {
static CInputValidator instance;
return &instance;
}
//--- Basic validation functions
bool IsValidLotSize(double lots, const string symbol = "") {
if(lots <= 0) return false;
if(symbol != "" && !m_symbol.Name(symbol)) {
Print("Error: Invalid symbol for lot size validation");
return false;
}
double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
// Check against minimum and maximum lot size
if(lots < minLot || lots > maxLot) {
PrintFormat("Error: Lot size %.2f is outside valid range [%.2f, %.2f]", lots, minLot, maxLot);
return false;
}
// Check if lot size is a multiple of the lot step
if(MathMod(lots, lotStep) > 0.0001) {
PrintFormat("Error: Lot size %.2f is not a multiple of step size %.2f", lots, lotStep);
return false;
}
return true;
}
bool IsValidPrice(double price, const string symbol = "") {
if(price <= 0) return false;
if(symbol != "") {
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
double spread = ask - bid;
// Allow some buffer for price validation
if(price < bid - spread * 10 || price > ask + spread * 10) {
PrintFormat("Warning: Price %.5f is far from current market [%.5f, %.5f]",
price, bid, ask);
// Don't return false here as it might be a pending order
}
}
return true;
}
bool IsValidStopLoss(double price, double stopLoss, const string symbol = "") {
if(!IsValidPrice(price, symbol) || !IsValidPrice(stopLoss, symbol)) {
return false;
}
if(symbol != "") {
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
double stopLevel = SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL) * point;
double minStopDistance = stopLevel * 1.5; // Add 50% buffer to minimum stop level
if(MathAbs(price - stopLoss) < minStopDistance) {
PrintFormat("Error: Stop loss too close to entry price. Min distance: %.5f", minStopDistance);
return false;
}
}
return true;
}
bool IsValidTakeProfit(double price, double takeProfit, const string symbol = "") {
if(!IsValidPrice(price, symbol) || !IsValidPrice(takeProfit, symbol)) {
return false;
}
if(symbol != "") {
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
double stopLevel = SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL) * point;
double minTakeProfitDistance = stopLevel * 1.5; // Add 50% buffer
if(MathAbs(price - takeProfit) < minTakeProfitDistance) {
PrintFormat("Error: Take profit too close to entry price. Min distance: %.5f", minTakeProfitDistance);
return false;
}
}
return true;
}
bool IsValidMagicNumber(ulong magic) {
// Magic number should be non-zero and within reasonable range
if(magic == 0 || magic > 999999999) {
Print("Error: Invalid magic number");
return false;
}
return true;
}
bool IsValidSlippage(int slippage) {
// Slippage should be reasonable (0-100 pips)
if(slippage < 0 || slippage > 1000) { // 1000 points = 10 pips for 5-digit brokers
Print("Error: Invalid slippage value");
return false;
}
return true;
}
bool IsValidRiskPercentage(double riskPercent) {
// Risk per trade should be between 0.1% and 5%
if(riskPercent < 0.1 || riskPercent > 5.0) {
Print("Error: Risk percentage should be between 0.1% and 5.0%");
return false;
}
return true;
}
bool IsValidLeverage(double leverage) {
// Leverage should be between 1:1 and 1:500
if(leverage < 1.0 || leverage > 500.0) {
Print("Error: Leverage should be between 1 and 500");
return false;
}
return true;
}
//--- String validation functions
bool ValidateStringPattern(const string text, const string pattern, const string fieldName = "") {
if(!CheckRateLimit()) {
Print("Rate limit exceeded for string validation");
return false;
}
int textLen = StringLen(text);
if(textLen == 0) {
if(fieldName != "") Print("Error: ", fieldName, " cannot be empty");
return false;
}
// Simple pattern matching (MQL5 doesn't have built-in regex)
if(StringFind(text, "\0") >= 0) {
if(fieldName != "") Print("Error: ", fieldName, " contains null bytes");
return false;
}
// More comprehensive pattern matching can be added here
return true;
}
// Validate symbol name (alphanumeric and some special chars)
bool IsValidSymbolName(const string &symbol) {
if(!ValidateStringPattern(symbol, PATTERN_SYMBOL_NAME, "Symbol name")) {
return false;
}
// Additional symbol-specific validation
if(StringLen(symbol) < 2 || StringLen(symbol) > 32) {
Print("Error: Symbol name must be between 2 and 32 characters");
return false;
}
return true;
}
// Validate trade comment
bool IsValidTradeComment(const string &comment) {
if(!ValidateStringPattern(comment, PATTERN_COMMENT, "Trade comment")) {
return false;
}
// Comment length check
if(StringLen(comment) > 128) {
Print("Error: Trade comment is too long (max 128 characters)");
return false;
}
return true;
}
// Validate file path
bool IsValidFilePath(const string &path) {
if(!ValidateStringPattern(path, "", "File path")) {
return false;
}
// Basic path traversal prevention
if(StringFind(path, "..\\") >= 0 || StringFind(path, "../") >= 0) {
Print("Error: Invalid file path - path traversal not allowed");
return false;
}
return true;
}
// Set rate limit for validation functions
bool SetRateLimit(int maxRequests, int timeWindow, int limitIndex = 0) {
if(limitIndex < 0) return false;
if(limitIndex >= ArraySize(m_rateLimits)) {
int oldSize = ArraySize(m_rateLimits);
if(ArrayResize(m_rateLimits, limitIndex + 1) <= 0) return false;
// Initialize new entries
for(int i = oldSize; i < ArraySize(m_rateLimits); i++) {
m_rateLimits[i].count = 0;
m_rateLimits[i].lastReset = TimeCurrent();
m_rateLimits[i].maxRequests = 5; // Default
m_rateLimits[i].timeWindow = 60; // Default 1 minute
}
}
m_rateLimits[limitIndex].maxRequests = maxRequests;
m_rateLimits[limitIndex].timeWindow = timeWindow;
return true;
}
};
// Global instance
CInputValidator* ExtInputValidator = NULL;