289 righe
9,5 KiB
MQL5
289 righe
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;
|