289 lines
9.5 KiB
MQL5
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;
|